From 1947f2198e223dda1ed8047e841c03876d3b94e0 Mon Sep 17 00:00:00 2001 From: brunota20 Date: Tue, 16 Jun 2026 23:08:11 -0300 Subject: [PATCH] chore: rust-idiomatic compliance pass across M3 + M2 modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA pass against the team's rust-idiomatic skill ahead of M4. All mandatory rules now hold; the cleanup is mostly mechanical with a handful of small typing improvements where the rule asked for one thiserror enum per error type. ## Em-dash purge (rule: "no em-dashes anywhere") Replaced every U+2014 with " - " across .rs / .toml / .md: - 51 source-file occurrences - 5 Cargo.toml comments - 366 occurrences across docs/*.md (most in ADRs and the deployment / tutorial / sdk landings) Grep gate: `grep -rn '—' crates/ modules/ docs/` returns 0. ## `#![cfg_attr(not(test), warn(unused_crate_dependencies))]` Added to every crate root that previously lacked it: - crates/shepherd-sdk/src/lib.rs - crates/shepherd-sdk-test/src/lib.rs - modules/{example,twap-monitor,ethflow-watcher}/src/lib.rs - modules/examples/{price-alert,balance-tracker}/src/lib.rs `crates/nexum-engine/src/main.rs` already had it. ## Unused-dep cleanup driven by the lint - shepherd-sdk dropped `serde` (only `serde_json` is actually imported; cowprotocol re-exports carry their own serde derive transitively). - balance-tracker dropped its direct `alloy-primitives` dep — now goes through `shepherd_sdk::prelude::{Address, U256, address}`. Tests adapt. ## thiserror conversions (rule: "one flat thiserror enum per ## module/backend; no String-wrapping of upstream errors") - `shepherd_sdk::host::HostError` gains `#[derive(thiserror:: Error)]` + `#[error("{domain}: {message} (code={code}, kind={kind:?})")]`. Was a plain struct without Display. Added `thiserror = "2"` as a dep. - `modules/twap-monitor::BuildError`: hand-rolled Display impl replaced with `#[derive(thiserror::Error)]` + per-variant `#[error(...)]` + `#[from] cowprotocol::Error`. The map_err at the call site collapses to `?`. - `modules/ethflow-watcher::BuildError`: same conversion (4 variants, one of them `#[from]`). Both modules add `thiserror = "2"` as a direct dep. ## Verification - `cargo clippy --all-targets --workspace -- -D warnings` clean. - `cargo test --workspace`: 121 tests pass. - nexum-engine 41, shepherd-sdk 27, shepherd-sdk-test 8 + 1 doctest, twap-monitor 13, ethflow-watcher 7, price-alert 11, balance-tracker 13. ## Architecture notes (no code changes) - `#[non_exhaustive]` is *not* applied to public enums (`HostErrorKind`, `LogLevel`, `RetryAction`, `PollOutcome`). The first two mirror the WIT 0.2 enums (locked at the WIT contract layer); the last two are intentional 3- and 5-arm contracts with no expected growth. If a future kind shows up, the rule applies then. - `parse_config` / `parse_settings` in the example modules return `Result` rather than a typed enum. The rule's "no string-wrapping" applies to error variants that *wrap* an upstream `std::error::Error`; one-shot config parsers with bespoke per-field messages are pragmatic. The error surface is internal to the module's `init` and not part of the orderbook retry contract. --- crates/shepherd-sdk-test/Cargo.toml | 2 +- crates/shepherd-sdk-test/src/lib.rs | 5 +- crates/shepherd-sdk/Cargo.toml | 4 +- crates/shepherd-sdk/README.md | 2 +- crates/shepherd-sdk/src/cow/composable.rs | 18 +- crates/shepherd-sdk/src/cow/error.rs | 4 +- crates/shepherd-sdk/src/cow/order.rs | 2 +- crates/shepherd-sdk/src/host.rs | 19 +- crates/shepherd-sdk/src/lib.rs | 15 +- crates/shepherd-sdk/src/prelude.rs | 2 +- docs/00-overview.md | 138 +++++++-------- docs/01-runtime-environment.md | 56 +++--- docs/02-modules-events-packaging.md | 38 ++-- docs/03-module-discovery.md | 20 +-- docs/04-state-store.md | 32 ++-- docs/07-rpc-namespace-design.md | 88 +++++----- docs/08-platform-generalisation.md | 166 +++++++++--------- ...01-engine-toml-separate-from-nexum-toml.md | 8 +- .../0002-provider-pool-transport-by-scheme.md | 6 +- docs/adr/0003-local-store-namespacing.md | 2 +- .../adr/0006-cow-twap-ethflow-host-helpers.md | 16 +- .../0007-upstream-protocol-logic-to-cow-rs.md | 10 +- .../0008-factory-subscriptions-in-manifest.md | 2 +- docs/diagrams/diagrams.md | 74 ++++---- docs/migration/0.1-to-0.2.md | 32 ++-- docs/sdk.md | 22 +-- docs/tutorial-first-module.md | 24 +-- modules/ethflow-watcher/Cargo.toml | 1 + modules/ethflow-watcher/module.toml | 2 +- modules/ethflow-watcher/src/lib.rs | 37 ++-- modules/example/module.toml | 2 +- modules/example/nexum.toml | 2 +- modules/example/src/lib.rs | 1 + modules/examples/balance-tracker/Cargo.toml | 1 - modules/examples/balance-tracker/src/lib.rs | 9 +- modules/examples/price-alert/src/lib.rs | 11 +- modules/twap-monitor/Cargo.toml | 1 + modules/twap-monitor/src/lib.rs | 38 ++-- 38 files changed, 452 insertions(+), 460 deletions(-) diff --git a/crates/shepherd-sdk-test/Cargo.toml b/crates/shepherd-sdk-test/Cargo.toml index 67e888c..2735cc8 100644 --- a/crates/shepherd-sdk-test/Cargo.toml +++ b/crates/shepherd-sdk-test/Cargo.toml @@ -7,7 +7,7 @@ repository.workspace = true description = "In-memory host mocks for Shepherd module unit tests. Implements shepherd_sdk::host::{ChainHost, LocalStoreHost, CowApiHost, LoggingHost}." [lib] -# Plain library, host-only — module Cargo.toml lists this under +# Plain library, host-only - module Cargo.toml lists this under # [dev-dependencies] so it never ships in the wasm bundle. [dependencies] diff --git a/crates/shepherd-sdk-test/src/lib.rs b/crates/shepherd-sdk-test/src/lib.rs index f93e5f5..ed978ea 100644 --- a/crates/shepherd-sdk-test/src/lib.rs +++ b/crates/shepherd-sdk-test/src/lib.rs @@ -52,9 +52,10 @@ //! The traits use [`shepherd_sdk::host::HostError`] rather than the //! `HostError` `wit_bindgen::generate!` emits per-module. A module //! bridges with two trivial `From` impls (one each direction) on its -//! own crate boundary — see the M3 tutorial (BLEU-848) for the exact +//! own crate boundary - see the M3 tutorial (BLEU-848) for the exact //! shape. +#![cfg_attr(not(test), warn(unused_crate_dependencies))] #![warn(missing_docs)] use std::cell::RefCell; @@ -213,7 +214,7 @@ impl MockLocalStore { self.rows.borrow().is_empty() } - /// Direct read for assertions — bypasses the trait. + /// Direct read for assertions - bypasses the trait. pub fn snapshot(&self) -> HashMap> { self.rows.borrow().clone() } diff --git a/crates/shepherd-sdk/Cargo.toml b/crates/shepherd-sdk/Cargo.toml index 85522ab..96c0bd3 100644 --- a/crates/shepherd-sdk/Cargo.toml +++ b/crates/shepherd-sdk/Cargo.toml @@ -7,7 +7,7 @@ repository.workspace = true description = "Guest-side SDK for Shepherd modules: re-exports, helpers, and prelude on top of cowprotocol + alloy types." [lib] -# Plain library — modules link this and emit their own cdylib for the +# Plain library - modules link this and emit their own cdylib for the # WASM Component. Building shepherd-sdk on the host target is also # supported so the helpers are unit-testable without a wasm toolchain. @@ -15,5 +15,5 @@ description = "Guest-side SDK for Shepherd modules: re-exports, helpers, and pre cowprotocol = { version = "1.0.0-alpha.3", default-features = false } alloy-primitives = { version = "1.6", default-features = false, features = ["std", "serde"] } alloy-sol-types = { version = "1.5", default-features = false, features = ["std"] } -serde = { version = "1", features = ["derive"] } serde_json = { version = "1", default-features = false, features = ["alloc"] } +thiserror = "2" diff --git a/crates/shepherd-sdk/README.md b/crates/shepherd-sdk/README.md index 4726465..c3c0f51 100644 --- a/crates/shepherd-sdk/README.md +++ b/crates/shepherd-sdk/README.md @@ -19,7 +19,7 @@ use shepherd_sdk::chain::{eth_call_params, parse_eth_call_result}; | Module | What it provides | |---|---| | `prelude` | One-liner `use ::*` for alloy primitives + cowprotocol order / signing / orderbook surface. | -| `cow::order` | `gpv2_to_order_data` — `GPv2OrderData` -> typed `OrderData`. | +| `cow::order` | `gpv2_to_order_data` - `GPv2OrderData` -> typed `OrderData`. | | `cow::composable` | `sol! IConditionalOrder` errors + `PollOutcome` + `decode_revert`. | | `cow::error` | `RetryAction` enum + `classify_api_error` + `try_decode_api_error`. | | `chain::eth_call` | `eth_call_params`, `parse_eth_call_result`, `decode_revert_hex`. | diff --git a/crates/shepherd-sdk/src/cow/composable.rs b/crates/shepherd-sdk/src/cow/composable.rs index 2b63a90..686a1fb 100644 --- a/crates/shepherd-sdk/src/cow/composable.rs +++ b/crates/shepherd-sdk/src/cow/composable.rs @@ -20,18 +20,18 @@ sol! { /// computed here match what the contract emits. #[derive(Debug)] interface IConditionalOrder { - /// `OrderNotValid(string)` — the order condition is permanently + /// `OrderNotValid(string)` - the order condition is permanently /// not met. Watch towers drop. error OrderNotValid(string reason); - /// `PollTryNextBlock(string)` — try again on the next block. + /// `PollTryNextBlock(string)` - try again on the next block. error PollTryNextBlock(string reason); - /// `PollTryAtBlock(uint256, string)` — try at or after the + /// `PollTryAtBlock(uint256, string)` - try at or after the /// given block number. error PollTryAtBlock(uint256 blockNumber, string reason); - /// `PollTryAtEpoch(uint256, string)` — try at or after the + /// `PollTryAtEpoch(uint256, string)` - try at or after the /// given Unix timestamp (seconds). error PollTryAtEpoch(uint256 timestamp, string reason); - /// `PollNever(string)` — the conditional order is dead. + /// `PollNever(string)` - the conditional order is dead. error PollNever(string reason); } } @@ -40,7 +40,7 @@ sol! { /// `Ready` carries the materials the submit path needs; the other /// variants drive the lifecycle handler (BLEU-830). /// -/// `Ready` is intentionally never produced by [`decode_revert`] — it +/// `Ready` is intentionally never produced by [`decode_revert`] - it /// only comes from the successful return path the poll module /// constructs at the call site. #[derive(Debug)] @@ -56,7 +56,7 @@ pub enum PollOutcome { /// orderbook prepends `from` before settlement). signature: Bytes, }, - /// Retry on the very next block — typical for time-sliced TWAP + /// Retry on the very next block - typical for time-sliced TWAP /// schedules and other handlers that re-check on every tick. TryNextBlock, /// Retry once block number reaches the embedded value. @@ -64,7 +64,7 @@ pub enum PollOutcome { /// Retry once the wall clock (Unix seconds, UTC) reaches the /// embedded value. TryAtEpoch(u64), - /// Order is dead — drop the watch. Aggregates `OrderNotValid` and + /// Order is dead - drop the watch. Aggregates `OrderNotValid` and /// `PollNever` reverts; the original reason string is dropped /// because the lifecycle handler does not key off it today. DontTryAgain, @@ -74,7 +74,7 @@ pub enum PollOutcome { /// [`PollOutcome`]. /// /// Returns `None` when the selector is not one of the five -/// [`IConditionalOrder`] errors — including a bare `Error(string)` +/// [`IConditionalOrder`] errors - including a bare `Error(string)` /// require-revert. Callers should treat that as `TryNextBlock` (the /// safe default) so a transient RPC blip does not drop a still-valid /// watch. diff --git a/crates/shepherd-sdk/src/cow/error.rs b/crates/shepherd-sdk/src/cow/error.rs index 1874465..f8e342c 100644 --- a/crates/shepherd-sdk/src/cow/error.rs +++ b/crates/shepherd-sdk/src/cow/error.rs @@ -4,7 +4,7 @@ //! the lifecycle layer dispatches on. The orderbook returns a typed //! [`ApiError`] JSON body on permanent / transient failures; the host //! forwards that JSON in `host-error.data` (once the chain backend -//! supports it — see the ADR follow-up). Until then, +//! supports it - see the ADR follow-up). Until then, //! [`classify_api_error`] falls back to `TryNextBlock` so a flaky //! orderbook does not poison still-valid orders. //! @@ -24,7 +24,7 @@ pub enum RetryAction { /// Leave the watch / placement in place; the next event will /// re-attempt. TryNextBlock, - /// Persist `next_attempt = now + seconds`. Reserved — no producer + /// Persist `next_attempt = now + seconds`. Reserved - no producer /// today (kept so the dispatch contract is stable). #[allow(dead_code)] Backoff { diff --git a/crates/shepherd-sdk/src/cow/order.rs b/crates/shepherd-sdk/src/cow/order.rs index a499c0e..b1fe2ab 100644 --- a/crates/shepherd-sdk/src/cow/order.rs +++ b/crates/shepherd-sdk/src/cow/order.rs @@ -19,7 +19,7 @@ use cowprotocol::{ /// the wire as `bytes32` markers (the `keccak256` of the lowercase /// variant name). This helper hands them off to cowprotocol's /// `from_contract_bytes` classifiers and returns `None` when the on- -/// chain payload carries a marker the SDK doesn't recognise — the +/// chain payload carries a marker the SDK doesn't recognise - the /// caller skips the order rather than ship a malformed body. /// /// `receiver = Address::ZERO` is normalised to `None`; `OrderCreation:: diff --git a/crates/shepherd-sdk/src/host.rs b/crates/shepherd-sdk/src/host.rs index 3aee7bb..794813a 100644 --- a/crates/shepherd-sdk/src/host.rs +++ b/crates/shepherd-sdk/src/host.rs @@ -1,4 +1,4 @@ -//! Host traits — the seam between strategy logic and the wit-bindgen +//! Host traits - the seam between strategy logic and the wit-bindgen //! shims a module generates per-cdylib. //! //! Each trait mirrors one nexum / shepherd host interface @@ -13,7 +13,7 @@ //! //! `wit_bindgen::generate!` emits a `HostError` struct into each //! module's own crate, so its identity is per-module. The SDK -//! exposes [`HostError`] (this module) with the same field shape — +//! exposes [`HostError`] (this module) with the same field shape - //! modules wire a one-liner `From` impl between the two so the //! traits stay world-neutral and the mocks compile without a wasm //! toolchain. See `shepherd-sdk-test`'s README for the adapter @@ -29,9 +29,9 @@ pub enum LogLevel { Debug, /// Steady-state events. Info, - /// Recoverable errors — operator notice but no immediate action. + /// Recoverable errors - operator notice but no immediate action. Warn, - /// Unrecoverable errors — operator should investigate. + /// Unrecoverable errors - operator should investigate. Error, } @@ -59,7 +59,8 @@ pub enum HostErrorKind { /// SDK-side counterpart to wit-bindgen's `HostError`. Same field shape /// so a module bridges between the two with a trivial `From` impl on /// each side. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] +#[error("{domain}: {message} (code={code}, kind={kind:?})")] pub struct HostError { /// Short subsystem identifier (`"chain"`, `"local-store"`, /// `"cow-api"`, `"logging"`). @@ -88,7 +89,7 @@ impl HostError { } } -/// `nexum:host/chain` — raw JSON-RPC dispatch. +/// `nexum:host/chain` - raw JSON-RPC dispatch. pub trait ChainHost { /// Execute a JSON-RPC request against the given chain. The host /// routes to its configured provider; the SDK does not care which @@ -96,7 +97,7 @@ pub trait ChainHost { fn request(&self, chain_id: u64, method: &str, params: &str) -> Result; } -/// `nexum:host/local-store` — per-module key-value persistence. +/// `nexum:host/local-store` - per-module key-value persistence. pub trait LocalStoreHost { /// Fetch a value. `Ok(None)` when the key is absent. fn get(&self, key: &str) -> Result>, HostError>; @@ -108,14 +109,14 @@ pub trait LocalStoreHost { fn list_keys(&self, prefix: &str) -> Result, HostError>; } -/// `shepherd:cow/cow-api` — orderbook submission path. +/// `shepherd:cow/cow-api` - orderbook submission path. pub trait CowApiHost { /// Submit an `OrderCreation` JSON body. The host returns the /// canonical order UID on success. fn submit_order(&self, chain_id: u64, body: &[u8]) -> Result; } -/// `nexum:host/logging` — structured runtime logs. +/// `nexum:host/logging` - structured runtime logs. pub trait LoggingHost { /// Emit a log line at the given level. fn log(&self, level: LogLevel, message: &str); diff --git a/crates/shepherd-sdk/src/lib.rs b/crates/shepherd-sdk/src/lib.rs index 0fde315..db4cc93 100644 --- a/crates/shepherd-sdk/src/lib.rs +++ b/crates/shepherd-sdk/src/lib.rs @@ -9,30 +9,30 @@ //! //! ## What lives here //! -//! - [`prelude`] — `use shepherd_sdk::prelude::*` imports alloy +//! - [`prelude`] - `use shepherd_sdk::prelude::*` imports alloy //! primitives ([`Address`], [`B256`], [`Bytes`], [`U256`], //! [`keccak256`]) and cowprotocol's order / signing / orderbook //! surface ([`OrderCreation`], [`OrderData`], [`OrderUid`], //! [`OrderKind`], [`Signature`], [`Chain`], [`GPv2OrderData`], //! [`EMPTY_APP_DATA_JSON`], [`ApiError`], [`OrderPostErrorKind`]). //! -//! - [`cow`] — `GPv2OrderData` -> `OrderData` bridging +//! - [`cow`] - `GPv2OrderData` -> `OrderData` bridging //! ([`gpv2_to_order_data`]), `IConditionalOrder` revert decoding //! ([`PollOutcome`] + [`decode_revert`]), and the //! [`RetryAction`] classifier driving submit-failure dispatch. //! -//! - [`chain`] — `eth_call` JSON plumbing +//! - [`chain`] - `eth_call` JSON plumbing //! ([`eth_call_params`], [`parse_eth_call_result`], //! [`decode_revert_hex`]). //! -//! - [`host`] — host trait seam ([`Host`] / [`ChainHost`] / +//! - [`host`] - host trait seam ([`Host`] / [`ChainHost`] / //! [`LocalStoreHost`] / [`CowApiHost`] / [`LoggingHost`]) plus a //! host-neutral [`HostError`]. Modules that want host-free tests //! structure their strategy logic against these traits and slot //! in the `shepherd-sdk-test` mocks. See the host module docs for //! the wit-bindgen adapter pattern. //! -//! - `store` — placeholder for `WatchSet` / `BackoffLedger` +//! - `store` - placeholder for `WatchSet` / `BackoffLedger` //! per ADR-0006. Populated when a second strategy module needs //! the same key conventions. //! @@ -44,7 +44,7 @@ //! Helpers in this SDK therefore take primitive types (`&[u8]`, //! `Option<&str>`, slices) rather than the per-module `HostError` //! struct; modules unpack their `HostError` on the way in. Trade-off -//! documented in ADR-0006 / ADR-0007 — the SDK stays on the guest +//! documented in ADR-0006 / ADR-0007 - the SDK stays on the guest //! side, neutral to which world the module exports. //! //! [`Address`]: alloy_primitives::Address @@ -76,6 +76,7 @@ //! [`LoggingHost`]: host::LoggingHost //! [`HostError`]: host::HostError +#![cfg_attr(not(test), warn(unused_crate_dependencies))] #![warn(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] @@ -88,7 +89,7 @@ pub mod prelude; #[cfg(test)] mod tests { //! The skeleton has no behaviour to exercise; this test just - //! locks the prelude's surface — the build itself proves the + //! locks the prelude's surface - the build itself proves the //! re-exports compile against both `wasm32-wasip2` and the //! host target. diff --git a/crates/shepherd-sdk/src/prelude.rs b/crates/shepherd-sdk/src/prelude.rs index 75cf3d3..aab6810 100644 --- a/crates/shepherd-sdk/src/prelude.rs +++ b/crates/shepherd-sdk/src/prelude.rs @@ -32,7 +32,7 @@ pub use cowprotocol::{ SigningScheme, }; -/// Re-exported `ApiError` typed error surface from the orderbook — +/// Re-exported `ApiError` typed error surface from the orderbook - /// guest-side helpers (BLEU-840) read this back out of host-error JSON /// to drive the `RetryAction` dispatch. pub use cowprotocol::error::{ApiError, OrderPostErrorKind}; diff --git a/docs/00-overview.md b/docs/00-overview.md index a09de9b..2d6cc24 100755 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -1,19 +1,19 @@ # Nexum: Universal WASM Component Model Runtime -Nexum is a WASM Component Model runtime that provides secure, sandboxed execution for WebAssembly modules. Modules react to blockchain events, read chain state, persist data locally and to decentralised storage, communicate via decentralised messaging — all within a capability-based sandbox with zero implicit permissions. +Nexum is a WASM Component Model runtime that provides secure, sandboxed execution for WebAssembly modules. Modules react to blockchain events, read chain state, persist data locally and to decentralised storage, communicate via decentralised messaging - all within a capability-based sandbox with zero implicit permissions. -**Shepherd** is the Nexum distribution that includes CoW Protocol extensions (`shepherd:cow` WIT package). A module compiled against the universal `nexum:host/event-module` world runs on any Nexum-compatible host. A module compiled against `shepherd:cow/shepherd` additionally gains access to CoW Protocol APIs and order submission — and requires a Shepherd host. +**Shepherd** is the Nexum distribution that includes CoW Protocol extensions (`shepherd:cow` WIT package). A module compiled against the universal `nexum:host/event-module` world runs on any Nexum-compatible host. A module compiled against `shepherd:cow/shepherd` additionally gains access to CoW Protocol APIs and order submission - and requires a Shepherd host. ### Vocabulary: engine vs. host (`nexum-engine` vs. `nexum:host`) -Two project names look similar but mean different things — keeping them straight is load-bearing for everything that follows: +Two project names look similar but mean different things - keeping them straight is load-bearing for everything that follows: | Term | What it is | Where you find it | |---|---|---| -| **engine** (`nexum-engine`) | A concrete *implementation* that loads and runs WASM components. The 0.2 reference engine is a wasmtime-based server daemon. Mobile / browser / embedded engines could exist later — each is a separate engine. | `crates/nexum-engine/`, the binary, `cargo run -p nexum-engine` | -| **host** (`nexum:host`) | The WIT *contract* — the set of host-imported interfaces (chain, identity, local-store, etc.), types, and worlds that every engine must implement and every module imports. The contract is one; engines are many. | `wit/nexum-host/`, `package nexum:host@0.2.0`, Rust path `nexum::host::*` | +| **engine** (`nexum-engine`) | A concrete *implementation* that loads and runs WASM components. The 0.2 reference engine is a wasmtime-based server daemon. Mobile / browser / embedded engines could exist later - each is a separate engine. | `crates/nexum-engine/`, the binary, `cargo run -p nexum-engine` | +| **host** (`nexum:host`) | The WIT *contract* - the set of host-imported interfaces (chain, identity, local-store, etc.), types, and worlds that every engine must implement and every module imports. The contract is one; engines are many. | `wit/nexum-host/`, `package nexum:host@0.2.0`, Rust path `nexum::host::*` | -The relationship: an engine *implements* `nexum:host` so that modules *built against* `nexum:host` can run on it. The `nexum:host` package itself does not run anything — it's a specification. When this doc says "the host", it means whichever engine the module currently runs on, as seen through the `nexum:host` contract. +The relationship: an engine *implements* `nexum:host` so that modules *built against* `nexum:host` can run on it. The `nexum:host` package itself does not run anything - it's a specification. When this doc says "the host", it means whichever engine the module currently runs on, as seen through the `nexum:host` contract. > **Upgrading from 0.1?** See the [Migration Guide](migration/0.1-to-0.2.md) for the full rename table (`web3:runtime` → `nexum:host`, `csn` → `chain`, `msg` → `messaging`, `headless-module` → `event-module`, etc.), the unified `host-error` model, and the manifest-driven capability negotiation introduced in 0.2. @@ -32,7 +32,7 @@ flowchart TB mc["Module C"] end - subgraph host["Host API — WIT Interfaces"] + subgraph host["Host API - WIT Interfaces"] uni["nexum:host\nchain · identity · local-store · remote-store · messaging · logging"] ext["shepherd:cow\ncow-api"] end @@ -58,11 +58,11 @@ flowchart TB ## Design Principles -- **Component Model from day 1** — WIT-defined API contract; structural sandboxing (no WASI, no FS, no network); multi-language guests. -- **Declarative subscriptions** — modules declare events in their manifest; the runtime wires sources. -- **Transactional state** — per-event all-or-nothing semantics; commit on success, rollback on trap. -- **Content-addressed distribution** — modules are fetched by hash (Swarm, IPFS, OCI, HTTPS); integrity always verified. -- **Self-hosted** — no centralised dependency; operator runs their own node. +- **Component Model from day 1** - WIT-defined API contract; structural sandboxing (no WASI, no FS, no network); multi-language guests. +- **Declarative subscriptions** - modules declare events in their manifest; the runtime wires sources. +- **Transactional state** - per-event all-or-nothing semantics; commit on success, rollback on trap. +- **Content-addressed distribution** - modules are fetched by hash (Swarm, IPFS, OCI, HTTPS); integrity always verified. +- **Self-hosted** - no centralised dependency; operator runs their own node. ## The Six Primitives @@ -79,20 +79,20 @@ Every module has access to six orthogonal capabilities through the `nexum:host` These primitives are orthogonal: -- **Chain** is the source of truth — the blockchain consensus state. Modules read chain state and (indirectly) write to it via order submission or transactions. -- **Identity** is cryptographic identity — key management and signing. The `chain` host implementation depends on `identity` internally: signing RPC methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) delegate to the identity backend. Modules can also import `identity` directly for raw signing operations. -- **Local Store** is the module's private scratchpad — fast, local, scoped to one module on one device. Does not replicate. -- **Remote Store** is shared persistent content — content-addressed, decentralised, survives independent of any device. Any module on any device can read what another module wrote. -- **Messaging** is real-time communication — ephemeral pub/sub messages between modules, devices, or users. Transient and topic-based. -- **Logging** is diagnostics — one-way output for debugging and monitoring. Not a data channel. +- **Chain** is the source of truth - the blockchain consensus state. Modules read chain state and (indirectly) write to it via order submission or transactions. +- **Identity** is cryptographic identity - key management and signing. The `chain` host implementation depends on `identity` internally: signing RPC methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) delegate to the identity backend. Modules can also import `identity` directly for raw signing operations. +- **Local Store** is the module's private scratchpad - fast, local, scoped to one module on one device. Does not replicate. +- **Remote Store** is shared persistent content - content-addressed, decentralised, survives independent of any device. Any module on any device can read what another module wrote. +- **Messaging** is real-time communication - ephemeral pub/sub messages between modules, devices, or users. Transient and topic-based. +- **Logging** is diagnostics - one-way output for debugging and monitoring. Not a data channel. ## Additive 0.2 Capabilities In addition to the six core primitives, the 0.2 WIT introduces three optional capabilities that modules can declare in their manifest: -- **`clock`** — wall-clock (`now-ms`, UTC milliseconds since Unix epoch) and monotonic (`monotonic-ns`) time, replacing the 0.1 workaround of reading `block.timestamp` inside `on_block`. -- **`random`** — a CSPRNG (`fill(len)`), since 0.1 modules had no source of secure randomness at all. -- **`http`** — an allowlisted outbound HTTP client (`fetch(request)`), gated by a `[capabilities.http].allow` domain list. The host MUST enforce the allowlist. This replaces the 0.1 anti-pattern of tunnelling notifications through Waku. +- **`clock`** - wall-clock (`now-ms`, UTC milliseconds since Unix epoch) and monotonic (`monotonic-ns`) time, replacing the 0.1 workaround of reading `block.timestamp` inside `on_block`. +- **`random`** - a CSPRNG (`fill(len)`), since 0.1 modules had no source of secure randomness at all. +- **`http`** - an allowlisted outbound HTTP client (`fetch(request)`), gated by a `[capabilities.http].allow` domain list. The host MUST enforce the allowlist. This replaces the 0.1 anti-pattern of tunnelling notifications through Waku. 0.2 also publishes (but does not yet host) the experimental **`query-module`** world for request/response modules (wallet rule evaluators, signature validators, pricing oracles). The WIT is stable enough to target with `MockHost` tests; production host support lands in 0.3. See the migration guide for the full WIT. @@ -102,12 +102,12 @@ The WIT is split into layered packages. The universal layer (`nexum:host`) provi ```mermaid graph TB - subgraph l3["Layer 3 — Domain Extensions"] + subgraph l3["Layer 3 - Domain Extensions"] cow["shepherd:cow\ncow-api"] other["future:domain\nvault · strategy · …"] end - subgraph l1["Layer 1 — Universal Runtime"] + subgraph l1["Layer 1 - Universal Runtime"] pkg["nexum:host"] ifaces["chain · identity · local-store · remote-store · messaging · logging"] exports["Exports: init · on-event"] @@ -118,19 +118,19 @@ graph TB ``` ``` -// Universal layer — any platform, any blockchain app +// Universal layer - any platform, any blockchain app package nexum:host@0.2.0 world event-module { - import chain — consensus access (JSON-RPC passthrough) - import identity — key management and message signing - import local-store — local key-value persistence - import remote-store — decentralised storage (Swarm) - import messaging — decentralised messaging (Waku) - import logging — log (trace/debug/info/warn/error) - - export init(config) — called once on load - export on_event(event)— called per subscribed event (block, logs, tick, message) + import chain - consensus access (JSON-RPC passthrough) + import identity - key management and message signing + import local-store - local key-value persistence + import remote-store - decentralised storage (Swarm) + import messaging - decentralised messaging (Waku) + import logging - log (trace/debug/info/warn/error) + + export init(config) - called once on load + export on_event(event) - called per subscribed event (block, logs, tick, message) } // CoW Protocol extension @@ -138,13 +138,13 @@ package shepherd:cow@0.2.0 world shepherd { include event-module - import cow-api — CoW Protocol REST API + order submission + import cow-api - CoW Protocol REST API + order submission } ``` -The `event-module` world imports **six** interfaces — chain, identity, local-store, remote-store, messaging, logging. The 0.1 WIT framing claimed six primitives but only actually imported five; 0.2 brings `identity` into the world definition so the contract matches the documentation. +The `event-module` world imports **six** interfaces - chain, identity, local-store, remote-store, messaging, logging. The 0.1 WIT framing claimed six primitives but only actually imported five; 0.2 brings `identity` into the world definition so the contract matches the documentation. -No WASI interfaces are imported. All I/O is mediated through host interfaces. The `chain` interface exposes a single generic `request` function (plus an additive `request-batch` in 0.2) — the SDK implements alloy's `Transport` trait on top of it, giving modules the full alloy `Provider` API (80+ methods) with zero WIT churn. +No WASI interfaces are imported. All I/O is mediated through host interfaces. The `chain` interface exposes a single generic `request` function (plus an additive `request-batch` in 0.2) - the SDK implements alloy's `Transport` trait on top of it, giving modules the full alloy `Provider` API (80+ methods) with zero WIT churn. > Design rationale: [07-rpc-namespace-design.md](07-rpc-namespace-design.md) | Platform generalisation: [08-platform-generalisation.md](08-platform-generalisation.md) @@ -156,15 +156,15 @@ No WASI interfaces are imported. All I/O is mediated through host interfaces. Th |---------|--------|---------| | Language | Rust | 1.90+ | | WASM runtime | wasmtime (Component Model) | 45.x | -| API contract | WIT (`nexum:host@0.2.0`, `shepherd:cow@0.2.0`) | — | +| API contract | WIT (`nexum:host@0.2.0`, `shepherd:cow@0.2.0`) | - | | Guest bindings | wit-bindgen | 0.57.x | -| Async | Tokio | — | +| Async | Tokio | - | | Ethereum RPC | alloy | 1.5.x | | Local store | redb | 3.1.x | -| Logging | tracing + tracing-subscriber | — | -| Metrics | metrics + metrics-exporter-prometheus | — | -| Deployment | Docker | — | -| License | AGPL-3.0 | — | +| Logging | tracing + tracing-subscriber | - | +| Metrics | metrics + metrics-exporter-prometheus | - | +| Deployment | Docker | - | +| License | AGPL-3.0 | - | ## Module Package @@ -198,7 +198,7 @@ cow_api_url = "https://api.cow.fi/arbitrum" slippage_bps = 50 # integers stay integers in 0.2 ``` -The manifest declares identity, resource caps, chain requirements, event subscriptions, capability grants, and typed module config — everything the runtime needs to load and run the module. In 0.2, `[capabilities]` is the canonical place to declare what host primitives a module needs; imports listed as `optional` install trap stubs that return `host-error { kind: unsupported }` on call rather than failing instantiation. Omitting `[capabilities]` falls back to "all imports required" with a deprecation warning. +The manifest declares identity, resource caps, chain requirements, event subscriptions, capability grants, and typed module config - everything the runtime needs to load and run the module. In 0.2, `[capabilities]` is the canonical place to declare what host primitives a module needs; imports listed as `optional` install trap stubs that return `host-error { kind: unsupported }` on call rather than failing instantiation. Omitting `[capabilities]` falls back to "all imports required" with a deprecation warning. -> Full spec: [02-modules-events-packaging.md](02-modules-events-packaging.md) @@ -238,8 +238,8 @@ stateDiagram-v2 - **Load**: compile `Component`, validate WIT world, create `InstancePre`. - **Init**: create `Store`, instantiate, call `init(config)`. - **Run**: dispatch subscribed events to `on_event`. Each call gets a fuel budget. -- **Restart**: on crash — exponential backoff (1s -> 5min cap), fresh `Store`, state persists. -- **Dead**: after N consecutive failures (poison pill) — requires manual intervention. +- **Restart**: on crash - exponential backoff (1s -> 5min cap), fresh `Store`, state persists. +- **Dead**: after N consecutive failures (poison pill) - requires manual intervention. -> Full lifecycle: [02-modules-events-packaging.md](02-modules-events-packaging.md) @@ -248,7 +248,7 @@ stateDiagram-v2 - **Sources**: `block` (new heads via `eth_subscribe`), `log` (filtered contract events), `cron` (schedule-based), `message` (Waku content topics). - **Shared subscriptions**: one block subscription per chain, fanned out to all subscribed modules. - **Dispatch**: concurrent across modules, sequential within a module (ordered delivery). -- **Declared in manifest**: `[[subscription]]` blocks — the runtime wires sources, not the module. +- **Declared in manifest**: `[[subscription]]` blocks - the runtime wires sources, not the module. -> Full design: [02-modules-events-packaging.md](02-modules-events-packaging.md) @@ -256,7 +256,7 @@ stateDiagram-v2 - **Backend**: redb (pure Rust, ACID, MVCC, crash-safe). - **Isolation**: one database file per module; modules cannot access each other's state. -- **Transactions**: each `on_event` runs in an implicit write transaction — commit on success, rollback on failure. +- **Transactions**: each `on_event` runs in an implicit write transaction - commit on success, rollback on failure. - **Survives restarts**: state is external to WASM instance. - **Size enforcement**: `max_state_bytes` from manifest, enforced host-side. - **Prefix scanning**: `list-keys(prefix)` for namespaced key organisation. @@ -269,23 +269,23 @@ The SDK mirrors the WIT layering: `nexum-sdk` (universal) and `shepherd-sdk` (Co | Crate | Provides | |-------|----------| -| `nexum-sdk` | `provider(chain_id)` — full alloy `Provider` backed by host RPC via `HostTransport` | -| | `Signer` — signing client (get accounts, sign messages, sign EIP-712 typed data) | -| | `TypedState` — serde-based typed local state (postcard serialisation) | -| | `RemoteStore` — typed decentralised storage client (upload, download, feeds) | -| | `Messaging` — typed messaging client (publish, query) | -| | `abi::sol!` — compile-time Ethereum ABI codec (alloy-sol-types) | -| | `log::{info!, …}` — formatted logging macros | -| | `HostError` / `HostErrorKind` — unified host error type with `?` support | -| | `#[nexum::module]` — proc macro for universal modules | -| `shepherd-sdk` | `Cow` — typed CoW Protocol API client backed by host `cow-api` interface | -| | `#[shepherd::module]` — proc macro for CoW modules (extends `#[nexum::module]`) | -| | `prelude::*` — all types, interfaces, helpers in one import | -| Both | `testing::MockHost` — native-Rust unit tests with mock host | -| | `testing::WasmTestHarness` — integration tests in real wasmtime | -| | `cargo nexum` — CLI: new / build / package / publish / check / migrate | - -Multi-language support: module authors can use Rust, C/C++, Go, JavaScript, or Python — all compile to valid components against the same WIT world. +| `nexum-sdk` | `provider(chain_id)` - full alloy `Provider` backed by host RPC via `HostTransport` | +| | `Signer` - signing client (get accounts, sign messages, sign EIP-712 typed data) | +| | `TypedState` - serde-based typed local state (postcard serialisation) | +| | `RemoteStore` - typed decentralised storage client (upload, download, feeds) | +| | `Messaging` - typed messaging client (publish, query) | +| | `abi::sol!` - compile-time Ethereum ABI codec (alloy-sol-types) | +| | `log::{info!, …}` - formatted logging macros | +| | `HostError` / `HostErrorKind` - unified host error type with `?` support | +| | `#[nexum::module]` - proc macro for universal modules | +| `shepherd-sdk` | `Cow` - typed CoW Protocol API client backed by host `cow-api` interface | +| | `#[shepherd::module]` - proc macro for CoW modules (extends `#[nexum::module]`) | +| | `prelude::*` - all types, interfaces, helpers in one import | +| Both | `testing::MockHost` - native-Rust unit tests with mock host | +| | `testing::WasmTestHarness` - integration tests in real wasmtime | +| | `cargo nexum` - CLI: new / build / package / publish / check / migrate | + +Multi-language support: module authors can use Rust, C/C++, Go, JavaScript, or Python - all compile to valid components against the same WIT world. -> Full design: [05-sdk-design.md](05-sdk-design.md) @@ -322,16 +322,16 @@ Metrics cover three groups: runtime-level (modules loaded/dead), per-module (eve ## Platform Generalisation -Nexum is **designed** to be portable to mobile and browser hosts: the WIT contract is the universal interface and any host that implements it can run modules unchanged. The **0.2 reference runtime ships server-only** — a Rust/Tokio/wasmtime binary. The mobile, WebView, and super-app targets remain on the roadmap and live in the docs as architectural direction, not shipping artifacts. +Nexum is **designed** to be portable to mobile and browser hosts: the WIT contract is the universal interface and any host that implements it can run modules unchanged. The **0.2 reference runtime ships server-only** - a Rust/Tokio/wasmtime binary. The mobile, WebView, and super-app targets remain on the roadmap and live in the docs as architectural direction, not shipping artifacts. | Platform | WASM Engine | Local Store | RPC Backend | Status | |----------|-------------|-------------|-------------|--------| | **Server** (reference) | wasmtime | redb | alloy provider | **Shipping in 0.2** | -| **Mobile** (Flutter/Dart) | wasmtime C API / wasm3 | SQLite | HTTP client | Planned — see roadmap | -| **WebView** | Browser engine + `jco` | IndexedDB | JS bridge / wallet | Planned — see roadmap | -| **Super app** | All of the above | SQLite | HTTP + wallet | Planned — see roadmap | +| **Mobile** (Flutter/Dart) | wasmtime C API / wasm3 | SQLite | HTTP client | Planned - see roadmap | +| **WebView** | Browser engine + `jco` | IndexedDB | JS bridge / wallet | Planned - see roadmap | +| **Super app** | All of the above | SQLite | HTTP + wallet | Planned - see roadmap | -The mobile/wallet host story — including the experimental `query-module` world's production support, the C ABI for non-Rust embedders, and the `nexum-host` embedder facade — is on the 0.3 roadmap, conditional on a named design partner. +The mobile/wallet host story - including the experimental `query-module` world's production support, the C ABI for non-Rust embedders, and the `nexum-host` embedder facade - is on the 0.3 roadmap, conditional on a named design partner. -> Full design (and the design rationale for each target): [08-platform-generalisation.md](08-platform-generalisation.md) diff --git a/docs/01-runtime-environment.md b/docs/01-runtime-environment.md index 3f72ab0..261c58d 100755 --- a/docs/01-runtime-environment.md +++ b/docs/01-runtime-environment.md @@ -26,13 +26,13 @@ The Component Model is **production-viable in wasmtime 45** and gives us critical advantages over raw core modules: -1. **Structural sandboxing.** A component compiled against a WIT world with no filesystem import literally *cannot* access the filesystem — enforced at the type level, not just by omission of host functions. This is stronger than core module sandboxing where imports are stringly-typed. +1. **Structural sandboxing.** A component compiled against a WIT world with no filesystem import literally *cannot* access the filesystem - enforced at the type level, not just by omission of host functions. This is stronger than core module sandboxing where imports are stringly-typed. 2. **Type-safe API contract.** The WIT definition *is* the API spec. Both host and guest get generated bindings (`wasmtime::component::bindgen!` on the host, `wit_bindgen::generate!` on the guest). No manual ABI wrangling, no serialisation disagreements. 3. **Resource types.** Opaque handles with lifecycle management (constructors, methods, destructors via `ResourceTable`). Ideal for subscription handles, RPC connections, etc. -4. **Multi-language guests from day 1.** Module authors can use Rust, C/C++, Go, JavaScript (ComponentizeJS), or Python (componentize-py) — all producing valid components against the same WIT world. This dramatically lowers the barrier for community modules. +4. **Multi-language guests from day 1.** Module authors can use Rust, C/C++, Go, JavaScript (ComponentizeJS), or Python (componentize-py) - all producing valid components against the same WIT world. This dramatically lowers the barrier for community modules. 5. **No WASI required.** The Component Model and WASI are architecturally separate. We define a pure `nexum:host` world with exactly our host APIs. Zero WASI imports means zero implicit capabilities. @@ -47,9 +47,9 @@ The Component Model is **production-viable in wasmtime 45** and gives us critica | Aspect | Risk | |--------|------| -| `bindgen!` macro, custom worlds, resource types | Low — stable, well-documented | -| `wit-bindgen` guest bindings | Medium — API churn between versions | -| Component Model native async (streams/futures) | High — not needed yet, avoid for now | +| `bindgen!` macro, custom worlds, resource types | Low - stable, well-documented | +| `wit-bindgen` guest bindings | Medium - API churn between versions | +| Component Model native async (streams/futures) | High - not needed yet, avoid for now | ## Core Concepts @@ -210,7 +210,7 @@ interface chain { } /// Additive 0.2 method: batched JSON-RPC. The alloy-backed HostTransport - /// routes RequestPacket::Batch through this — `provider.multicall(...)` + /// routes RequestPacket::Batch through this - `provider.multicall(...)` /// actually batches on the wire in 0.2. Hosts that cannot batch natively /// MUST fall back to sequential `request` calls; the returned list is /// the same length as `requests` and in the same order. @@ -227,7 +227,7 @@ interface identity { /// Sign a message with `personal_sign` semantics. The host MUST prepend /// the EIP-191 prefix (`\x19Ethereum Signed Message:\n`) before /// hashing and signing. Hosts MUST NOT expose a raw-bytes signing path - /// through this function — a raw signer can be tricked into signing + /// through this function - a raw signer can be tricked into signing /// EIP-155 transactions or EIP-712 payloads disguised as plain bytes. /// /// Returns a 65-byte ECDSA secp256k1 signature (r || s || v). @@ -257,7 +257,7 @@ interface logging { /// The universal event-driven module world. Platform-agnostic: no CoW, /// no domain-specific imports. Suitable for any web3 automation. /// -/// In 0.2 this imports all six primitives — the identity import was +/// In 0.2 this imports all six primitives - the identity import was /// missing from the 0.1 WIT despite being part of the documented primitive /// taxonomy, and is now present. world event-module { @@ -276,7 +276,7 @@ world event-module { } ``` -In addition to the six core imports, 0.2 publishes three additive optional capabilities — `clock` (`now-ms` / `monotonic-ns`), `random` (CSPRNG `fill`), and `http` (allowlisted outbound HTTP) — which modules can declare in their `nexum.toml` `[capabilities]` section. The migration guide carries the full WIT for each. 0.2 also publishes the experimental **`query-module`** world for request/response modules; the WIT is stable but no host implementation ships in 0.2, so it's a target for `MockHost` testing only. +In addition to the six core imports, 0.2 publishes three additive optional capabilities - `clock` (`now-ms` / `monotonic-ns`), `random` (CSPRNG `fill`), and `http` (allowlisted outbound HTTP) - which modules can declare in their `nexum.toml` `[capabilities]` section. The migration guide carries the full WIT for each. 0.2 also publishes the experimental **`query-module`** world for request/response modules; the WIT is stable but no host implementation ships in 0.2, so it's a target for `MockHost` testing only. ### CoW-Specific Package: `shepherd:cow@0.2.0` @@ -318,14 +318,14 @@ world shepherd { ### Key properties -- **No WASI** — by default, modules cannot access FS, network, clocks, or random. The additive 0.2 capabilities (`clock`, `random`, `http`) provide controlled access to time, entropy, and allowlisted HTTP — but only when declared in the manifest's `[capabilities]` section. -- **All I/O through our interfaces** — RPC reads, identity/signing, CoW API, local-store, order submission, logging. -- **Generic JSON-RPC passthrough** — the `chain` interface exposes a single `request` function (plus an additive `request-batch`). The SDK implements alloy's `Transport` trait on top of it, giving modules the full alloy `Provider` API. See doc 07 for details. -- **Identity as a first-class primitive** — the `identity` interface provides key management and signing. The `chain` host implementation depends on `identity` internally: signing RPC methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) are intercepted and delegated to the identity backend. Modules can also import `identity` directly for `personal_sign`-style message signing, EIP-712 typed data signing, and listing accounts. (Raw-bytes signing, gated by an explicit capability, is on the 0.3 roadmap; the current `sign` MUST prepend the EIP-191 prefix.) -- **Unified `host-error` taxonomy** — every host function returns `result`. The 0.1 per-protocol error types (`json-rpc-error`, `identity-error`, `msg-error`, `store-error`, `api-error`) are gone. Modules match on `host-error-kind` (`unsupported`, `unavailable`, `denied`, `rate-limited`, `timeout`, `invalid-input`, `internal`) for retry/backoff decisions. -- **`list` for raw bytes** — local-store values, order payloads, signatures, accounts, etc. The SDK provides typed wrappers. +- **No WASI** - by default, modules cannot access FS, network, clocks, or random. The additive 0.2 capabilities (`clock`, `random`, `http`) provide controlled access to time, entropy, and allowlisted HTTP - but only when declared in the manifest's `[capabilities]` section. +- **All I/O through our interfaces** - RPC reads, identity/signing, CoW API, local-store, order submission, logging. +- **Generic JSON-RPC passthrough** - the `chain` interface exposes a single `request` function (plus an additive `request-batch`). The SDK implements alloy's `Transport` trait on top of it, giving modules the full alloy `Provider` API. See doc 07 for details. +- **Identity as a first-class primitive** - the `identity` interface provides key management and signing. The `chain` host implementation depends on `identity` internally: signing RPC methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) are intercepted and delegated to the identity backend. Modules can also import `identity` directly for `personal_sign`-style message signing, EIP-712 typed data signing, and listing accounts. (Raw-bytes signing, gated by an explicit capability, is on the 0.3 roadmap; the current `sign` MUST prepend the EIP-191 prefix.) +- **Unified `host-error` taxonomy** - every host function returns `result`. The 0.1 per-protocol error types (`json-rpc-error`, `identity-error`, `msg-error`, `store-error`, `api-error`) are gone. Modules match on `host-error-kind` (`unsupported`, `unavailable`, `denied`, `rate-limited`, `timeout`, `invalid-input`, `internal`) for retry/backoff decisions. +- **`list` for raw bytes** - local-store values, order payloads, signatures, accounts, etc. The SDK provides typed wrappers. - **Resource types** can be added later (e.g. subscription handles, cursor-based log iteration). -- **Two worlds in 0.2's reference runtime** — `nexum:host/event-module` for platform-agnostic modules; `shepherd:cow/shepherd` for CoW Protocol modules that need the `cow-api` import. The experimental `nexum:host/query-module` world is published but not yet hosted. +- **Two worlds in 0.2's reference runtime** - `nexum:host/event-module` for platform-agnostic modules; `shepherd:cow/shepherd` for CoW Protocol modules that need the `cow-api` import. The experimental `nexum:host/query-module` world is published but not yet hosted. ## Host-Side Embedding @@ -409,7 +409,7 @@ impl nexum::host::chain::Host for NexumHostState { let provider = self.provider_for(chain_id)?; let raw_params: Box = RawValue::from_string(params)?; - // One function handles the entire eth_ namespace — alloy's provider + // One function handles the entire eth_ namespace - alloy's provider // stack (timeout, retry, rate-limit, fallback) applies transparently. match provider.raw_request_dyn(method.into(), &raw_params).await { Ok(result) => Ok(Ok(result.get().to_string())), @@ -492,7 +492,7 @@ See doc 07 for the full `chain` and `cow-api` host implementations, method allow ### Universal modules (`nexum-sdk`) -Module authors targeting the universal `event-module` world add the `nexum-sdk` crate and use the `#[nexum::module]` proc macro. Modules can access identity for signing operations — either indirectly through `chain` (signing RPC methods are handled transparently) or directly via the `identity` interface for raw signing: +Module authors targeting the universal `event-module` world add the `nexum-sdk` crate and use the `#[nexum::module]` proc macro. Modules can access identity for signing operations - either indirectly through `chain` (signing RPC methods are handled transparently) or directly via the `identity` interface for raw signing: ```rust use nexum_sdk::prelude::*; @@ -518,7 +518,7 @@ impl BlockLogger { ### CoW Protocol modules (`shepherd-sdk`) -Module authors targeting the CoW-specific `shepherd` world add the `shepherd-sdk` crate and use the `#[shepherd::module]` proc macro. The macro provides **named event handlers** (`on_block`, `on_logs`, `on_tick`, `on_message`) — it generates the `on_event` match dispatch, WIT export wrapper, and optional provider injection. Handlers can be `async fn` for natural `.await`: +Module authors targeting the CoW-specific `shepherd` world add the `shepherd-sdk` crate and use the `#[shepherd::module]` proc macro. The macro provides **named event handlers** (`on_block`, `on_logs`, `on_tick`, `on_message`) - it generates the `on_event` match dispatch, WIT export wrapper, and optional provider injection. Handlers can be `async fn` for natural `.await`: ```rust use shepherd_sdk::prelude::*; @@ -538,11 +538,11 @@ impl TwapMonitor { Ok(()) } - // Named handler — macro generates on_event match dispatch. + // Named handler - macro generates on_event match dispatch. // provider is injected from block.chain_id. - // async fn — macro wraps in block_on (single-poll, zero overhead). + // async fn - macro wraps in block_on (single-poll, zero overhead). async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { - // Full alloy Provider API — natural .await + // Full alloy Provider API - natural .await let block_num = provider.get_block_number().await?; let balance = provider.get_balance(owner).latest().await?; @@ -590,12 +590,12 @@ All produce valid components against the same WIT worlds (`nexum:host/event-modu ### Fuel (deterministic cost accounting) -- `Config::consume_fuel(true)` — each WASM op consumes fuel; exhaustion traps. +- `Config::consume_fuel(true)` - each WASM op consumes fuel; exhaustion traps. - Use for **per-invocation budgets**: cap a single `on_event` callback. ### Epoch Interruption (cooperative time-slicing) -- `Config::epoch_interruption(true)` — background Tokio task calls `engine.increment_epoch()` on a fixed interval. +- `Config::epoch_interruption(true)` - background Tokio task calls `engine.increment_epoch()` on a fixed interval. - Stores yield at epoch boundaries via `epoch_deadline_async_yield_and_update`. - Use for **wall-clock fairness**: prevent one module from starving others. @@ -605,9 +605,9 @@ Both are needed: fuel for correctness, epochs for liveness. Implement `ResourceLimiter` to cap per-module: -- **Memory growth** — target <10 MB default. -- **Table growth** — max entries. -- **Instance count** — max concurrent. +- **Memory growth** - target <10 MB default. +- **Table growth** - max entries. +- **Instance count** - max concurrent. Enforced synchronously on every `memory.grow` / `table.grow`. @@ -628,7 +628,7 @@ All RPC and CoW API I/O is async (alloy / reqwest on the host). wasmtime bridges - WASI 0.2.1 is stable in wasmtime. WASI 0.3 (native async) is in preview. - The `event-module` world imports **zero WASI interfaces**. - This is a security feature: components structurally cannot access FS/network/clocks via WASI. -- The 0.2 additive capabilities (`clock`, `random`, `http`) cover the common needs that would otherwise drive a WASI import, but as first-class Nexum interfaces — capability-negotiated via the manifest, allowlisted (in the HTTP case), and consistent with the rest of the host surface (`host-error` returns, no panics on capability absence). +- The 0.2 additive capabilities (`clock`, `random`, `http`) cover the common needs that would otherwise drive a WASI import, but as first-class Nexum interfaces - capability-negotiated via the manifest, allowlisted (in the HTTP case), and consistent with the rest of the host surface (`host-error` returns, no panics on capability absence). ## Summary: Nexum <-> wasmtime Mapping diff --git a/docs/02-modules-events-packaging.md b/docs/02-modules-events-packaging.md index bebdb96..8cc7cd0 100755 --- a/docs/02-modules-events-packaging.md +++ b/docs/02-modules-events-packaging.md @@ -2,7 +2,7 @@ ## Module Package: the Nexum Module Bundle -A module is distributed as a **bundle** — a WASM component plus a manifest that declares its identity, event subscriptions, chain requirements, and resource limits. The manifest is the bridge between packaging, the event system, and the runtime lifecycle. +A module is distributed as a **bundle** - a WASM component plus a manifest that declares its identity, event subscriptions, chain requirements, and resource limits. The manifest is the bridge between packaging, the event system, and the runtime lifecycle. ### Manifest (`nexum.toml`) @@ -26,12 +26,12 @@ max_state_bytes = 52_428_800 # 50 MB [module.restart] max_consecutive_failures = 10 # Dead after this many consecutive failures -# Chain requirements — the runtime provides RPC for these +# Chain requirements - the runtime provides RPC for these [chains] required = [42161] # Arbitrum (must have) optional = [1, 100] # Mainnet, Gnosis (used if available) -# Capability negotiation (new in 0.2) — which host primitives the module needs. +# Capability negotiation (new in 0.2) - which host primitives the module needs. # Optional imports trap with host-error { kind: unsupported } on call rather # than failing instantiation. Omitting this section falls back to # "all imports required" with a deprecation warning. @@ -43,7 +43,7 @@ denied = [] [capabilities.http] allow = ["api.cow.fi"] # outbound HTTP domain allowlist -# Event subscriptions — declares what the runtime should feed this module +# Event subscriptions - declares what the runtime should feed this module [[subscription]] kind = "block" chain_id = 42161 @@ -58,7 +58,7 @@ topics = ["0x…"] # ComposableCoW ConditionalOrderCreated kind = "cron" schedule = "*/5 * * * *" # every 5 minutes -# Typed config — TOML values preserve their type at the guest (0.2) +# Typed config - TOML values preserve their type at the guest (0.2) [config] cow_api_url = "https://api.cow.fi/arbitrum" min_twap_interval_secs = 120 # integer stays integer @@ -67,11 +67,11 @@ enable_alerts = true # boolean stays boolean Key design points: -- **`component` is a content hash**, not a filename. The runtime resolves it via the content store (see below). (Was `wasm = ...` in 0.1 — see the migration guide.) -- **`[[subscription]]` blocks are declarative.** The module doesn't set up its own subscriptions imperatively — the runtime reads the manifest and wires up event sources before calling `init`. The 0.1 spelling was `[[subscribe]]` with `type = ...`; 0.2 uses `[[subscription]]` with `kind = ...` because `type` is a reserved word in several binding languages. +- **`component` is a content hash**, not a filename. The runtime resolves it via the content store (see below). (Was `wasm = ...` in 0.1 - see the migration guide.) +- **`[[subscription]]` blocks are declarative.** The module doesn't set up its own subscriptions imperatively - the runtime reads the manifest and wires up event sources before calling `init`. The 0.1 spelling was `[[subscribe]]` with `type = ...`; 0.2 uses `[[subscription]]` with `kind = ...` because `type` is a reserved word in several binding languages. - **`[capabilities]`** is new in 0.2 and now drives what the runtime links into the module's import space. See the migration guide for the full schema (including `[capabilities.http]` allowlists and `[capabilities.identity].methods` subsets). - **`resources` are caps**, not requests. The runtime enforces them via wasmtime's `ResourceLimiter` and fuel system. -- **`chains.required`** — if the runtime doesn't have an RPC endpoint for a required chain, the module fails to load (fast, clear error). +- **`chains.required`** - if the runtime doesn't have an RPC endpoint for a required chain, the module fails to load (fast, clear error). - **`config`** is opaque to the runtime. 0.2 keeps 0.1's stringly-typed shape (`list>`); the host flattens TOML scalars (numbers, booleans) to their string form on the way through. A typed `config-value` variant is on the 0.3 roadmap, bundled with the manifest-parser work. ### Bundle Format @@ -98,7 +98,7 @@ How the directory is represented depends on the content backend: ## Content-Addressed Distribution -Distribution is **agnostic** — the runtime resolves content by hash through pluggable backends. The manifest's `wasm` field is a content address; the `source` in the runtime config tells the runtime *where* to look. +Distribution is **agnostic** - the runtime resolves content by hash through pluggable backends. The manifest's `wasm` field is a content address; the `source` in the runtime config tells the runtime *where* to look. ### Content Reference Scheme @@ -152,7 +152,7 @@ registry = "ghcr.io" This means: - A **local dev** just drops `.wasm` files in a directory. - A **production deployment** fetches from Swarm or OCI on first load, then caches locally. -- **Integrity is always verified** — the content hash in the manifest is the trust anchor, not the transport. +- **Integrity is always verified** - the content hash in the manifest is the trust anchor, not the transport. ## Module Lifecycle @@ -178,7 +178,7 @@ stateDiagram-v2 |-------|-------------| | **Resolve** | Content store resolves `component` hash to local path. Fail -> `Dead`. | | **Load** | `Component::from_file`, create `InstancePre`. Validates that the component satisfies the target WIT world (`nexum:host/event-module` or `shepherd:cow/shepherd`). Installs trap stubs for capabilities the manifest declares `optional` but the host does not provide. Fail -> `Dead`. | -| **Init** | Create `Store`, instantiate, call `init(config)` inside an implicit write transaction (same semantics as `on_event` — commit on success, rollback on failure). Module sets up internal state. Fail -> `Restart` (might be transient). | +| **Init** | Create `Store`, instantiate, call `init(config)` inside an implicit write transaction (same semantics as `on_event` - commit on success, rollback on failure). Module sets up internal state. Fail -> `Restart` (might be transient). | | **Run** | Runtime dispatches events to `on_event`. Each call gets a fuel budget. Module processes events and may call host imports (chain, local-store, identity, cow-api, etc.). | | **Restart** | After a trap or error. Backoff: 1s -> 2s -> 4s -> ... -> 5min cap. A fresh `Store` is created (clean memory), but **local-store data persists** (it's in redb, external to the WASM instance). | | **Dead** | After N consecutive failures (poison pill detection) or explicit operator shutdown. No further event dispatch. Requires manual intervention. | @@ -186,10 +186,10 @@ stateDiagram-v2 ### Key Lifecycle Properties - **State survives restarts.** The redb key-value store is external to the WASM instance. A restarted module picks up where it left off. -- **Memory does not survive restarts.** Each restart creates a fresh `Store` — clean linear memory, no stale pointers. +- **Memory does not survive restarts.** Each restart creates a fresh `Store` - clean linear memory, no stale pointers. - **`InstancePre` is reused.** Compilation and linking are done once at Load. Restarts only create a new `Store` and call `init` again. - **Config is immutable for a loaded module.** Changing config requires a reload (new Load cycle). -- **Hot-reload sequence.** When a module update is detected (e.g. ENS contenthash changed): (1) let the current in-flight `on_event` complete, (2) stop event dispatch for this module, (3) fetch and compile the new `Component`, (4) create new `InstancePre`, (5) create fresh `Store`, (6) call `init` with new config — state table is inherited (module handles migration), (7) resume event dispatch. The old `InstancePre` is dropped. +- **Hot-reload sequence.** When a module update is detected (e.g. ENS contenthash changed): (1) let the current in-flight `on_event` complete, (2) stop event dispatch for this module, (3) fetch and compile the new `Component`, (4) create new `InstancePre`, (5) create fresh `Store`, (6) call `init` with new config - state table is inherited (module handles migration), (7) resume event dispatch. The old `InstancePre` is dropped. ## Event System @@ -257,7 +257,7 @@ When an event fires: - **Sequential within a module.** Events for the same module are dispatched in order. A module sees block N before block N+1. This is enforced by a per-module dispatch queue (Tokio `mpsc` channel). - **Best-effort delivery.** If a module is in Restart state when an event arrives, the event is queued (bounded buffer). If the buffer fills, oldest events are dropped and a warning is logged. - **No acknowledgement.** A successful return from `on_event` is not an ack. The module is responsible for using the local-store to track its own progress (e.g. "last processed block"). -- **Catch-up after gaps.** Events can be dropped during restart (bounded buffer overflow). Modules should query for missed data on startup — e.g. in `init`, read `last_block` from local-store, use the alloy `Provider` (backed by `chain::request`) to call `get_block_number()` and `get_logs()` to backfill any gap. This is a best practice, not enforced by the runtime. +- **Catch-up after gaps.** Events can be dropped during restart (bounded buffer overflow). Modules should query for missed data on startup - e.g. in `init`, read `last_block` from local-store, use the alloy `Provider` (backed by `chain::request`) to call `get_block_number()` and `get_logs()` to backfill any gap. This is a best practice, not enforced by the runtime. ### Event Type Encoding @@ -283,7 +283,7 @@ record tick { } ``` -The runtime serialises event data via the canonical ABI (handled automatically by `bindgen!`). Note the 0.2 semantic change: all `u64` timestamps in 0.2 are **milliseconds since Unix epoch, UTC**. The 0.1 WIT did not specify a unit and several sources used seconds — audit any timestamp arithmetic. The `tick` variant (formerly `timer(u64)`) is now a record so bindings read `event.tick.firedAt` instead of comparing a bare integer. +The runtime serialises event data via the canonical ABI (handled automatically by `bindgen!`). Note the 0.2 semantic change: all `u64` timestamps in 0.2 are **milliseconds since Unix epoch, UTC**. The 0.1 WIT did not specify a unit and several sources used seconds - audit any timestamp arithmetic. The `tick` variant (formerly `timer(u64)`) is now a record so bindings read `event.tick.firedAt` instead of comparing a bare integer. ## Updated WIT Worlds @@ -384,7 +384,7 @@ interface identity { sign-typed-data: func(account: list, typed-data: string) -> result, host-error>; } -/// Universal event-driven module world — platform-agnostic. Imports the six +/// Universal event-driven module world - platform-agnostic. Imports the six /// primitives in 0.2 (identity was missing from the 0.1 WIT despite being /// part of the primitive taxonomy). world event-module { @@ -424,7 +424,7 @@ interface cow-api { -> result; } -/// CoW Protocol module world — extends event-module with cow-api. +/// CoW Protocol module world - extends event-module with cow-api. world shepherd { include nexum:host/event-module; @@ -466,11 +466,11 @@ Operator deploys a module: → Router → twap-monitor's dispatch queue → Tokio task calls on_event(Event::Block(…)) → Module calls chain::request (via alloy Provider), local-store get, cow-api submit-order - → Returns Ok(()) — runtime logs success + → Returns Ok(()) - runtime logs success 7. On crash: → Module trapped (fuel exhaustion / panic) → Runtime logs error, enters Restart state → Backoff 1s, creates fresh Store, calls init again - → Local-store data still intact — module resumes + → Local-store data still intact - module resumes ``` diff --git a/docs/03-module-discovery.md b/docs/03-module-discovery.md index 56067cb..4459239 100755 --- a/docs/03-module-discovery.md +++ b/docs/03-module-discovery.md @@ -1,6 +1,6 @@ # Module Discovery -Doc 02 defines how modules are packaged (bundle = `nexum.toml` + `module.wasm`) and how content is fetched by hash (pluggable content store). This document defines how the runtime **discovers which modules to load** — the layer above content resolution. +Doc 02 defines how modules are packaged (bundle = `nexum.toml` + `module.wasm`) and how content is fetched by hash (pluggable content store). This document defines how the runtime **discovers which modules to load** - the layer above content resolution. Three discovery sources, from simplest to most decentralised: @@ -54,7 +54,7 @@ twap-monitor.shepherd.eth └── text: shepherd.name → "twap-monitor" ``` -The `contenthash` points to the full bundle on Swarm (a directory containing `nexum.toml` + `module.wasm`). Text records provide lightweight metadata the runtime can read without fetching the bundle — useful for filtering or display. +The `contenthash` points to the full bundle on Swarm (a directory containing `nexum.toml` + `module.wasm`). Text records provide lightweight metadata the runtime can read without fetching the bundle - useful for filtering or display. ### Runtime resolution flow @@ -98,11 +98,11 @@ When the module author publishes a new version, they: 1. Upload the new bundle to Swarm → get new content hash 2. Update the ENS `contenthash` record -The runtime detects the change on its next poll (or via event — see below), fetches the new bundle, and hot-reloads the module. +The runtime detects the change on its next poll (or via event - see below), fetches the new bundle, and hot-reloads the module. ## 3. On-Chain Registry (Contract Events) -For fully autonomous discovery — the runtime watches a contract for registration events and auto-loads modules without operator intervention. +For fully autonomous discovery - the runtime watches a contract for registration events and auto-loads modules without operator intervention. ### Option A: Dedicated registry contract @@ -130,7 +130,7 @@ interface INexumRegistry { The runtime subscribes to `ModuleRegistered` events, resolves the ENS name from the event, and enters the ENS resolution flow above. -### Option B: No ad-hoc registry — contracts self-declare via ENS +### Option B: No ad-hoc registry - contracts self-declare via ENS This is the more decentralised approach. Instead of a central registry: @@ -169,7 +169,7 @@ ethflow.modules.shepherd.eth → contenthash of Ethflow bundle *.modules.shepherd.eth → resolved by registry contract ``` -The wildcard resolver is itself the registry — anyone can register a subdomain. The runtime subscribes to events from the resolver contract to discover new modules. +The wildcard resolver is itself the registry - anyone can register a subdomain. The runtime subscribes to events from the resolver contract to discover new modules. This gives us human-readable, permissionless module discovery under a shared namespace. @@ -196,8 +196,8 @@ Discovery is permissionless, but **execution requires operator consent**. The ru ```toml [discovery] -# "allowlist" — only load modules from these sources -# "auto" — load anything discovered (use with caution) +# "allowlist" - only load modules from these sources +# "auto" - load anything discovered (use with caution) mode = "allowlist" # If mode = "allowlist", only these ENS names / registries are trusted @@ -222,8 +222,8 @@ In `auto` mode, the runtime loads any module it discovers (useful for a public " Suggested naming under a shared parent (e.g. `shepherd.eth` or a subdomain of the protocol): ``` -.shepherd.eth — community / independent modules -..eth — protocol-owned modules +.shepherd.eth - community / independent modules +..eth - protocol-owned modules Examples: twap-monitor.shepherd.eth diff --git a/docs/04-state-store.md b/docs/04-state-store.md index 1eff78c..98fa172 100755 --- a/docs/04-state-store.md +++ b/docs/04-state-store.md @@ -4,26 +4,26 @@ Every Nexum module has access to a persistent key-value store that survives restarts, crashes, and module updates. The store is backed by **redb** (v3.1, pure Rust, embedded, ACID, MVCC) and exposed to modules through the `local-store` WIT interface. -The local store is the only durable memory a module has — WASM linear memory is wiped on every restart. Modules must be written to reconstruct their working state from the store on `init`. +The local store is the only durable memory a module has - WASM linear memory is wiped on every restart. Modules must be written to reconstruct their working state from the store on `init`. ## redb Fundamentals | Property | Detail | |----------|--------| | Engine | Copy-on-write B-tree | -| Concurrency | MVCC — concurrent readers, single writer, no blocking | +| Concurrency | MVCC - concurrent readers, single writer, no blocking | | Durability | Crash-safe by default (fsync on commit) | -| Transactions | Full ACID — read txns and write txns | +| Transactions | Full ACID - read txns and write txns | | Key types | `&str`, `&[u8]`, integers, tuples, `Option`, fixed arrays | | Value types | All key types + `Vec`, `f32`/`f64`, `()` | | Size | No hard limit; v3 file format starts at ~50 KiB | ## Isolation Model -Each module gets its own **redb database file**. Modules cannot read or write each other's state — enforced by filesystem-level separation. +Each module gets its own **redb database file**. Modules cannot read or write each other's state - enforced by filesystem-level separation. ```rust -// Runtime side — one database per module +// Runtime side - one database per module fn open_module_db(module_id: &str) -> Result { let path = format!("/var/nexum/state/{module_id}.redb"); Database::create(&path) @@ -33,7 +33,7 @@ fn open_module_db(module_id: &str) -> Result { const LOCAL_STORE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("state"); ``` -Module identity = `name` from `nexum.toml`. If two module instances share a name, they share state (intentional — enables hot-reload with state continuity). Different modules have different names and fully isolated database files. +Module identity = `name` from `nexum.toml`. If two module instances share a name, they share state (intentional - enables hot-reload with state continuity). Different modules have different names and fully isolated database files. ``` /var/nexum/state/ @@ -56,7 +56,7 @@ interface local-store { /// Set a key-value pair. Overwrites existing value. /// Returns host-error { domain: "store", kind: invalid-input | internal | ... } on failure. /// Quota exhaustion surfaces as host-error { domain: "store", kind: invalid-input } - /// (or a future dedicated `quota-exceeded` kind) — see the migration guide. + /// (or a future dedicated `quota-exceeded` kind) - see the migration guide. set: func(key: string, value: list) -> result<_, host-error>; /// Delete a key. No-op if key doesn't exist. @@ -69,7 +69,7 @@ interface local-store { In 0.1 `local-store` errors were bare `string` values. 0.2 replaces them with the unified `host-error` type (see [migration guide §2](migration/0.1-to-0.2.md#2-error-model-unification-both)) so modules can match on `host-error-kind` rather than parsing error strings. -Keys are UTF-8 strings. Values are opaque bytes — the SDK provides typed wrappers (see doc 05). +Keys are UTF-8 strings. Values are opaque bytes - the SDK provides typed wrappers (see doc 05). `list-keys` enables prefix-based namespacing within a module's state: @@ -107,7 +107,7 @@ flowchart TD B --> C["No state changes persisted -- atomically rolled back"] ``` -This gives us **all-or-nothing semantics per call**: either all state mutations from a single `init` or `on_event` callback are applied, or none are. This is critical for correctness — a module that crashes halfway through processing a block doesn't leave behind partial state. Equally, a failed `init` during restart doesn't corrupt state from the previous version. +This gives us **all-or-nothing semantics per call**: either all state mutations from a single `init` or `on_event` callback are applied, or none are. This is critical for correctness - a module that crashes halfway through processing a block doesn't leave behind partial state. Equally, a failed `init` during restart doesn't corrupt state from the previous version. ### Read-your-own-writes @@ -123,7 +123,7 @@ This works because all operations within one event go through the same `WriteTra ### Concurrency: One Database Per Module -redb allows only **one `WriteTransaction` at a time** per `Database` — a second `begin_write()` blocks until the first commits or aborts. Since modules dispatch events concurrently (doc 02), a single shared redb file would serialise all write transactions across modules, negating concurrency. +redb allows only **one `WriteTransaction` at a time** per `Database` - a second `begin_write()` blocks until the first commits or aborts. Since modules dispatch events concurrently (doc 02), a single shared redb file would serialise all write transactions across modules, negating concurrency. **Design decision:** each module gets its own redb `Database` file: @@ -134,7 +134,7 @@ redb allows only **one `WriteTransaction` at a time** per `Database` — a secon └── price-alert.redb ``` -This gives true write isolation — module A's transaction never blocks module B. The cost is more file handles (one per module), which is negligible for the expected module count. +This gives true write isolation - module A's transaction never blocks module B. The cost is more file handles (one per module), which is negligible for the expected module count. Within a single module, events are already sequential (doc 02 dispatch semantics), so there is never contention on a module's own database. @@ -178,7 +178,7 @@ On first load, the module's table is empty. The module's `init` function should ```rust fn init(config: Config) -> Result<(), HostError> { if local_store::get("initialized")?.is_none() { - // First run — set up initial state + // First run - set up initial state local_store::set("initialized", &[1])?; local_store::set("last_block", &0u64.to_le_bytes())?; } @@ -224,8 +224,8 @@ fn init(config: Config) -> Result<(), HostError> { ### Module Removal When an operator removes a module, its state table can optionally be: -- **Retained** (default) — in case the module is re-added later. -- **Purged** — operator explicitly requests deletion via CLI. +- **Retained** (default) - in case the module is re-added later. +- **Purged** - operator explicitly requests deletion via CLI. ```bash nexum state purge --module twap-monitor @@ -294,9 +294,9 @@ impl ModuleStateCtx { | Key type | UTF-8 string | | Value type | Opaque bytes (`list` in WIT) | | Namespacing within module | Convention: slash-separated prefixes + `list-keys` | -| Transaction scope | Per `init` / `on_event` call — commit on success, rollback on failure | +| Transaction scope | Per `init` / `on_event` call - commit on success, rollback on failure | | Read-your-own-writes | Yes (same `WriteTransaction`) | | Size limit | Enforced per-module via manifest `max_state_bytes` | -| Survives restart | Yes — state is external to WASM instance | +| Survives restart | Yes - state is external to WASM instance | | Module update | New version inherits state; `init` handles migration | | Backup | Online copy under read transaction | diff --git a/docs/07-rpc-namespace-design.md b/docs/07-rpc-namespace-design.md index 8f443a3..ba31db8 100755 --- a/docs/07-rpc-namespace-design.md +++ b/docs/07-rpc-namespace-design.md @@ -22,19 +22,19 @@ This creates several problems: 1. **Boilerplate multiplication.** Every new `eth_` method requires changes in three places: WIT definition, host trait implementation, and SDK wrapper. The Ethereum JSON-RPC namespace has 30+ methods; most modules will need more than the three currently exposed. -2. **Alloy incompatibility.** Module authors using Rust cannot use alloy's `Provider` API — which provides 80+ typed convenience methods — because the transport layer is locked behind per-method WIT functions. They're forced to manually ABI-encode calldata, call `blockchain::eth_call`, and ABI-decode the result for every interaction. +2. **Alloy incompatibility.** Module authors using Rust cannot use alloy's `Provider` API - which provides 80+ typed convenience methods - because the transport layer is locked behind per-method WIT functions. They're forced to manually ABI-encode calldata, call `blockchain::eth_call`, and ABI-decode the result for every interaction. 3. **Namespace rigidity.** Adding a `cow_` namespace for CoW Protocol API calls would duplicate the same per-method pattern. Future namespaces (debug_, trace_, etc.) compound this further. -The goal: **one WIT function to rule the entire `eth_` namespace**, with a guest-side SDK that gives module authors the full alloy `Provider` API — no manual ABI wrangling, no WIT changes when new methods are needed. +The goal: **one WIT function to rule the entire `eth_` namespace**, with a guest-side SDK that gives module authors the full alloy `Provider` API - no manual ABI wrangling, no WIT changes when new methods are needed. ## Design: Generic JSON-RPC Passthrough ### Core Insight -alloy's `Transport` trait is a Tower `Service`. If we expose a single JSON-RPC dispatch function in WIT, the SDK can implement `Transport` on top of it. This gives guest modules the entire alloy `Provider` API for free — every current and future `eth_` method works automatically. +alloy's `Transport` trait is a Tower `Service`. If we expose a single JSON-RPC dispatch function in WIT, the SDK can implement `Transport` on top of it. This gives guest modules the entire alloy `Provider` API for free - every current and future `eth_` method works automatically. -From the guest's perspective, host function calls are synchronous (they block until the host returns). The returned future resolves in a single poll. This means alloy's async `Provider` methods work with a trivial executor — no real async machinery needed. +From the guest's perspective, host function calls are synchronous (they block until the host returns). The returned future resolves in a single poll. This means alloy's async `Provider` methods work with a trivial executor - no real async machinery needed. ### Architecture @@ -89,11 +89,11 @@ interface chain { } ``` -Errors are reported via the unified `host-error` (see doc 00 and the [migration guide §2](migration/0.1-to-0.2.md#2-error-model-unification-both)) — the 0.1 `json-rpc-error` shape is gone. Modules match on `host-error-kind` (`unavailable`, `rate-limited`, `timeout`, `denied`, `invalid-input`, ...) for retry/backoff decisions rather than parsing numeric JSON-RPC codes. +Errors are reported via the unified `host-error` (see doc 00 and the [migration guide §2](migration/0.1-to-0.2.md#2-error-model-unification-both)) - the 0.1 `json-rpc-error` shape is gone. Modules match on `host-error-kind` (`unavailable`, `rate-limited`, `timeout`, `denied`, `invalid-input`, ...) for retry/backoff decisions rather than parsing numeric JSON-RPC codes. The `types` interface is unchanged in shape (it now exposes `host-error` / `host-error-kind`). The `local-store`, `remote-store`, `messaging`, and `logging` interfaces are unchanged. -The `identity` interface provides cryptographic identity — key management and signing: +The `identity` interface provides cryptographic identity - key management and signing: ```wit interface identity { @@ -111,7 +111,7 @@ interface identity { } ``` -The universal `event-module` world (in `nexum:host`) contains the platform-agnostic interfaces — six imports in 0.2: +The universal `event-module` world (in `nexum:host`) contains the platform-agnostic interfaces - six imports in 0.2: ```wit world event-module { @@ -143,21 +143,21 @@ world shepherd { | `blockchain::eth-call(chain-id, to, data)` | `chain::request(chain-id, "eth_call", params_json)` | | `blockchain::eth-get-logs(filter)` | `chain::request(chain-id, "eth_getLogs", params_json)` | | `blockchain::eth-block-number(chain-id)` | `chain::request(chain-id, "eth_blockNumber", "[]")` | -| *n/a — not exposed* | `chain::request(chain-id, "eth_getBalance", params_json)` | -| *n/a — not exposed* | `chain::request(chain-id, "eth_getCode", params_json)` | -| *n/a — not exposed* | `chain::request(chain-id, "eth_getStorageAt", params_json)` | -| *n/a — not exposed* | Any `eth_*` method — no WIT change needed | +| *n/a - not exposed* | `chain::request(chain-id, "eth_getBalance", params_json)` | +| *n/a - not exposed* | `chain::request(chain-id, "eth_getCode", params_json)` | +| *n/a - not exposed* | `chain::request(chain-id, "eth_getStorageAt", params_json)` | +| *n/a - not exposed* | Any `eth_*` method - no WIT change needed | ### Why JSON Strings (Not `list`) -- The Ethereum JSON-RPC spec is JSON. alloy serialises params to JSON internally. Using `string` means zero intermediate format — the guest produces JSON, the host forwards JSON to alloy's `raw_request_dyn` which accepts `&RawValue` (a JSON string). +- The Ethereum JSON-RPC spec is JSON. alloy serialises params to JSON internally. Using `string` means zero intermediate format - the guest produces JSON, the host forwards JSON to alloy's `raw_request_dyn` which accepts `&RawValue` (a JSON string). - Debuggability: JSON is human-readable in logs and traces. - The canonical ABI cost of copying a JSON string across the component boundary is negligible relative to the network RTT of an actual RPC call. - Binary encoding (CBOR, postcard) would require custom (de)serialisation on both sides, defeating the purpose of minimising boilerplate. ## Host Implementation -The host implementation is minimal — one function handles the entire `eth_` namespace: +The host implementation is minimal - one function handles the entire `eth_` namespace: ```rust use serde_json::value::RawValue; @@ -251,7 +251,7 @@ This could be made configurable per-module via `nexum.toml`: ```toml [module.chain] # Additional methods beyond the default read-only set. -# Use with caution — write methods can have side-effects. +# Use with caution - write methods can have side-effects. extra_allowed_methods = ["eth_createAccessList"] ``` @@ -526,7 +526,7 @@ use tower::Service; use std::task::{Context, Poll}; /// An alloy-compatible transport that routes JSON-RPC requests through the -/// Nexum host engine. Synchronous from the guest's perspective — the host +/// Nexum host engine. Synchronous from the guest's perspective - the host /// function blocks until the RPC response is available. #[derive(Debug, Clone)] pub struct HostTransport { @@ -545,7 +545,7 @@ impl Service for HostTransport { type Future = TransportFut<'static>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - // Always ready — host function calls are synchronous from the guest. + // Always ready - host function calls are synchronous from the guest. Poll::Ready(Ok(())) } @@ -594,7 +594,7 @@ fn dispatch_single( let params_json = req.params().map(|p| p.get()).unwrap_or("[]"); // This calls the WIT-imported host function. Synchronous from the guest's - // perspective — the host executes the RPC call asynchronously and returns + // perspective - the host executes the RPC call asynchronously and returns // the result when ready. match chain::request(chain_id, method, params_json) { Ok(result_json) => { @@ -626,13 +626,13 @@ fn dispatch_single( ### Why This Works Without Real Async -The `call()` method returns a `Box::pin(async move { ... })` — but the body is entirely synchronous. The `chain::request` host function blocks from the guest's perspective (the host runs the actual RPC call asynchronously via wasmtime's `func_wrap_async`, but the guest sees a normal function call that returns a value). The future resolves in a single poll. +The `call()` method returns a `Box::pin(async move { ... })` - but the body is entirely synchronous. The `chain::request` host function blocks from the guest's perspective (the host runs the actual RPC call asynchronously via wasmtime's `func_wrap_async`, but the guest sees a normal function call that returns a value). The future resolves in a single poll. -This means alloy's `Provider` methods — which `await` the transport internally — complete immediately when driven by any executor. The SDK provides a minimal single-threaded executor: +This means alloy's `Provider` methods - which `await` the transport internally - complete immediately when driven by any executor. The SDK provides a minimal single-threaded executor: ```rust /// Drive a future to completion. Since the HostTransport resolves -/// synchronously, this is a single-poll operation — no actual async +/// synchronously, this is a single-poll operation - no actual async /// scheduling occurs. pub fn block_on(future: F) -> F::Output { futures_executor::block_on(future) @@ -649,8 +649,8 @@ use alloy_rpc_client::RpcClient; /// Create an alloy `Provider` backed by the Nexum host engine. /// -/// The returned provider supports the full alloy `Provider` API — all `eth_*` -/// methods, builder patterns, typed responses — routing every request through +/// The returned provider supports the full alloy `Provider` API - all `eth_*` +/// methods, builder patterns, typed responses - routing every request through /// the host's RPC stack (timeout, retry, rate-limit, failover). /// /// ```rust @@ -675,15 +675,15 @@ let block_num = block_on(provider.get_block_number())?; // noisy let balance = block_on(provider.get_balance(addr).latest())?; // everywhere ``` -This is verbose and obscures the actual logic. But we can't reimplement every `Provider` method as a synchronous wrapper — that defeats the purpose of the generic passthrough. +This is verbose and obscures the actual logic. But we can't reimplement every `Provider` method as a synchronous wrapper - that defeats the purpose of the generic passthrough. ### The Solution: Named Event Handlers + `async fn` The proc macro (see doc 05) already generates the WIT export boilerplate. We extend it in two ways. For universal modules, the `#[nexum::module]` macro is used; for CoW modules, the `#[shepherd::module]` macro (which extends the universal one with CoW-specific imports): -1. **Named event handlers** — instead of writing the `match event { ... }` dispatch manually, module authors implement `on_block`, `on_logs`, `on_tick`, and/or `on_message`. The macro generates the `on_event` match. -2. **`async fn` support** — handlers can be async. The macro wraps the generated `on_event` in `block_on()`, so `.await` works naturally. -3. **Provider injection** — if a handler accepts `&RootProvider` as a second parameter, the macro creates the provider from the event's chain_id and passes it in. +1. **Named event handlers** - instead of writing the `match event { ... }` dispatch manually, module authors implement `on_block`, `on_logs`, `on_tick`, and/or `on_message`. The macro generates the `on_event` match. +2. **`async fn` support** - handlers can be async. The macro wraps the generated `on_event` in `block_on()`, so `.await` works naturally. +3. **Provider injection** - if a handler accepts `&RootProvider` as a second parameter, the macro creates the provider from the event's chain_id and passes it in. **What the module author writes (universal module):** @@ -748,7 +748,7 @@ impl Guest for MyModule { } ``` -The generated code calls `block_on` exactly once — at the top-level export boundary. Inside the async block, all `.await` calls resolve immediately (the `HostTransport` is synchronous under the hood). No real async scheduler runs. No tokio. No waker machinery. It's syntactic sugar that costs nothing at runtime. +The generated code calls `block_on` exactly once - at the top-level export boundary. Inside the async block, all `.await` calls resolve immediately (the `HostTransport` is synchronous under the hood). No real async scheduler runs. No tokio. No waker machinery. It's syntactic sugar that costs nothing at runtime. ### Named Handler Conventions @@ -765,11 +765,11 @@ The macro inspects each handler's signature: - **Async handlers** -> wrapped in `block_on`; sync handlers called directly - **Missing handlers** -> `Ok(())` for that variant (no-op) -**Escape hatch:** defining `on_event` directly takes precedence — the macro uses it as-is (wrapping in `block_on` if async) and ignores named handlers. +**Escape hatch:** defining `on_event` directly takes precedence - the macro uses it as-is (wrapping in `block_on` if async) and ignores named handlers. ### Why This Works -1. **WIT exports are synchronous.** The Component Model export signature is `func(event) -> result<_, string>` — no async. The macro bridges this by wrapping the generated dispatch in `block_on`. +1. **WIT exports are synchronous.** The Component Model export signature is `func(event) -> result<_, string>` - no async. The macro bridges this by wrapping the generated dispatch in `block_on`. 2. **The transport resolves in one poll.** `HostTransport::call()` returns a future whose body is entirely synchronous (it calls the WIT host function, which blocks). When alloy's `Provider` awaits the transport, the future completes immediately. @@ -779,10 +779,10 @@ The macro inspects each handler's signature: ```rust async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { - // EthCall builder — .latest() and .await both work + // EthCall builder - .latest() and .await both work let result = provider.call(tx).latest().await?; - // Filter builder — standard alloy ergonomics + // Filter builder - standard alloy ergonomics let logs = provider.get_logs(&filter).await?; // Raw request for unlisted methods @@ -832,7 +832,7 @@ impl MyModule { // Manual ABI encode let calldata = balanceOfCall { owner: addr }.abi_encode(); - // Raw host call — returns opaque bytes + // Raw host call - returns opaque bytes let result_bytes = blockchain::eth_call( block.chain_id, &token_addr.to_vec(), @@ -863,9 +863,9 @@ sol! { struct MyModule; impl MyModule { - // Named handler — macro generates the match dispatch + provider injection + // Named handler - macro generates the match dispatch + provider injection async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { - // Full alloy Provider API — natural .await, provider injected + // Full alloy Provider API - natural .await, provider injected let block_num = provider.get_block_number().await?; let eth_balance = provider.get_balance(addr).latest().await?; let code = provider.get_code_at(contract).latest().await?; @@ -904,7 +904,7 @@ Every alloy `Provider` method works. No WIT changes. No host-side per-method cod CoW Protocol's API is REST-based, not JSON-RPC. Two options: -### Option A: Separate REST Interface (Recommended — chosen for 0.2) +### Option A: Separate REST Interface (Recommended - chosen for 0.2) In 0.1 this was two interfaces, `cow` (REST passthrough) and `order` (typed `submit`). 0.2 merges them into a single `cow-api` interface, dropping the `cow::cow::request` triple-stutter: @@ -1023,7 +1023,7 @@ async fn request(&mut self, chain_id: u64, method: String, params: String) } ``` -**Option A is recommended and is what 0.2 ships.** The CoW API is REST, not JSON-RPC — forcing it into JSON-RPC semantics adds a translation layer on both sides. A separate `cow-api` interface keeps the contract explicit and makes it clear in the WIT world what capabilities a module has. It also allows independent evolution — the `chain` interface doesn't need to know about CoW, and vice versa. +**Option A is recommended and is what 0.2 ships.** The CoW API is REST, not JSON-RPC - forcing it into JSON-RPC semantics adds a translation layer on both sides. A separate `cow-api` interface keeps the contract explicit and makes it clear in the WIT world what capabilities a module has. It also allows independent evolution - the `chain` interface doesn't need to know about CoW, and vice versa. ### SDK: `Cow` @@ -1077,7 +1077,7 @@ Usage in a module: async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { let cow = Cow::new(block.chain_id); - // Read chain state via alloy — provider injected by macro + // Read chain state via alloy - provider injected by macro let block_num = provider.get_block_number().await?; // Submit order via CoW API @@ -1086,7 +1086,7 @@ async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { buy_token: weth, sell_amount: U256::from(1_000_000_000), kind: OrderKind::Sell, - // block.timestamp is ms-since-epoch in 0.2 — divide for seconds + // block.timestamp is ms-since-epoch in 0.2 - divide for seconds valid_to: (provider.get_block(block_num.into(), false).await? .unwrap().header.timestamp / 1000) + 300, ..Default::default() @@ -1187,8 +1187,8 @@ use nexum_sdk::testing::MockProvider; #[test] fn test_reads_balance() { - // block_on is still useful in tests — tests are sync by default. - // (Or use #[tokio::test] — MockProvider works with any executor.) + // block_on is still useful in tests - tests are sync by default. + // (Or use #[tokio::test] - MockProvider works with any executor.) let mut mock = MockProvider::new(42161); // Queue mock responses (FIFO) @@ -1232,7 +1232,7 @@ fn test_submits_order() { | **WIT changes for new methods** | None | New function + types per method | | **Host implementation** | ~20 lines total | Per-method impl + dispatch | | **Guest API** | Full alloy Provider (80+ methods) | Only what WIT exposes | -| **alloy compatibility** | Native — IS an alloy transport | Manual ABI encode/decode | +| **alloy compatibility** | Native - IS an alloy transport | Manual ABI encode/decode | | **Type safety at WIT boundary** | Runtime (JSON strings) | Compile-time (WIT types) | | **Method allowlisting** | Runtime string match | Implicit (only exposed methods exist) | | **Debugging** | JSON in/out visible in traces | Structured WIT types in traces | @@ -1240,7 +1240,7 @@ fn test_submits_order() { The primary trade-off is **type safety at the WIT boundary**: JSON strings vs. structured WIT types. This is mitigated by: -1. **Rust guests** use alloy's type system — serialisation errors surface as alloy `TransportError` with clear messages. +1. **Rust guests** use alloy's type system - serialisation errors surface as alloy `TransportError` with clear messages. 2. **Non-Rust guests** (JS, Python, Go) typically work with JSON natively, so JSON strings are actually *more* natural than WIT record types. 3. **Tracing**: the host can log method + params as structured JSON before forwarding, providing equal or better debuggability. @@ -1255,8 +1255,8 @@ For modules and embedders moving from 0.1 to 0.2, follow the [Migration Guide](m | Component | What 0.2 ships | |---|---| | **WIT** | `chain` interface with `request` + additive `request-batch`. `identity` (accounts, sign, sign-typed-data). Merged `cow-api` in `shepherd:cow`. `event-module` imports 6 interfaces: chain, identity, local-store, remote-store, messaging, logging. Plus additive `clock` / `random` / `http` capabilities and the experimental `query-module` world. | -| **Host** | `ChainHost` — one `chain::request` impl that forwards read-only methods to `provider.raw_request_dyn` and delegates signing methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) to the `Identity` backend. Plus `chain::request-batch` that actually pipelines. One `identity::Host` impl delegating to the same backend. One `cow-api::request` + `submit-order` impl forwarding to HTTP client. All host functions return `host-error`. | +| **Host** | `ChainHost` - one `chain::request` impl that forwards read-only methods to `provider.raw_request_dyn` and delegates signing methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) to the `Identity` backend. Plus `chain::request-batch` that actually pipelines. One `identity::Host` impl delegating to the same backend. One `cow-api::request` + `submit-order` impl forwarding to HTTP client. All host functions return `host-error`. | | **SDK** | `nexum-sdk`: `HostTransport` (alloy `Transport` impl, batches via `chain::request-batch`), `provider()` constructor, `Signer` (typed identity wrapper), `HostError` / `HostErrorKind`. `shepherd-sdk`: `Cow` (extends `nexum-sdk`). `block_on` is internal. | | **`#[nexum::module]` / `#[shepherd::module]` macros** | Named event handlers (`on_block`, `on_logs`, `on_tick`, `on_message`) with generated match dispatch. `async fn` support. Optional `&RootProvider` injection. `#[nexum::module]` for universal modules; `#[shepherd::module]` for CoW modules. | | **Module author experience** | Full alloy `Provider` API via injected provider. Signing via `Signer` or transparently through `chain::request` signing methods. Full CoW API via `Cow`. No match boilerplate. No `block_on`. No manual ABI wrangling for RPC calls. Match on `HostErrorKind` for retry/backoff. | -| **Existing ABI helpers** | Unchanged — `sol!` macro and `alloy-sol-types` still used for contract calldata encoding/decoding. | +| **Existing ABI helpers** | Unchanged - `sol!` macro and `alloy-sol-types` still used for contract calldata encoding/decoding. | diff --git a/docs/08-platform-generalisation.md b/docs/08-platform-generalisation.md index afc50ab..7326873 100755 --- a/docs/08-platform-generalisation.md +++ b/docs/08-platform-generalisation.md @@ -1,17 +1,17 @@ # Platform Generalisation -> **Status (0.2):** Nexum is **designed** to be portable to mobile and browser hosts; the 0.2 **reference runtime is server-only**. The mobile, WebView, and super-app targets in this document describe architectural direction, not shipping artifacts. They remain in the docs because they're load-bearing design — the WIT contract is shaped by the requirement that all four can implement it — but they are **planned** work, conditional on a named design partner for 0.3. See the per-target rows below for current status. +> **Status (0.2):** Nexum is **designed** to be portable to mobile and browser hosts; the 0.2 **reference runtime is server-only**. The mobile, WebView, and super-app targets in this document describe architectural direction, not shipping artifacts. They remain in the docs because they're load-bearing design - the WIT contract is shaped by the requirement that all four can implement it - but they are **planned** work, conditional on a named design partner for 0.3. See the per-target rows below for current status. ## Motivation -The Nexum runtime (docs 01-07) is designed as a server-side Rust binary embedding wasmtime. But the core abstractions — WIT-defined host interfaces, content-addressed module distribution, declarative manifests — are not inherently server-specific. The same module binary, the same packaging, and the same distribution mechanism are intended to serve multiple platform targets: +The Nexum runtime (docs 01-07) is designed as a server-side Rust binary embedding wasmtime. But the core abstractions - WIT-defined host interfaces, content-addressed module distribution, declarative manifests - are not inherently server-specific. The same module binary, the same packaging, and the same distribution mechanism are intended to serve multiple platform targets: -1. **Server runtime** *(shipping in 0.2)* — the current design (Rust/Tokio/wasmtime). Headless automation: blockchain event monitoring, order submission, background computation. -2. **Mobile app (Flutter/Dart)** *(planned — see roadmap)* — a WASM runtime embedded in a native mobile application via FFI. Modules run on-device, backed by local state (SQLite) and RPC over HTTP. -3. **WebView** *(planned — see roadmap)* — a browser engine (V8/JSC/SpiderMonkey) executing WASM natively, with host functions injected from the native layer via a JavaScript bridge. Enables rich web-based UIs with blockchain-native capabilities. -4. **Decentralised super app** *(planned — see roadmap)* — a shell application (mobile or desktop) that dynamically loads modules discovered via ENS and fetched from Swarm. Some modules are headless (automation); others are interactive (UI). All are sandboxed, all are distributed without a central app store. +1. **Server runtime** *(shipping in 0.2)* - the current design (Rust/Tokio/wasmtime). Headless automation: blockchain event monitoring, order submission, background computation. +2. **Mobile app (Flutter/Dart)** *(planned - see roadmap)* - a WASM runtime embedded in a native mobile application via FFI. Modules run on-device, backed by local state (SQLite) and RPC over HTTP. +3. **WebView** *(planned - see roadmap)* - a browser engine (V8/JSC/SpiderMonkey) executing WASM natively, with host functions injected from the native layer via a JavaScript bridge. Enables rich web-based UIs with blockchain-native capabilities. +4. **Decentralised super app** *(planned - see roadmap)* - a shell application (mobile or desktop) that dynamically loads modules discovered via ENS and fetched from Swarm. Some modules are headless (automation); others are interactive (UI). All are sandboxed, all are distributed without a central app store. -The key insight: **the WIT contract is the universal interface**. Any host that implements the required interfaces can run the same module binary. The differences between platforms are in *how* the host implements those interfaces — not in what the module sees. +The key insight: **the WIT contract is the universal interface**. Any host that implements the required interfaces can run the same module binary. The differences between platforms are in *how* the host implements those interfaces - not in what the module sees. This document defines the layered architecture that enables this generalisation and specifies the universal interface set. The 0.2 server runtime is the first host implementation; the experimental `nexum:host/query-module` WIT world (published but unhosted in 0.2) exists to give mobile/wallet embedders a stable target to implement against before 0.3. @@ -22,7 +22,7 @@ Before diving into WIT definitions, the universal runtime is built on six primit | Primitive | Interface | Backed by | Purpose | |-----------|-----------|-----------|---------| | **Chain** | `chain` | JSON-RPC (eth_*) | Read/write blockchain consensus state | -| **Identity** | `identity` | Keystore / KMS / device keychain / wallet extension | Cryptographic identity — key management and signing | +| **Identity** | `identity` | Keystore / KMS / device keychain / wallet extension | Cryptographic identity - key management and signing | | **Local Store** | `local-store` | redb / SQLite / IndexedDB | Per-module private persistence on the device | | **Remote Store** | `remote-store` | Ethereum Swarm | Decentralised content-addressed storage | | **Messaging** | `messaging` | Waku | Decentralised pub/sub messaging | @@ -30,16 +30,16 @@ Before diving into WIT definitions, the universal runtime is built on six primit These six primitives are orthogonal: -- **Chain** is the source of truth — the blockchain consensus state. Modules read chain state and (indirectly) write to it via order submission or transactions. -- **Identity** is cryptographic agency — key management and signing. Modules can enumerate available accounts and request signatures (ECDSA secp256k1 by default, extensible). The `chain` host implementation depends on `identity` internally — signing RPC methods (e.g. `eth_sendTransaction`) delegate to `identity` for the actual signature. -- **Local Store** is the module's private scratchpad — fast, local, scoped to one module on one device. Does not replicate. -- **Remote Store** is shared persistent content — content-addressed, decentralised, survives independent of any device. Any module on any device can read what another module wrote. -- **Messaging** is real-time communication — ephemeral pub/sub messages between modules, devices, or users. Unlike remote store (persistent, content-addressed), messaging is transient and topic-based. -- **Logging** is diagnostics — one-way output for debugging and monitoring. Not a data channel. +- **Chain** is the source of truth - the blockchain consensus state. Modules read chain state and (indirectly) write to it via order submission or transactions. +- **Identity** is cryptographic agency - key management and signing. Modules can enumerate available accounts and request signatures (ECDSA secp256k1 by default, extensible). The `chain` host implementation depends on `identity` internally - signing RPC methods (e.g. `eth_sendTransaction`) delegate to `identity` for the actual signature. +- **Local Store** is the module's private scratchpad - fast, local, scoped to one module on one device. Does not replicate. +- **Remote Store** is shared persistent content - content-addressed, decentralised, survives independent of any device. Any module on any device can read what another module wrote. +- **Messaging** is real-time communication - ephemeral pub/sub messages between modules, devices, or users. Unlike remote store (persistent, content-addressed), messaging is transient and topic-based. +- **Logging** is diagnostics - one-way output for debugging and monitoring. Not a data channel. Together they cover the full spectrum: persistent truth (chain), cryptographic agency (identity), local scratch (local-store), shared content (remote-store), real-time coordination (messaging), and diagnostics (logging). -The 0.2 `event-module` world imports all six. (In 0.1 the WIT inadvertently omitted `identity` from the world definition despite the docs claiming six primitives; 0.2 makes the contract match the taxonomy.) Three additional **additive** capabilities — `clock`, `random`, and `http` (allowlisted) — are available via the manifest's `[capabilities]` section but are not part of the six-primitive core. +The 0.2 `event-module` world imports all six. (In 0.1 the WIT inadvertently omitted `identity` from the world definition despite the docs claiming six primitives; 0.2 makes the contract match the taxonomy.) Three additional **additive** capabilities - `clock`, `random`, and `http` (allowlisted) - are available via the manifest's `[capabilities]` section but are not part of the six-primitive core. ## Architectural Principle: Layered WIT Worlds @@ -48,22 +48,22 @@ The current `shepherd` world conflates universal blockchain runtime capabilities ```mermaid graph TD subgraph L3["Layer 3: Application-Specific Worlds"] - COW["shepherd:cow — cow + order (CoW Protocol automation)"] - DEFI["myapp:defi — vault + strategy (DeFi yield app)"] - GAME["game:engine — physics + assets (on-chain game)"] + COW["shepherd:cow - cow + order (CoW Protocol automation)"] + DEFI["myapp:defi - vault + strategy (DeFi yield app)"] + GAME["game:engine - physics + assets (on-chain game)"] end subgraph L2["Layer 2: Capability Extensions (optional, composable)"] - UI["ui — user interface bridge (interactive modules)"] + UI["ui - user interface bridge (interactive modules)"] end subgraph L1["Layer 1: Universal Runtime Interfaces"] - CSN["chain — consensus access (JSON-RPC passthrough)"] - ID["identity — cryptographic identity (key management, signing)"] - LS["local-store — local key-value persistence"] - RS["remote-store — decentralised content-addressed storage"] - MSG["messaging — decentralised pub/sub messaging"] - LOG["logging — structured logging"] + CSN["chain - consensus access (JSON-RPC passthrough)"] + ID["identity - cryptographic identity (key management, signing)"] + LS["local-store - local key-value persistence"] + RS["remote-store - decentralised content-addressed storage"] + MSG["messaging - decentralised pub/sub messaging"] + LOG["logging - structured logging"] EXP["Exports: init(config) + on-event(event)"] end @@ -75,11 +75,11 @@ Each layer builds on the one below via WIT `include`. A module compiled against ## Layer 1: Universal Interfaces -These six interfaces form the universal runtime contract. Any platform — server, mobile, WebView, desktop — can implement them. +These six interfaces form the universal runtime contract. Any platform - server, mobile, WebView, desktop - can implement them. -### `chain` — Consensus Access +### `chain` - Consensus Access -The module's window into blockchain consensus. A single generic function that forwards JSON-RPC requests to the host's provider infrastructure, plus an additive batched variant. The host decides *how* to reach the chain — the module only specifies *what* to ask. +The module's window into blockchain consensus. A single generic function that forwards JSON-RPC requests to the host's provider infrastructure, plus an additive batched variant. The host decides *how* to reach the chain - the module only specifies *what* to ask. ```wit interface chain { @@ -112,13 +112,13 @@ interface chain { | WebView | JavaScript bridge -> `window.ethereum` (injected wallet) or native HTTP via message channel | | Super app | Same as mobile, with per-module chain permissions | -The Rust SDK's `HostTransport` (doc 07) works identically on all platforms — it implements alloy's `Transport` trait over `chain::request`, so module authors get the full alloy `Provider` API regardless of where the module runs. +The Rust SDK's `HostTransport` (doc 07) works identically on all platforms - it implements alloy's `Transport` trait over `chain::request`, so module authors get the full alloy `Provider` API regardless of where the module runs. -### `identity` — Cryptographic Identity +### `identity` - Cryptographic Identity Provides key management and signing capabilities to modules. ECDSA secp256k1 by default (the Ethereum standard), extensible to other schemes. Modules can enumerate available accounts and request signatures over arbitrary data. -The `chain` host implementation depends on `identity` internally — signing RPC methods such as `eth_sendTransaction` or `eth_signTypedData_v4` delegate to `identity` for the actual cryptographic signature. Modules can also import `identity` directly for raw signing operations outside of JSON-RPC (e.g. signing EIP-712 typed data for off-chain order submission). +The `chain` host implementation depends on `identity` internally - signing RPC methods such as `eth_sendTransaction` or `eth_signTypedData_v4` delegate to `identity` for the actual cryptographic signature. Modules can also import `identity` directly for raw signing operations outside of JSON-RPC (e.g. signing EIP-712 typed data for off-chain order submission). ```wit interface identity { @@ -156,11 +156,11 @@ The `chain` host implementation uses `identity` internally when it encounters si 2. Calls `identity::sign` to produce the signature. 3. Sends the signed transaction via the provider. -This means modules that only need to sign transactions via standard JSON-RPC methods do not need to import `identity` directly — `chain` handles it transparently. Modules that need raw signing (e.g. off-chain message signing for order submission, attestations, or custom protocols) import `identity` explicitly. +This means modules that only need to sign transactions via standard JSON-RPC methods do not need to import `identity` directly - `chain` handles it transparently. Modules that need raw signing (e.g. off-chain message signing for order submission, attestations, or custom protocols) import `identity` explicitly. -### `local-store` — Local Key-Value Persistence +### `local-store` - Local Key-Value Persistence -The module's private scratchpad. **Local to the device/process** — does not replicate, sync, or share across instances. Scoped to one module: module A cannot read module B's local state. +The module's private scratchpad. **Local to the device/process** - does not replicate, sync, or share across instances. Scoped to one module: module A cannot read module B's local state. ```wit interface local-store { @@ -190,15 +190,15 @@ interface local-store { | WebView | IndexedDB (per-module object store) or `localStorage` | | Super app | SQLite (shared database, per-module namespace isolation) | -The semantics are deliberately minimal — get, set, delete, prefix scan. This is the LCD (lowest common denominator) that every platform can implement efficiently. Advanced features (transactions, MVCC, crash-safety) are host-specific and not exposed in the WIT. +The semantics are deliberately minimal - get, set, delete, prefix scan. This is the LCD (lowest common denominator) that every platform can implement efficiently. Advanced features (transactions, MVCC, crash-safety) are host-specific and not exposed in the WIT. The server runtime's all-or-nothing transactional semantics (doc 04) remain an implementation detail of the Nexum host, not a guarantee modules can rely on across platforms. Modules that need stronger guarantees should design for idempotency. -### `remote-store` — Decentralised Content-Addressed Storage +### `remote-store` - Decentralised Content-Addressed Storage -Backed by Ethereum Swarm. Provides decentralised persistence beyond the local device — content-addressed, censorship-resistant, and accessible from any host on any device. +Backed by Ethereum Swarm. Provides decentralised persistence beyond the local device - content-addressed, censorship-resistant, and accessible from any host on any device. -Swarm is both the distribution mechanism (modules are fetched from Swarm) and a runtime capability. This interface closes the loop — modules can publish to the same network they were distributed through. +Swarm is both the distribution mechanism (modules are fetched from Swarm) and a runtime capability. This interface closes the loop - modules can publish to the same network they were distributed through. ```wit interface remote-store { @@ -208,7 +208,7 @@ interface remote-store { /// Returns the 32-byte content reference (Swarm address). /// /// The host routes to its configured Bee node. Postage batch - /// management is the host's responsibility — the module only + /// management is the host's responsibility - the module only /// provides data and gets back a reference. upload: func(data: list) -> result, host-error>; @@ -235,7 +235,7 @@ interface remote-store { /// /// The host signs the feed update with its configured identity /// (Bee node's Ethereum key). Only the host's own feeds can be - /// updated — the owner is implicit (the host's address). + /// updated - the owner is implicit (the host's address). /// /// `topic`: 32-byte topic hash. /// `data`: the payload to publish. @@ -260,13 +260,13 @@ interface remote-store { **Why remote-store as a universal interface:** - **Decentralised persistence.** `local-store` is device-local. `remote-store` gives modules access to content-addressed storage that persists independent of any single device. -- **Content distribution.** Modules can publish data (feeds, references) that other modules or users can consume — without a central server. -- **Cross-device coordination.** Two instances of the same module on different devices can share data via feed topics — one writes via `write-feed`, the other reads via `read-feed`. +- **Content distribution.** Modules can publish data (feeds, references) that other modules or users can consume - without a central server. +- **Cross-device coordination.** Two instances of the same module on different devices can share data via feed topics - one writes via `write-feed`, the other reads via `read-feed`. - **Consistency with distribution model.** Modules are already fetched from Swarm (doc 02, 03). Exposing `remote-store` at runtime means modules participate in the same content-addressed network they were distributed through. -### `messaging` — Decentralised Messaging +### `messaging` - Decentralised Messaging -Backed by Waku. Provides real-time, privacy-preserving pub/sub messaging between modules, devices, and users. Unlike `remote-store` (persistent, content-addressed), `messaging` is transient and topic-based — fire-and-forget messages on content topics. +Backed by Waku. Provides real-time, privacy-preserving pub/sub messaging between modules, devices, and users. Unlike `remote-store` (persistent, content-addressed), `messaging` is transient and topic-based - fire-and-forget messages on content topics. ```wit interface messaging { @@ -345,11 +345,11 @@ This follows the same pattern as all other event sources: sending uses the impor - **Module-to-module communication.** Two modules on different devices can exchange real-time messages via shared content topics. The TWAP monitor on a server can notify a mobile dashboard module that a new part was posted. - **User notifications.** A headless server module can publish an alert to a content topic; the user's mobile app module subscribes and displays a notification. -- **Decentralised coordination.** Multiple instances of the same module (e.g. running on different operator nodes) can coordinate via messaging — leader election, work distribution, heartbeats. +- **Decentralised coordination.** Multiple instances of the same module (e.g. running on different operator nodes) can coordinate via messaging - leader election, work distribution, heartbeats. - **Privacy.** Waku supports encrypted messaging and ephemeral relay. Modules can communicate without exposing data to the public chain. - **Complementary to remote-store.** `remote-store` is for persistent content (data that should survive). `messaging` is for ephemeral signals (notifications, coordination, real-time feeds). Together they cover the full persistence spectrum. -### `logging` — Structured Logging +### `logging` - Structured Logging Unchanged from the current design: @@ -427,7 +427,7 @@ interface types { // ... chain, identity, local-store, remote-store, messaging, logging interfaces as above ... -/// Event-driven module — automation, background processing. +/// Event-driven module - automation, background processing. /// No UI capabilities. Runs on any conforming host. Six imports in 0.2. world event-module { import chain; @@ -446,7 +446,7 @@ A module compiled against `nexum:host/event-module` is the **maximally portable* ## Layer 2: UI Interface -Interactive modules — those with a user-facing presence in a super app or WebView container — import the `ui` interface in addition to the Layer 1 universals. +Interactive modules - those with a user-facing presence in a super app or WebView container - import the `ui` interface in addition to the Layer 1 universals. ### Design Approach @@ -507,7 +507,7 @@ interface ui { Interactive modules export additional lifecycle hooks beyond `init` and `on-event`: ```wit -/// Interactive module — has a UI presence. +/// Interactive module - has a UI presence. world app-module { include event-module; import ui; @@ -605,7 +605,7 @@ world yield-module { } ``` -The `include` mechanism ensures that any domain-specific module inherits the full universal interface set. A `shepherd` module can call `chain::request`, `identity::sign`, `local-store::get`, `remote-store::upload`, `messaging::publish`, and `logging::log` — plus the CoW-specific `cow-api::request` and `cow-api::submit-order`. +The `include` mechanism ensures that any domain-specific module inherits the full universal interface set. A `shepherd` module can call `chain::request`, `identity::sign`, `local-store::get`, `remote-store::upload`, `messaging::publish`, and `logging::log` - plus the CoW-specific `cow-api::request` and `cow-api::submit-order`. ## Complete WIT Package Layout @@ -625,7 +625,7 @@ wit/ │ ├── ui.wit # ui interface + host-capabilities (planned hosts only) │ ├── event-module.wit # event-module world (6 imports) │ ├── query-module.wit # experimental: query-module world (no host impl in 0.2) -│ └── app-module.wit # app-module world (includes ui) — design only +│ └── app-module.wit # app-module world (includes ui) - design only │ └── shepherd-cow/ ├── cow-api.wit # merged cow-api interface (request + submit-order) @@ -636,23 +636,23 @@ The `nexum-host` package is domain-agnostic and reusable. The `shepherd-cow` pac ## Platform Targets -### Server Runtime (Reference Implementation — Nexum) +### Server Runtime (Reference Implementation - Nexum) This is the current design (docs 01-07), adapted for the layered WIT. Shepherd is the Nexum distribution with CoW Protocol support. | Interface | Implementation | |-----------|---------------| | `chain` | alloy provider with tower middleware (timeout, retry, rate-limit, fallback) | -| `identity` | Keystore file, AWS KMS, or HSM — operator-configured signing backend | +| `identity` | Keystore file, AWS KMS, or HSM - operator-configured signing backend | | `local-store` | redb (per-module database file, ACID, MVCC, crash-safe) | -| `remote-store` | Bee API (`http://localhost:1633`) — operator runs a Bee node | +| `remote-store` | Bee API (`http://localhost:1633`) - operator runs a Bee node | | `messaging` | Waku node (nwaku) via JSON-RPC or REST API | | `logging` | `tracing` crate -> JSON structured logs | | `cow-api` | reqwest HTTP client -> CoW Protocol API (REST passthrough + typed `submit-order`) | | Event sources | `eth_subscribe` (blocks, logs), cron (Tokio interval), Waku relay (messages) | | WASM engine | wasmtime 45.x (Component Model, fuel, epoch metering) | -### Mobile App (Flutter/Dart) — Planned +### Mobile App (Flutter/Dart) - Planned > **Status:** No mobile host ships in 0.2. The design below is the target architecture for a future release (0.3+, conditional on a named design partner). It's retained because the WIT contract was shaped to make this implementation possible, and the `query-module` world in 0.2 is the experimental contract a mobile/wallet embedder would target. @@ -692,7 +692,7 @@ flowchart TD |--------|----------------|----------------|-------| | wasmtime (C API) | Full | aarch64 (iOS/Android ARM64) | Best compatibility, largest binary size (~15 MB) | | wasmer | Partial | Good (wasmer_dart exists) | Component Model support is partial | -| wasm3 | None | Excellent (tiny C library, ~100 KB) | Interpreter only, no Component Model — requires core module + shim | +| wasm3 | None | Excellent (tiny C library, ~100 KB) | Interpreter only, no Component Model - requires core module + shim | For full Component Model support (identical module binaries across server and mobile), **wasmtime via C API** is the recommended path. Dart's FFI (`dart:ffi`) can call the wasmtime C API directly. The binary size cost (~15 MB) is acceptable for a mobile app. @@ -703,7 +703,7 @@ For full Component Model support (identical module binaries across server and mo - **Connectivity.** Mobile networks are intermittent. Host functions should handle offline gracefully (queue requests, retry on reconnect). - **Waku light client.** Mobile devices should use Waku's light push and filter protocols rather than full relay to minimise bandwidth and battery consumption. -### WebView (Browser Engine + Injected Host Functions) — Planned +### WebView (Browser Engine + Injected Host Functions) - Planned > **Status:** No WebView host ships in 0.2. The architecture below describes a future target. The `jco`-based transpilation path is the strongest candidate, but it depends on Component Model browser support stabilising and on a concrete embedder design partner. @@ -743,7 +743,7 @@ Browsers don't natively support the WASM Component Model (as of early 2026). Two 2. **Core module variant.** Compile the module as a core WASM module (not a component) with a JS shim layer that maps the WIT interface to JavaScript imports. This requires a separate build target but avoids the `jco` dependency. -Approach 1 is preferred — it preserves the single-artifact property (one `.wasm` component, multiple platforms). +Approach 1 is preferred - it preserves the single-artifact property (one `.wasm` component, multiple platforms). **WebView-specific capability: `window.ethereum`** @@ -764,22 +764,22 @@ chain: { } ``` -This is powerful: the same module that runs headless on a server (reading chain state via a configured RPC endpoint) can run in a WebView and read chain state via the user's wallet — gaining access to the user's connected accounts and signing capabilities. +This is powerful: the same module that runs headless on a server (reading chain state via a configured RPC endpoint) can run in a WebView and read chain state via the user's wallet - gaining access to the user's connected accounts and signing capabilities. Similarly, the `identity` interface in a WebView context can delegate to `window.ethereum` for account enumeration and signing, providing a seamless bridge between the module's signing needs and the user's wallet extension. **WebView-specific capability: `js-waku`** -For messaging in the browser, `js-waku` provides a pure JavaScript Waku client. The `messaging` host function can route through `js-waku` directly in the WebView without needing the native bridge — peer-to-peer messaging from the browser. +For messaging in the browser, `js-waku` provides a pure JavaScript Waku client. The `messaging` host function can route through `js-waku` directly in the WebView without needing the native bridge - peer-to-peer messaging from the browser. -### Decentralised Super App — Planned +### Decentralised Super App - Planned > **Status:** The super app is the convergence of the mobile and WebView targets. No super-app host ships in 0.2. The content below describes the target architecture for a future release once mobile and WebView are live. The super app is the convergence of all targets. A native shell (Flutter) that would: -1. **Discover modules** via ENS (doc 03) — the same discovery mechanism as the server runtime. -2. **Fetch modules** from Swarm/IPFS — the same content-addressed distribution. +1. **Discover modules** via ENS (doc 03) - the same discovery mechanism as the server runtime. +2. **Fetch modules** from Swarm/IPFS - the same content-addressed distribution. 3. **Run event-driven modules** in an embedded WASM runtime (automation, background tasks). 4. **Run interactive modules** in WebViews (UI, dashboards, transaction builders). 5. **Provide the universal interfaces** to all modules (chain, identity, local-store, remote-store, messaging, logging). @@ -846,40 +846,40 @@ The super app adds a capability-grant layer on top of the WIT world. When a modu ``` "TWAP Monitor" requests: - ✓ chain — read blockchain state (chains: 42161) - ✓ identity — sign with your accounts - ✓ local-store — store data on your device - ✓ remote-store — read/write to Swarm network - ✓ messaging — send/receive messages (topics: /nexum/1/twap-*) - ✗ ui — (not requested — event-driven module) - ✓ cow-api — interact with CoW Protocol API and submit orders + ✓ chain - read blockchain state (chains: 42161) + ✓ identity - sign with your accounts + ✓ local-store - store data on your device + ✓ remote-store - read/write to Swarm network + ✓ messaging - send/receive messages (topics: /nexum/1/twap-*) + ✗ ui - (not requested - event-driven module) + ✓ cow-api - interact with CoW Protocol API and submit orders [Allow] [Deny] ``` -The host only links interfaces the user has approved. A module that doesn't import `messaging` structurally cannot publish messages — the same structural sandboxing property that the server runtime uses (doc 01). +The host only links interfaces the user has approved. A module that doesn't import `messaging` structurally cannot publish messages - the same structural sandboxing property that the server runtime uses (doc 01). ## Host Adapter Specification -Any platform that wants to run modules must implement the **Host Adapter** — the set of host functions backing the WIT interfaces. The specification defines the contract: +Any platform that wants to run modules must implement the **Host Adapter** - the set of host functions backing the WIT interfaces. The specification defines the contract: ### Required Behaviours -In 0.2 every host function returns `result`. The `host-error.kind` discriminant (`unsupported`, `unavailable`, `denied`, `rate-limited`, `timeout`, `invalid-input`, `internal`) is normative — embedders MUST pick the most specific kind for each backend failure. See the [migration guide §2](migration/0.1-to-0.2.md#2-error-model-unification-both) for the embedder-side mapping table. +In 0.2 every host function returns `result`. The `host-error.kind` discriminant (`unsupported`, `unavailable`, `denied`, `rate-limited`, `timeout`, `invalid-input`, `internal`) is normative - embedders MUST pick the most specific kind for each backend failure. See the [migration guide §2](migration/0.1-to-0.2.md#2-error-model-unification-both) for the embedder-side mapping table. **`chain::request` / `chain::request-batch`** (Chain) - MUST forward the JSON-RPC request to a provider for the given chain. - MUST return the JSON-encoded result (the `result` field from the JSON-RPC response). - MUST return `host-error` with `domain = "chain"` for provider errors, method-not-found, and transport failures. Use `kind: invalid-input` for method-not-found, `unavailable`/`timeout` for transport, `rate-limited` for 429s, `denied` for 401/403. - SHOULD enforce a method allowlist (configurable by the operator/user). -- MAY apply middleware (timeout, retry, rate-limit, fallback) — this is platform-specific. +- MAY apply middleware (timeout, retry, rate-limit, fallback) - this is platform-specific. **`identity::accounts/sign/sign-typed-data`** (Identity) - `accounts` MUST return the list of available account identifiers (addresses) for the current host configuration. - `sign` MUST produce a valid cryptographic signature over the provided data using the specified account's private key. - `sign-typed-data` MUST produce a valid EIP-712 signature over the provided typed data structure. - MUST return `host-error` with `domain = "identity"`. User rejection is `kind: denied`; unknown account is `kind: invalid-input`; backend offline is `kind: unavailable`. -- MAY prompt the user for approval before signing (platform-dependent — e.g. wallet extension popup in WebView, biometric prompt on mobile). +- MAY prompt the user for approval before signing (platform-dependent - e.g. wallet extension popup in WebView, biometric prompt on mobile). - SHOULD NOT expose private key material to the module. The module sends data in, gets a signature out. **`local-store::get/set/delete/list-keys`** @@ -970,13 +970,13 @@ graph TD ShepherdSDK -->|"extends"| NexumSDK ``` -- **`nexum-sdk`** — the universal Rust SDK for any module targeting `nexum:host/event-module`. Provides `HostTransport` (alloy `Transport` trait over `chain::request` / `chain::request-batch`), `provider(chain_id)`, `TypedState` (serde over `local-store`), `RemoteStore` (typed wrapper over `remote-store`), `Messaging` (typed wrapper over `messaging`), `Signer` (typed wrapper over `identity`), logging macros, `HostError`/`HostErrorKind`. Any module author — CoW, DeFi, gaming, whatever — uses this. +- **`nexum-sdk`** - the universal Rust SDK for any module targeting `nexum:host/event-module`. Provides `HostTransport` (alloy `Transport` trait over `chain::request` / `chain::request-batch`), `provider(chain_id)`, `TypedState` (serde over `local-store`), `RemoteStore` (typed wrapper over `remote-store`), `Messaging` (typed wrapper over `messaging`), `Signer` (typed wrapper over `identity`), logging macros, `HostError`/`HostErrorKind`. Any module author - CoW, DeFi, gaming, whatever - uses this. -- **`shepherd-sdk`** — extends `nexum-sdk` with the typed `Cow` client and the `#[shepherd::module]` proc macro (which generates the `cow-api` import in addition to the universals). +- **`shepherd-sdk`** - extends `nexum-sdk` with the typed `Cow` client and the `#[shepherd::module]` proc macro (which generates the `cow-api` import in addition to the universals). A module author building a generic blockchain automation module depends only on `nexum-sdk`. A module author building a CoW Protocol module depends on `shepherd-sdk` (which re-exports `nexum-sdk`). -For **non-Rust** module authors (JavaScript, Python, Go, C++), the SDK is unnecessary — they use `wit-bindgen` directly against the WIT package for their target world. The WIT is the universal contract; the SDK is a Rust ergonomics layer on top. +For **non-Rust** module authors (JavaScript, Python, Go, C++), the SDK is unnecessary - they use `wit-bindgen` directly against the WIT package for their target world. The WIT is the universal contract; the SDK is a Rust ergonomics layer on top. ## Migration from 0.1 @@ -1006,10 +1006,10 @@ For the full 0.1 → 0.2 rename and behaviour change list, see the [Migration Gu | Concept | Scope | |---------|-------| -| `nexum:host` WIT package | Universal — any blockchain app, any platform | -| `event-module` world (0.2, shipping) | Event-driven modules — server today, mobile/background planned | -| `query-module` world (0.2 experimental) | Request/response modules — WIT published, no host impl in 0.2 | -| `app-module` world | Interactive modules — design only; planned hosts | +| `nexum:host` WIT package | Universal - any blockchain app, any platform | +| `event-module` world (0.2, shipping) | Event-driven modules - server today, mobile/background planned | +| `query-module` world (0.2 experimental) | Request/response modules - WIT published, no host impl in 0.2 | +| `app-module` world | Interactive modules - design only; planned hosts | | `shepherd:cow` WIT package | CoW Protocol domain extension | | `shepherd` world | CoW automation modules (includes event-module + cow-api) | | `nexum-sdk` crate | Universal Rust SDK (HostTransport, TypedState, RemoteStore, Messaging, Signer, HostError) | @@ -1017,4 +1017,4 @@ For the full 0.1 → 0.2 rename and behaviour change list, see the [Migration Gu | Content-addressed distribution | Platform-agnostic (Swarm/IPFS, ENS discovery, hash verification) | | Host Adapter | Platform-specific implementation of universal interfaces | -The module binary is the portable artifact. The WIT contract is the universal interface. The host adapter is the platform-specific implementation. Everything else — packaging, distribution, discovery, SDK — layers cleanly on top. +The module binary is the portable artifact. The WIT contract is the universal interface. The host adapter is the platform-specific implementation. Everything else - packaging, distribution, discovery, SDK - layers cleanly on top. diff --git a/docs/adr/0001-engine-toml-separate-from-nexum-toml.md b/docs/adr/0001-engine-toml-separate-from-nexum-toml.md index ab6c1f4..3e54548 100644 --- a/docs/adr/0001-engine-toml-separate-from-nexum-toml.md +++ b/docs/adr/0001-engine-toml-separate-from-nexum-toml.md @@ -15,21 +15,21 @@ The filenames need to signal who owns each file directly. An operator opening a Two distinct files, distinct schemas, distinct loaders: -- **`engine.toml`** — operator-owned, lives next to the engine binary or pointed to by `--engine-config`. Defines `[engine]` (state_dir, log_level), `[chains.]` (rpc_url), and `[[modules]]` (path, manifest). Loaded by `engine_config::EngineConfig::load`. -- **`module.toml`** — module-developer-owned, ships in the module's bundle alongside its `.wasm` component. Defines `[module]`, `[capabilities]` (required, optional, http allowlist), `[config]`. Loaded by `manifest::load`. +- **`engine.toml`** - operator-owned, lives next to the engine binary or pointed to by `--engine-config`. Defines `[engine]` (state_dir, log_level), `[chains.]` (rpc_url), and `[[modules]]` (path, manifest). Loaded by `engine_config::EngineConfig::load`. +- **`module.toml`** - module-developer-owned, ships in the module's bundle alongside its `.wasm` component. Defines `[module]`, `[capabilities]` (required, optional, http allowlist), `[config]`. Loaded by `manifest::load`. The engine config carries the path to each module's manifest; the two never collapse into one file. The names `engine.toml` and `module.toml` map directly onto the two distinct roles, so a reader reaching either file knows whose concerns it covers. ## Considered options -- **Single `shepherd.toml` with `[engine]`, `[chains]`, `[[modules]]` *and* nested `[modules..capabilities]` per module.** Rejected: conflates operator and developer concerns. A module's capability declaration is a property of the build, not the deployment — it belongs in the artifact, not in the operator's local file. Auditing a module's capabilities also becomes a per-deployment exercise instead of a property visible in the published bundle. +- **Single `shepherd.toml` with `[engine]`, `[chains]`, `[[modules]]` *and* nested `[modules..capabilities]` per module.** Rejected: conflates operator and developer concerns. A module's capability declaration is a property of the build, not the deployment - it belongs in the artifact, not in the operator's local file. Auditing a module's capabilities also becomes a per-deployment exercise instead of a property visible in the published bundle. - **Keep the `nexum.toml` filename for the module manifest.** Rejected: the name does not signal who owns the file (engine vs module). `module.toml` reads as "the module's manifest" without prior context. - **`module.toml` inside the engine config (module entries embed it inline).** Rejected for the same reason as the single-file proposal; also bloats `engine.toml`. - **Drop `engine.toml` entirely; pass everything as CLI flags or env vars.** Rejected: per-chain RPC URLs and module lists are awkward as flags, and `RUST_LOG` already covers the only thing that env vars naturally express. ## Consequences -- A deployment needs both files. A missing `engine.toml` falls back to "no chains, default state_dir" — the example logging module still runs; cow-api / chain backends report `unsupported`. +- A deployment needs both files. A missing `engine.toml` falls back to "no chains, default state_dir" - the example logging module still runs; cow-api / chain backends report `unsupported`. - A missing `module.toml` triggers the 0.1-compat deprecation warning in `manifest::fallback_manifest()` (defined in `crates/nexum-engine/src/manifest.rs`) and treats every linked capability as required. This fallback is scheduled for removal in 0.3 per `docs/migration/0.1-to-0.2.md`. - Module-bundle redistribution carries `module.toml` with the artifact; engines do not need to ship templates. - Future content-addressed module distribution (0.3) embeds `module.toml` in the bundle hash; `engine.toml` references modules by content address rather than filesystem path. The split survives that migration unchanged. diff --git a/docs/adr/0002-provider-pool-transport-by-scheme.md b/docs/adr/0002-provider-pool-transport-by-scheme.md index c8e8fab..5506879 100644 --- a/docs/adr/0002-provider-pool-transport-by-scheme.md +++ b/docs/adr/0002-provider-pool-transport-by-scheme.md @@ -28,12 +28,12 @@ Alloy is capable of emulating `eth_subscribe` on HTTP via polling, but this is i ## Considered options - **Force WSS everywhere.** Rejected: many providers (Alchemy, Infura, self-hosted RPC) expose HTTP-only on free tiers, and modules that only need `request` (no subscriptions) shouldn't be blocked by a WSS requirement. -- **Explicit `transport = "ws" | "http"` field per chain in `engine.toml`.** Rejected for 0.2: redundant with the URL scheme, and operators already distinguish `wss://` from `https://` endpoints when copying them from their RPC provider's dashboard. Revisit if we add IPC (`/path/to/geth.ipc`) — scheme alone won't carry that. +- **Explicit `transport = "ws" | "http"` field per chain in `engine.toml`.** Rejected for 0.2: redundant with the URL scheme, and operators already distinguish `wss://` from `https://` endpoints when copying them from their RPC provider's dashboard. Revisit if we add IPC (`/path/to/geth.ipc`) - scheme alone won't carry that. - **Open both an HTTP and a WSS connection per chain.** Rejected: doubles connection count for the common case where one endpoint serves both, and forces operators to provide two URLs even when their provider returns identical data on both. ## Consequences - Operators that need subscriptions must supply WSS URLs; HTTP-only chains downgrade to request-only mode at the host call boundary. -- Connection failures at boot are fatal (the engine refuses to start with a broken chain). This is intentional — silent fall-back to a half-functioning state masks misconfiguration that a module then rediscovers at first event. +- Connection failures at boot are fatal (the engine refuses to start with a broken chain). This is intentional - silent fall-back to a half-functioning state masks misconfiguration that a module then rediscovers at first event. - Adding IPC support is additive: extend the scheme match with `/` / `file://` and call `connect_ipc`. -- The `DynProvider` erasure costs a virtual dispatch per call — a measurable concern at scale, deferred to M4 if profiling shows it. +- The `DynProvider` erasure costs a virtual dispatch per call - a measurable concern at scale, deferred to M4 if profiling shows it. diff --git a/docs/adr/0003-local-store-namespacing.md b/docs/adr/0003-local-store-namespacing.md index 47d5378..a7a5ac1 100644 --- a/docs/adr/0003-local-store-namespacing.md +++ b/docs/adr/0003-local-store-namespacing.md @@ -41,7 +41,7 @@ Modules see plain key strings on both the read and write paths; the prefix is in ## Consequences -- The prefix is fixed-size (32 bytes) and independent of module name length. Range scans over a single module's keys are O(log n + module-key-count) — fine for our workload. +- The prefix is fixed-size (32 bytes) and independent of module name length. Range scans over a single module's keys are O(log n + module-key-count) - fine for our workload. - Migrations changing the prefix derivation (e.g., switching the local-mode hash function or the ENS resolver) would orphan every existing module's persisted state. The derivation must stay stable through 0.x; ENS-mode introduction in 0.3 happens additively via the alias mechanism, not by changing existing prefixes. - A module's `list-keys` iterates over the namespace range (32-byte prefix scan); the host strips the prefix before returning to the guest. - Module data versioning (schema migrations across module versions) is the module's responsibility. The local-store does not version values; modules MAY embed a `schema_version` byte in their stored payloads and migrate on `init` when the read value's version differs from the current code's expectation. diff --git a/docs/adr/0006-cow-twap-ethflow-host-helpers.md b/docs/adr/0006-cow-twap-ethflow-host-helpers.md index 0e9e776..1a139d2 100644 --- a/docs/adr/0006-cow-twap-ethflow-host-helpers.md +++ b/docs/adr/0006-cow-twap-ethflow-host-helpers.md @@ -8,7 +8,7 @@ status: proposed TWAP (over ComposableCoW) and EthFlow are the two CoW workflows the M2 grant ships modules for. The natural-seeming approach is to add `shepherd:cow/twap` and `shepherd:cow/ethflow` WIT interfaces that the host implements on top of `cowprotocol` crate primitives, so modules would call `twap.poll-and-submit(...)` and `ethflow.submit-from-log(...)` as host functions. This ADR rejects that direction. -The dividing line is protocol vs implementation. CoW Protocol primitives — order types, signing schemes, the orderbook REST surface — are protocol concerns and belong in shared layers (`cowprotocol` crate, `shepherd:cow/cow-api` interface). TWAP is one of many strategies built _on top of_ those primitives; ComposableCoW is the contract surface a TWAP module observes, but the act of polling, deciding when to submit, and reacting to orderbook errors is application logic. Putting that application logic in the host or in `cowprotocol` couples every consumer to one implementation and one error-handling policy. +The dividing line is protocol vs implementation. CoW Protocol primitives - order types, signing schemes, the orderbook REST surface - are protocol concerns and belong in shared layers (`cowprotocol` crate, `shepherd:cow/cow-api` interface). TWAP is one of many strategies built _on top of_ those primitives; ComposableCoW is the contract surface a TWAP module observes, but the act of polling, deciding when to submit, and reacting to orderbook errors is application logic. Putting that application logic in the host or in `cowprotocol` couples every consumer to one implementation and one error-handling policy. Embedding a concrete TWAP implementation in an SDK is an architectural smell the grant explicitly seeks to alleviate. The grant seeks to enable Shepherd as the runtime where many independent strategy implementations coexist, each compiled to its own WASM module. A specialised `twap` interface in the host would defeat that goal: every Shepherd deployment would have to use the same polling implementation, the same error-mapping, the same retry hints, with no room for different strategies to differ on those choices. @@ -18,12 +18,12 @@ The `shepherd:cow` WIT package contains only the existing `cow-api` interface (R TWAP and EthFlow modules implement their logic in Rust guest code using: -- **`nexum:host/chain`** — `request` (for `eth_call`, `eth_getLogs`, etc.), `subscribe-blocks`, `subscribe-logs`. -- **`nexum:host/local-store`** — for watch lists, cursors, and backoff state. -- **`nexum:host/logging`** — for structured logs. -- **`shepherd:cow/cow-api`** — `submit-order` for orderbook submission. -- **`cowprotocol` crate** (consumed directly by the module, gated on the wasm32 feature work in ADR-0007) — for protocol types: `Order`, `OrderCreation`, `OrderUid`, signing schemes, `OrderPostError`, etc. -- **`alloy_sol_types`** (or equivalent) — for ABI-aware decoding of `ConditionalOrderCreated`, `OrderPlacement`, `getTradeableOrderWithSignature` return values, and similar Solidity-typed payloads. +- **`nexum:host/chain`** - `request` (for `eth_call`, `eth_getLogs`, etc.), `subscribe-blocks`, `subscribe-logs`. +- **`nexum:host/local-store`** - for watch lists, cursors, and backoff state. +- **`nexum:host/logging`** - for structured logs. +- **`shepherd:cow/cow-api`** - `submit-order` for orderbook submission. +- **`cowprotocol` crate** (consumed directly by the module, gated on the wasm32 feature work in ADR-0007) - for protocol types: `Order`, `OrderCreation`, `OrderUid`, signing schemes, `OrderPostError`, etc. +- **`alloy_sol_types`** (or equivalent) - for ABI-aware decoding of `ConditionalOrderCreated`, `OrderPlacement`, `getTradeableOrderWithSignature` return values, and similar Solidity-typed payloads. Concretely, a TWAP module's `on_event(block)` handler iterates the local-store watch set, makes an `eth_call` to `ComposableCoW.getTradeableOrderWithSignature(owner, params, "", [])` via `chain.request`, decodes the return (or revert reason) with `alloy_sol_types`, constructs an `OrderCreation` with `cowprotocol` types, and submits via `cow-api/submit-order`. Orderbook errors are interpreted via `OrderPostError::retry_hint()` (ADR-0007). Backoff state is persisted to `local-store`. All of this lives in module Rust source, not in the engine. @@ -33,7 +33,7 @@ An EthFlow module's `on_event(log)` handler decodes the `OrderPlacement` event w - **Specialised `shepherd:cow/twap` and `shepherd:cow/ethflow` interfaces** with rich `PollOutcome` variants and per-event host helpers, backed by `composable::poll_and_build_order` and `eth_flow::decode_placement` primitives in the `cowprotocol` crate. Rejected: this puts a single concrete TWAP / EthFlow implementation behind a WIT boundary, forcing every Shepherd deployment to use the same polling policy, the same error-mapping, the same retry hints. It also blurs the protocol-vs-implementation boundary the grant is meant to clarify. Multiple TWAP implementations (different polling cadences, different error tolerances, different cancel-on-loss thresholds) must be able to coexist as separate modules without changing the host or the SDK. - **Move TWAP / EthFlow primitives into `cowprotocol` crate but skip the WIT interfaces**, leaving modules to call `composable::poll_and_build_order` from guest code. Rejected for the same reason: `cowprotocol` is the protocol SDK, not the strategy SDK. Putting TWAP logic there embeds an implementation in the shared layer, which is the smell the grant seeks to fix. -- **Ship a thin `shepherd-sdk` helper crate** that wraps the low-level primitive calls (eth_call, decode, submit) into a convenient `Twap::poll(...)` interface for guest modules. **Acceptable for M3** because the helper would live in guest-callable code, not behind a WIT boundary — a module that wants different polling policy just doesn't use the SDK helper. The host stays neutral. +- **Ship a thin `shepherd-sdk` helper crate** that wraps the low-level primitive calls (eth_call, decode, submit) into a convenient `Twap::poll(...)` interface for guest modules. **Acceptable for M3** because the helper would live in guest-callable code, not behind a WIT boundary - a module that wants different polling policy just doesn't use the SDK helper. The host stays neutral. - **EthFlow as pure passive observer (no submission)**. Rejected on closer read of `cowprotocol/services/crates/autopilot/src/database/onchain_order_events/ethflow_events.rs`: the canonical CoW flow expects the event to be relayed into the orderbook, which is what autopilot currently does internally. Shepherd's `ethflow-watcher` externalises that role, so the module does submit; just from guest code, not via a specialised host interface. - **TWAP merkle-proof / `setRoot` support in v1.** Deferred. The 0.2 module only handles `ComposableCoW.create()` (empty proof, single conditional order). `setRoot` polling requires off-chain proof derivation; when a real module needs it, it will be implemented in guest code using the same low-level primitives, possibly with an SDK helper to encapsulate the proof bookkeeping. diff --git a/docs/adr/0007-upstream-protocol-logic-to-cow-rs.md b/docs/adr/0007-upstream-protocol-logic-to-cow-rs.md index e8e949c..3a828f5 100644 --- a/docs/adr/0007-upstream-protocol-logic-to-cow-rs.md +++ b/docs/adr/0007-upstream-protocol-logic-to-cow-rs.md @@ -14,15 +14,15 @@ The line between **protocol primitives** (which belong in `cowprotocol`) and **s ## Decision -Protocol-level CoW logic — anything that an indexer, a bot, or a non-`nexum` Rust consumer of CoW Protocol would also need to interact with the protocol — lands as additional commits on `cowdao-grants/cow-rs` PR #5 first (head branch `bleu/cow-rs:main`), and is consumed by `nexum-engine` and by guest modules via the `[patch.crates-io]` rev bump (ADR-0004). The engine and the modules never write throwaway local copies of the same logic with the intent to "port later". +Protocol-level CoW logic - anything that an indexer, a bot, or a non-`nexum` Rust consumer of CoW Protocol would also need to interact with the protocol - lands as additional commits on `cowdao-grants/cow-rs` PR #5 first (head branch `bleu/cow-rs:main`), and is consumed by `nexum-engine` and by guest modules via the `[patch.crates-io]` rev bump (ADR-0004). The engine and the modules never write throwaway local copies of the same logic with the intent to "port later". The concrete set of primitives this ADR commits to upstream, in priority order: -1. **`cowprotocol::OrderPostError` rich variants + `retry_hint(&self) -> RetryHint`** — typed orderbook submission errors (`QuoteNotFound`, `InvalidQuote`, `InsufficientAllowance`, `InsufficientBalance`, `TooManyLimitOrders`, `InvalidAppData`, `AppDataFromMismatch`, `SellAmountOverflow`, `ZeroAmount`, `TransferSimulationFailed`, `ExcessiveValidTo`, …) with a `retry_hint()` helper classifying each into `TryNextBlock`, `BackoffSeconds(u64)`, or `Drop`. Mirrors watch-tower's `API_ERRORS_TRY_NEXT_BLOCK` / `API_ERRORS_BACKOFF` / `API_ERRORS_DROP` tables. Without this, every Rust consumer of CoW reinvents the same mapping, and modules spam the orderbook with permanently-broken orders. **Critical-path, not optional.** +1. **`cowprotocol::OrderPostError` rich variants + `retry_hint(&self) -> RetryHint`** - typed orderbook submission errors (`QuoteNotFound`, `InvalidQuote`, `InsufficientAllowance`, `InsufficientBalance`, `TooManyLimitOrders`, `InvalidAppData`, `AppDataFromMismatch`, `SellAmountOverflow`, `ZeroAmount`, `TransferSimulationFailed`, `ExcessiveValidTo`, …) with a `retry_hint()` helper classifying each into `TryNextBlock`, `BackoffSeconds(u64)`, or `Drop`. Mirrors watch-tower's `API_ERRORS_TRY_NEXT_BLOCK` / `API_ERRORS_BACKOFF` / `API_ERRORS_DROP` tables. Without this, every Rust consumer of CoW reinvents the same mapping, and modules spam the orderbook with permanently-broken orders. **Critical-path, not optional.** -2. **`cowprotocol::OrderBookApi::with_base_url(chain_id, base_url)`** — custom-URL constructor for barn / staging / forked deployments. Unblocks per-chain orderbook URL overrides in `engine.toml` (ADR-0005). +2. **`cowprotocol::OrderBookApi::with_base_url(chain_id, base_url)`** - custom-URL constructor for barn / staging / forked deployments. Unblocks per-chain orderbook URL overrides in `engine.toml` (ADR-0005). -3. **`cowprotocol` `wasm32` compatibility** — feature-gate the `reqwest` dependency so guest modules can use the pure types (`Order`, `OrderCreation`, `OrderUid`, signing schemes, error variants) without dragging in an HTTP client. **Critical for ADR-0006**: modules implement TWAP and EthFlow logic in guest code and need `cowprotocol` types compiled to wasm32. Without this, guest modules fall back to duplicating type definitions. +3. **`cowprotocol` `wasm32` compatibility** - feature-gate the `reqwest` dependency so guest modules can use the pure types (`Order`, `OrderCreation`, `OrderUid`, signing schemes, error variants) without dragging in an HTTP client. **Critical for ADR-0006**: modules implement TWAP and EthFlow logic in guest code and need `cowprotocol` types compiled to wasm32. Without this, guest modules fall back to duplicating type definitions. Lower-priority follow-ons (`OrderUid::from_slice`, retry middleware on `OrderBookApi`, `OrderCreation::from_gpv2`) are good-to-have but are not blocking for the M2 host or module scope. @@ -42,5 +42,5 @@ Lower-priority follow-ons (`OrderUid::from_slice`, retry middleware on `OrderBoo - Commits added to PR #5 follow its established conventions: alloy reuse over local reimplementation, GPL-3.0, edition 2024, terse rustdoc. - The engine repo stays small: `nexum-engine` contains WIT, host wiring, supervisor, redb store, alloy provider pool, and `engine.toml` schema, with nothing about CoW Protocol semantics. - Guest modules consume `cowprotocol` types directly (gated on the wasm32 feature in item 3). The `shepherd-sdk` crate in M3 may add ergonomic wrappers on top, but those live on the guest side, not behind a WIT boundary. -- A follow-on Bleu module — the Rust-side equivalent of `cowprotocol/refunder` (permissionless `invalidateOrder` triggering for expired EthFlow orders) — becomes natural to ship once an ethflow-watcher module lands. Out of scope for M2 but explicitly enabled by the same primitives. +- A follow-on Bleu module - the Rust-side equivalent of `cowprotocol/refunder` (permissionless `invalidateOrder` triggering for expired EthFlow orders) - becomes natural to ship once an ethflow-watcher module lands. Out of scope for M2 but explicitly enabled by the same primitives. - TWAP polling logic (decode `ConditionalOrderCreated`, eth_call `getTradeableOrderWithSignature`, decode return, build `OrderCreation`) and EthFlow event decoding stay entirely in guest module code. The `cowprotocol` crate provides only the types and the orderbook client; the strategy is the module's. diff --git a/docs/adr/0008-factory-subscriptions-in-manifest.md b/docs/adr/0008-factory-subscriptions-in-manifest.md index 5a23356..f7fff7d 100644 --- a/docs/adr/0008-factory-subscriptions-in-manifest.md +++ b/docs/adr/0008-factory-subscriptions-in-manifest.md @@ -28,7 +28,7 @@ Combined: the dynamic-subscription design is not load-bearing for M2 deliverable ## Reference design (not adopted in 0.2) -The original proposal — kept here so future discussions have a starting point — was a hybrid of static topics and dynamic addresses: +The original proposal - kept here so future discussions have a starting point - was a hybrid of static topics and dynamic addresses: - `[[subscription.template]]` block in `module.toml` declaring `chain_id`, `name`, `event_topics` (no address). - `chain.register-address(chain_id, template_name, address)` host function for the module to add addresses at runtime. diff --git a/docs/diagrams/diagrams.md b/docs/diagrams/diagrams.md index 762e6db..710717b 100644 --- a/docs/diagrams/diagrams.md +++ b/docs/diagrams/diagrams.md @@ -1,8 +1,8 @@ -# Shepherd — Architecture Diagrams +# Shepherd - Architecture Diagrams Visual reference for the Shepherd engine, its interactions with Nexum, CoW Protocol, and the WASM module layer. Derived from ADRs 0001–0008 and the internal architecture document. -> **Scope note** — diagrams 1–4 and 7–8 reflect the **M1 implemented state** plus the **M2 target design** as described by the ADRs. Diagrams 5–6 (TWAP, EthFlow) describe **guest-module-driven flows**: the modules do all the protocol work themselves using low-level host primitives, with no specialised `twap` or `ethflow` host interfaces. Where the current code differs from the target design, a note is included in the relevant block reference. +> **Scope note** - diagrams 1–4 and 7–8 reflect the **M1 implemented state** plus the **M2 target design** as described by the ADRs. Diagrams 5–6 (TWAP, EthFlow) describe **guest-module-driven flows**: the modules do all the protocol work themselves using low-level host primitives, with no specialised `twap` or `ethflow` host interfaces. Where the current code differs from the target design, a note is included in the relevant block reference. --- @@ -16,7 +16,7 @@ graph TD SUP["Supervisor::boot"] POOLS["ProviderPool · OrderBookPool · LocalStore"] HS["HostState (per module)\nnexum:host@0.2.0 + shepherd:cow@0.2.0"] - EL["EventLoop — futures::stream::select_all\nfan-out block/log streams to subscribers"] + EL["EventLoop - futures::stream::select_all\nfan-out block/log streams to subscribers"] MODS["WASM Modules\ntwap.wasm · eth-flow.wasm\n(self-contained protocol logic in guest)"] BC["Blockchain (Sepolia / Mainnet / …)\nComposableCoW · CowEthFlow · RPC Node"] CR["bleu/cow-rs ← [patch.crates-io]\nOrder · OrderCreation · OrderUid · signing schemes\nOrderBookApi · OrderPostError + retry_hint\nOrderBookApi::with_base_url · wasm32 feature"] @@ -42,12 +42,12 @@ graph TD | **engine.toml** | Written by the operator. Declares which chains to connect to (RPC URLs), where to store state on disk, and which WASM modules to load at boot. | | **module.toml** | Written by the module developer and shipped inside the module bundle. Declares which capabilities the module needs (`required`), which on-chain events to subscribe to, and any module-specific config keys. Renamed from `nexum.toml` per ADR-0001 so the operator/module split is directly apparent. | | **Supervisor::boot** | The boot orchestrator. Reads both config files, creates the shared resource pools, loads each `.wasm` component via wasmtime, and wires their subscriptions into the event streams. | -| **ProviderPool · OrderBookPool · LocalStore** | The three shared backends. `ProviderPool` holds one alloy RPC client per chain. `OrderBookPool` holds one CoW orderbook HTTP client per chain. `LocalStore` is a single redb key-value database shared by all modules (with per-module 32-byte hash namespacing — ADR-0003). | +| **ProviderPool · OrderBookPool · LocalStore** | The three shared backends. `ProviderPool` holds one alloy RPC client per chain. `OrderBookPool` holds one CoW orderbook HTTP client per chain. `LocalStore` is a single redb key-value database shared by all modules (with per-module 32-byte hash namespacing - ADR-0003). | | **HostState (per module)** | The per-module bridge between WASM guest code and Rust host code. When a module calls a WIT function (`local-store/set`, `cow-api/submit-order`, etc.), wasmtime routes that call to the corresponding method on that module's `HostState`. Checks capability permissions before dispatching. | | **EventLoop** | The main async loop. Runs all block-header and log-event streams concurrently via `futures::stream::select_all`. When a stream fires, it routes the event to every module that subscribed to it in their `module.toml`. | -| **WASM Modules** | The guest programs. Each module exports `init(config)` (called once at boot) and `on_event(event)` (called on every relevant block or log). They contain the protocol logic themselves: TWAP polling, EthFlow event decoding, OrderCreation construction. They call back into the host through universal WIT interfaces only — no CoW-specific helper interfaces (ADR-0006). | +| **WASM Modules** | The guest programs. Each module exports `init(config)` (called once at boot) and `on_event(event)` (called on every relevant block or log). They contain the protocol logic themselves: TWAP polling, EthFlow event decoding, OrderCreation construction. They call back into the host through universal WIT interfaces only - no CoW-specific helper interfaces (ADR-0006). | | **Blockchain** | The EVM chain being watched. Delivers new block headers and contract log events over a persistent WebSocket (`eth_subscribe`). Also handles `eth_call` for on-chain reads (e.g. checking whether a TWAP order is ready). | -| **bleu/cow-rs [patch.crates-io]** | The Rust crate containing CoW Protocol **primitives**: order types, signing schemes, the orderbook HTTP client, and the typed orderbook error model (`OrderPostError` + `retry_hint`). Pulled via `[patch.crates-io]` pointing at the head of upstream PR #5. Modules consume the types directly via the `wasm32` feature; the engine consumes the orderbook client via its `cow-api` host backend. No TWAP or EthFlow strategy logic lives here — that stays in module code (ADR-0007). | +| **bleu/cow-rs [patch.crates-io]** | The Rust crate containing CoW Protocol **primitives**: order types, signing schemes, the orderbook HTTP client, and the typed orderbook error model (`OrderPostError` + `retry_hint`). Pulled via `[patch.crates-io]` pointing at the head of upstream PR #5. Modules consume the types directly via the `wasm32` feature; the engine consumes the orderbook client via its `cow-api` host backend. No TWAP or EthFlow strategy logic lives here - that stays in module code (ADR-0007). | | **api.cow.fi (Orderbook REST)** | The CoW Protocol orderbook service. Accepts `POST /orders` to register new orders. Trader-uploaded app-data documents are PUT to `/app_data/{hash}` separately by whoever signed the order (not by the relayer module). | --- @@ -143,15 +143,15 @@ classDiagram |---|---| | **EngineConfig** | Deserialized from `engine.toml`. Holds the database path (`state_dir`), one `ChainConfig` per chain (just an RPC URL), and the list of module paths to load. | | **Manifest** | Deserialized from `module.toml`, which ships inside the module bundle. Declares what capabilities the module needs, which on-chain events to watch, and any module-level config values. | -| **Subscription** | One event declaration inside `module.toml`. `kind=Block` fires on every new block for a given chain. `kind=Log` fires when a specific contract emits an event matching the given address and topics. Factory-style dynamic subscriptions (`[[subscription.template]]` + `register-address`) are deferred to 0.3 — see ADR-0008. | +| **Subscription** | One event declaration inside `module.toml`. `kind=Block` fires on every new block for a given chain. `kind=Log` fires when a specific contract emits an event matching the given address and topics. Factory-style dynamic subscriptions (`[[subscription.template]]` + `register-address`) are deferred to 0.3 - see ADR-0008. | | **Supervisor** | Orchestrates boot and event dispatch. Creates one `HostState` per module. On each incoming block or log, calls `dispatch_block` / `dispatch_log` to fan the event out to subscribed modules. | -| **ProviderPool** | Holds one alloy `DynProvider` per chain. `wss://` chains get a pubsub provider that supports both subscriptions and requests. `https://` chains get HTTP-only (subscriptions unavailable, by design — ADR-0002). | +| **ProviderPool** | Holds one alloy `DynProvider` per chain. `wss://` chains get a pubsub provider that supports both subscriptions and requests. `https://` chains get HTTP-only (subscriptions unavailable, by design - ADR-0002). | | **OrderBookPool** | Holds one `OrderBookApi` client per known CoW chain (Mainnet, Gnosis, Sepolia, ArbitrumOne, Base). Instantiated via `OrderBookPool::default()` at boot (ADR-0005). | | **LocalStore** | A single redb embedded database at `state_dir`. All modules write into the same file. Keys are prefixed host-side as `[32-byte module namespace][raw_key]` so two modules never collide, and the namespace is unspoofable (ADR-0003). The namespace is `keccak256(module_name)` for locally-loaded modules and `ens_namehash(name)` for ENS-discovered modules. | | **HostState** | The per-module runtime context. `wasmtime::component::bindgen!` generates one trait per WIT interface (e.g. `shepherd::cow::cow_api::Host`); `HostState` implements each trait. `Shepherd::add_to_linker` registers all trait implementations with the `Linker` once at boot. **Current fields** (M1): `wasi: WasiCtx`, `table: ResourceTable`, `http_allowlist: Vec`, `monotonic_baseline: Instant`. **M2 additions** will add `module_namespace: [u8; 32]`, `provider_pool: Arc`, `ob_pool: Arc`, `local_store: Arc`. | | **EventLoop** | Runs `futures::stream::select_all` over a `Vec + Send>>>`. The loop never exits until SIGINT/SIGTERM. Each fired event is forwarded to `Supervisor` for fan-out. | | **TwapModule** | The TWAP watcher WASM component. On a `Log` event (ConditionalOrderCreated): persists the registration in `local-store`. On a `Block` event: iterates all watches and, for each, makes an `eth_call` via `chain.request`, decodes the result via `alloy_sol_types` (in-module), builds an `OrderCreation` via `cowprotocol` types (consumed via wasm32 feature), and submits via `cow-api.submit-order`. Orderbook errors flow through `OrderPostError::retry_hint`. All polling logic lives in the module, not the host (ADR-0006). | -| **EthFlowModule** | The EthFlow watcher WASM component. On a `Log` event (OrderPlacement): decodes the event via `alloy_sol_types` in-module, builds the `OrderCreation` with the EIP-1271 signing scheme via `cowprotocol` types, and submits via `cow-api.submit-order`. No polling loop — one log equals one submission attempt. | +| **EthFlowModule** | The EthFlow watcher WASM component. On a `Log` event (OrderPlacement): decodes the event via `alloy_sol_types` in-module, builds the `OrderCreation` with the EIP-1271 signing scheme via `cowprotocol` types, and submits via `cow-api.submit-order`. No polling loop - one log equals one submission attempt. | --- @@ -161,12 +161,12 @@ Two WIT packages: the universal `nexum:host` and the CoW-specific `shepherd:cow` ```mermaid graph TD - NH["nexum:host@0.2.0\n(universal — no CoW knowledge)"] + NH["nexum:host@0.2.0\n(universal - no CoW knowledge)"] SC["shepherd:cow@0.2.0\n(CoW Protocol extensions)"] - NH --> n1["chain ✅ implemented\nrequest(chain-id, method, params)\nrequest-batch(chain-id, requests)\n—\nsubscribe-blocks · subscribe-logs →\n engine-managed via module.toml subscriptions\nregister-address · unregister-address →\n 🕓 deferred to 0.3 (ADR-0008)"] + NH --> n1["chain ✅ implemented\nrequest(chain-id, method, params)\nrequest-batch(chain-id, requests)\n - \nsubscribe-blocks · subscribe-logs →\n engine-managed via module.toml subscriptions\nregister-address · unregister-address →\n 🕓 deferred to 0.3 (ADR-0008)"] NH --> n2["local-store ✅ implemented\nget(key) · set(key, value)\ndelete(key) · list-keys(prefix)\nnamespacing: 32-byte hash prefix (ADR-0003)"] - NH --> n3["identity · messaging · http · remote-store\n✅ stubs (Unsupported) — full impl in 0.3"] + NH --> n3["identity · messaging · http · remote-store\n✅ stubs (Unsupported) - full impl in 0.3"] NH --> n4["logging · clock · random ✅ implemented"] SC -->|"use nexum:host/types"| NH @@ -182,13 +182,13 @@ graph TD | Interface | What it does | |---|---| -| **nexum:host@0.2.0** | The base WIT package. Any module running in the engine — CoW-aware or not — imports from here. Defines shared types (`chain-id`, `log`, `host-error`) used by both packages. | -| **chain** | Reads from the blockchain via JSON-RPC. `request` sends a single call; `request-batch` sends several in one round-trip. **Subscriptions are not callable WIT functions** — they are declared in `module.toml` and opened by the engine at boot. Dynamic `register-address` for factory patterns is deferred to 0.3 (ADR-0008). | +| **nexum:host@0.2.0** | The base WIT package. Any module running in the engine - CoW-aware or not - imports from here. Defines shared types (`chain-id`, `log`, `host-error`) used by both packages. | +| **chain** | Reads from the blockchain via JSON-RPC. `request` sends a single call; `request-batch` sends several in one round-trip. **Subscriptions are not callable WIT functions** - they are declared in `module.toml` and opened by the engine at boot. Dynamic `register-address` for factory patterns is deferred to 0.3 (ADR-0008). | | **local-store** | Persistent key-value storage that survives restarts. Operations: `get(key)`, `set(key, value)`, `delete(key)`, `list-keys(prefix)`. The host prefixes every key with a 32-byte deterministic namespace (`keccak256(module_name)` locally, or `ens_namehash(name)` when ENS-loaded) so modules are fully isolated and the namespace cannot be spoofed (ADR-0003). | -| **identity · messaging · http · remote-store** | Capabilities stubbed at 0.2 — they return `Unsupported`. `identity` will provide keystore-backed signing. `messaging` will send Waku messages. `http` will allow direct outbound HTTP calls (subject to the manifest's allowlist). `remote-store` will read/write Swarm/IPFS. | +| **identity · messaging · http · remote-store** | Capabilities stubbed at 0.2 - they return `Unsupported`. `identity` will provide keystore-backed signing. `messaging` will send Waku messages. `http` will allow direct outbound HTTP calls (subject to the manifest's allowlist). `remote-store` will read/write Swarm/IPFS. | | **logging · clock · random** | Lightweight utilities. `logging` emits to the engine's `tracing` subscriber (inherits `RUST_LOG` filters). `clock` returns wall-clock time. `random` returns cryptographically-secure random bytes. | | **shepherd:cow@0.2.0** | The CoW Protocol extension package. Imports `nexum:host/types` for shared types so modules don't re-define `chain-id` or `log`. Only CoW-aware modules need to import this package. Contains exactly **one** interface in 0.2: `cow-api`. | -| **cow-api** | Generic orderbook access. `request` is a raw REST passthrough (returns JSON string). `submit-order` takes raw order bytes and returns a `result` where the string is the order UID. Routes through the engine's `OrderBookPool`. This is the only protocol-level CoW interface in 0.2 — the boundary between "what CoW Protocol *is*" (orderbook submission, order types) and "what's implemented *on top* of CoW" (TWAP polling, EthFlow event handling). | +| **cow-api** | Generic orderbook access. `request` is a raw REST passthrough (returns JSON string). `submit-order` takes raw order bytes and returns a `result` where the string is the order UID. Routes through the engine's `OrderBookPool`. This is the only protocol-level CoW interface in 0.2 - the boundary between "what CoW Protocol *is*" (orderbook submission, order types) and "what's implemented *on top* of CoW" (TWAP polling, EthFlow event handling). | | **(no twap interface)** | Per ADR-0006, no specialised TWAP host interface exists. The TWAP module implements polling, decoding, and submission entirely in guest code, using `chain.request` for `eth_call`, `local-store` for state, `alloy_sol_types` (in-module) for ABI decoding, `cowprotocol` types for `OrderCreation`, and `cow-api.submit-order` for orderbook submission. Multiple TWAP strategies can coexist as separate modules with different polling policies and error tolerances. | | **(no ethflow interface)** | Per ADR-0006, no specialised EthFlow host interface exists. The EthFlow module decodes `OrderPlacement` directly in guest code via `alloy_sol_types`, constructs the `OrderCreation` with the EIP-1271 signing scheme via `cowprotocol` types, and submits via `cow-api`. | @@ -203,7 +203,7 @@ flowchart TD ReadConfig --> InitTracing InitTracing["2. Init tracing\n(RUST_LOG / log_level)"] InitTracing --> ProvPool - ProvPool["3. ProviderPool::from_config\nFor each chain:\n wss:// → pubsub DynProvider\n https:// → http DynProvider\n(fatal on connection error — ADR-0002)"] + ProvPool["3. ProviderPool::from_config\nFor each chain:\n wss:// → pubsub DynProvider\n https:// → http DynProvider\n(fatal on connection error - ADR-0002)"] ProvPool --> OpenStore OpenStore["4. LocalStore::open(state_dir)\nOpen/create redb DB\nnexum:local-store table\n(ADR-0003)"] OpenStore --> OBPoolInit @@ -230,7 +230,7 @@ flowchart TD |---|---| | **1. Read engine.toml** | Deserializes the operator config. If the file is missing, the engine falls back to defaults (no chains, default `state_dir`). Modules that need chains will receive `Unsupported` errors at runtime. | | **2. Init tracing** | Sets up the `tracing` subscriber using `RUST_LOG` or the `log_level` field from `engine.toml`. All host log output flows through here, including per-capability trace events. | -| **3. ProviderPool** | Opens one alloy connection per chain declared in `[chains]`. WebSocket URLs get a full pubsub provider (the recommended setup for any chain a module subscribes to). HTTP URLs get a request-only provider. Any connection failure at this step is fatal — the engine refuses to start with a broken chain rather than silently degrading. Failover and retry are out of scope; they live in alloy middleware (ADR-0002). | +| **3. ProviderPool** | Opens one alloy connection per chain declared in `[chains]`. WebSocket URLs get a full pubsub provider (the recommended setup for any chain a module subscribes to). HTTP URLs get a request-only provider. Any connection failure at this step is fatal - the engine refuses to start with a broken chain rather than silently degrading. Failover and retry are out of scope; they live in alloy middleware (ADR-0002). | | **4. LocalStore** | Opens (or creates) the redb database at `state_dir`. Creates the `nexum:local-store` table if it doesn't exist. Per-module namespacing uses a 32-byte deterministic hash prefix. Module state from previous runs is immediately available. | | **5. OrderBookPool** | Constructs one `OrderBookApi` HTTP client for each supported CoW chain via the `Default` implementation. Built upfront so config errors (unknown chain IDs) surface at boot, not on the first order submission. | | **6. Supervisor::boot (per module)** | For each module listed in `engine.toml`: reads its `module.toml`, loads the `.wasm` component into wasmtime, creates a dedicated `HostState`, calls the module's `init(config)` export, and records which subscriptions the module declared. | @@ -256,11 +256,11 @@ sequenceDiagram participant OB as api.cow.fi
(Orderbook) participant LS as LocalStore
(redb) - Note over User,CC: Step 0 — On-chain registration (off-engine) + Note over User,CC: Step 0 - On-chain registration (off-engine) User->>CC: ComposableCoW.create(twapParams) CC-->>RPC: emit ConditionalOrderCreated(owner, params, proof) - Note over RPC,LS: Step 1 — Indexing (once per TWAP) + Note over RPC,LS: Step 1 - Indexing (once per TWAP) RPC->>EL: log batch (eth_subscribe logs) EL->>TM: on_event(Event::Logs([registration_log])) TM->>SD: decode ConditionalOrderCreated @@ -268,7 +268,7 @@ sequenceDiagram TM->>HS: local-store.set("watch:{owner}:{hash}", params) HS->>LS: write [module_namespace][watch:...] = params_bytes - Note over RPC,LS: Step 2 — Poll loop (every block) + Note over RPC,LS: Step 2 - Poll loop (every block) loop Every block on chain_id RPC->>EL: block header (eth_subscribe newHeads) EL->>TM: on_event(Event::Block(Block { number: N, ... })) @@ -310,13 +310,13 @@ sequenceDiagram | Participant | Role in this flow | |---|---| -| **User** | The trader. Interacts with the blockchain directly — the engine never touches private keys. | +| **User** | The trader. Interacts with the blockchain directly - the engine never touches private keys. | | **ComposableCoW Contract** | The on-chain conditional order registry. Accepts TWAP parameters via `create()` and emits `ConditionalOrderCreated`. Also exposes `getTradeableOrderWithSignature()`, which the engine polls to check whether the current TWAP part is ready to trade. | | **RPC Node** | The WebSocket connection to the chain. Delivers log events (subscriptions) and handles `eth_call` (synchronous reads). Must be `wss://` for this flow since it uses subscriptions. | -| **EventLoop** | Receives raw events from the RPC node and routes them to the module that subscribed to them. Opaque to the flow — it just calls `on_event`. | +| **EventLoop** | Receives raw events from the RPC node and routes them to the module that subscribed to them. Opaque to the flow - it just calls `on_event`. | | **twap module (WASM guest)** | Contains the entire TWAP strategy: decoding registrations, deciding when to poll (using stored hints), reacting to revert reasons, building orders, interpreting orderbook errors. Calls into the host only through universal WIT primitives. | | **alloy_sol_types (in module)** | The ABI-aware decoder. Compiled into the module's WASM. Decodes `ConditionalOrderCreated` from raw log bytes; decodes the `getTradeableOrderWithSignature` return; interprets revert reasons. No host involvement for decoding. | -| **cowprotocol types (in module)** | The protocol-level types from `bleu/cow-rs`, consumed by the module via the wasm32 feature (ADR-0007 item 3). Used to build `OrderCreation`, manipulate `OrderUid`, and pattern-match `OrderPostError`. The crate's HTTP client (`OrderBookApi`) is **not** used directly by the module — orderbook submission goes through the host's `cow-api`. | +| **cowprotocol types (in module)** | The protocol-level types from `bleu/cow-rs`, consumed by the module via the wasm32 feature (ADR-0007 item 3). Used to build `OrderCreation`, manipulate `OrderUid`, and pattern-match `OrderPostError`. The crate's HTTP client (`OrderBookApi`) is **not** used directly by the module - orderbook submission goes through the host's `cow-api`. | | **HostState (Rust)** | Provides only the universal primitives (`chain.request`, `local-store.*`, `cow-api.submit-order`). Knows nothing about TWAP semantics. | | **api.cow.fi (Orderbook)** | Receives the signed `OrderCreation`, validates it, and returns a 56-byte `OrderUid`. The order is now visible to CoW solvers. | | **LocalStore (redb)** | Persistent state for the TWAP module. `watch:{owner}:{hash}` entries hold registrations. `submitted:{uid}` entries record completed submissions. `next_attempt` hints (epoch or block) let the module skip polling during the gap between TWAP parts. All entries survive engine restarts. | @@ -338,16 +338,16 @@ sequenceDiagram participant OB as api.cow.fi
(Orderbook) participant LS as LocalStore
(redb) - Note over User,EFC: Step 0 — User creates ETH order on-chain + Note over User,EFC: Step 0 - User creates ETH order on-chain User->>EFC: createOrder(order, msg.value=ETH) EFC->>EFC: store orders[hash] = onchainData,
emit OrderPlacement(sender, order, EIP1271-sig, data) EFC-->>RPC: log emitted on block N - Note over RPC,LS: Step 1 — Log arrives via subscription + Note over RPC,LS: Step 1 - Log arrives via subscription RPC->>EL: log batch matching CoWSwapEthFlow address + OrderPlacement topic EL->>EM: on_event(Event::Logs([placement_log])) - Note over EM,LS: Step 2 — Decode and submit (1 log = 1 submission) + Note over EM,LS: Step 2 - Decode and submit (1 log = 1 submission) EM->>SD: decode OrderPlacement(sender, order, sig, data) SD-->>EM: (sender, GPv2OrderData, EIP-1271 sig, data) @@ -382,7 +382,7 @@ sequenceDiagram |---|---| | **User** | The trader. Deposits native ETH into the `CoWSwapEthFlow` contract and specifies swap parameters. The contract is the EIP-1271 signer on behalf of the user. | | **CoWSwapEthFlow Contract** | Custodies the ETH, stores the order metadata on-chain, and emits `OrderPlacement` so off-chain relayers (this module, plus CoW's own internal autopilot indexer) can pick up the order. | -| **RPC Node** | Delivers the `OrderPlacement` log via the persistent WebSocket subscription. No `eth_call` is needed in this flow — the log contains everything required to reconstruct the order. | +| **RPC Node** | Delivers the `OrderPlacement` log via the persistent WebSocket subscription. No `eth_call` is needed in this flow - the log contains everything required to reconstruct the order. | | **EventLoop** | Routes the log to the eth-flow module based on the `[[subscription]]` entry in its `module.toml` (matching the `CoWSwapEthFlow` contract address and the `OrderPlacement` topic). | | **eth-flow module (WASM guest)** | Contains the entire EthFlow relay logic: decoding, OrderCreation construction, submission, error handling. No polling loop; one log equals one submission attempt. | | **alloy_sol_types (in module)** | Decodes the `OrderPlacement` event in module-side Rust. The event payload carries the typed `GPv2OrderData`, the EIP-1271 signature blob, and the extra data field. | @@ -420,11 +420,11 @@ flowchart TD | Node | What it does | |---|---| -| **WASM Module** | The guest program. It calls imported WIT functions exactly like regular function calls — it has no visibility into the host machinery behind them. | -| **wasmtime Linker** | `Linker` built once at startup. `wasmtime::component::bindgen!` generates a `Shepherd` world struct and one trait per WIT interface (e.g. `shepherd::cow::cow_api::Host`, `nexum::host::local_store::Host`). `Shepherd::add_to_linker(&mut linker, \|state\| state)` registers every trait method as a host function. After that, calls from WASM resolve with zero dynamic dispatch overhead — the vtable is built at link time, not per-call. | -| **HostState — manifest.required check** | Before dispatching, `HostState` checks that the called capability is listed under `[capabilities].required` in the module's `module.toml`. If not, it returns `host-error { kind: denied }` immediately. The 0.2 engine validates known capability names at boot via `KNOWN_CAPABILITIES`; per-call gating is the M2 target. | +| **WASM Module** | The guest program. It calls imported WIT functions exactly like regular function calls - it has no visibility into the host machinery behind them. | +| **wasmtime Linker** | `Linker` built once at startup. `wasmtime::component::bindgen!` generates a `Shepherd` world struct and one trait per WIT interface (e.g. `shepherd::cow::cow_api::Host`, `nexum::host::local_store::Host`). `Shepherd::add_to_linker(&mut linker, \|state\| state)` registers every trait method as a host function. After that, calls from WASM resolve with zero dynamic dispatch overhead - the vtable is built at link time, not per-call. | +| **HostState - manifest.required check** | Before dispatching, `HostState` checks that the called capability is listed under `[capabilities].required` in the module's `module.toml`. If not, it returns `host-error { kind: denied }` immediately. The 0.2 engine validates known capability names at boot via `KNOWN_CAPABILITIES`; per-call gating is the M2 target. | | **tracing::info!** | Every host call emits a structured trace event (capability name, chain id, etc.). Operators use `RUST_LOG=shepherd=debug` to see every call a module makes. | -| **host backend Rust function** | `HostState` implements one generated trait per WIT interface. Each `async fn` in the trait receives `&mut self` (giving access to all host resources) and returns the WIT-mapped Rust type. There are no CoW-strategy-specific backends — only the universal ones plus `cow-api` (ADR-0006). | +| **host backend Rust function** | `HostState` implements one generated trait per WIT interface. Each `async fn` in the trait receives `&mut self` (giving access to all host resources) and returns the WIT-mapped Rust type. There are no CoW-strategy-specific backends - only the universal ones plus `cow-api` (ADR-0006). | | **OrderBookPool** | Looks up the `OrderBookApi` client for the requested chain and calls `post_order`. Returns a 56-byte `OrderUid` on success or an `OrderPostError`-bearing host error on failure. | | **ProviderPool (chain.request)** | Looks up the alloy provider for the requested chain and dispatches the JSON-RPC call (`eth_call`, `eth_getLogs`, etc.). | | **engine-managed streams (chain.subscribe-*)** | Subscriptions are not exposed as runtime-callable host functions in 0.2. They are opened by the engine at boot from each module's declared `[[subscription]]` entries; events flow into the module via `on_event`. Dynamic `register-address` for factory patterns is deferred (ADR-0008). | @@ -437,7 +437,7 @@ flowchart TD ```mermaid graph TD - upstream["cowdao-grants/cow-rs\n(alpha.3 on crates.io — PR #5 base)"] + upstream["cowdao-grants/cow-rs\n(alpha.3 on crates.io - PR #5 base)"] bleu_cr["bleu/cow-rs\n(PR #5 head branch)"] prims["Protocol primitives added to PR #5:\n• OrderPostError + retry_hint\n• OrderBookApi::with_base_url\n• wasm32 feature-gate"] existing["Already in PR #5:\nOrder · OrderCreation · OrderUid\nsigning schemes · OrderBookApi"] @@ -462,11 +462,11 @@ graph TD | Node | What it is | |---|---| -| **cowdao-grants/cow-rs** | The upstream CoW Protocol Rust SDK, maintained by the DAO. Version `alpha.3` is published to crates.io but predates 18 follow-up commits Bleu has been pushing through PR #5. This is the PR base — changes land here eventually. | -| **bleu/cow-rs** | Bleu's repository, which is simultaneously the head branch of the DAO's open PR #5. Every commit Bleu pushes here also advances PR #5 for upstream review. This is not a long-lived parallel fork — it is the active PR branch (ADR-0004). | -| **Protocol primitives added to PR #5** | The three additions Bleu is pushing into PR #5: `OrderPostError` rich variants + `retry_hint()` (critical for module error handling), `OrderBookApi::with_base_url` (barn / staging / forked deployments), and `wasm32` feature-gating (critical so guest modules can consume `cowprotocol` types). All three are protocol primitives — they describe what CoW Protocol *is*, not how a particular strategy uses it. TWAP polling and EthFlow event decoding are explicitly *not* added here; they stay in module code (ADR-0007). | +| **cowdao-grants/cow-rs** | The upstream CoW Protocol Rust SDK, maintained by the DAO. Version `alpha.3` is published to crates.io but predates 18 follow-up commits Bleu has been pushing through PR #5. This is the PR base - changes land here eventually. | +| **bleu/cow-rs** | Bleu's repository, which is simultaneously the head branch of the DAO's open PR #5. Every commit Bleu pushes here also advances PR #5 for upstream review. This is not a long-lived parallel fork - it is the active PR branch (ADR-0004). | +| **Protocol primitives added to PR #5** | The three additions Bleu is pushing into PR #5: `OrderPostError` rich variants + `retry_hint()` (critical for module error handling), `OrderBookApi::with_base_url` (barn / staging / forked deployments), and `wasm32` feature-gating (critical so guest modules can consume `cowprotocol` types). All three are protocol primitives - they describe what CoW Protocol *is*, not how a particular strategy uses it. TWAP polling and EthFlow event decoding are explicitly *not* added here; they stay in module code (ADR-0007). | | **Already in PR #5** | The types and orderbook client Bleu's modules consume but did not add: `Order`, `OrderCreation`, `OrderUid`, signing-scheme enums, and `OrderBookApi`. These existed in PR #5 before the M2 work. | | **[patch.crates-io]** | A single line in the workspace `Cargo.toml` that tells Cargo to use `bleu/cow-rs` at a specific git rev instead of the `alpha.3` release on crates.io. Bumping the rev is the only change needed to pick up a new primitive after it is pushed to `bleu/cow-rs` (ADR-0004). | -| **nexum-engine** | The engine binary. Contains the WIT host implementations, Supervisor, EventLoop, config loaders, and alloy/redb integration. Contains no CoW Protocol logic — protocol primitives live in `bleu/cow-rs`; strategy logic lives in guest modules. | +| **nexum-engine** | The engine binary. Contains the WIT host implementations, Supervisor, EventLoop, config loaders, and alloy/redb integration. Contains no CoW Protocol logic - protocol primitives live in `bleu/cow-rs`; strategy logic lives in guest modules. | | **shepherd:cow/cow-api WIT** | The only CoW-specific WIT interface in 0.2. The engine implements it (host side); WASM modules import it (guest side). Backed by `OrderBookPool` (and through that, `OrderBookApi` from `cow-rs`). | | **WASM modules (twap · eth-flow)** | The grant deliverables. Compiled to `.wasm` Component Model binaries. Import only universal WIT interfaces (`chain`, `local-store`, `logging`) plus `shepherd:cow/cow-api`. Consume `cowprotocol` types directly through the wasm32 feature for building `OrderCreation` and pattern-matching on `OrderPostError`. Contain all TWAP and EthFlow strategy logic themselves (ADR-0006). | diff --git a/docs/migration/0.1-to-0.2.md b/docs/migration/0.1-to-0.2.md index 8a036ab..ece1be7 100644 --- a/docs/migration/0.1-to-0.2.md +++ b/docs/migration/0.1-to-0.2.md @@ -4,14 +4,14 @@ Nexum 0.2 is a single coordinated breaking-change release. It does the renames, This guide is written for two audiences: -- **Module authors** — you write WASM components that import the Nexum WIT. -- **Host embedders** — you build the runtime that loads modules (the server daemon, a mobile wallet, a browser host). +- **Module authors** - you write WASM components that import the Nexum WIT. +- **Host embedders** - you build the runtime that loads modules (the server daemon, a mobile wallet, a browser host). Each section is tagged `[author]`, `[embedder]`, or `[both]`. --- -## TL;DR — what changed [both] +## TL;DR - what changed [both] | Area | 0.1 | 0.2 | |---|---|---| @@ -30,9 +30,9 @@ Each section is tagged `[author]`, `[embedder]`, or `[both]`. | Manifest field | `wasm = "sha256:..."` | `component = "sha256:..."` | | Manifest section | `[[subscribe]]` | `[[subscription]]` | | Config type | `list>` (stringified) | unchanged in 0.2; typed variant on the 0.3 roadmap | -| New capabilities | — | `clock`, `random`, `http` (allowlisted) | -| New RPC method | — | `chain::request-batch` (additive) | -| New world | — | `query-module` (experimental, no host impl shipped) | +| New capabilities | - | `clock`, `random`, `http` (allowlisted) | +| New RPC method | - | `chain::request-batch` (additive) | +| New world | - | `query-module` (experimental, no host impl shipped) | If you only do four things: update your `nexum.toml`, run the sed cheat-sheet at the bottom, replace your error handling with the new `host-error` taxonomy, and declare your capabilities explicitly. Everything else is mechanical. @@ -138,7 +138,7 @@ The five 0.1 error shapes (`json-rpc-error`, `identity-error`, `msg-error`, `sto interface types { record host-error { domain: string, // "chain" | "store" | "messaging" | "identity" | "cow" | ... - kind: host-error-kind, // normative discriminant — see below + kind: host-error-kind, // normative discriminant - see below code: s32, // domain-specific message: string, data: option, // JSON for richer context @@ -174,7 +174,7 @@ interface types { + } ``` -`local-store` errors are no longer bare `string`s. The same `host-error` shape applies — `domain: "store"`, `kind` indicates whether you hit a quota, the key doesn't exist (for write-conditional ops), etc. +`local-store` errors are no longer bare `string`s. The same `host-error` shape applies - `domain: "store"`, `kind` indicates whether you hit a quota, the key doesn't exist (for write-conditional ops), etc. Module export signatures also change: @@ -239,7 +239,7 @@ If any code, docs, or scripts reference `shepherd.toml`, change to `nexum.toml`. ### Capability declaration (new, required) -In 0.1 the world declared which interfaces a module imported, and instantiation failed if any were unsatisfied. In 0.2, imports declared `optional` in the manifest install a trap stub on the host side — calling them returns `host-error { kind: unsupported }` rather than failing instantiation. +In 0.1 the world declared which interfaces a module imported, and instantiation failed if any were unsatisfied. In 0.2, imports declared `optional` in the manifest install a trap stub on the host side - calling them returns `host-error { kind: unsupported }` rather than failing instantiation. ```toml [capabilities] @@ -254,11 +254,11 @@ allow = ["api.coingecko.com", "discord.com"] methods = ["sign-typed-data"] # subset of identity surface used ``` -If you omit `[capabilities]` entirely, 0.2 falls back to "all imports required" — same as 0.1 behaviour — and prints a deprecation warning at load. Add the section in your next module update; the implicit-all fallback will be removed in 0.3. +If you omit `[capabilities]` entirely, 0.2 falls back to "all imports required" - same as 0.1 behaviour - and prints a deprecation warning at load. Add the section in your next module update; the implicit-all fallback will be removed in 0.3. ### Config: unchanged in 0.2 -`[config]` values continue to flow through to the guest as `list>` — the host flattens TOML scalars (numbers, booleans) to their string form on the way through, same as 0.1. If you currently parse `"50"` into `u64`, that code continues to work unchanged: +`[config]` values continue to flow through to the guest as `list>` - the host flattens TOML scalars (numbers, booleans) to their string form on the way through, same as 0.1. If you currently parse `"50"` into `u64`, that code continues to work unchanged: ```rust let bps: u64 = config.iter() @@ -373,7 +373,7 @@ interface chain { } ``` -Additive. The alloy-backed `HostTransport` now routes `RequestPacket::Batch` through `request-batch` — your existing `provider.multicall(...).await` actually batches on the wire in 0.2 (it didn't in 0.1, despite the docs). +Additive. The alloy-backed `HostTransport` now routes `RequestPacket::Batch` through `request-batch` - your existing `provider.multicall(...).await` actually batches on the wire in 0.2 (it didn't in 0.1, despite the docs). --- @@ -441,7 +441,7 @@ The Rust API surface is otherwise unchanged in 0.2. The C ABI and `nexum-host` e ### Non-Rust SDKs -The WIT renames propagate mechanically through `wit-bindgen`. Regenerate your bindings against the 0.2 WIT and your existing call sites — adjusted for the renames in §1 — will type-check. +The WIT renames propagate mechanically through `wit-bindgen`. Regenerate your bindings against the 0.2 WIT and your existing call sites - adjusted for the renames in §1 - will type-check. --- @@ -453,7 +453,7 @@ For mechanical search/replace in your codebase. Apply in order; some replacement # WIT package rg -l 'web3:runtime' | xargs sed -i 's/web3:runtime/nexum:host/g' -# Interface names (do these before function names — some functions reference the old interface in paths) +# Interface names (do these before function names - some functions reference the old interface in paths) rg -l '\bcsn\b' | xargs sed -i 's/\bcsn\b/chain/g' rg -l '\bmsg\b' | xargs sed -i 's/\bmsg\b/messaging/g' @@ -489,7 +489,7 @@ rg -l '\[\[subscribe\]\]' | xargs sed -i 's/\[\[subscribe\]\]/[[subscription]]/g rg -l '^wasm = ' | xargs sed -i 's/^wasm = /component = /' ``` -Things that **cannot** be sedded — do these by hand: +Things that **cannot** be sedded - do these by hand: - `timer(u64)` → `tick(tick)` with the new `tick { fired-at: u64 }` record. Call sites that pattern-match `Event::Timer(ts)` become `Event::Tick(tick) => tick.fired_at`. - Error handling. The five old error types are gone; you can't mechanically rewrite a `match` against `JsonRpcError { code, .. }` into the new `HostError { kind, .. }` discriminant. Do these per-call-site. @@ -505,7 +505,7 @@ After running the renames: - [ ] `cargo check --workspace --all-targets` is clean (Rust + bindings). - [ ] `cargo check --target wasm32-wasip2 -p ` is clean. - [ ] `cargo test --workspace --no-fail-fast` passes. -- [ ] Your bindgen invocations point at the package's own WIT dir (`wit/nexum-host/`) — or, when consuming both `nexum:host` and a domain-extension package, list both paths explicitly. The 0.1 vendored `deps/` pattern is no longer used in the reference repo. +- [ ] Your bindgen invocations point at the package's own WIT dir (`wit/nexum-host/`) - or, when consuming both `nexum:host` and a domain-extension package, list both paths explicitly. The 0.1 vendored `deps/` pattern is no longer used in the reference repo. - [ ] `nexum.toml` has a `[capabilities]` section listing what the module uses. - [ ] `nexum.toml` references `component = "sha256:..."` not `wasm = ...`. - [ ] All `[[subscribe]]` sections renamed to `[[subscription]]` with `kind` (not `type`). diff --git a/docs/sdk.md b/docs/sdk.md index d9dbbed..11bed4b 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -14,7 +14,7 @@ RUSTDOCFLAGS="-D warnings -D missing-docs" cargo doc -p shepherd-sdk --no-deps - ## Supported host capabilities -`shepherd-sdk` is host-neutral — it does not call wit-bindgen- +`shepherd-sdk` is host-neutral - it does not call wit-bindgen- generated functions directly. Instead, it exposes traits that mirror the on-the-wire host interfaces, and modules adapt their wit-bindgen imports to the traits at the cdylib boundary. The traits in @@ -36,34 +36,34 @@ seam one-for-one. ## Modules -- [`prelude`](../target/doc/shepherd_sdk/prelude/index.html) — bulk +- [`prelude`](../target/doc/shepherd_sdk/prelude/index.html) - bulk re-exports. `use shepherd_sdk::prelude::*;` and every module path resolves: alloy primitives (`Address`, `B256`, `Bytes`, `U256`, `keccak256`) plus cowprotocol order / signing / orderbook surface. -- [`cow`](../target/doc/shepherd_sdk/cow/index.html) — CoW Protocol +- [`cow`](../target/doc/shepherd_sdk/cow/index.html) - CoW Protocol bridging: - - `cow::order::gpv2_to_order_data` — convert the on-chain + - `cow::order::gpv2_to_order_data` - convert the on-chain `GPv2OrderData` (12-field Solidity tuple with bytes32 markers) into the typed `OrderData` shape the orderbook signs against. - `cow::composable::PollOutcome` + `cow::composable::decode_revert` - — typed dispatch over the five `IConditionalOrder` custom errors + - typed dispatch over the five `IConditionalOrder` custom errors (`OrderNotValid`, `PollTryNextBlock`, `PollTryAtBlock`, `PollTryAtEpoch`, `PollNever`). - - `cow::error::RetryAction` + `cow::error::classify_api_error` — + - `cow::error::RetryAction` + `cow::error::classify_api_error` - map `cow_api::submit_order` failures into `TryNextBlock` / `Backoff(s)` / `Drop`. -- [`chain`](../target/doc/shepherd_sdk/chain/index.html) — `eth_call` +- [`chain`](../target/doc/shepherd_sdk/chain/index.html) - `eth_call` JSON plumbing: - - `chain::eth_call_params(to, data)` — build the `[{to, data}, + - `chain::eth_call_params(to, data)` - build the `[{to, data}, "latest"]` params array. - - `chain::parse_eth_call_result(json)` — parse the `"0x..."` hex + - `chain::parse_eth_call_result(json)` - parse the `"0x..."` hex response into bytes. - - `chain::decode_revert_hex(s)` — `host-error.data` hex blob -> + - `chain::decode_revert_hex(s)` - `host-error.data` hex blob -> typed `PollOutcome`. -- [`host`](../target/doc/shepherd_sdk/host/index.html) — host trait +- [`host`](../target/doc/shepherd_sdk/host/index.html) - host trait seam plus the SDK's host-neutral `HostError` (same field shape as wit-bindgen's, bridged via one-liner `From` impls per module). diff --git a/docs/tutorial-first-module.md b/docs/tutorial-first-module.md index 84311da..777180c 100644 --- a/docs/tutorial-first-module.md +++ b/docs/tutorial-first-module.md @@ -40,7 +40,7 @@ cargo run -p nexum-engine -- \ modules/example/nexum.toml ``` -You should see two log lines from the example module — one in +You should see two log lines from the example module - one in `init`, one on the synthetic block event. Stop here and triage if the build fails or those log lines do not appear; the rest of the tutorial assumes a working local engine. @@ -81,13 +81,13 @@ shepherd-sdk-test = { path = "../../../crates/shepherd-sdk-test" } Note the four key features: -- **`crate-type = ["cdylib"]`** — produces a WASM Component when +- **`crate-type = ["cdylib"]`** - produces a WASM Component when built for `wasm32-wasip2`. -- **`shepherd-sdk` path dep** — brings in the helpers (`cow::`, +- **`shepherd-sdk` path dep** - brings in the helpers (`cow::`, `chain::`, `host::`, `prelude`). -- **`shepherd-sdk-test` as a dev-dep** — `MockHost` + assertion +- **`shepherd-sdk-test` as a dev-dep** - `MockHost` + assertion helpers, only linked under `cargo test`. -- **No direct `nexum-engine` dep** — modules never link the engine; +- **No direct `nexum-engine` dep** - modules never link the engine; they communicate via wit-bindgen-generated shims. Add the new crate to the workspace `members` list in `Cargo.toml` @@ -102,7 +102,7 @@ members = [ ``` `cargo check --target wasm32-wasip2 -p stop-loss` should fail with -"no library targets found" — expected, you have not written any +"no library targets found" - expected, you have not written any source yet. ## 2. Author the manifest (10 minutes) @@ -148,7 +148,7 @@ valid_to_seconds = "4294967295" # u32::MAX (no expiry) Two patterns worth noting: - **`required` matches the WIT imports the module uses.** The - engine enforces this at instantiation — declaring a capability + engine enforces this at instantiation - declaring a capability the module does not use is fine; missing a capability the module does use is a hard error. - **`[config]` values are stringly-typed in 0.2.** Your `init` @@ -160,7 +160,7 @@ Two patterns worth noting: The strategy logic splits into two layers: - A pure function that takes `&impl Host` and runs the decision - tree. This is what your tests exercise — no `wit-bindgen`, no + tree. This is what your tests exercise - no `wit-bindgen`, no `wasmtime`, fast iteration. - A thin `Guest` impl in `lib.rs` that adapts the wit-bindgen- generated host imports into a struct implementing @@ -261,7 +261,7 @@ The shape to internalise: and assert on the side effects (calls + log lines + state writes). - **Errors propagate but the loop should not abort on transient failure.** Wrap upstream calls so a single bad event does not - poison the supervisor — see `price-alert`'s warn-and-return + poison the supervisor - see `price-alert`'s warn-and-return pattern. ### 3b. The Guest adapter (15 minutes) @@ -491,7 +491,7 @@ mod tests { ``` Run with `cargo test -p stop-loss`. Both tests should pass on a -plain host — no wasm toolchain involved. +plain host - no wasm toolchain involved. The takeaway: any time you can express a behaviour as "given this host state, do that", the `MockHost` route is faster to iterate @@ -505,7 +505,7 @@ ls -lh target/wasm32-wasip2/release/stop_loss.wasm ``` Expected size: 250–350 KB. If it ballooned past ~500 KB, look at -`cargo tree -p stop-loss --target wasm32-wasip2` — usually a fresh +`cargo tree -p stop-loss --target wasm32-wasip2` - usually a fresh dependency pulled `reqwest` or `tokio` into the wasm graph. ## 5. Wire `engine.toml` and run it (10 minutes) @@ -546,7 +546,7 @@ imports the strategy actually uses. (see [`docs/deployment.md`](./deployment.md)). - **Real order assembly**: the `build_order_body` `todo!` in §3a is the only piece this tutorial elided. Cross-reference - [`modules/twap-monitor/src/lib.rs::build_order_creation`] — + [`modules/twap-monitor/src/lib.rs::build_order_creation`] - it's the canonical assembly path (`cowprotocol::OrderCreation::from_signed_order_data` + `serde_json::to_vec`). diff --git a/modules/ethflow-watcher/Cargo.toml b/modules/ethflow-watcher/Cargo.toml index 7e20023..929867b 100644 --- a/modules/ethflow-watcher/Cargo.toml +++ b/modules/ethflow-watcher/Cargo.toml @@ -14,4 +14,5 @@ cowprotocol = { version = "1.0.0-alpha.3", default-features = false } alloy-primitives = { version = "1.5", default-features = false, features = ["std"] } alloy-sol-types = { version = "1.5", default-features = false, features = ["std"] } serde_json = { version = "1", default-features = false, features = ["alloc"] } +thiserror = "2" wit-bindgen = { version = "0.57", default-features = false, features = ["macros", "realloc"] } diff --git a/modules/ethflow-watcher/module.toml b/modules/ethflow-watcher/module.toml index 9a78dfa..c169f9c 100644 --- a/modules/ethflow-watcher/module.toml +++ b/modules/ethflow-watcher/module.toml @@ -10,7 +10,7 @@ version = "0.1.0" component = "sha256:0000000000000000000000000000000000000000000000000000000000000000" [capabilities] -# Same set as twap-monitor for symmetry and future-proofing — the module +# Same set as twap-monitor for symmetry and future-proofing - the module # imports logging, local-store and cow-api today; `chain` is declared # because a follow-up may add an eth_call (e.g. to read the EthFlow # refund pointer) without churning the manifest. diff --git a/modules/ethflow-watcher/src/lib.rs b/modules/ethflow-watcher/src/lib.rs index 35d8e81..bbb12ff 100644 --- a/modules/ethflow-watcher/src/lib.rs +++ b/modules/ethflow-watcher/src/lib.rs @@ -1,5 +1,6 @@ // wit_bindgen::generate! expands to host-import shims whose arity matches // the WIT signatures, which can exceed clippy's too-many-arguments threshold. +#![cfg_attr(not(test), warn(unused_crate_dependencies))] #![allow(clippy::too_many_arguments)] wit_bindgen::generate!({ @@ -25,10 +26,10 @@ use shepherd::cow::cow_api; /// friendly through the submit path. #[derive(Debug)] struct DecodedPlacement { - /// EthFlow contract that emitted the event — also the EIP-1271 + /// EthFlow contract that emitted the event - also the EIP-1271 /// verifier `from` for the submitted `OrderCreation`. contract: Address, - /// Original native-token seller — logged for diagnostics; the + /// Original native-token seller - logged for diagnostics; the /// orderbook's `from` is the contract (EIP-1271 owner), not this. sender: Address, order: Box, @@ -69,7 +70,7 @@ impl Guest for EthFlowWatcher { /// /// Returns `None` when: /// - the log's contract address is neither `ETH_FLOW_PRODUCTION` nor -/// `ETH_FLOW_STAGING` (defensive — the host's `[[subscription]]` +/// `ETH_FLOW_STAGING` (defensive - the host's `[[subscription]]` /// filter already pins the address, but a misconfigured engine could /// still leak through); /// - topic0 does not match the event signature; or @@ -107,25 +108,16 @@ fn decode_order_placement( // ---- BLEU-833: submit + retry ---- -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] enum BuildError { + #[error("GPv2OrderData carried an unknown enum marker")] UnknownMarker, + #[error("OnchainSignature carried an unknown scheme variant")] UnknownSignatureScheme, + #[error("chain {0} is not supported by cowprotocol")] UnsupportedChain(u64), - Cowprotocol(cowprotocol::Error), -} - -impl core::fmt::Display for BuildError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::UnknownMarker => f.write_str("GPv2OrderData carried an unknown enum marker"), - Self::UnknownSignatureScheme => { - f.write_str("OnchainSignature carried an unknown scheme variant") - } - Self::UnsupportedChain(id) => write!(f, "chain {id} is not supported by cowprotocol"), - Self::Cowprotocol(e) => write!(f, "{e}"), - } - } + #[error(transparent)] + Cowprotocol(#[from] cowprotocol::Error), } /// Lift `OnchainSignature` into the orderbook-typed `Signature`. The @@ -145,7 +137,7 @@ fn to_signature(sig: &OnchainSignature) -> Option { /// Assemble `(OrderCreation, OrderUid)` from a placement. `from` is the /// EthFlow contract (EIP-1271 owner). `app_data` is fixed to -/// `EMPTY_APP_DATA_JSON` — placements pinning a real IPFS document get +/// `EMPTY_APP_DATA_JSON` - placements pinning a real IPFS document get /// rejected by `from_signed_order_data` (digest mismatch) and skipped, /// same scope limitation as the TWAP module. fn build_eth_flow_creation( @@ -163,8 +155,7 @@ fn build_eth_flow_creation( placement.contract, EMPTY_APP_DATA_JSON.to_string(), None, - ) - .map_err(BuildError::Cowprotocol)?; + )?; Ok((creation, uid)) } @@ -188,7 +179,7 @@ fn submit_placement(chain_id: u64, placement: &DecodedPlacement) -> Result<(), H // OrderPlacement log; without the guard we would attempt a second // submit, the orderbook would reject `DuplicateOrder` (permanent), // and we would end up with both `submitted:` AND `dropped:` written - // for the same UID. `backoff:` is *not* a short-circuit — a previous + // for the same UID. `backoff:` is *not* a short-circuit - a previous // transient error deserves a fresh attempt on re-delivery. match prior_outcome(&uid_hex)? { PriorOutcome::Submitted => { @@ -281,7 +272,7 @@ fn apply_submit_retry(err: &HostError, uid_hex: &str) -> Result<(), HostError> { RetryAction::Drop => { local_store::set(&format!("dropped:{uid_hex}"), b"")?; // Clear `backoff:` if a prior transient attempt left it - // behind — the terminal `dropped:` flag now supersedes it, + // behind - the terminal `dropped:` flag now supersedes it, // and we want at most one "outcome" marker per UID at rest. let _ = local_store::delete(&format!("backoff:{uid_hex}")); logging::log( diff --git a/modules/example/module.toml b/modules/example/module.toml index e17a547..528c84b 100644 --- a/modules/example/module.toml +++ b/modules/example/module.toml @@ -1,4 +1,4 @@ -# Example module manifest — exercises the 0.2 manifest schema end-to-end. +# Example module manifest - exercises the 0.2 manifest schema end-to-end. [module] name = "example" diff --git a/modules/example/nexum.toml b/modules/example/nexum.toml index e17a547..528c84b 100644 --- a/modules/example/nexum.toml +++ b/modules/example/nexum.toml @@ -1,4 +1,4 @@ -# Example module manifest — exercises the 0.2 manifest schema end-to-end. +# Example module manifest - exercises the 0.2 manifest schema end-to-end. [module] name = "example" diff --git a/modules/example/src/lib.rs b/modules/example/src/lib.rs index a008a3d..832f596 100644 --- a/modules/example/src/lib.rs +++ b/modules/example/src/lib.rs @@ -1,5 +1,6 @@ // wit_bindgen::generate! expands to host-import shims whose arity matches // the WIT signatures, which can exceed clippy's too-many-arguments threshold. +#![cfg_attr(not(test), warn(unused_crate_dependencies))] #![allow(clippy::too_many_arguments)] wit_bindgen::generate!({ diff --git a/modules/examples/balance-tracker/Cargo.toml b/modules/examples/balance-tracker/Cargo.toml index 60271b9..5fe8607 100644 --- a/modules/examples/balance-tracker/Cargo.toml +++ b/modules/examples/balance-tracker/Cargo.toml @@ -11,5 +11,4 @@ crate-type = ["cdylib"] [dependencies] shepherd-sdk = { path = "../../../crates/shepherd-sdk" } -alloy-primitives = { version = "1.5", default-features = false, features = ["std"] } wit-bindgen = { version = "0.57", default-features = false, features = ["macros", "realloc"] } diff --git a/modules/examples/balance-tracker/src/lib.rs b/modules/examples/balance-tracker/src/lib.rs index 65f5a2c..32c68b2 100644 --- a/modules/examples/balance-tracker/src/lib.rs +++ b/modules/examples/balance-tracker/src/lib.rs @@ -23,6 +23,7 @@ //! change_threshold = "100000000000000000" # 0.1 ETH //! ``` +#![cfg_attr(not(test), warn(unused_crate_dependencies))] #![allow(clippy::too_many_arguments)] wit_bindgen::generate!({ @@ -33,7 +34,7 @@ wit_bindgen::generate!({ use std::sync::OnceLock; -use alloy_primitives::{Address, U256}; +use shepherd_sdk::prelude::{Address, U256}; use nexum::host::types::HostErrorKind; use nexum::host::{chain, local_store, logging, types}; @@ -82,7 +83,7 @@ impl Guest for BalanceTracker { if let types::Event::Block(block) = event { for addr in &s.addresses { if let Err(err) = check_one(block.chain_id, *addr, s.change_threshold) { - // Surface but do not propagate — a single flaky + // Surface but do not propagate - a single flaky // eth_getBalance shouldn't stop the loop. logging::log( logging::Level::Warn, @@ -110,7 +111,7 @@ fn check_one(chain_id: u64, addr: Address, threshold: U256) -> Result<(), HostEr if abs_diff(current, prior) >= threshold { // Distinguish first-seen (prior == ZERO and we have no - // record) from a real change — the Warn line carries the + // record) from a real change - the Warn line carries the // delta direction so an operator can grep. let direction = if current > prior { "+" } else { "-" }; logging::log( @@ -227,7 +228,7 @@ export!(BalanceTracker); #[cfg(test)] mod tests { use super::*; - use alloy_primitives::address; + use shepherd_sdk::prelude::address; #[test] fn parse_balance_hex_decodes_canonical_response() { diff --git a/modules/examples/price-alert/src/lib.rs b/modules/examples/price-alert/src/lib.rs index cf61d74..7f1b5b2 100644 --- a/modules/examples/price-alert/src/lib.rs +++ b/modules/examples/price-alert/src/lib.rs @@ -31,6 +31,7 @@ // wit_bindgen::generate! expands to host-import shims whose arity matches // the WIT signatures, which can exceed clippy's too-many-arguments threshold. +#![cfg_attr(not(test), warn(unused_crate_dependencies))] #![allow(clippy::too_many_arguments)] wit_bindgen::generate!({ @@ -49,7 +50,7 @@ use nexum::host::types::HostErrorKind; use nexum::host::{chain, logging, types}; sol! { - /// Chainlink AggregatorV3Interface — only the function this + /// Chainlink AggregatorV3Interface - only the function this /// module needs. interface AggregatorV3 { function latestRoundData() external view returns ( @@ -102,7 +103,7 @@ impl Guest for PriceAlert { cfg.every_n_blocks, ), ); - // OnceLock::set fails only if already set — in a + // OnceLock::set fails only if already set - in a // single-init module that means a re-entry from the // supervisor, which is not a hard error; we keep the // first parse. @@ -137,7 +138,7 @@ impl Guest for PriceAlert { /// Build + dispatch the `latestRoundData` eth_call. Result is /// logged: Info if the threshold is not crossed, Warn if it is. /// Returns nothing so a single bad RPC reply does not propagate -/// into the supervisor — the next block re-polls. +/// into the supervisor - the next block re-polls. fn poll_oracle(chain_id: u64, cfg: &Settings) { let call_data = AggregatorV3::latestRoundDataCall {}.abi_encode(); let params = eth_call_params(&cfg.oracle_address, &call_data); @@ -189,7 +190,7 @@ fn poll_oracle(chain_id: u64, cfg: &Settings) { } /// `true` when `answer` is on the firing side of `threshold` per -/// `direction`. Pure — exercised by the unit tests. +/// `direction`. Pure - exercised by the unit tests. fn classify(answer: I256, threshold: I256, direction: Direction) -> bool { match direction { Direction::Above => answer >= threshold, @@ -279,7 +280,7 @@ fn scale_threshold(threshold_decimal: &str, decimals: u32) -> Result], @@ -194,7 +195,7 @@ fn poll_one(chain_id: u64, owner: &Address, params: &ConditionalOrderParams) -> /// Decode a successful `getTradeableOrderWithSignature` return into /// `Ready { order, signature }`. The wire format is `abi.encode(order, -/// signature)` — the canonical Solidity return tuple — so the two-tuple +/// signature)` - the canonical Solidity return tuple - so the two-tuple /// parameter decode lines up. fn decode_return(data: &[u8]) -> Option { let (order, signature) = <(GPv2OrderData, Bytes)>::abi_decode_params(data).ok()?; @@ -259,37 +260,30 @@ fn read_u64(key: &str) -> Result, HostError> { /// place so the next poll can either re-construct or transition on /// its own (the typical case is the conditional order's `app_data` /// pinning a non-empty IPFS document we cannot resolve). -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] enum BuildError { /// `GPv2OrderData` carried a marker (`kind`, balance enum) we don't /// know how to map. + #[error("GPv2OrderData carried an unknown enum marker")] UnknownMarker, - /// `cowprotocol` rejected the body — typically `keccak256(app_data) + /// `cowprotocol` rejected the body - typically `keccak256(app_data) /// != order.app_data` or `from == Address::ZERO`. - Cowprotocol(cowprotocol::Error), -} - -impl core::fmt::Display for BuildError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::UnknownMarker => f.write_str("GPv2OrderData carried an unknown enum marker"), - Self::Cowprotocol(e) => write!(f, "{e}"), - } - } + #[error(transparent)] + Cowprotocol(#[from] cowprotocol::Error), } /// Assemble the `OrderCreation` body the orderbook expects from a /// freshly-polled TWAP tranche. /// /// `signature` is the EIP-1271 blob `ComposableCoW. -/// getTradeableOrderWithSignature` returns — in orderbook wire form +/// getTradeableOrderWithSignature` returns - in orderbook wire form /// (raw verifier bytes; the orderbook re-prepends `from` before /// settlement). `from` is the watch owner. /// /// `app_data` is left at `EMPTY_APP_DATA_JSON`. Conditional orders that /// pin a non-empty IPFS document get rejected by /// `from_signed_order_data` (digest mismatch) and the watch is left in -/// place — resolving the document is a future concern. +/// place - resolving the document is a future concern. fn build_order_creation( order: &GPv2OrderData, signature: Bytes, @@ -297,14 +291,14 @@ fn build_order_creation( ) -> Result { let order_data = gpv2_to_order_data(order).ok_or(BuildError::UnknownMarker)?; let signature = Signature::Eip1271(signature.to_vec()); - OrderCreation::from_signed_order_data( + let creation = OrderCreation::from_signed_order_data( &order_data, signature, from, EMPTY_APP_DATA_JSON.to_string(), None, - ) - .map_err(BuildError::Cowprotocol) + )?; + Ok(creation) } fn submit_ready( @@ -338,7 +332,7 @@ fn submit_ready( match cow_api::submit_order(chain_id, &body) { Ok(uid) => { let key = format!("submitted:{uid}"); - // Empty marker — presence of the key is the receipt. BLEU-830 + // Empty marker - presence of the key is the receipt. BLEU-830 // may later attach metadata (block, attempt count) but the // bare flag is enough to suppress double submits. local_store::set(&key, b"")?; @@ -414,7 +408,7 @@ enum WatchUpdate { /// Write `next_epoch:` so subsequent polls skip until the given /// Unix-seconds timestamp is reached. SetNextEpoch(u64), - /// Delete the watch and any stale gate keys — TWAP completed, + /// Delete the watch and any stale gate keys - TWAP completed, /// cancelled, or otherwise irrecoverable. DropWatch, } @@ -659,7 +653,7 @@ mod tests { // Ready never reaches outcome_to_update in poll_all_watches (the // match routes it to submit_ready). The mapping is a safety net: // if a future refactor accidentally pipes Ready through here, the - // watch must NOT be erased — submit_ready owns the post-submit + // watch must NOT be erased - submit_ready owns the post-submit // book-keeping. let order = Box::new(submittable_order()); let outcome = PollOutcome::Ready {