From 818f9d04aa3799f819d739a1722761e39be0f2bc Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 30 May 2026 10:53:54 +0000 Subject: [PATCH 01/17] docs: add 0.1 to 0.2 migration guide --- docs/migration/0.1-to-0.2.md | 527 +++++++++++++++++++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100644 docs/migration/0.1-to-0.2.md diff --git a/docs/migration/0.1-to-0.2.md b/docs/migration/0.1-to-0.2.md new file mode 100644 index 0000000..6bfe8ac --- /dev/null +++ b/docs/migration/0.1-to-0.2.md @@ -0,0 +1,527 @@ +# Migrating from Nexum 0.1 to 0.2 + +Nexum 0.2 is a single coordinated breaking-change release. It does the renames, the error-model unification, the missing primitives, and the capability-negotiation work in one window so module authors only pay the migration tax once. There will not be another breaking release of comparable scope before 1.0. + +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). + +Each section is tagged `[author]`, `[embedder]`, or `[both]`. + +--- + +## TL;DR — what changed [both] + +| Area | 0.1 | 0.2 | +|---|---|---| +| WIT package | `web3:runtime` | `nexum:runtime` | +| Consensus interface | `csn` | `chain` | +| Messaging interface | `msg` | `messaging` | +| Default world | `headless-module` | `event-module` | +| CoW world | `shepherd:cow/shepherd-module` | `shepherd:cow/shepherd` | +| CoW interfaces | `cow` + `order` | `cow-api` (merged) | +| Feed methods | `feed-get` / `feed-set` | `read-feed` / `write-feed` | +| Event variants | `block-data` / `log-entry` / `message-data` / `timer(u64)` | `block` / `log` / `message` / `tick { fired-at }` | +| Errors | 5 different shapes + bare `string` | single `host-error` with `host-error-kind` discriminant | +| Capabilities | All six imports mandatory | Manifest-negotiated, optional imports trap on call | +| Engine crate | `nxm-engine` | `nexum-engine` | +| Manifest file | `nexum.toml` (some docs said `shepherd.toml`) | `nexum.toml` (canonical) | +| Manifest field | `wasm = "sha256:..."` | `component = "sha256:..."` | +| Manifest section | `[[subscribe]]` | `[[subscription]]` | +| Config type | `list>` (stringified) | `list>` (typed variant) | +| 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. + +--- + +## 1. WIT renames [author] + +### Package rename + +```diff +- use web3:runtime/types.{config, event}; +- use web3:runtime/chain.{chain-id}; ++ use nexum:runtime/types.{config, event}; ++ use nexum:runtime/chain.{chain-id}; +``` + +Why: `web3:` precommitted the engine to crypto-only branding. The package is now named after the engine; web3-specific capabilities live inside it as interfaces. + +### Interface renames + +| 0.1 | 0.2 | Rationale | +|---|---|---| +| `csn` | `chain` | `csn` was unreadable; `chain.request(chainId, method, params)` reads itself. | +| `msg` | `messaging` | `msg` collided with its own `message` record; ambiguous in non-Rust bindings. | +| `cow` + `order` | `cow-api` (one interface) | `cow::cow::request` triple-stutter eliminated; `order::submit` merged as `cow-api::submit-order`. | + +### World renames + +```diff +- world headless-module { ++ world event-module { + import chain; + import identity; // NOTE: was missing from 0.1 WIT; now present + import local-store; + import remote-store; + import messaging; + import logging; + export init: func(config: config) -> result<_, string>; + export on-event: func(event: event) -> result<_, string>; + } +``` + +```diff +- world shepherd-module { +- include headless-module; +- import cow; +- import order; ++ world shepherd { ++ include event-module; ++ import cow-api; + } +``` + +### Function renames (verb-first, fully spelled) + +```diff + interface remote-store { +- feed-get: func(owner: list, topic: list) -> result>, store-error>; +- feed-set: func(topic: list, data: list) -> result, store-error>; ++ read-feed: func(owner: list, topic: list) -> result>, host-error>; ++ write-feed: func(topic: list, data: list) -> result, host-error>; + } +``` + +### Type and field renames + +```diff + interface types { +- record block-data { ... } +- record log-entry { ..., tx-hash: list, ... } +- record message-data { ... } +- variant event { +- block(block-data), +- logs(list), +- timer(u64), +- message(message-data), +- } ++ record block { ... } ++ record log { ..., transaction-hash: list, ... } ++ record message { ... } ++ record tick { fired-at: u64 } // milliseconds since Unix epoch, UTC ++ variant event { ++ block(block), ++ logs(list), ++ tick(tick), ++ message(message), ++ } + } +``` + +Two semantic notes: + +- 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 you do. +- `tick` (formerly `timer`) is now a record, not a bare `u64`. In bindings it reads `event.tick.firedAt` instead of `event.timer === 1700000000`. + +--- + +## 2. Error model unification [both] + +The five 0.1 error shapes (`json-rpc-error`, `identity-error`, `msg-error`, `store-error`, `api-error`) plus bare `string` errors collapse to one record: + +```wit +interface types { + record host-error { + domain: string, // "chain" | "store" | "messaging" | "identity" | "cow" | ... + kind: host-error-kind, // normative discriminant — see below + code: s32, // domain-specific + message: string, + data: option, // JSON for richer context + } + + variant host-error-kind { + unsupported, // host does not implement this capability + unavailable, // capability exists, backend is down/offline + denied, // user or policy rejected + rate-limited, + timeout, + invalid-input, + internal, // host bug + } +} +``` + +### Author migration + +```diff +- match chain::request(1, "eth_call", params) { +- Ok(s) => parse(s), +- Err(JsonRpcError { code, message, .. }) if code == -32000 => retry(), +- Err(e) => bail!("rpc failed: {}", e.message), +- } ++ use nexum_sdk::error::{HostError, HostErrorKind}; ++ match chain::request(1, "eth_call", params) { ++ Ok(s) => parse(s), ++ Err(HostError { kind: HostErrorKind::Unavailable, .. }) => retry(), ++ Err(HostError { kind: HostErrorKind::RateLimited, .. }) => backoff(), ++ Err(HostError { kind: HostErrorKind::Denied, .. }) => abort("user rejected"), ++ Err(e) => bail!("{}::{} ({}): {}", e.domain, e.code, e.kind, e.message), ++ } +``` + +`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: + +```diff +- export init: func(config: config) -> result<_, string>; +- export on-event: func(event: event) -> result<_, string>; ++ export init: func(config: config) -> result<_, host-error>; ++ export on-event: func(event: event) -> result<_, host-error>; +``` + +For module errors, set `domain` to your module name and pick the closest `kind`. The SDK provides `HostError::module(name, kind, message)` to make this ergonomic. + +### Embedder migration + +Hosts implementing capability traits must now return `HostError`, not protocol-specific error types. Map each backend failure to the right `kind`: + +| Backend signal | `host-error-kind` | +|---|---| +| Connection refused / DNS fail / offline | `unavailable` | +| Provider HTTP 4xx (other than 401/403/429) | `invalid-input` | +| Provider HTTP 401/403 | `denied` | +| Provider HTTP 429 | `rate-limited` | +| Provider HTTP 5xx / timeout | `unavailable` or `timeout` (prefer the more specific) | +| User rejected signing in wallet UI | `denied` | +| Module asked for a capability the host doesn't provide | `unsupported` | +| Bug / panic / internal invariant violated | `internal` | + +--- + +## 3. Manifest changes [both] + +### File rename + +If any code, docs, or scripts reference `shepherd.toml`, change to `nexum.toml`. This was a doc/code inconsistency in 0.1; canonical is `nexum.toml`. + +### Field and section renames + +```diff + [module] + name = "twap-monitor" + version = "0.3.0" +- wasm = "sha256:9f86d081..." ++ component = "sha256:9f86d081..." + + [module.resources] + max_memory_bytes = 10_485_760 + max_fuel_per_event = 100_000 + max_state_bytes = 52_428_800 + + [chains] + required = [42161] + +- [[subscribe]] +- type = "block" +- chain_id = 42161 ++ [[subscription]] ++ kind = "block" ++ chain_id = 42161 +``` + +`type` → `kind` because `type` is reserved in several binding languages. + +### 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. + +```toml +[capabilities] +required = ["chain", "local-store", "logging"] +optional = ["messaging", "remote-store"] # module continues if host doesn't provide +denied = [] # explicit "do not grant even if available" + +[capabilities.http] +allow = ["api.coingecko.com", "discord.com"] + +[capabilities.identity] +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. + +### Typed config + +`[config]` values are no longer flattened to strings: + +```toml +# 0.1: every value became a string at the guest +[config] +cow_api_url = "https://api.cow.fi/arbitrum" +slippage_bps = 50 +enable_alerts = true + +# 0.2: TOML types are preserved through the typed config variant +[config] +cow_api_url = "https://api.cow.fi/arbitrum" # string +slippage_bps = 50 # integer +enable_alerts = true # boolean +allow_list = ["arb1", "base"] # list of string +``` + +If you currently parse `"50"` into `u64`, that code becomes: + +```diff +- let bps: u64 = config.get("slippage_bps")?.parse()?; ++ let bps: u64 = config.get_int("slippage_bps")?; // typed accessor +``` + +Or with the derive macro: + +```rust +#[derive(NexumConfig)] +struct Config { + cow_api_url: String, + slippage_bps: u64, + enable_alerts: bool, + allow_list: Vec, +} + +let config: Config = Config::from_host(raw_config)?; +``` + +--- + +## 4. New capabilities (additive) [author] + +These didn't exist in 0.1 and don't break anything. Adopt them to remove workarounds. + +### `clock` + +```wit +interface clock { + now-ms: func() -> u64; // wall-clock ms since Unix epoch, UTC + monotonic-ns: func() -> u64; // for measuring elapsed +} +``` + +Replaces the 0.1 workaround of "only know the time inside `on_block` via `block.timestamp`." + +### `random` + +```wit +interface random { + fill: func(len: u32) -> list; +} +``` + +CSPRNG. Replaces the 0.1 workaround of "you can't, period." + +### `http` (allowlisted) + +```wit +interface http { + record request { + method: string, + url: string, + headers: list>, + body: option>, + } + record response { + status: u16, + headers: list>, + body: list, + } + fetch: func(req: request) -> result; +} +``` + +Requires a domain allowlist in `nexum.toml`: + +```toml +[capabilities.http] +allow = ["api.coingecko.com", "discord.com"] +``` + +Hosts MUST enforce the allowlist. The operator sees the union of granted domains at module load. This replaces the 0.1 anti-pattern of tunnelling alerts through Waku. + +### `chain::request-batch` + +```wit +interface chain { + request: func(chain-id: chain-id, method: string, params: string) + -> result; + + request-batch: func(chain-id: chain-id, calls: list>) + -> result>, host-error>; +} +``` + +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). + +--- + +## 5. New world: `query-module` (experimental) [author] + +A request/response world for modules that aren't event-driven (wallet rule evaluators, signature validators, pricing oracles). + +```wit +world query-module { + import local-store; + import logging; + // chain, identity, http, etc. are optional via manifest + + export init: func(config: config) -> result<_, host-error>; + export evaluate: func(input: list) -> result, host-error>; +} +``` + +**Status: WIT is published, no host implementation ships in 0.2.** The 0.2 server runtime only supports `event-module` and `shepherd`. The world is published so module authors can target it experimentally and so embedders building mobile/wallet hosts have a stable contract to implement against. Production support lands in 0.3. + +If you're writing a module that fits this shape, target it now and stub the host with `MockHost` for testing. + +--- + +## 6. Engine crate rename [embedder] + +```diff + [dependencies] +- nxm-engine = "0.1" ++ nexum-engine = "0.2" +``` + +The 0.1 release renamed `nexum-runtime` → `nxm-engine`. 0.2 reverses that to `nexum-engine` for consistency with `nexum-sdk`, `shepherd-sdk`, `cargo-nexum`. + +```diff +- use nxm_engine::{Engine, Module}; ++ use nexum_engine::{Engine, Module}; +``` + +The Rust API surface is otherwise unchanged in 0.2. The C ABI and `nexum-host` embedder facade (for non-Rust hosts) are explicitly **deferred to a later release** pending mobile validation; do not assume they exist in 0.2. + +--- + +## 7. SDK changes [author] + +### Rust SDK + +```diff +- use nexum_sdk::{provider, Identity, MsgClient, RemoteStore}; ++ use nexum_sdk::{provider, Signer, Messaging, RemoteStore}; +``` + +| 0.1 type | 0.2 type | Notes | +|---|---|---| +| `IdentityClient` | `Signer` | Trait renamed to reflect what it does | +| `MsgClient` | `Messaging` | Drops the meaningless `Client` suffix | +| `CowClient` | `Cow` | Same | +| `HostTransport` | (internal) | Now `pub(crate)`; you access it through `provider()` | +| `block_on` (re-export) | (removed from public API) | Hidden behind the `#[nexum::module]` macro | +| `Error` (multiple variants per domain) | `HostError` + `HostErrorKind` | Single shape; see §2 | + +### Proc macro + +`#[nexum::module]` and `#[shepherd::module]` are unchanged in shape. They now generate against `event-module` / `shepherd` worlds. If you targeted `headless-module` explicitly anywhere, rename to `event-module`. + +### 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. + +--- + +## 8. Mechanical rename cheat sheet [both] + +For mechanical search/replace in your codebase. Apply in order; some replacements depend on earlier ones. + +```bash +# WIT package +rg -l 'web3:runtime' | xargs sed -i 's/web3:runtime/nexum:runtime/g' + +# 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' + +# Worlds +rg -l 'headless-module' | xargs sed -i 's/headless-module/event-module/g' +rg -l 'headless_module' | xargs sed -i 's/headless_module/event_module/g' + +# CoW interface stutter +rg -l '\bcow::cow::' | xargs sed -i 's/\bcow::cow::/cow_api::/g' +# (manual: merge `order` imports into `cow-api`; rename `order::submit` to `cow-api::submit-order`) + +# Feed methods +rg -l '\bfeed-get\b' | xargs sed -i 's/\bfeed-get\b/read-feed/g' +rg -l '\bfeed-set\b' | xargs sed -i 's/\bfeed-set\b/write-feed/g' +rg -l '\bfeed_get\b' | xargs sed -i 's/\bfeed_get\b/read_feed/g' +rg -l '\bfeed_set\b' | xargs sed -i 's/\bfeed_set\b/write_feed/g' + +# Type renames +rg -l '\bblock-data\b' | xargs sed -i 's/\bblock-data\b/block/g' +rg -l '\blog-entry\b' | xargs sed -i 's/\blog-entry\b/log/g' +rg -l '\bmessage-data\b' | xargs sed -i 's/\bmessage-data\b/message/g' +rg -l '\btx-hash\b' | xargs sed -i 's/\btx-hash\b/transaction-hash/g' +rg -l '\btx_hash\b' | xargs sed -i 's/\btx_hash\b/transaction_hash/g' + +# Crate rename (Cargo.toml + use statements) +rg -l '\bnxm-engine\b' | xargs sed -i 's/\bnxm-engine\b/nexum-engine/g' +rg -l '\bnxm_engine\b' | xargs sed -i 's/\bnxm_engine\b/nexum_engine/g' + +# Manifest section +rg -l '\[\[subscribe\]\]' | xargs sed -i 's/\[\[subscribe\]\]/[[subscription]]/g' + +# Manifest field +rg -l '^wasm = ' | xargs sed -i 's/^wasm = /component = /' +``` + +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. +- Splitting `cow` + `order` into a single `cow-api`. Rewrite the imports and adjust function paths. +- Adding `[capabilities]` to `nexum.toml`. Declare what your module actually uses; this is a meaningful audit. + +--- + +## 9. Verification checklist [both] + +After running the renames: + +- [ ] `cargo nexum check` (new in 0.2) reports no warnings against the 0.2 WIT. +- [ ] `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`). +- [ ] No remaining references to `web3:runtime`, `csn`, `msg`, `headless-module`, `nxm-engine`, `shepherd.toml`, `feed-get`/`feed-set`, `block-data`/`log-entry`/`message-data`, `tx-hash`. +- [ ] All `Result<_, String>` from module exports replaced with `Result<_, HostError>`. +- [ ] Error matching code uses `HostErrorKind` discriminant, not protocol-specific error codes. +- [ ] If you used `chrono`/timestamp arithmetic, audited for the seconds-vs-ms change (0.2 is always ms UTC). +- [ ] If you used `provider.multicall(...).await`, confirmed it now actually batches on the wire (`chain::request-batch` shows in tracing). +- [ ] Tests pass under `cargo nexum run --mock`. + +--- + +## 10. Deprecation policy going forward [both] + +0.2 is the breaking-change window. The contracts below are stable starting at 0.2.0: + +- WIT package name `nexum:runtime` and interface names within it. +- The `host-error` / `host-error-kind` shape. +- The `nexum.toml` manifest schema. +- The `#[nexum::module]` macro surface. + +Additive changes (new interfaces, new manifest fields, new SDK helpers) may land in any 0.2.x release. Existing identifiers will not be removed or repurposed before 1.0 without a deprecation cycle of at least one minor release. + +The mobile/wallet host story (`query-module` production support, C ABI, `nexum-host` embedder crate) is on the 0.3 roadmap, conditional on a named design partner. The 0.2 `query-module` WIT is an experimental option, not a stable contract; expect changes to its error variants and request/response payload conventions before the 0.3 host ships. + +--- + +## 11. Getting help + +- Open an issue at the repo with the `migration-0.2` label. +- The 0.1 → 0.2 codemod (a Rust binary that does the safe mechanical renames in §8 for you) ships in the 0.2 release as `cargo nexum migrate --from 0.1`. +- The full 0.2 WIT lives in `wit/nexum-runtime/` (formerly `wit/web3-runtime/`). From 571f8498f476bd7fa16402b21bf72f3e084932be Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 30 May 2026 10:53:08 +0000 Subject: [PATCH 02/17] wit: rename web3:runtime to nexum:runtime, unify error model, add identity/clock/random/http/query-module --- wit/nexum-runtime/chain.wit | 37 +++++++++ wit/nexum-runtime/clock.wit | 13 +++ wit/nexum-runtime/event-module.wit | 17 ++++ wit/nexum-runtime/http.wit | 39 +++++++++ wit/nexum-runtime/identity.wit | 24 ++++++ .../local-store.wit | 12 +-- .../logging.wit | 2 +- wit/nexum-runtime/messaging.wit | 19 +++++ wit/nexum-runtime/query-module.wit | 25 ++++++ wit/nexum-runtime/random.wit | 7 ++ .../remote-store.wit | 19 ++--- wit/nexum-runtime/types.wit | 83 +++++++++++++++++++ wit/shepherd-cow/{cow.wit => cow-api.wit} | 21 ++--- wit/shepherd-cow/deps/nexum-runtime/chain.wit | 37 +++++++++ wit/shepherd-cow/deps/nexum-runtime/clock.wit | 13 +++ .../deps/nexum-runtime/event-module.wit | 17 ++++ wit/shepherd-cow/deps/nexum-runtime/http.wit | 39 +++++++++ .../deps/nexum-runtime/identity.wit | 24 ++++++ .../local-store.wit | 12 +-- .../deps/nexum-runtime}/logging.wit | 2 +- .../deps/nexum-runtime/messaging.wit | 19 +++++ .../deps/nexum-runtime/query-module.wit | 25 ++++++ .../deps/nexum-runtime/random.wit | 7 ++ .../remote-store.wit | 19 ++--- wit/shepherd-cow/deps/nexum-runtime/types.wit | 83 +++++++++++++++++++ wit/shepherd-cow/deps/web3-runtime/csn.wit | 23 ----- .../deps/web3-runtime/headless-module.wit | 16 ---- wit/shepherd-cow/deps/web3-runtime/msg.wit | 30 ------- wit/shepherd-cow/deps/web3-runtime/types.wit | 39 --------- wit/shepherd-cow/order.wit | 12 --- wit/shepherd-cow/shepherd-module.wit | 8 -- wit/shepherd-cow/shepherd.wit | 7 ++ wit/web3-runtime/csn.wit | 23 ----- wit/web3-runtime/headless-module.wit | 16 ---- wit/web3-runtime/msg.wit | 30 ------- wit/web3-runtime/types.wit | 39 --------- 36 files changed, 578 insertions(+), 280 deletions(-) create mode 100644 wit/nexum-runtime/chain.wit create mode 100644 wit/nexum-runtime/clock.wit create mode 100644 wit/nexum-runtime/event-module.wit create mode 100644 wit/nexum-runtime/http.wit create mode 100644 wit/nexum-runtime/identity.wit rename wit/{web3-runtime => nexum-runtime}/local-store.wit (52%) rename wit/{shepherd-cow/deps/web3-runtime => nexum-runtime}/logging.wit (90%) create mode 100644 wit/nexum-runtime/messaging.wit create mode 100644 wit/nexum-runtime/query-module.wit create mode 100644 wit/nexum-runtime/random.wit rename wit/{web3-runtime => nexum-runtime}/remote-store.wit (67%) create mode 100644 wit/nexum-runtime/types.wit rename wit/shepherd-cow/{cow.wit => cow-api.wit} (57%) create mode 100644 wit/shepherd-cow/deps/nexum-runtime/chain.wit create mode 100644 wit/shepherd-cow/deps/nexum-runtime/clock.wit create mode 100644 wit/shepherd-cow/deps/nexum-runtime/event-module.wit create mode 100644 wit/shepherd-cow/deps/nexum-runtime/http.wit create mode 100644 wit/shepherd-cow/deps/nexum-runtime/identity.wit rename wit/shepherd-cow/deps/{web3-runtime => nexum-runtime}/local-store.wit (52%) rename wit/{web3-runtime => shepherd-cow/deps/nexum-runtime}/logging.wit (90%) create mode 100644 wit/shepherd-cow/deps/nexum-runtime/messaging.wit create mode 100644 wit/shepherd-cow/deps/nexum-runtime/query-module.wit create mode 100644 wit/shepherd-cow/deps/nexum-runtime/random.wit rename wit/shepherd-cow/deps/{web3-runtime => nexum-runtime}/remote-store.wit (67%) create mode 100644 wit/shepherd-cow/deps/nexum-runtime/types.wit delete mode 100644 wit/shepherd-cow/deps/web3-runtime/csn.wit delete mode 100644 wit/shepherd-cow/deps/web3-runtime/headless-module.wit delete mode 100644 wit/shepherd-cow/deps/web3-runtime/msg.wit delete mode 100644 wit/shepherd-cow/deps/web3-runtime/types.wit delete mode 100644 wit/shepherd-cow/order.wit delete mode 100644 wit/shepherd-cow/shepherd-module.wit create mode 100644 wit/shepherd-cow/shepherd.wit delete mode 100644 wit/web3-runtime/csn.wit delete mode 100644 wit/web3-runtime/headless-module.wit delete mode 100644 wit/web3-runtime/msg.wit delete mode 100644 wit/web3-runtime/types.wit diff --git a/wit/nexum-runtime/chain.wit b/wit/nexum-runtime/chain.wit new file mode 100644 index 0000000..0b49a75 --- /dev/null +++ b/wit/nexum-runtime/chain.wit @@ -0,0 +1,37 @@ +package nexum:runtime@0.2.0; + +interface chain { + use types.{chain-id, host-error}; + + /// A single JSON-RPC request to be executed as part of a batch. + record rpc-request { + method: string, + params: string, + } + + /// Result of a single request inside a batch. Each entry is independent; + /// one failing call does not abort the others. + variant rpc-result { + ok(string), + err(host-error), + } + + /// Execute a JSON-RPC request against the specified chain. + /// + /// The host routes to its configured provider for the given chain, + /// applying whatever middleware is appropriate for the platform + /// (timeout, retry, rate-limit, fallback on server; simple HTTP + /// on mobile; window.ethereum or injected provider in WebView). + /// + /// `method` includes the namespace prefix (e.g. "eth_call"). + /// `params` and the success value are JSON-encoded strings. + request: func(chain-id: chain-id, method: string, params: string) + -> result; + + /// Execute several JSON-RPC requests against the same chain in a single + /// round trip where the host transport supports it. 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. + request-batch: func(chain-id: chain-id, requests: list) + -> result, host-error>; +} diff --git a/wit/nexum-runtime/clock.wit b/wit/nexum-runtime/clock.wit new file mode 100644 index 0000000..edc18b6 --- /dev/null +++ b/wit/nexum-runtime/clock.wit @@ -0,0 +1,13 @@ +package nexum:runtime@0.2.0; + +/// Host-provided clock. Guest modules MUST use this rather than relying on +/// WASI clocks so the host can virtualise time during replay/testing. +interface clock { + /// Wall-clock time in milliseconds since the Unix epoch, UTC. + now-ms: func() -> u64; + + /// Monotonic timer in nanoseconds. The origin is unspecified; only + /// differences between successive calls are meaningful. Suitable for + /// measuring elapsed time without exposure to wall-clock jumps. + monotonic-ns: func() -> u64; +} diff --git a/wit/nexum-runtime/event-module.wit b/wit/nexum-runtime/event-module.wit new file mode 100644 index 0000000..0a69131 --- /dev/null +++ b/wit/nexum-runtime/event-module.wit @@ -0,0 +1,17 @@ +package nexum:runtime@0.2.0; + +/// Event-driven module — automation, background processing. +/// No UI capabilities. Runs on any conforming host. +world event-module { + use types.{config, event, host-error}; + + import chain; + import local-store; + import remote-store; + import messaging; + import identity; + import logging; + + export init: func(config: config) -> result<_, host-error>; + export on-event: func(event: event) -> result<_, host-error>; +} diff --git a/wit/nexum-runtime/http.wit b/wit/nexum-runtime/http.wit new file mode 100644 index 0000000..e529a28 --- /dev/null +++ b/wit/nexum-runtime/http.wit @@ -0,0 +1,39 @@ +package nexum:runtime@0.2.0; + +/// Generic HTTP client capability. Modules that need this MUST opt in via +/// their manifest; the host enforces an allow-list of destinations. +interface http { + use types.{host-error}; + + /// A single HTTP header. Header names are case-insensitive on the wire; + /// the host normalises them. + record header { + name: string, + value: string, + } + + record request { + /// HTTP method, e.g. "GET", "POST". + method: string, + /// Absolute URL (scheme + host + path + query). + url: string, + headers: list
, + /// Optional request body. Empty for methods like GET. + body: option>, + /// Optional per-request timeout in milliseconds. The host MAY clamp + /// this to its own configured maximum. + timeout-ms: option, + } + + record response { + status: u16, + headers: list
, + body: list, + } + + /// Perform a single HTTP request. Transport-level failures (DNS, TLS, + /// timeout, host policy rejection) surface as `host-error`; HTTP-level + /// non-2xx responses are returned as an `ok(response)` with the status + /// set accordingly so the caller can inspect headers/body. + fetch: func(req: request) -> result; +} diff --git a/wit/nexum-runtime/identity.wit b/wit/nexum-runtime/identity.wit new file mode 100644 index 0000000..25234ad --- /dev/null +++ b/wit/nexum-runtime/identity.wit @@ -0,0 +1,24 @@ +package nexum:runtime@0.2.0; + +/// Identity / signing capability. +/// +/// 0.2 ships a single, minimal interface. A future release (0.4+) is +/// expected to split this into `identity-read` and `identity-sign` and to +/// introduce a richer `signing-result` variant; for 0.2 the simple shape is +/// sufficient because user rejection can already be expressed via +/// `host-error { kind: denied, .. }`. +interface identity { + use types.{host-error}; + + /// Return the list of account addresses (20-byte EVM addresses) the host + /// is willing to sign for. Empty list means no signing capability. + accounts: func() -> result>, host-error>; + + /// Sign an arbitrary message with personal_sign semantics (prepends the + /// "\x19Ethereum Signed Message:\n" prefix). Returns a 65-byte signature. + sign: func(account: list, message: list) -> result, host-error>; + + /// Sign EIP-712 typed data. `typed-data` is a JSON-encoded EIP-712 payload. + /// Returns a 65-byte signature. + sign-typed-data: func(account: list, typed-data: string) -> result, host-error>; +} diff --git a/wit/web3-runtime/local-store.wit b/wit/nexum-runtime/local-store.wit similarity index 52% rename from wit/web3-runtime/local-store.wit rename to wit/nexum-runtime/local-store.wit index 29fda92..ba58be8 100644 --- a/wit/web3-runtime/local-store.wit +++ b/wit/nexum-runtime/local-store.wit @@ -1,16 +1,18 @@ -package web3:runtime@0.1.0; +package nexum:runtime@0.2.0; interface local-store { + use types.{host-error}; + /// Get a value by key. Returns none if the key does not exist. - get: func(key: string) -> result>, string>; + get: func(key: string) -> result>, host-error>; /// Set a key-value pair. Overwrites any existing value. /// The host may enforce a size quota; if exceeded, returns err. - set: func(key: string, value: list) -> result<_, string>; + set: func(key: string, value: list) -> result<_, host-error>; /// Delete a key. No-op if the key does not exist. - delete: func(key: string) -> result<_, string>; + delete: func(key: string) -> result<_, host-error>; /// List all keys matching a prefix. Empty prefix returns all keys. - list-keys: func(prefix: string) -> result, string>; + list-keys: func(prefix: string) -> result, host-error>; } diff --git a/wit/shepherd-cow/deps/web3-runtime/logging.wit b/wit/nexum-runtime/logging.wit similarity index 90% rename from wit/shepherd-cow/deps/web3-runtime/logging.wit rename to wit/nexum-runtime/logging.wit index a3b6d1a..ac4286d 100644 --- a/wit/shepherd-cow/deps/web3-runtime/logging.wit +++ b/wit/nexum-runtime/logging.wit @@ -1,4 +1,4 @@ -package web3:runtime@0.1.0; +package nexum:runtime@0.2.0; interface logging { enum level { diff --git a/wit/nexum-runtime/messaging.wit b/wit/nexum-runtime/messaging.wit new file mode 100644 index 0000000..f24f3d0 --- /dev/null +++ b/wit/nexum-runtime/messaging.wit @@ -0,0 +1,19 @@ +package nexum:runtime@0.2.0; + +interface messaging { + use types.{host-error, message}; + + /// Publish a message to a content topic. + /// + /// Content topics follow the format: //// + /// e.g. "/nexum/1/twap-updates/proto" + publish: func(content-topic: string, payload: list) -> result<_, host-error>; + + /// Query historical messages from the Waku store protocol. + query: func( + content-topic: string, + start-time: option, + end-time: option, + limit: option, + ) -> result, host-error>; +} diff --git a/wit/nexum-runtime/query-module.wit b/wit/nexum-runtime/query-module.wit new file mode 100644 index 0000000..a531a1b --- /dev/null +++ b/wit/nexum-runtime/query-module.wit @@ -0,0 +1,25 @@ +package nexum:runtime@0.2.0; + +/// Query module — synchronous, side-effect-free evaluation. +/// +/// EXPERIMENTAL (0.2): the shape of this world is provisional and may +/// change in a future minor release without a major bump. Hosts and SDKs +/// should expect breakage here until the world is stabilised. +/// +/// A query module exposes a single pure `evaluate` entry point. It is given +/// read-only access to the local store (for cached/derived state) and to +/// logging; everything else (chain access, network, messaging, signing) is +/// deliberately excluded so the host can run queries inside a tight +/// deterministic sandbox. +world query-module { + use types.{config, host-error}; + + import local-store; + import logging; + + export init: func(config: config) -> result<_, host-error>; + + /// Evaluate the query. `input` and the returned bytes are opaque to the + /// host; the module and its caller agree on the encoding. + export evaluate: func(input: list) -> result, host-error>; +} diff --git a/wit/nexum-runtime/random.wit b/wit/nexum-runtime/random.wit new file mode 100644 index 0000000..14c8574 --- /dev/null +++ b/wit/nexum-runtime/random.wit @@ -0,0 +1,7 @@ +package nexum:runtime@0.2.0; + +/// Cryptographically secure randomness from the host. +interface random { + /// Return `len` bytes of cryptographically secure random data. + fill: func(len: u32) -> list; +} diff --git a/wit/web3-runtime/remote-store.wit b/wit/nexum-runtime/remote-store.wit similarity index 67% rename from wit/web3-runtime/remote-store.wit rename to wit/nexum-runtime/remote-store.wit index ab788cf..09f793b 100644 --- a/wit/web3-runtime/remote-store.wit +++ b/wit/nexum-runtime/remote-store.wit @@ -1,27 +1,24 @@ -package web3:runtime@0.1.0; +package nexum:runtime@0.2.0; interface remote-store { - record store-error { - code: u16, - message: string, - } + use types.{host-error}; /// Upload raw data to the decentralised store. /// Returns the 32-byte content reference (Swarm address). - upload: func(data: list) -> result, store-error>; + upload: func(data: list) -> result, host-error>; /// Download raw data by 32-byte content reference. - download: func(reference: list) -> result, store-error>; + download: func(reference: list) -> result, host-error>; /// Read the latest value from a mutable feed. /// /// Feeds are mutable pointers: (owner, topic) -> latest chunk. /// `owner`: 20-byte Ethereum address of the feed owner. /// `topic`: 32-byte topic hash. - feed-get: func( + read-feed: func( owner: list, topic: list, - ) -> result>, store-error>; + ) -> result>, host-error>; /// Update a mutable feed with new data. /// @@ -29,8 +26,8 @@ interface remote-store { /// `topic`: 32-byte topic hash. /// `data`: the payload to publish. /// Returns the 32-byte reference of the new chunk. - feed-set: func( + write-feed: func( topic: list, data: list, - ) -> result, store-error>; + ) -> result, host-error>; } diff --git a/wit/nexum-runtime/types.wit b/wit/nexum-runtime/types.wit new file mode 100644 index 0000000..d4d38c3 --- /dev/null +++ b/wit/nexum-runtime/types.wit @@ -0,0 +1,83 @@ +package nexum:runtime@0.2.0; + +/// Common types shared across all runtime interfaces. +/// +/// All `u64` timestamps in this package are milliseconds since the Unix +/// epoch, UTC, unless otherwise noted (e.g. `clock::monotonic-ns` is +/// nanoseconds from an arbitrary monotonic origin). +interface types { + type chain-id = u64; + + record block { + chain-id: chain-id, + number: u64, + hash: list, + timestamp: u64, + } + + record log { + chain-id: chain-id, + address: list, + topics: list>, + data: list, + block-number: u64, + transaction-hash: list, + log-index: u32, + } + + /// A message delivered over the messaging interface. Defined here (rather + /// than only in `messaging.wit`) so the `event` variant can reference it + /// without a cross-interface use clause. + record message { + content-topic: string, + payload: list, + timestamp: u64, + /// Optional sender identity (protocol-dependent). + sender: option>, + } + + /// Fired by the host on a configured cadence. `fired-at` is the host's + /// wall-clock time (ms since Unix epoch, UTC) at which the tick was + /// generated. + record tick { + fired-at: u64, + } + + variant event { + block(block), + logs(list), + tick(tick), + message(message), + } + + /// Opaque config from nexum.toml [config] section. + type config = list>; + + /// Coarse categorisation of host-side failures. The kind is suitable for + /// programmatic dispatch by guests; `message` carries a human-readable + /// detail and `code` carries a domain-specific numeric (e.g. a JSON-RPC + /// error code, HTTP status, etc.). + variant host-error-kind { + unsupported, + unavailable, + denied, + rate-limited, + timeout, + invalid-input, + internal, + } + + /// Unified error returned by every host-imported function. + /// + /// `domain` is a short identifier for the originating subsystem + /// (e.g. "chain", "local-store", "remote-store", "messaging", + /// "identity", "http"). `data` is an optional opaque payload (often a + /// JSON-encoded blob). + record host-error { + domain: string, + kind: host-error-kind, + code: s32, + message: string, + data: option, + } +} diff --git a/wit/shepherd-cow/cow.wit b/wit/shepherd-cow/cow-api.wit similarity index 57% rename from wit/shepherd-cow/cow.wit rename to wit/shepherd-cow/cow-api.wit index 7b9fdd6..c4ec6f8 100644 --- a/wit/shepherd-cow/cow.wit +++ b/wit/shepherd-cow/cow-api.wit @@ -1,13 +1,7 @@ -package shepherd:cow@0.1.0; +package shepherd:cow@0.2.0; -interface cow { - use web3:runtime/types@0.1.0.{chain-id}; - - record api-error { - status: u16, - message: string, - body: option, - } +interface cow-api { + use nexum:runtime/types@0.2.0.{chain-id, host-error}; /// HTTP-style request to the CoW Protocol API. /// @@ -24,5 +18,12 @@ interface cow { method: string, path: string, body: option, - ) -> result; + ) -> result; + + /// Submit an order to the CoW Protocol. + /// + /// `order-data`: the serialised order payload. + /// Returns the order UID on success. + submit-order: func(chain-id: chain-id, order-data: list) + -> result; } diff --git a/wit/shepherd-cow/deps/nexum-runtime/chain.wit b/wit/shepherd-cow/deps/nexum-runtime/chain.wit new file mode 100644 index 0000000..0b49a75 --- /dev/null +++ b/wit/shepherd-cow/deps/nexum-runtime/chain.wit @@ -0,0 +1,37 @@ +package nexum:runtime@0.2.0; + +interface chain { + use types.{chain-id, host-error}; + + /// A single JSON-RPC request to be executed as part of a batch. + record rpc-request { + method: string, + params: string, + } + + /// Result of a single request inside a batch. Each entry is independent; + /// one failing call does not abort the others. + variant rpc-result { + ok(string), + err(host-error), + } + + /// Execute a JSON-RPC request against the specified chain. + /// + /// The host routes to its configured provider for the given chain, + /// applying whatever middleware is appropriate for the platform + /// (timeout, retry, rate-limit, fallback on server; simple HTTP + /// on mobile; window.ethereum or injected provider in WebView). + /// + /// `method` includes the namespace prefix (e.g. "eth_call"). + /// `params` and the success value are JSON-encoded strings. + request: func(chain-id: chain-id, method: string, params: string) + -> result; + + /// Execute several JSON-RPC requests against the same chain in a single + /// round trip where the host transport supports it. 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. + request-batch: func(chain-id: chain-id, requests: list) + -> result, host-error>; +} diff --git a/wit/shepherd-cow/deps/nexum-runtime/clock.wit b/wit/shepherd-cow/deps/nexum-runtime/clock.wit new file mode 100644 index 0000000..edc18b6 --- /dev/null +++ b/wit/shepherd-cow/deps/nexum-runtime/clock.wit @@ -0,0 +1,13 @@ +package nexum:runtime@0.2.0; + +/// Host-provided clock. Guest modules MUST use this rather than relying on +/// WASI clocks so the host can virtualise time during replay/testing. +interface clock { + /// Wall-clock time in milliseconds since the Unix epoch, UTC. + now-ms: func() -> u64; + + /// Monotonic timer in nanoseconds. The origin is unspecified; only + /// differences between successive calls are meaningful. Suitable for + /// measuring elapsed time without exposure to wall-clock jumps. + monotonic-ns: func() -> u64; +} diff --git a/wit/shepherd-cow/deps/nexum-runtime/event-module.wit b/wit/shepherd-cow/deps/nexum-runtime/event-module.wit new file mode 100644 index 0000000..0a69131 --- /dev/null +++ b/wit/shepherd-cow/deps/nexum-runtime/event-module.wit @@ -0,0 +1,17 @@ +package nexum:runtime@0.2.0; + +/// Event-driven module — automation, background processing. +/// No UI capabilities. Runs on any conforming host. +world event-module { + use types.{config, event, host-error}; + + import chain; + import local-store; + import remote-store; + import messaging; + import identity; + import logging; + + export init: func(config: config) -> result<_, host-error>; + export on-event: func(event: event) -> result<_, host-error>; +} diff --git a/wit/shepherd-cow/deps/nexum-runtime/http.wit b/wit/shepherd-cow/deps/nexum-runtime/http.wit new file mode 100644 index 0000000..e529a28 --- /dev/null +++ b/wit/shepherd-cow/deps/nexum-runtime/http.wit @@ -0,0 +1,39 @@ +package nexum:runtime@0.2.0; + +/// Generic HTTP client capability. Modules that need this MUST opt in via +/// their manifest; the host enforces an allow-list of destinations. +interface http { + use types.{host-error}; + + /// A single HTTP header. Header names are case-insensitive on the wire; + /// the host normalises them. + record header { + name: string, + value: string, + } + + record request { + /// HTTP method, e.g. "GET", "POST". + method: string, + /// Absolute URL (scheme + host + path + query). + url: string, + headers: list
, + /// Optional request body. Empty for methods like GET. + body: option>, + /// Optional per-request timeout in milliseconds. The host MAY clamp + /// this to its own configured maximum. + timeout-ms: option, + } + + record response { + status: u16, + headers: list
, + body: list, + } + + /// Perform a single HTTP request. Transport-level failures (DNS, TLS, + /// timeout, host policy rejection) surface as `host-error`; HTTP-level + /// non-2xx responses are returned as an `ok(response)` with the status + /// set accordingly so the caller can inspect headers/body. + fetch: func(req: request) -> result; +} diff --git a/wit/shepherd-cow/deps/nexum-runtime/identity.wit b/wit/shepherd-cow/deps/nexum-runtime/identity.wit new file mode 100644 index 0000000..25234ad --- /dev/null +++ b/wit/shepherd-cow/deps/nexum-runtime/identity.wit @@ -0,0 +1,24 @@ +package nexum:runtime@0.2.0; + +/// Identity / signing capability. +/// +/// 0.2 ships a single, minimal interface. A future release (0.4+) is +/// expected to split this into `identity-read` and `identity-sign` and to +/// introduce a richer `signing-result` variant; for 0.2 the simple shape is +/// sufficient because user rejection can already be expressed via +/// `host-error { kind: denied, .. }`. +interface identity { + use types.{host-error}; + + /// Return the list of account addresses (20-byte EVM addresses) the host + /// is willing to sign for. Empty list means no signing capability. + accounts: func() -> result>, host-error>; + + /// Sign an arbitrary message with personal_sign semantics (prepends the + /// "\x19Ethereum Signed Message:\n" prefix). Returns a 65-byte signature. + sign: func(account: list, message: list) -> result, host-error>; + + /// Sign EIP-712 typed data. `typed-data` is a JSON-encoded EIP-712 payload. + /// Returns a 65-byte signature. + sign-typed-data: func(account: list, typed-data: string) -> result, host-error>; +} diff --git a/wit/shepherd-cow/deps/web3-runtime/local-store.wit b/wit/shepherd-cow/deps/nexum-runtime/local-store.wit similarity index 52% rename from wit/shepherd-cow/deps/web3-runtime/local-store.wit rename to wit/shepherd-cow/deps/nexum-runtime/local-store.wit index 29fda92..ba58be8 100644 --- a/wit/shepherd-cow/deps/web3-runtime/local-store.wit +++ b/wit/shepherd-cow/deps/nexum-runtime/local-store.wit @@ -1,16 +1,18 @@ -package web3:runtime@0.1.0; +package nexum:runtime@0.2.0; interface local-store { + use types.{host-error}; + /// Get a value by key. Returns none if the key does not exist. - get: func(key: string) -> result>, string>; + get: func(key: string) -> result>, host-error>; /// Set a key-value pair. Overwrites any existing value. /// The host may enforce a size quota; if exceeded, returns err. - set: func(key: string, value: list) -> result<_, string>; + set: func(key: string, value: list) -> result<_, host-error>; /// Delete a key. No-op if the key does not exist. - delete: func(key: string) -> result<_, string>; + delete: func(key: string) -> result<_, host-error>; /// List all keys matching a prefix. Empty prefix returns all keys. - list-keys: func(prefix: string) -> result, string>; + list-keys: func(prefix: string) -> result, host-error>; } diff --git a/wit/web3-runtime/logging.wit b/wit/shepherd-cow/deps/nexum-runtime/logging.wit similarity index 90% rename from wit/web3-runtime/logging.wit rename to wit/shepherd-cow/deps/nexum-runtime/logging.wit index a3b6d1a..ac4286d 100644 --- a/wit/web3-runtime/logging.wit +++ b/wit/shepherd-cow/deps/nexum-runtime/logging.wit @@ -1,4 +1,4 @@ -package web3:runtime@0.1.0; +package nexum:runtime@0.2.0; interface logging { enum level { diff --git a/wit/shepherd-cow/deps/nexum-runtime/messaging.wit b/wit/shepherd-cow/deps/nexum-runtime/messaging.wit new file mode 100644 index 0000000..f24f3d0 --- /dev/null +++ b/wit/shepherd-cow/deps/nexum-runtime/messaging.wit @@ -0,0 +1,19 @@ +package nexum:runtime@0.2.0; + +interface messaging { + use types.{host-error, message}; + + /// Publish a message to a content topic. + /// + /// Content topics follow the format: //// + /// e.g. "/nexum/1/twap-updates/proto" + publish: func(content-topic: string, payload: list) -> result<_, host-error>; + + /// Query historical messages from the Waku store protocol. + query: func( + content-topic: string, + start-time: option, + end-time: option, + limit: option, + ) -> result, host-error>; +} diff --git a/wit/shepherd-cow/deps/nexum-runtime/query-module.wit b/wit/shepherd-cow/deps/nexum-runtime/query-module.wit new file mode 100644 index 0000000..a531a1b --- /dev/null +++ b/wit/shepherd-cow/deps/nexum-runtime/query-module.wit @@ -0,0 +1,25 @@ +package nexum:runtime@0.2.0; + +/// Query module — synchronous, side-effect-free evaluation. +/// +/// EXPERIMENTAL (0.2): the shape of this world is provisional and may +/// change in a future minor release without a major bump. Hosts and SDKs +/// should expect breakage here until the world is stabilised. +/// +/// A query module exposes a single pure `evaluate` entry point. It is given +/// read-only access to the local store (for cached/derived state) and to +/// logging; everything else (chain access, network, messaging, signing) is +/// deliberately excluded so the host can run queries inside a tight +/// deterministic sandbox. +world query-module { + use types.{config, host-error}; + + import local-store; + import logging; + + export init: func(config: config) -> result<_, host-error>; + + /// Evaluate the query. `input` and the returned bytes are opaque to the + /// host; the module and its caller agree on the encoding. + export evaluate: func(input: list) -> result, host-error>; +} diff --git a/wit/shepherd-cow/deps/nexum-runtime/random.wit b/wit/shepherd-cow/deps/nexum-runtime/random.wit new file mode 100644 index 0000000..14c8574 --- /dev/null +++ b/wit/shepherd-cow/deps/nexum-runtime/random.wit @@ -0,0 +1,7 @@ +package nexum:runtime@0.2.0; + +/// Cryptographically secure randomness from the host. +interface random { + /// Return `len` bytes of cryptographically secure random data. + fill: func(len: u32) -> list; +} diff --git a/wit/shepherd-cow/deps/web3-runtime/remote-store.wit b/wit/shepherd-cow/deps/nexum-runtime/remote-store.wit similarity index 67% rename from wit/shepherd-cow/deps/web3-runtime/remote-store.wit rename to wit/shepherd-cow/deps/nexum-runtime/remote-store.wit index ab788cf..09f793b 100644 --- a/wit/shepherd-cow/deps/web3-runtime/remote-store.wit +++ b/wit/shepherd-cow/deps/nexum-runtime/remote-store.wit @@ -1,27 +1,24 @@ -package web3:runtime@0.1.0; +package nexum:runtime@0.2.0; interface remote-store { - record store-error { - code: u16, - message: string, - } + use types.{host-error}; /// Upload raw data to the decentralised store. /// Returns the 32-byte content reference (Swarm address). - upload: func(data: list) -> result, store-error>; + upload: func(data: list) -> result, host-error>; /// Download raw data by 32-byte content reference. - download: func(reference: list) -> result, store-error>; + download: func(reference: list) -> result, host-error>; /// Read the latest value from a mutable feed. /// /// Feeds are mutable pointers: (owner, topic) -> latest chunk. /// `owner`: 20-byte Ethereum address of the feed owner. /// `topic`: 32-byte topic hash. - feed-get: func( + read-feed: func( owner: list, topic: list, - ) -> result>, store-error>; + ) -> result>, host-error>; /// Update a mutable feed with new data. /// @@ -29,8 +26,8 @@ interface remote-store { /// `topic`: 32-byte topic hash. /// `data`: the payload to publish. /// Returns the 32-byte reference of the new chunk. - feed-set: func( + write-feed: func( topic: list, data: list, - ) -> result, store-error>; + ) -> result, host-error>; } diff --git a/wit/shepherd-cow/deps/nexum-runtime/types.wit b/wit/shepherd-cow/deps/nexum-runtime/types.wit new file mode 100644 index 0000000..d4d38c3 --- /dev/null +++ b/wit/shepherd-cow/deps/nexum-runtime/types.wit @@ -0,0 +1,83 @@ +package nexum:runtime@0.2.0; + +/// Common types shared across all runtime interfaces. +/// +/// All `u64` timestamps in this package are milliseconds since the Unix +/// epoch, UTC, unless otherwise noted (e.g. `clock::monotonic-ns` is +/// nanoseconds from an arbitrary monotonic origin). +interface types { + type chain-id = u64; + + record block { + chain-id: chain-id, + number: u64, + hash: list, + timestamp: u64, + } + + record log { + chain-id: chain-id, + address: list, + topics: list>, + data: list, + block-number: u64, + transaction-hash: list, + log-index: u32, + } + + /// A message delivered over the messaging interface. Defined here (rather + /// than only in `messaging.wit`) so the `event` variant can reference it + /// without a cross-interface use clause. + record message { + content-topic: string, + payload: list, + timestamp: u64, + /// Optional sender identity (protocol-dependent). + sender: option>, + } + + /// Fired by the host on a configured cadence. `fired-at` is the host's + /// wall-clock time (ms since Unix epoch, UTC) at which the tick was + /// generated. + record tick { + fired-at: u64, + } + + variant event { + block(block), + logs(list), + tick(tick), + message(message), + } + + /// Opaque config from nexum.toml [config] section. + type config = list>; + + /// Coarse categorisation of host-side failures. The kind is suitable for + /// programmatic dispatch by guests; `message` carries a human-readable + /// detail and `code` carries a domain-specific numeric (e.g. a JSON-RPC + /// error code, HTTP status, etc.). + variant host-error-kind { + unsupported, + unavailable, + denied, + rate-limited, + timeout, + invalid-input, + internal, + } + + /// Unified error returned by every host-imported function. + /// + /// `domain` is a short identifier for the originating subsystem + /// (e.g. "chain", "local-store", "remote-store", "messaging", + /// "identity", "http"). `data` is an optional opaque payload (often a + /// JSON-encoded blob). + record host-error { + domain: string, + kind: host-error-kind, + code: s32, + message: string, + data: option, + } +} diff --git a/wit/shepherd-cow/deps/web3-runtime/csn.wit b/wit/shepherd-cow/deps/web3-runtime/csn.wit deleted file mode 100644 index f6b7b2f..0000000 --- a/wit/shepherd-cow/deps/web3-runtime/csn.wit +++ /dev/null @@ -1,23 +0,0 @@ -package web3:runtime@0.1.0; - -interface csn { - use types.{chain-id}; - - record json-rpc-error { - code: s64, - message: string, - data: option, - } - - /// Execute a JSON-RPC request against the specified chain. - /// - /// The host routes to its configured provider for the given chain, - /// applying whatever middleware is appropriate for the platform - /// (timeout, retry, rate-limit, fallback on server; simple HTTP - /// on mobile; window.ethereum or injected provider in WebView). - /// - /// `method` includes the namespace prefix (e.g. "eth_call"). - /// `params` and the success value are JSON-encoded strings. - request: func(chain-id: chain-id, method: string, params: string) - -> result; -} diff --git a/wit/shepherd-cow/deps/web3-runtime/headless-module.wit b/wit/shepherd-cow/deps/web3-runtime/headless-module.wit deleted file mode 100644 index 196c410..0000000 --- a/wit/shepherd-cow/deps/web3-runtime/headless-module.wit +++ /dev/null @@ -1,16 +0,0 @@ -package web3:runtime@0.1.0; - -/// Headless module — automation, background processing. -/// No UI capabilities. Runs on any conforming host. -world headless-module { - use types.{config, event}; - - import csn; - import local-store; - import remote-store; - import msg; - import logging; - - export init: func(config: config) -> result<_, string>; - export on-event: func(event: event) -> result<_, string>; -} diff --git a/wit/shepherd-cow/deps/web3-runtime/msg.wit b/wit/shepherd-cow/deps/web3-runtime/msg.wit deleted file mode 100644 index a222da2..0000000 --- a/wit/shepherd-cow/deps/web3-runtime/msg.wit +++ /dev/null @@ -1,30 +0,0 @@ -package web3:runtime@0.1.0; - -interface msg { - record msg-error { - code: u16, - message: string, - } - - record message { - content-topic: string, - payload: list, - timestamp: u64, - /// Optional sender identity (protocol-dependent). - sender: option>, - } - - /// Publish a message to a content topic. - /// - /// Content topics follow the format: //// - /// e.g. "/shepherd/1/twap-updates/proto" - publish: func(content-topic: string, payload: list) -> result<_, msg-error>; - - /// Query historical messages from the Waku store protocol. - query: func( - content-topic: string, - start-time: option, - end-time: option, - limit: option, - ) -> result, msg-error>; -} diff --git a/wit/shepherd-cow/deps/web3-runtime/types.wit b/wit/shepherd-cow/deps/web3-runtime/types.wit deleted file mode 100644 index 64c0608..0000000 --- a/wit/shepherd-cow/deps/web3-runtime/types.wit +++ /dev/null @@ -1,39 +0,0 @@ -package web3:runtime@0.1.0; - -interface types { - type chain-id = u64; - - record block-data { - chain-id: chain-id, - number: u64, - hash: list, - timestamp: u64, - } - - record log-entry { - chain-id: chain-id, - address: list, - topics: list>, - data: list, - block-number: u64, - tx-hash: list, - log-index: u32, - } - - record message-data { - content-topic: string, - payload: list, - timestamp: u64, - sender: option>, - } - - variant event { - block(block-data), - logs(list), - timer(u64), - message(message-data), - } - - /// Opaque config from shepherd.toml [config] section. - type config = list>; -} diff --git a/wit/shepherd-cow/order.wit b/wit/shepherd-cow/order.wit deleted file mode 100644 index 52e4429..0000000 --- a/wit/shepherd-cow/order.wit +++ /dev/null @@ -1,12 +0,0 @@ -package shepherd:cow@0.1.0; - -interface order { - use web3:runtime/types@0.1.0.{chain-id}; - - /// Submit an order to the CoW Protocol. - /// - /// `order-data`: the serialised order payload. - /// Returns the order UID on success. - submit: func(chain-id: chain-id, order-data: list) - -> result; -} diff --git a/wit/shepherd-cow/shepherd-module.wit b/wit/shepherd-cow/shepherd-module.wit deleted file mode 100644 index dbc2ca0..0000000 --- a/wit/shepherd-cow/shepherd-module.wit +++ /dev/null @@ -1,8 +0,0 @@ -package shepherd:cow@0.1.0; - -/// Shepherd module — headless module with CoW Protocol extensions. -world shepherd-module { - include web3:runtime/headless-module@0.1.0; - import cow; - import order; -} diff --git a/wit/shepherd-cow/shepherd.wit b/wit/shepherd-cow/shepherd.wit new file mode 100644 index 0000000..be1f386 --- /dev/null +++ b/wit/shepherd-cow/shepherd.wit @@ -0,0 +1,7 @@ +package shepherd:cow@0.2.0; + +/// Shepherd module — event-driven Nexum module with CoW Protocol extensions. +world shepherd { + include nexum:runtime/event-module@0.2.0; + import cow-api; +} diff --git a/wit/web3-runtime/csn.wit b/wit/web3-runtime/csn.wit deleted file mode 100644 index f6b7b2f..0000000 --- a/wit/web3-runtime/csn.wit +++ /dev/null @@ -1,23 +0,0 @@ -package web3:runtime@0.1.0; - -interface csn { - use types.{chain-id}; - - record json-rpc-error { - code: s64, - message: string, - data: option, - } - - /// Execute a JSON-RPC request against the specified chain. - /// - /// The host routes to its configured provider for the given chain, - /// applying whatever middleware is appropriate for the platform - /// (timeout, retry, rate-limit, fallback on server; simple HTTP - /// on mobile; window.ethereum or injected provider in WebView). - /// - /// `method` includes the namespace prefix (e.g. "eth_call"). - /// `params` and the success value are JSON-encoded strings. - request: func(chain-id: chain-id, method: string, params: string) - -> result; -} diff --git a/wit/web3-runtime/headless-module.wit b/wit/web3-runtime/headless-module.wit deleted file mode 100644 index 196c410..0000000 --- a/wit/web3-runtime/headless-module.wit +++ /dev/null @@ -1,16 +0,0 @@ -package web3:runtime@0.1.0; - -/// Headless module — automation, background processing. -/// No UI capabilities. Runs on any conforming host. -world headless-module { - use types.{config, event}; - - import csn; - import local-store; - import remote-store; - import msg; - import logging; - - export init: func(config: config) -> result<_, string>; - export on-event: func(event: event) -> result<_, string>; -} diff --git a/wit/web3-runtime/msg.wit b/wit/web3-runtime/msg.wit deleted file mode 100644 index a222da2..0000000 --- a/wit/web3-runtime/msg.wit +++ /dev/null @@ -1,30 +0,0 @@ -package web3:runtime@0.1.0; - -interface msg { - record msg-error { - code: u16, - message: string, - } - - record message { - content-topic: string, - payload: list, - timestamp: u64, - /// Optional sender identity (protocol-dependent). - sender: option>, - } - - /// Publish a message to a content topic. - /// - /// Content topics follow the format: //// - /// e.g. "/shepherd/1/twap-updates/proto" - publish: func(content-topic: string, payload: list) -> result<_, msg-error>; - - /// Query historical messages from the Waku store protocol. - query: func( - content-topic: string, - start-time: option, - end-time: option, - limit: option, - ) -> result, msg-error>; -} diff --git a/wit/web3-runtime/types.wit b/wit/web3-runtime/types.wit deleted file mode 100644 index 64c0608..0000000 --- a/wit/web3-runtime/types.wit +++ /dev/null @@ -1,39 +0,0 @@ -package web3:runtime@0.1.0; - -interface types { - type chain-id = u64; - - record block-data { - chain-id: chain-id, - number: u64, - hash: list, - timestamp: u64, - } - - record log-entry { - chain-id: chain-id, - address: list, - topics: list>, - data: list, - block-number: u64, - tx-hash: list, - log-index: u32, - } - - record message-data { - content-topic: string, - payload: list, - timestamp: u64, - sender: option>, - } - - variant event { - block(block-data), - logs(list), - timer(u64), - message(message-data), - } - - /// Opaque config from shepherd.toml [config] section. - type config = list>; -} From 15cf23118003e7f55fd123c56d8ecd2be8bc141d Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 30 May 2026 10:47:34 +0000 Subject: [PATCH 03/17] chore: rename crate nxm-engine to nexum-engine; bump to 0.2.0 --- Cargo.toml | 2 +- crates/{nxm-engine => nexum-engine}/Cargo.toml | 4 ++-- crates/{nxm-engine => nexum-engine}/src/main.rs | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename crates/{nxm-engine => nexum-engine}/Cargo.toml (86%) rename crates/{nxm-engine => nexum-engine}/src/main.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index ef89fef..d14c23e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = [ - "crates/nxm-engine", + "crates/nexum-engine", "modules/example", ] resolver = "2" diff --git a/crates/nxm-engine/Cargo.toml b/crates/nexum-engine/Cargo.toml similarity index 86% rename from crates/nxm-engine/Cargo.toml rename to crates/nexum-engine/Cargo.toml index 91eb722..8e67c0d 100644 --- a/crates/nxm-engine/Cargo.toml +++ b/crates/nexum-engine/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "nxm-engine" -version = "0.1.0" +name = "nexum-engine" +version = "0.2.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/crates/nxm-engine/src/main.rs b/crates/nexum-engine/src/main.rs similarity index 100% rename from crates/nxm-engine/src/main.rs rename to crates/nexum-engine/src/main.rs From 60886760d505b67092603efb3daf897f5a7426fe Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 30 May 2026 10:52:37 +0000 Subject: [PATCH 04/17] build: update justfile and CI for nexum-runtime + nexum-engine rename --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- justfile | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 65e55d8..636c5e8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -24,7 +24,7 @@ body: id: scope attributes: label: Scope - description: Universal `web3:runtime` change, domain extension (e.g. `shepherd:cow`), runtime-only, or SDK-only? + description: Universal `nexum:runtime` change, domain extension (e.g. `shepherd:cow`), runtime-only, or SDK-only? - type: textarea id: extra attributes: diff --git a/justfile b/justfile index 2f7f569..cc11906 100644 --- a/justfile +++ b/justfile @@ -1,11 +1,11 @@ -# Sync WIT deps (copies web3-runtime into shepherd-cow/deps) +# Sync WIT deps (copies nexum-runtime into shepherd-cow/deps) sync-wit: - rm -rf wit/shepherd-cow/deps/web3-runtime - cp -r wit/web3-runtime wit/shepherd-cow/deps/web3-runtime + rm -rf wit/shepherd-cow/deps/nexum-runtime + cp -r wit/nexum-runtime wit/shepherd-cow/deps/nexum-runtime # Build the host runtime build-runtime: sync-wit - cargo build -p nxm-engine + cargo build -p nexum-engine # Build the example WASM module build-module: @@ -16,9 +16,9 @@ build: build-runtime build-module # Build the module then run the runtime with it run: build-module build-runtime - cargo run -p nxm-engine -- target/wasm32-wasip2/release/example.wasm + cargo run -p nexum-engine -- target/wasm32-wasip2/release/example.wasm # Check the entire workspace check: sync-wit cargo check --target wasm32-wasip2 -p example - cargo check -p nxm-engine + cargo check -p nexum-engine From 74fe1ab3609d87416a81c26f3e2abbd4c6f404fc Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 30 May 2026 22:55:47 +0000 Subject: [PATCH 05/17] docs: rename to nexum:runtime, unify error model, mark non-server platforms as planned --- README.md | 11 +- docs/00-overview.md | 136 +++++---- docs/01-runtime-environment.md | 291 ++++++++++--------- docs/02-modules-events-packaging.md | 221 +++++++++------ docs/04-state-store.md | 31 +- docs/05-sdk-design.md | 300 ++++++++++---------- docs/06-production-hardening.md | 12 +- docs/07-rpc-namespace-design.md | 420 +++++++++++++++------------ docs/08-platform-generalisation.md | 424 ++++++++++++++-------------- 9 files changed, 1008 insertions(+), 838 deletions(-) diff --git a/README.md b/README.md index a494e80..c44821d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ Shepherd is the [CoW Protocol](https://cow.fi) distribution of **Nexum**, a WebAssembly Component Model runtime for secure, sandboxed execution of capability-scoped modules. -A module compiled against the universal `web3:runtime/headless-module` world runs on any Nexum-compatible host. A module compiled against `shepherd:cow/shepherd-module` additionally gains access to CoW Protocol APIs and order submission — and requires a Shepherd host. +A module compiled against the universal `nexum:runtime/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. + +> **Upgrading from 0.1?** See the [Migration Guide](docs/migration/0.1-to-0.2.md) for the full rename table, the new `host-error` model, and the manifest-driven capability negotiation introduced in 0.2. ## Why @@ -20,9 +22,9 @@ A module compiled against the universal `web3:runtime/headless-module` world run | Path | Purpose | | --- | --- | -| `crates/nxm-engine/` | Host runtime — wasmtime-based component loader and host implementations. | +| `crates/nexum-engine/` | Host runtime — wasmtime-based component loader and host implementations. | | `modules/example/` | Reference guest module demonstrating the module ABI. | -| `wit/web3-runtime/` | Universal `web3:runtime` WIT package (csn, identity, local-store, remote-store, msg, logging). | +| `wit/nexum-runtime/` | Universal `nexum:runtime` WIT package (chain, identity, local-store, remote-store, messaging, logging). | | `wit/shepherd-cow/` | `shepherd:cow` WIT package — CoW Protocol-specific extensions. | | `docs/` | Architecture, design notes, and the universal primitive taxonomy. Start with [`docs/00-overview.md`](docs/00-overview.md). | @@ -57,8 +59,9 @@ The `docs/` directory contains the design corpus: - [`04-state-store.md`](docs/04-state-store.md) — local + remote state - [`05-sdk-design.md`](docs/05-sdk-design.md) — guest SDK - [`06-production-hardening.md`](docs/06-production-hardening.md) — operational concerns -- [`07-rpc-namespace-design.md`](docs/07-rpc-namespace-design.md) — `csn` namespace +- [`07-rpc-namespace-design.md`](docs/07-rpc-namespace-design.md) — `chain` namespace - [`08-platform-generalisation.md`](docs/08-platform-generalisation.md) — beyond CoW +- [`migration/0.1-to-0.2.md`](docs/migration/0.1-to-0.2.md) — upgrading from Nexum 0.1 ## Contributing diff --git a/docs/00-overview.md b/docs/00-overview.md index 939e9e5..e56fb2d 100755 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -2,7 +2,9 @@ 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 `web3:runtime/headless-module` world runs on any Nexum-compatible host. A module compiled against `shepherd:cow/shepherd-module` 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:runtime/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. + +> **Upgrading from 0.1?** See the [Migration Guide](migration/0.1-to-0.2.md) for the full rename table (`web3:runtime` → `nexum:runtime`, `csn` → `chain`, `msg` → `messaging`, `headless-module` → `event-module`, etc.), the unified `host-error` model, and the manifest-driven capability negotiation introduced in 0.2. ## Architecture @@ -20,8 +22,8 @@ flowchart TB end subgraph host["Host API — WIT Interfaces"] - uni["web3:runtime\ncsn · identity · local-store · remote-store · msg · logging"] - ext["shepherd:cow\ncow · order"] + uni["nexum:runtime\nchain · identity · local-store · remote-store · messaging · logging"] + ext["shepherd:cow\ncow-api"] end subgraph back["Backends"] @@ -53,74 +55,85 @@ flowchart TB ## The Six Primitives -Every module has access to six orthogonal capabilities through the `web3:runtime` WIT package: +Every module has access to six orthogonal capabilities through the `nexum:runtime` WIT package: | Primitive | Interface | Purpose | Scope | Backend (Server) | |-----------|-----------|---------|-------|-------------------| -| **Consensus** | `csn` | Read/write blockchain state via JSON-RPC | Global (per chain) | alloy Provider | +| **Chain** | `chain` | Read/write blockchain state via JSON-RPC | Global (per chain) | alloy Provider | | **Identity** | `identity` | Key management and message signing | Per-account | Keystore / KMS / HSM | | **Local Store** | `local-store` | Per-module key-value persistence | Device-local, per-module | redb | | **Remote Store** | `remote-store` | Decentralised content-addressed storage | Global (content-addressed) | Ethereum Swarm | -| **Messaging** | `msg` | Decentralised pub/sub messaging | Topic-based | Waku | +| **Messaging** | `messaging` | Decentralised pub/sub messaging | Topic-based | Waku | | **Logging** | `logging` | Diagnostic output | Per-module | tracing | These primitives are orthogonal: -- **Consensus** is the source of truth — the blockchain. Modules read chain state and (indirectly) write to it via order submission or transactions. -- **Identity** is cryptographic identity — key management and signing. The `csn` 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. +- **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. + +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. + ## WIT Worlds -The WIT is split into layered packages. The universal layer (`web3:runtime`) provides blockchain-agnostic capabilities. Domain extensions (e.g. `shepherd:cow`) add protocol-specific interfaces. +The WIT is split into layered packages. The universal layer (`nexum:runtime`) provides blockchain-agnostic capabilities. Domain extensions (e.g. `shepherd:cow`) add protocol-specific interfaces. ```mermaid graph TB subgraph l3["Layer 3 — Domain Extensions"] - cow["shepherd:cow\ncow · order"] + cow["shepherd:cow\ncow-api"] other["future:domain\nvault · strategy · …"] end subgraph l1["Layer 1 — Universal Runtime"] - pkg["web3:runtime"] - ifaces["csn · identity · local-store · remote-store · msg · logging"] + pkg["nexum:runtime"] + ifaces["chain · identity · local-store · remote-store · messaging · logging"] exports["Exports: init · on-event"] end - cow -->|include headless-module| l1 - other -->|include headless-module| l1 + cow -->|include event-module| l1 + other -->|include event-module| l1 ``` ``` // Universal layer — any platform, any blockchain app -package web3:runtime@0.1.0 +package nexum:runtime@0.2.0 -world headless-module { - import csn — consensus access (JSON-RPC passthrough) +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 msg — decentralised messaging (Waku) + 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, timer, message) + export on_event(event)— called per subscribed event (block, logs, tick, message) } // CoW Protocol extension -package shepherd:cow@0.1.0 +package shepherd:cow@0.2.0 -world shepherd-module { - include headless-module - import cow — CoW Protocol REST API access - import order — submit orders +world shepherd { + include event-module + import cow-api — CoW Protocol REST API + order submission } ``` -No WASI interfaces are imported. All I/O is mediated through host interfaces. The `csn` interface exposes a single generic `request` function — the SDK implements alloy's `Transport` trait on top of it, giving modules the full alloy `Provider` API (80+ methods) with zero WIT churn. The `identity` interface provides key management and signing — `csn` depends on it internally for signing RPC methods, and modules can also use it directly. +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. > Design rationale: [07-rpc-namespace-design.md](07-rpc-namespace-design.md) | Platform generalisation: [08-platform-generalisation.md](08-platform-generalisation.md) @@ -131,9 +144,9 @@ No WASI interfaces are imported. All I/O is mediated through host interfaces. Th | Concern | Choice | Version | |---------|--------|---------| | Language | Rust | 1.90+ | -| WASM runtime | wasmtime (Component Model) | 41.x | -| API contract | WIT (`web3:runtime@0.1.0`, `shepherd:cow@0.1.0`) | — | -| Guest bindings | wit-bindgen | 0.53.x | +| WASM runtime | wasmtime (Component Model) | 45.x | +| API contract | WIT (`nexum:runtime@0.2.0`, `shepherd:cow@0.2.0`) | — | +| Guest bindings | wit-bindgen | 0.57.x | | Async | Tokio | — | | Ethereum RPC | alloy | 1.5.x | | Local store | redb | 3.1.x | @@ -150,8 +163,8 @@ A module ships as a **bundle**: a manifest (`nexum.toml`) plus a compiled WASM c # nexum.toml [module] name = "twap-monitor" -version = "0.2.0" -wasm = "sha256:9f86d081…" # content hash of module.wasm +version = "0.3.0" +component = "sha256:9f86d081…" # content hash of module.wasm [module.resources] max_memory_bytes = 10_485_760 # 10 MB @@ -161,15 +174,20 @@ max_state_bytes = 52_428_800 # 50 MB [chains] required = [42161] # must have RPC for these chains -[[subscribe]] -type = "block" +[capabilities] +required = ["chain", "local-store", "logging"] +optional = ["messaging", "remote-store"] + +[[subscription]] +kind = "block" chain_id = 42161 [config] 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, and opaque module config — everything the runtime needs to load and run the module. +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) @@ -219,7 +237,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**: `[[subscribe]]` 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) @@ -241,20 +259,20 @@ 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` | -| | `Identity` — signing client (get accounts, sign messages, sign EIP-712 typed data) | +| | `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) | -| | `MsgClient` — typed messaging client (publish, query) | +| | `Messaging` — typed messaging client (publish, query) | | | `abi::sol!` — compile-time Ethereum ABI codec (alloy-sol-types) | | | `log::{info!, …}` — formatted logging macros | -| | `Error` / `Result` — proper error type with `?` support | +| | `HostError` / `HostErrorKind` — unified host error type with `?` support | | | `#[nexum::module]` — proc macro for universal modules | -| `shepherd-sdk` | `CowClient` — typed CoW Protocol API client backed by host `cow` interface | +| `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 | +| | `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. @@ -269,12 +287,16 @@ Multi-language support: module authors can use Rust, C/C++, Go, JavaScript, or P | CPU (deterministic) | Fuel | Trap -> rollback -> restart | | CPU (wall-clock) | Epoch interruption | Yield to Tokio | | Memory | `ResourceLimiter` | `memory.grow` denied | -| Storage | Host-side tracking | `local-store::set` returns `Err` | +| Storage | Host-side tracking | `local-store::set` returns `host-error { kind: quota-like }` | ### RPC Resilience Tower layer stack per chain: timeout -> retry (exponential + jitter) -> rate limit -> fallback endpoint. WebSocket subscriptions auto-reconnect with missed-block backfill. +### Error Model + +All host functions return `result` in 0.2. `host-error` carries a `domain` string (e.g. `"chain"`, `"store"`, `"messaging"`), a normative `host-error-kind` discriminant (`unsupported`, `unavailable`, `denied`, `rate-limited`, `timeout`, `invalid-input`, `internal`), a numeric `code`, a `message`, and optional JSON `data`. Modules match on `kind` for retry/backoff decisions; the per-protocol error types from 0.1 (`json-rpc-error`, `msg-error`, `store-error`, `api-error`) are gone. See the [migration guide](migration/0.1-to-0.2.md#2-error-model-unification-both) for the full shape and the embedder mapping table. + ### Observability | Signal | Stack | Endpoint | @@ -289,16 +311,18 @@ Metrics cover three groups: runtime-level (modules loaded/dead), per-module (eve ## Platform Generalisation -The WIT contract is the universal interface — any host that implements it can run modules unchanged. The architecture generalises beyond the server runtime to four platform targets: +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 | -| Platform | WASM Engine | Local Store | RPC Backend | Use Case | -|----------|-------------|-------------|-------------|----------| -| **Server** (reference) | wasmtime | redb | alloy provider | Headless automation | -| **Mobile** (Flutter/Dart) | wasmtime C API / wasm3 | SQLite | HTTP client | On-device automation | -| **WebView** | Browser engine + `jco` | IndexedDB | JS bridge / wallet | Rich web UIs | -| **Super app** | All of the above | SQLite | HTTP + wallet | Decentralised mini-programs | +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: [08-platform-generalisation.md](08-platform-generalisation.md) +-> Full design (and the design rationale for each target): [08-platform-generalisation.md](08-platform-generalisation.md) ## Grant Milestones @@ -315,17 +339,17 @@ The WIT contract is the universal interface — any host that implements it can ``` nexum/ ├── crates/ -│ ├── nxm-engine/ Core WASM host (server), event system, local store -│ ├── nexum-sdk/ Universal Rust SDK (HostTransport, Identity, TypedState, RemoteStore, MsgClient) -│ ├── shepherd-sdk/ CoW Protocol SDK (CowClient, extends nexum-sdk) +│ ├── nexum-engine/ Core WASM host (server), event system, local store +│ ├── nexum-sdk/ Universal Rust SDK (HostTransport, Signer, TypedState, RemoteStore, Messaging) +│ ├── shepherd-sdk/ CoW Protocol SDK (Cow, extends nexum-sdk) │ ├── cli/ nexum operator CLI (run, module, state) -│ └── cargo-nexum/ cargo subcommand for module authors (new, build, package, publish) +│ └── cargo-nexum/ cargo subcommand for module authors (new, build, package, publish, check, migrate) ├── modules/ │ ├── twap-monitor/ TWAP order monitoring module │ └── ethflow-watcher/ Ethflow order monitoring module ├── wit/ -│ ├── web3-runtime/ Universal WIT package (csn, identity, local-store, remote-store, msg, logging) -│ └── shepherd-cow/ CoW Protocol WIT package (cow, order, shepherd-module) +│ ├── nexum-runtime/ Universal WIT package (chain, identity, local-store, remote-store, messaging, logging) +│ └── shepherd-cow/ CoW Protocol WIT package (cow-api, shepherd) ├── docker/ │ └── Dockerfile └── docs/ @@ -337,5 +361,7 @@ nexum/ ├── 05-sdk-design.md ├── 06-production-hardening.md ├── 07-rpc-namespace-design.md - └── 08-platform-generalisation.md + ├── 08-platform-generalisation.md + └── migration/ + └── 0.1-to-0.2.md ``` diff --git a/docs/01-runtime-environment.md b/docs/01-runtime-environment.md index d87263f..ad66765 100755 --- a/docs/01-runtime-environment.md +++ b/docs/01-runtime-environment.md @@ -2,7 +2,7 @@ ## Version Target -**wasmtime 41.x** (latest stable as of Feb 2026). +**wasmtime 45.x** (latest stable as of Feb 2026). - Release cadence: new major on the 20th of each month. - LTS every 12th version (24 months support). Nearest LTS: v36. @@ -24,7 +24,7 @@ ### Rationale -The Component Model is **production-viable in wasmtime 41** and gives us critical advantages over raw core modules: +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. @@ -34,14 +34,14 @@ The Component Model is **production-viable in wasmtime 41** and gives us critica 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 `web3:runtime` world with exactly our host APIs. Zero WASI imports means zero implicit capabilities. +5. **No WASI required.** The Component Model and WASI are architecturally separate. We define a pure `nexum:runtime` world with exactly our host APIs. Zero WASI imports means zero implicit capabilities. 6. **Acceptable overhead.** The canonical ABI adds marshalling for strings/lists (memory copy across boundary), but for a plugin system with coarse-grained calls this is negligible. `InstancePre` front-loads validation costs. ### What we give up -- **Tooling churn.** `wit-bindgen` (v0.53) and `cargo-component` (v0.21) are functional but APIs are not yet stable. Pin versions in the SDK. -- **Native async Component Model** (`stream`, `future`) is still evolving (v41 had breaking changes to the async canonical ABI). We use basic async host functions (`func_wrap_async`) which are stable. +- **Tooling churn.** `wit-bindgen` (v0.57) and `cargo-component` (v0.21) are functional but APIs are not yet stable. Pin versions in the SDK. +- **Native async Component Model** (`stream`, `future`) is still evolving. We use basic async host functions (`func_wrap_async`) which are stable. ### Risk assessment @@ -90,63 +90,95 @@ store.epoch_deadline_async_yield_and_update(10); // yield after 10 epochs (~1s a ```rust let component = Component::from_file(&engine, "twap_monitor.wasm")?; let mut linker = Linker::new(&engine); -HeadlessModule::add_to_linker(&mut linker, |state| state)?; +EventModule::add_to_linker(&mut linker, |state| state)?; // Pre-validate once, instantiate many times (one per store) let pre = linker.instantiate_pre(&component)?; -let bindings = HeadlessModule::instantiate_pre(&mut store, &pre)?; +let bindings = EventModule::instantiate_pre(&mut store, &pre)?; ``` ## WIT Worlds: Universal and CoW-Specific -Nexum uses a two-layer WIT architecture. The **universal** package `web3:runtime` defines platform-agnostic interfaces and the `headless-module` world. The **CoW-specific** package `shepherd:cow` extends it with CoW Protocol interfaces and the `shepherd-module` world. +Nexum uses a two-layer WIT architecture. The **universal** package `nexum:runtime` defines platform-agnostic interfaces and the `event-module` world. The **CoW-specific** package `shepherd:cow` extends it with CoW Protocol interfaces and the `shepherd` world. -### Universal Package: `web3:runtime@0.1.0` +### Universal Package: `nexum:runtime@0.2.0` -The `web3:runtime` package is the single source of truth for the universal host-guest contract. It defines a custom world with **no WASI imports**: +The `nexum:runtime` package is the single source of truth for the universal host-guest contract. It defines a custom world with **no WASI imports**: ```wit -package web3:runtime@0.1.0; +package nexum:runtime@0.2.0; interface types { type chain-id = u64; - record block-data { + record block { chain-id: chain-id, number: u64, hash: list, - timestamp: u64, + timestamp: u64, // milliseconds since Unix epoch, UTC } - record log-entry { + record log { chain-id: chain-id, address: list, topics: list>, data: list, block-number: u64, - tx-hash: list, + transaction-hash: list, log-index: u32, } + record tick { + fired-at: u64, // milliseconds since Unix epoch, UTC + } + + record message { + content-topic: string, + payload: list, + timestamp: u64, // milliseconds since Unix epoch, UTC + sender: option>, + } + variant event { - block(block-data), - logs(list), - timer(u64), + block(block), + logs(list), + tick(tick), + message(message), } - /// Opaque config from nexum.toml [config] section. - type config = list>; -} + /// Typed config from nexum.toml [config] section. + /// Each entry pairs a key with a typed value (string/integer/boolean/list). + type config = list>; -interface csn { - use types.{chain-id}; + variant config-value { + string(string), + integer(s64), + boolean(bool), + list(list), + } - /// JSON-RPC error returned by the provider or the host. - record json-rpc-error { - code: s64, + /// Unified error type returned by every host function in 0.2. + record host-error { + domain: string, // "chain" | "store" | "messaging" | "identity" | "cow" | ... + kind: host-error-kind, // normative discriminant + code: s32, // domain-specific message: string, - data: option, + data: option, // JSON for richer context + } + + variant host-error-kind { + unsupported, // host does not implement this capability + unavailable, // capability exists, backend is down/offline + denied, // user or policy rejected + rate-limited, + timeout, + invalid-input, + internal, } +} + +interface chain { + use types.{chain-id, host-error}; /// Execute a JSON-RPC request against the specified chain. /// @@ -166,35 +198,39 @@ interface csn { /// Note: signing RPC methods (eth_sendTransaction, eth_accounts, /// eth_signTypedData_v4, personal_sign) are intercepted by the host and /// delegated to the identity backend. The module does not need to handle - /// key material directly when using csn for transactions. + /// key material directly when using chain for transactions. request: func(chain-id: chain-id, method: string, params: string) - -> result; + -> result; + + /// Additive 0.2 method: batched JSON-RPC. The alloy-backed HostTransport + /// routes RequestPacket::Batch through this — `provider.multicall(...)` + /// actually batches on the wire in 0.2. + request-batch: func(chain-id: chain-id, calls: list>) + -> result>, host-error>; } interface identity { - record identity-error { - code: u16, - message: string, - } + use types.{host-error}; /// Get available signing accounts (20-byte Ethereum addresses). - accounts: func() -> result>, identity-error>; + accounts: func() -> result>, host-error>; /// Sign raw bytes with the specified account. /// Returns a 65-byte ECDSA secp256k1 signature (r || s || v). /// Extensible to other signing schemes in future versions. - sign: func(account: list, data: list) -> result, identity-error>; + sign: func(account: list, data: list) -> result, host-error>; /// Sign EIP-712 typed data with the specified account. /// `typed-data` is the JSON-encoded EIP-712 TypedData structure. - sign-typed-data: func(account: list, typed-data: string) -> result, identity-error>; + sign-typed-data: func(account: list, typed-data: string) -> result, host-error>; } interface local-store { - get: func(key: string) -> result>, string>; - set: func(key: string, value: list) -> result<_, string>; - delete: func(key: string) -> result<_, string>; - list-keys: func(prefix: string) -> result, string>; + use types.{host-error}; + get: func(key: string) -> result>, host-error>; + set: func(key: string, value: list) -> result<_, host-error>; + delete: func(key: string) -> result<_, host-error>; + list-keys: func(prefix: string) -> result, host-error>; } interface logging { @@ -202,37 +238,39 @@ interface logging { log: func(level: level, message: string); } -/// The universal headless module world. Platform-agnostic: no CoW, +/// The universal event-driven module world. Platform-agnostic: no CoW, /// no domain-specific imports. Suitable for any web3 automation. -world headless-module { - import csn; +/// +/// 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 { + import chain; import identity; import local-store; + import remote-store; + import messaging; import logging; - /// Called once on load. Receives config from nexum.toml. - export init: func(config: types.config) -> result<_, string>; + /// Called once on load. Receives typed config from nexum.toml. + export init: func(config: types.config) -> result<_, host-error>; /// Called for each subscribed event. - export on-event: func(event: types.event) -> result<_, string>; + export on-event: func(event: types.event) -> result<_, host-error>; } ``` -### CoW-Specific Package: `shepherd:cow@0.1.0` +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. -The `shepherd:cow` package extends the universal world with CoW Protocol interfaces: +### CoW-Specific Package: `shepherd:cow@0.2.0` + +The `shepherd:cow` package extends the universal world with CoW Protocol interfaces. In 0.2 the two 0.1 interfaces (`cow` + `order`) merge into a single `cow-api` interface to eliminate the `cow::cow::request` triple-stutter: ```wit -package shepherd:cow@0.1.0; +package shepherd:cow@0.2.0; -interface cow { - use web3:runtime/types.{chain-id}; - - record api-error { - status: u16, - message: string, - body: option, - } +interface cow-api { + use nexum:runtime/types.{chain-id, host-error}; /// HTTP-style request to the CoW Protocol API. /// @@ -245,52 +283,50 @@ interface cow { method: string, path: string, body: option, - ) -> result; -} + ) -> result; -interface order { - use web3:runtime/types.{chain-id}; - - submit: func(chain-id: chain-id, order-data: list) - -> result; + /// Submit a serialised order to the CoW Protocol. + /// (Replaces the 0.1 `order::submit` interface.) + submit-order: func(chain-id: chain-id, order-data: list) + -> result; } -/// CoW Protocol module world. Extends the universal headless-module -/// with CoW-specific imports (cow API, order submission). -world shepherd-module { - include web3:runtime/headless-module; +/// CoW Protocol module world. Extends the universal event-module +/// with CoW-specific imports. +world shepherd { + include nexum:runtime/event-module; - import cow; - import order; + import cow-api; } ``` ### Key properties -- **No WASI** — modules cannot access FS, network, clocks, or random. +- **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 `csn` interface exposes a single `request` function. 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 `csn` 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 raw signing operations (sign arbitrary messages, get accounts). +- **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 raw signing operations (sign arbitrary messages, get accounts). +- **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** — `web3:runtime/headless-module` for platform-agnostic modules; `shepherd:cow/shepherd-module` for CoW Protocol modules that need `cow` and `order` imports. +- **Two worlds in 0.2's reference runtime** — `nexum:runtime/event-module` for platform-agnostic modules; `shepherd:cow/shepherd` for CoW Protocol modules that need the `cow-api` import. The experimental `nexum:runtime/query-module` world is published but not yet hosted. ## Host-Side Embedding -The host uses `wasmtime::component::bindgen!` to generate Rust traits from the WIT. For universal interfaces, the generated traits live under `web3::runtime::`. For CoW-specific interfaces, they live under `shepherd::cow::`. +The host uses `wasmtime::component::bindgen!` to generate Rust traits from the WIT. For universal interfaces, the generated traits live under `nexum::runtime::`. For CoW-specific interfaces, they live under `shepherd::cow::`. ```rust -// Universal headless-module world +// Universal event-module world wasmtime::component::bindgen!({ - path: "wit/web3-runtime", - world: "headless-module", + path: "wit/nexum-runtime", + world: "event-module", async: true, }); -// CoW-specific shepherd-module world (extends headless-module) +// CoW-specific shepherd world (extends event-module) wasmtime::component::bindgen!({ path: "wit/shepherd-cow", - world: "shepherd-module", + world: "shepherd", async: true, }); ``` @@ -307,18 +343,18 @@ trait Identity { } ``` -### Consensus depends on Identity +### Chain depends on Identity -The `csn` host implementation depends on `Identity` internally. When a module calls a signing RPC method through `csn::request` (e.g. `eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`), the host intercepts the call and delegates to the identity backend instead of forwarding to the RPC provider: +The `chain` host implementation depends on `Identity` internally. When a module calls a signing RPC method through `chain::request` (e.g. `eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`), the host intercepts the call and delegates to the identity backend instead of forwarding to the RPC provider: ```rust -impl web3::runtime::csn::Host for NexumHostState { +impl nexum::runtime::chain::Host for NexumHostState { async fn request( &mut self, chain_id: u64, method: String, params: String, - ) -> Result> { + ) -> Result> { // Signing methods are intercepted and delegated to identity. match method.as_str() { "eth_accounts" => { @@ -345,7 +381,9 @@ impl web3::runtime::csn::Host for NexumHostState { } if !self.is_method_allowed(&method) { - return Ok(Err(JsonRpcError { + return Ok(Err(HostError { + domain: "chain".into(), + kind: HostErrorKind::Denied, code: -32601, message: format!("method not allowed: {method}"), data: None, @@ -359,7 +397,7 @@ impl web3::runtime::csn::Host for NexumHostState { // 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())), - Err(e) => Ok(Err(e.into())), + Err(e) => Ok(Err(HostError::from_transport("chain", e))), } } } @@ -367,16 +405,19 @@ impl web3::runtime::csn::Host for NexumHostState { ### Identity Host Implementation -The `identity::Host` implementation delegates to the platform-specific `Identity` trait: +The `identity::Host` implementation delegates to the platform-specific `Identity` trait. Errors map to the unified `HostError`: ```rust -impl web3::runtime::identity::Host for NexumHostState { - async fn accounts(&mut self) -> Result>, IdentityError>> { +impl nexum::runtime::identity::Host for NexumHostState { + async fn accounts(&mut self) -> Result>, HostError>> { match self.identity.accounts() { Ok(addrs) => Ok(Ok(addrs.into_iter().map(|a| a.to_vec()).collect())), - Err(e) => Ok(Err(IdentityError { + Err(e) => Ok(Err(HostError { + domain: "identity".into(), + kind: HostErrorKind::Internal, code: 1, message: e.to_string(), + data: None, })), } } @@ -385,39 +426,36 @@ impl web3::runtime::identity::Host for NexumHostState { &mut self, account: Vec, data: Vec, - ) -> Result, IdentityError>> { + ) -> Result, HostError>> { let address = Address::from_slice(&account); match self.identity.sign(address, &data) { Ok(sig) => Ok(Ok(sig.to_vec())), - Err(e) => Ok(Err(IdentityError { + Err(IdentityBackendError::UserRejected) => Ok(Err(HostError { + domain: "identity".into(), + kind: HostErrorKind::Denied, code: 2, - message: e.to_string(), + message: "user rejected".into(), + data: None, })), - } - } - - async fn sign_typed_data( - &mut self, - account: Vec, - typed_data: String, - ) -> Result, IdentityError>> { - let address = Address::from_slice(&account); - match self.identity.sign_typed_data(address, &typed_data) { - Ok(sig) => Ok(Ok(sig.to_vec())), - Err(e) => Ok(Err(IdentityError { + Err(e) => Ok(Err(HostError { + domain: "identity".into(), + kind: HostErrorKind::Internal, code: 3, message: e.to_string(), + data: None, })), } } + + // sign_typed_data follows the same pattern. } ``` ### Local Store Host Implementation ```rust -impl web3::runtime::local_store::Host for NexumHostState { - async fn get(&mut self, key: String) -> Result>, String>> { +impl nexum::runtime::local_store::Host for NexumHostState { + async fn get(&mut self, key: String) -> Result>, HostError>> { // Read from the in-flight WriteTransaction (not a new ReadTransaction) // so the module sees its own uncommitted writes within a single on_event. let table = self.write_txn.open_table(self.local_store_table())?; @@ -426,19 +464,19 @@ impl web3::runtime::local_store::Host for NexumHostState { // ... } -impl shepherd::cow::cow::Host for NexumHostState { +impl shepherd::cow::cow_api::Host for NexumHostState { // CoW-specific host implementation // ... } ``` -See doc 07 for the full `csn` and `cow` host implementations, method allowlisting, and the `HostTransport` that bridges this to alloy's `Provider` API on the guest side. +See doc 07 for the full `chain` and `cow-api` host implementations, method allowlisting, and the `HostTransport` that bridges this to alloy's `Provider` API on the guest side. ## Guest-Side (Module Author) Experience ### Universal modules (`nexum-sdk`) -Module authors targeting the universal `headless-module` world add the `nexum-sdk` crate and use the `#[nexum::module]` proc macro. Modules can access identity for signing operations — either indirectly through `csn` (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::*; @@ -452,7 +490,7 @@ impl BlockLogger { Ok(()) } - async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { let block_num = provider.get_block_number().await?; info!("New block: {block_num}"); @@ -464,7 +502,7 @@ impl BlockLogger { ### CoW Protocol modules (`shepherd-sdk`) -Module authors targeting the CoW-specific `shepherd-module` world add the `shepherd-sdk` crate and use the `#[shepherd::module]` proc macro. The macro provides **named event handlers** (`on_block`, `on_logs`, `on_timer`) — 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::*; @@ -487,7 +525,7 @@ impl TwapMonitor { // 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 on_block(block: BlockData, provider: &RootProvider) -> Result<()> { + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { // Full alloy Provider API — natural .await let block_num = provider.get_block_number().await?; let balance = provider.get_balance(owner).latest().await?; @@ -502,7 +540,7 @@ impl TwapMonitor { let decoded = getTradeableOrderWithSignatureCall::abi_decode_returns(&result)?; // CoW API via typed client - let cow = CowClient::new(block.chain_id); + let cow = Cow::new(block.chain_id); cow.submit_order(&order)?; // State persistence @@ -511,7 +549,7 @@ impl TwapMonitor { } // Only define handlers for events you subscribe to. - // No on_logs or on_timer → those events are silently ignored. + // No on_logs, on_tick, or on_message → those events are silently ignored. } ``` @@ -530,7 +568,7 @@ See doc 05 for the full macro design (named handlers, provider injection, escape | **Python** | componentize-py (CPython) | Maturing | | **C#** | `wit-bindgen-csharp` | Emerging | -All produce valid components against the same WIT worlds (`web3:runtime/headless-module` for universal, `shepherd:cow/shepherd-module` for CoW). +All produce valid components against the same WIT worlds (`nexum:runtime/event-module` for universal, `shepherd:cow/shepherd` for CoW). ## Execution Metering @@ -569,29 +607,20 @@ All RPC and CoW API I/O is async (alloy / reqwest on the host). wasmtime bridges **Note:** We use wasmtime's basic async support (stable), *not* the Component Model native async (`stream`, `future`) which is still evolving. -## WASI: Intentionally Excluded (for now) +## WASI: Intentionally Excluded - WASI 0.2.1 is stable in wasmtime. WASI 0.3 (native async) is in preview. -- The `headless-module` world imports **zero WASI interfaces**. -- This is a security feature: components structurally cannot access FS/network/clocks. -- If a future use case needs selective WASI (e.g. `wasi:clocks` for timing), we can define an extended world: - -```wit -world headless-module-extended { - include headless-module; - import wasi:clocks/monotonic-clock@0.2.0; -} -``` - -The host only adds WASI to the linker for modules that request it — capability-based. +- 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). ## Summary: Nexum <-> wasmtime Mapping | Nexum Concept | wasmtime Primitive | |------------------|--------------------| | Runtime process | `Engine` (one, shared) | -| Universal API contract | WIT world (`web3:runtime/headless-module`) | -| CoW API contract | WIT world (`shepherd:cow/shepherd-module`) | +| Universal API contract | WIT world (`nexum:runtime/event-module`) | +| CoW API contract | WIT world (`shepherd:cow/shepherd`) | | Compiled module | `Component` (cached, thread-safe) | | Pre-validated module | `InstancePre` (linker + component) | | Running instance | `Store` + `Instance` | diff --git a/docs/02-modules-events-packaging.md b/docs/02-modules-events-packaging.md index 5de2901..bffdff2 100755 --- a/docs/02-modules-events-packaging.md +++ b/docs/02-modules-events-packaging.md @@ -11,12 +11,12 @@ Every module ships with a manifest: ```toml [module] name = "twap-monitor" -version = "0.2.0" +version = "0.3.0" description = "Monitors and posts TWAP order parts" authors = ["mfw78.eth"] # Content hash of the compiled .wasm component -wasm = "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" +component = "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" [module.resources] max_memory_bytes = 10_485_760 # 10 MB @@ -31,34 +31,48 @@ max_consecutive_failures = 10 # Dead after this many consecutive failures 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. +# 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. +[capabilities] +required = ["chain", "local-store", "logging"] +optional = ["messaging", "remote-store"] +denied = [] + +[capabilities.http] +allow = ["api.cow.fi"] # outbound HTTP domain allowlist + # Event subscriptions — declares what the runtime should feed this module -[[subscribe]] -type = "block" +[[subscription]] +kind = "block" chain_id = 42161 -[[subscribe]] -type = "log" +[[subscription]] +kind = "log" chain_id = 42161 address = "0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74" topics = ["0x…"] # ComposableCoW ConditionalOrderCreated -[[subscribe]] -type = "cron" +[[subscription]] +kind = "cron" schedule = "*/5 * * * *" # every 5 minutes -# Arbitrary key-value config passed to the module at init +# 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 +min_twap_interval_secs = 120 # integer stays integer +enable_alerts = true # boolean stays boolean ``` Key design points: -- **`wasm` is a content hash**, not a filename. The runtime resolves it via the content store (see below). -- **`subscribe` 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`. +- **`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). -- **`config`** is opaque to the runtime. All values are **stringified** before being passed to the module's `init` export as `list>` (e.g. TOML integer `120` becomes the string `"120"`). Modules are responsible for parsing values from strings. +- **`config`** is opaque to the runtime semantically but **typed on the wire in 0.2**. The 0.1 WIT flattened every value to a string (`list>`); 0.2 uses `list>` where `config-value` is a variant carrying string / integer / boolean / list. The SDK's `Config::from_host` or `#[derive(NexumConfig)]` give typed accessors. ### Bundle Format @@ -67,10 +81,10 @@ A bundle is a **directory** with a fixed layout: ``` twap-monitor/ ├── nexum.toml # manifest -└── module.wasm # compiled component (matches wasm hash) +└── module.wasm # compiled component (matches component hash) ``` -The runtime validates that `sha256(module.wasm)` matches the hash in the manifest's `wasm` field (after stripping the `sha256:` scheme prefix). This integrity check applies regardless of transport. +The runtime validates that `sha256(module.wasm)` matches the hash in the manifest's `component` field (after stripping the `sha256:` scheme prefix). This integrity check applies regardless of transport. How the directory is represented depends on the content backend: @@ -97,7 +111,7 @@ Distribution is **agnostic** — the runtime resolves content by hash through pl | `sha256` | `sha256:9f86d08…` | Local content store lookup | | `bzz` | `bzz:22cbb9cedc…` | Ethereum Swarm (64-char hex, 256-bit) | | `ipfs` | `ipfs:QmYwAPJz…` | IPFS CID | -| `oci` | `oci:ghcr.io/org/twap:0.2.0` | OCI registry (CNCF WASM artifact format) | +| `oci` | `oci:ghcr.io/org/twap:0.3.0` | OCI registry (CNCF WASM artifact format) | | `https` | `https://example.com/twap.wasm` | Direct HTTP fetch (hash-verified after download) | ### Runtime Content Store @@ -106,7 +120,7 @@ The runtime maintains a local content-addressed store (a directory of blobs keye ```mermaid flowchart TD - A[Read manifest] --> B[Extract wasm content reference] + A[Read manifest] --> B[Extract component content reference] B --> C{Hash in local store?} C -->|Hit| F[Return path to verified .wasm] C -->|Miss| D[Resolve via configured backend] @@ -162,10 +176,10 @@ stateDiagram-v2 | State | Description | |-------|-------------| -| **Resolve** | Content store resolves `wasm` hash to local path. Fail -> `Dead`. | -| **Load** | `Component::from_file`, create `InstancePre`. Validates that the component satisfies the target WIT world (`web3:runtime/headless-module` or `shepherd:cow/shepherd-module`). Fail -> `Dead`. | +| **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:runtime/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). | -| **Run** | Runtime dispatches events to `on_event`. Each call gets a fuel budget. Module processes events and may call host imports (csn, local-store, identity, cow, order). | +| **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. | @@ -243,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 `csn::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 @@ -251,79 +265,116 @@ Events cross the WASM boundary as the `event` variant defined in the WIT: ```wit variant event { - block(block-data), - logs(list), - timer(u64), + block(block), + logs(list), + tick(tick), + message(message), } -record block-data { +record block { chain-id: u64, number: u64, hash: list, - timestamp: u64, + timestamp: u64, // milliseconds since Unix epoch, UTC +} + +record tick { + fired-at: u64, // milliseconds since Unix epoch, UTC } ``` -The runtime serialises event data via the canonical ABI (handled automatically by `bindgen!`). +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 -The initial WIT in `01-runtime-environment.md` is extended to support the lifecycle and config. The architecture uses two packages: `web3:runtime` for universal interfaces and `shepherd:cow` for CoW Protocol extensions. +The initial WIT in `01-runtime-environment.md` is extended to support the lifecycle and config. The architecture uses two packages: `nexum:runtime` for universal interfaces and `shepherd:cow` for CoW Protocol extensions. -### Universal Package: `web3:runtime@0.1.0` +### Universal Package: `nexum:runtime@0.2.0` ```wit -package web3:runtime@0.1.0; +package nexum:runtime@0.2.0; interface types { type chain-id = u64; - record block-data { + record block { chain-id: chain-id, number: u64, hash: list, - timestamp: u64, + timestamp: u64, // ms since Unix epoch, UTC } - record log-entry { + record log { chain-id: chain-id, address: list, topics: list>, data: list, block-number: u64, - tx-hash: list, + transaction-hash: list, log-index: u32, } + record tick { + fired-at: u64, // ms since Unix epoch, UTC + } + + record message { + content-topic: string, + payload: list, + timestamp: u64, // ms since Unix epoch, UTC + sender: option>, + } + variant event { - block(block-data), - logs(list), - timer(u64), + block(block), + logs(list), + tick(tick), + message(message), } - /// Opaque config map from nexum.toml [config] section. - type config = list>; -} + /// Typed config map from nexum.toml [config] section. + type config = list>; -interface csn { - use types.{chain-id}; + variant config-value { + string(string), + integer(s64), + boolean(bool), + list(list), + } - record json-rpc-error { - code: s64, + /// Unified host error (replaces the five per-protocol errors from 0.1). + record host-error { + domain: string, + kind: host-error-kind, + code: s32, message: string, data: option, } + variant host-error-kind { + unsupported, unavailable, denied, rate-limited, + timeout, invalid-input, internal, + } +} + +interface chain { + use types.{chain-id, host-error}; + /// Generic JSON-RPC passthrough. See doc 07 for full design rationale. request: func(chain-id: chain-id, method: string, params: string) - -> result; + -> result; + + /// Additive 0.2: batched JSON-RPC. + request-batch: func(chain-id: chain-id, calls: list>) + -> result>, host-error>; } interface local-store { - get: func(key: string) -> result>, string>; - set: func(key: string, value: list) -> result<_, string>; - delete: func(key: string) -> result<_, string>; - list-keys: func(prefix: string) -> result, string>; + use types.{host-error}; + get: func(key: string) -> result>, host-error>; + set: func(key: string, value: list) -> result<_, host-error>; + delete: func(key: string) -> result<_, host-error>; + list-keys: func(prefix: string) -> result, host-error>; } interface logging { @@ -332,40 +383,38 @@ interface logging { } interface identity { - record identity-error { code: u16, message: string } - accounts: func() -> result>, identity-error>; - sign: func(account: list, data: list) -> result, identity-error>; - sign-typed-data: func(account: list, typed-data: string) -> result, identity-error>; + use types.{host-error}; + accounts: func() -> result>, host-error>; + sign: func(account: list, data: list) -> result, host-error>; + sign-typed-data: func(account: list, typed-data: string) -> result, host-error>; } -/// Universal headless module world — platform-agnostic. -world headless-module { - import csn; +/// 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 { + import chain; + import identity; import local-store; + import remote-store; + import messaging; import logging; - import identity; - /// Called once on load. Receives config from nexum.toml. - export init: func(config: types.config) -> result<_, string>; + /// Called once on load. Receives typed config from nexum.toml. + export init: func(config: types.config) -> result<_, host-error>; /// Called for each subscribed event. - export on-event: func(event: types.event) -> result<_, string>; + export on-event: func(event: types.event) -> result<_, host-error>; } ``` -### CoW-Specific Package: `shepherd:cow@0.1.0` +### CoW-Specific Package: `shepherd:cow@0.2.0` ```wit -package shepherd:cow@0.1.0; +package shepherd:cow@0.2.0; -interface cow { - use web3:runtime/types.{chain-id}; - - record api-error { - status: u16, - message: string, - body: option, - } +interface cow-api { + use nexum:runtime/types.{chain-id, host-error}; /// HTTP-style request to the CoW Protocol API. request: func( @@ -373,23 +422,18 @@ interface cow { method: string, path: string, body: option, - ) -> result; -} - -interface order { - use web3:runtime/types.{chain-id}; + ) -> result; - submit: func(chain-id: chain-id, order-data: list) - -> result; + /// Submit a serialised order. (Merged in from the 0.1 `order` interface.) + submit-order: func(chain-id: chain-id, order-data: list) + -> result; } -/// CoW Protocol module world — extends headless-module with -/// CoW-specific imports (cow API, order submission). -world shepherd-module { - include web3:runtime/headless-module; +/// CoW Protocol module world — extends event-module with cow-api. +world shepherd { + include nexum:runtime/event-module; - import cow; - import order; + import cow-api; } ``` @@ -404,19 +448,20 @@ Operator deploys a module: manifest = "/var/nexum/twap-monitor/nexum.toml" 2. Runtime reads manifest: - - Resolves wasm content hash → fetches from Swarm/local/OCI + - Resolves component content hash → fetches from Swarm/local/OCI - Verifies integrity (sha256 match) 3. Runtime compiles Component, creates InstancePre: - Validates component satisfies target world - (web3:runtime/headless-module or shepherd:cow/shepherd-module) + (nexum:runtime/event-module or shepherd:cow/shepherd) + - Installs trap stubs for any [capabilities].optional imports the host doesn't provide - Enforces resource limits from manifest 4. Runtime calls init(config): - - Module receives [config] section as key-value pairs + - Module receives [config] section as typed key-value pairs - Module sets up internal state, logs readiness -5. Runtime wires event sources from [[subscribe]] blocks: +5. Runtime wires event sources from [[subscription]] blocks: - Creates/reuses block subscriber for chain 42161 - Creates log watcher with address + topic filter - Registers cron schedule @@ -425,7 +470,7 @@ Operator deploys a module: Block 19_000_001 on Arbitrum → Router → twap-monitor's dispatch queue → Tokio task calls on_event(Event::Block(…)) - → Module calls csn::request (via alloy Provider), local_store_get, order_submit + → Module calls chain::request (via alloy Provider), local-store get, cow-api submit-order → Returns Ok(()) — runtime logs success 7. On crash: diff --git a/docs/04-state-store.md b/docs/04-state-store.md index 0f2f8a4..b76a6dd 100755 --- a/docs/04-state-store.md +++ b/docs/04-state-store.md @@ -48,20 +48,27 @@ This per-file design ensures concurrent modules never contend on write locks (se ```wit interface local-store { + use nexum:runtime/types.{host-error}; + /// Get a value by key. Returns none if key doesn't exist. - get: func(key: string) -> result>, string>; + get: func(key: string) -> result>, host-error>; /// Set a key-value pair. Overwrites existing value. - set: func(key: string, value: list) -> result<_, string>; + /// 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. + set: func(key: string, value: list) -> result<_, host-error>; /// Delete a key. No-op if key doesn't exist. - delete: func(key: string) -> result<_, string>; + delete: func(key: string) -> result<_, host-error>; /// List keys matching a prefix. Returns keys only (not values). - list-keys: func(prefix: string) -> result, string>; + list-keys: func(prefix: string) -> result, host-error>; } ``` +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). `list-keys` enables prefix-based namespacing within a module's state: @@ -138,13 +145,19 @@ The manifest declares `max_state_bytes`. The runtime tracks total bytes stored p ```rust // Host-side enforcement (simplified) impl local_store::Host for NexumHostState { - async fn set(&mut self, key: String, value: Vec) -> Result> { + async fn set(&mut self, key: String, value: Vec) -> Result> { let new_size = self.state_bytes_used - self.current_value_size(&key) + key.len() + value.len(); if new_size > self.module_config.max_state_bytes { - return Ok(Err("state quota exceeded".into())); + return Ok(Err(HostError { + domain: "store".into(), + kind: HostErrorKind::InvalidInput, + code: 1, + message: "state quota exceeded".into(), + data: None, + })); } self.write_txn.insert(&*key, value.as_slice())?; @@ -163,7 +176,7 @@ The tracking is approximate (doesn't account for B-tree overhead) but sufficient On first load, the module's table is empty. The module's `init` function should handle this: ```rust -fn init(config: Vec<(String, String)>) -> Result<(), String> { +fn init(config: Config) -> Result<(), HostError> { if local_store::get("initialized")?.is_none() { // First run — set up initial state local_store::set("initialized", &[1])?; @@ -180,7 +193,7 @@ On restart, the module gets a fresh WASM instance but the **same state table**. The module should read its checkpoint from state in `init` and resume: ```rust -fn init(_config: Vec<(String, String)>) -> Result<(), String> { +fn init(_config: Config) -> Result<(), HostError> { let last_block = local_store::get("last_block")? .map(|b| u64::from_le_bytes(b.try_into().unwrap())) .unwrap_or(0); @@ -194,7 +207,7 @@ fn init(_config: Vec<(String, String)>) -> Result<(), String> { When a module is updated (new WASM binary, same `name` in manifest), the new version inherits the existing state table. The new version's `init` is responsible for any migration: ```rust -fn init(config: Vec<(String, String)>) -> Result<(), String> { +fn init(config: Config) -> Result<(), HostError> { let version = local_store::get("schema_version")? .map(|b| u64::from_le_bytes(b.try_into().unwrap())) .unwrap_or(0); diff --git a/docs/05-sdk-design.md b/docs/05-sdk-design.md index 84c8f19..374ce77 100755 --- a/docs/05-sdk-design.md +++ b/docs/05-sdk-design.md @@ -4,20 +4,21 @@ The SDK is split into two layers: -1. **`nexum-sdk`** -- the universal SDK for any `web3:runtime/headless-module`. It provides: +1. **`nexum-sdk`** -- the universal SDK for any `nexum:runtime/event-module`. It provides: - WIT bindings (re-exported, version-pinned) - A proc macro (`#[nexum::module]`) that eliminates boilerplate (supports `async fn` for natural `.await`) - A full alloy `Provider` backed by the host's RPC stack (`HostTransport`) - Typed local-store helpers (serde over raw bytes) - - Typed identity helpers (`IdentityClient` for key management and signing) + - A typed `Signer` for key management and signing - Ethereum ABI helpers (alloy-sol-types integration) - A test harness with a mock host (`MockHost`) - A logging convenience layer + - The unified `HostError` / `HostErrorKind` error model 2. **`shepherd-sdk`** -- the CoW Protocol extension. It re-exports everything from `nexum-sdk` and adds: - CoW-specific WIT bindings (`shepherd:cow`) - - A typed CoW Protocol API client (`CowClient`) - - A proc macro (`#[shepherd::module]`) that targets the `shepherd:cow/shepherd-module` world + - A typed CoW Protocol API client (`Cow`) + - A proc macro (`#[shepherd::module]`) that targets the `shepherd:cow/shepherd` world - CoW-specific mock testing utilities Module authors should never interact with `wit-bindgen` or the canonical ABI directly. @@ -28,14 +29,14 @@ Module authors should never interact with `wit-bindgen` or the canonical ABI dir nexum-sdk/ ├── Cargo.toml ├── src/ -│ ├── lib.rs # re-exports, prelude, provider() constructor, block_on() +│ ├── lib.rs # re-exports, prelude, provider() constructor (block_on is internal) │ ├── bindings.rs # generated by wit-bindgen (checked in or build.rs) -│ ├── transport.rs # HostTransport -- alloy Transport impl over csn::request +│ ├── transport.rs # HostTransport -- alloy Transport impl over chain::request / chain::request-batch │ ├── local_store.rs # typed local-store helpers -│ ├── identity.rs # IdentityClient -- typed identity helpers (accounts, signing) +│ ├── signer.rs # Signer -- typed identity helpers (accounts, signing) │ ├── abi.rs # Ethereum ABI encoding/decoding │ ├── log.rs # logging convenience -│ ├── error.rs # error types +│ ├── error.rs # HostError / HostErrorKind │ └── testing.rs # mock host, test harness └── macros/ └── src/ @@ -46,19 +47,19 @@ shepherd-sdk/ ├── src/ │ ├── lib.rs # re-exports nexum-sdk, adds CoW-specific API │ ├── bindings.rs # generated CoW WIT bindings (shepherd:cow) -│ ├── cow.rs # CowClient -- typed CoW Protocol API wrapper +│ ├── cow.rs # Cow -- typed CoW Protocol API wrapper │ └── testing.rs # CoW-specific mock utilities └── macros/ └── src/ └── lib.rs # #[shepherd::module] proc macro (CoW variant) ``` -The workspace root `wit/web3-runtime/` is the **universal WIT definition**. The `wit/shepherd-cow/` directory extends it with CoW Protocol interfaces. The SDKs reference these via path (not a copy) to prevent drift: +The workspace root `wit/nexum-runtime/` is the **universal WIT definition**. The `wit/shepherd-cow/` directory extends it with CoW Protocol interfaces. The SDKs reference these via path (not a copy) to prevent drift: ```toml # nexum-sdk/Cargo.toml [package.metadata.component.target] -path = "../wit/web3-runtime" +path = "../wit/nexum-runtime" ``` ```toml @@ -73,20 +74,21 @@ Both SDKs pin a specific `wit-bindgen` version so module authors are insulated f ### Universal: `#[nexum::module]` -Without the macro, a module author writes: +Without the macro, a module author writes (against the typed 0.2 config): ```rust -wit_bindgen::generate!({ world: "headless-module", path: "..." }); +wit_bindgen::generate!({ world: "event-module", path: "..." }); struct MyModule; impl Guest for MyModule { - fn init(config: Vec<(String, String)>) -> Result<(), String> { ... } - fn on_event(event: Event) -> Result<(), String> { + fn init(config: Config) -> Result<(), HostError> { ... } + fn on_event(event: Event) -> Result<(), HostError> { match event { Event::Block(block) => { ... } Event::Logs(logs) => { ... } - Event::Timer(ts) => { ... } + Event::Tick(tick) => { ... } + Event::Message(msg) => { ... } } } } @@ -107,28 +109,28 @@ impl TwapMonitor { Ok(()) } - async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { let num = provider.get_block_number().await?; // ... Ok(()) } - async fn on_logs(logs: Vec, provider: &RootProvider) -> Result<()> { + async fn on_logs(logs: Vec, provider: &RootProvider) -> Result<()> { for log in &logs { // ... } Ok(()) } - // on_timer not defined -> timer events are silently ignored + // on_tick / on_message not defined -> those events are silently ignored } ``` -The `#[nexum::module]` macro generates code against the `web3:runtime/headless-module` world. +The `#[nexum::module]` macro generates code against the `nexum:runtime/event-module` world. ### CoW Protocol: `#[shepherd::module]` -For CoW Protocol modules, the `#[shepherd::module]` macro targets the `shepherd:cow/shepherd-module` world, which extends `headless-module` with CoW-specific imports: +For CoW Protocol modules, the `#[shepherd::module]` macro targets the `shepherd:cow/shepherd` world, which extends `event-module` with the merged `cow-api` import: ```rust use shepherd_sdk::prelude::*; @@ -141,8 +143,8 @@ impl CowTwapMonitor { Ok(()) } - async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { - let cow = CowClient::new(block.chain_id); + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { + let cow = Cow::new(block.chain_id); let quote = cow.get_quote(&OrderQuoteRequest { /* ... */ })?; // ... Ok(()) @@ -155,14 +157,14 @@ impl CowTwapMonitor { For the universal `#[nexum::module]`: ```rust -wit_bindgen::generate!({ world: "headless-module", path: "..." }); +wit_bindgen::generate!({ world: "event-module", path: "..." }); impl Guest for TwapMonitor { - fn init(config: Vec<(String, String)>) -> Result<(), String> { - TwapMonitor::init(config.into()).map_err(|e| e.to_string()) + fn init(config: Config) -> Result<(), HostError> { + TwapMonitor::init(config.into()).map_err(HostError::from) } - fn on_event(event: types::Event) -> Result<(), String> { + fn on_event(event: types::Event) -> Result<(), HostError> { nexum_sdk::block_on(async { match event { Event::Block(block) => { @@ -173,24 +175,26 @@ impl Guest for TwapMonitor { let provider = nexum_sdk::provider(logs[0].chain_id); TwapMonitor::on_logs(logs, &provider).await } - Event::Timer(_) => Ok(()), // no handler defined + Event::Tick(_) => Ok(()), // no handler defined + Event::Message(_) => Ok(()), // no handler defined } - }).map_err(|e| e.to_string()) + }).map_err(HostError::from) } } export!(TwapMonitor); ``` -For the CoW `#[shepherd::module]`, the generated code additionally imports `shepherd:cow` interfaces (cow, order) alongside the `web3:runtime` base. +For the CoW `#[shepherd::module]`, the generated code additionally imports `shepherd:cow/cow-api` alongside the `nexum:runtime` base. ### Named event handlers | Handler | Payload | Optional injectable context | |---|---|---| -| `on_block(block)` | `BlockData` | `provider: &RootProvider` (from `block.chain_id`) | -| `on_logs(logs)` | `Vec` | `provider: &RootProvider` (from `logs[0].chain_id`) | -| `on_timer(timestamp)` | `u64` | None (no chain context) | +| `on_block(block)` | `Block` | `provider: &RootProvider` (from `block.chain_id`) | +| `on_logs(logs)` | `Vec` | `provider: &RootProvider` (from `logs[0].chain_id`) | +| `on_tick(tick)` | `Tick` (`tick.fired_at`) | None (no chain context) | +| `on_message(message)` | `Message` | None | The macro inspects each handler's signature: @@ -224,7 +228,7 @@ impl CustomModule { Resolution order: 1. `on_event` defined -> use it directly (wrap in `block_on` if async) -2. Any of `on_block` / `on_logs` / `on_timer` defined -> generate the match dispatch +2. Any of `on_block` / `on_logs` / `on_tick` / `on_message` defined -> generate the match dispatch 3. Neither -> compile error > Full async design rationale: [07-rpc-namespace-design.md](07-rpc-namespace-design.md#eliminating-block_on-async-module-functions) @@ -235,19 +239,19 @@ Resolution order: ```rust // nexum_sdk::prelude -pub use crate::bindings::web3::runtime::types::*; -pub use crate::bindings::web3::runtime::csn; -pub use crate::bindings::web3::runtime::identity; -pub use crate::bindings::web3::runtime::local_store; -pub use crate::bindings::web3::runtime::remote_store; -pub use crate::bindings::web3::runtime::msg; -pub use crate::bindings::web3::runtime::logging; +pub use crate::bindings::nexum::runtime::types::*; +pub use crate::bindings::nexum::runtime::chain; +pub use crate::bindings::nexum::runtime::identity; +pub use crate::bindings::nexum::runtime::local_store; +pub use crate::bindings::nexum::runtime::remote_store; +pub use crate::bindings::nexum::runtime::messaging; +pub use crate::bindings::nexum::runtime::logging; pub use crate::log::{trace, debug, info, warn, error}; pub use crate::local_store::TypedState; -pub use crate::identity::IdentityClient; +pub use crate::signer::Signer; pub use crate::transport::HostTransport; -pub use crate::{provider, block_on}; -pub use crate::error::{Result, Error}; +pub use crate::provider; +pub use crate::error::{Result, HostError, HostErrorKind}; // Re-export alloy essentials so modules don't need direct alloy dependencies pub use alloy_primitives::{Address, B256, U256, Bytes}; @@ -256,19 +260,20 @@ pub use alloy_rpc_types::*; pub use alloy_provider::Provider; ``` -One `use nexum_sdk::prelude::*;` gives module authors everything they need -- including the alloy `Provider` trait, primitive types, `sol!` macro, and `IdentityClient` for signing. +One `use nexum_sdk::prelude::*;` gives module authors everything they need -- including the alloy `Provider` trait, primitive types, `sol!` macro, and `Signer` for signing. + +`block_on` is no longer a public re-export in 0.2 -- it's hidden behind the `#[nexum::module]` macro. See the [migration guide §7](migration/0.1-to-0.2.md#7-sdk-changes-author) for the full SDK rename table. ### CoW Protocol: `shepherd_sdk::prelude` ```rust // shepherd_sdk::prelude pub use nexum_sdk::prelude::*; // re-export universal prelude -pub use crate::bindings::shepherd::cow::cow; -pub use crate::bindings::shepherd::cow::order; -pub use crate::cow::CowClient; +pub use crate::bindings::shepherd::cow::cow_api; +pub use crate::cow::Cow; ``` -One `use shepherd_sdk::prelude::*;` gives CoW module authors everything from the universal SDK plus CoW-specific types and the `CowClient`. +One `use shepherd_sdk::prelude::*;` gives CoW module authors everything from the universal SDK plus the merged CoW `cow-api` interface and the typed `Cow` client. ## Typed Local-Store Helpers @@ -332,26 +337,26 @@ impl TypedState { Serialisation uses **postcard** (compact, no-std, deterministic) rather than JSON to minimise local-store storage overhead. -## Identity Client +## Signer -The `identity` WIT interface provides cryptographic identity -- key management and signing (ECDSA secp256k1 by default, extensible). The SDK wraps this with a typed `IdentityClient`: +The `identity` WIT interface provides cryptographic identity -- key management and signing (ECDSA secp256k1 by default, extensible). The SDK wraps this with a typed `Signer`: ```rust use nexum_sdk::prelude::*; // Get available signing accounts -let accounts = IdentityClient::accounts()?; +let accounts = Signer::accounts()?; for account in &accounts { info!("available signer: 0x{}", hex::encode(account)); } // Sign raw bytes with a specific account -let signature = IdentityClient::sign(&accounts[0], &data_to_sign)?; +let signature = Signer::sign(&accounts[0], &data_to_sign)?; // signature is 65 bytes: r (32) || s (32) || v (1) // Sign EIP-712 typed data let typed_data_json = r#"{"types":...,"primaryType":"Order","domain":...,"message":...}"#; -let signature = IdentityClient::sign_typed_data(&accounts[0], typed_data_json)?; +let signature = Signer::sign_typed_data(&accounts[0], typed_data_json)?; ``` Implementation: @@ -361,15 +366,12 @@ Implementation: /// /// Provides cryptographic signing operations backed by the host runtime's /// key management. The host manages private keys -- modules never see them. -pub struct IdentityClient; +pub struct Signer; -impl IdentityClient { +impl Signer { /// Get available signing accounts (20-byte Ethereum addresses). pub fn accounts() -> Result>> { - identity::accounts().map_err(|e| Error::Identity { - code: e.code, - message: e.message, - }) + identity::accounts().map_err(HostError::from) } /// Get available signing accounts as alloy `Address` types. @@ -379,10 +381,8 @@ impl IdentityClient { .into_iter() .map(|a| { Address::try_from(a.as_slice()) - .map_err(|_| Error::Identity { - code: 1, - message: "invalid address length".into(), - }) + .map_err(|_| HostError::module("identity", HostErrorKind::InvalidInput, + "invalid address length")) }) .collect() } @@ -390,29 +390,25 @@ impl IdentityClient { /// Sign raw bytes with the specified account. /// Returns a 65-byte ECDSA secp256k1 signature (r || s || v). pub fn sign(account: &[u8], data: &[u8]) -> Result> { - identity::sign(account, data).map_err(|e| Error::Identity { - code: e.code, - message: e.message, - }) + identity::sign(account, data).map_err(HostError::from) } /// Sign EIP-712 typed data with the specified account. /// `typed_data` is a JSON string conforming to the EIP-712 specification. /// Returns a 65-byte ECDSA secp256k1 signature (r || s || v). pub fn sign_typed_data(account: &[u8], typed_data: &str) -> Result> { - identity::sign_typed_data(account, typed_data).map_err(|e| Error::Identity { - code: e.code, - message: e.message, - }) + identity::sign_typed_data(account, typed_data).map_err(HostError::from) } } ``` -Note: modules can also use `identity` indirectly through `csn`. When a module calls `csn::request` with a signing method (e.g. `eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`), the host's `csn` implementation delegates to the `identity` backend internally. The `IdentityClient` is for modules that need direct, raw signing operations. +Note: modules can also use `identity` indirectly through `chain`. When a module calls `chain::request` with a signing method (e.g. `eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`), the host's `chain` implementation delegates to the `identity` backend internally. `Signer` is for modules that need direct, raw signing operations -- e.g. EIP-712 over an off-chain order payload. + +Modules can match on `HostErrorKind::Denied` to distinguish "user rejected" from a transport failure -- see the [migration guide §2](migration/0.1-to-0.2.md#2-error-model-unification-both) for the embedder mapping table. ## Ethereum ABI Helpers & alloy Provider -Modules frequently need to read chain state and encode/decode Ethereum calldata. The SDK provides a full alloy `Provider` (via `HostTransport` over `csn::request`) and integrates `alloy-sol-types` and `alloy-primitives` (compiled to WASM): +Modules frequently need to read chain state and encode/decode Ethereum calldata. The SDK provides a full alloy `Provider` (via `HostTransport` over `chain::request` / `chain::request-batch`) and integrates `alloy-sol-types` and `alloy-primitives` (compiled to WASM): ```rust use nexum_sdk::prelude::*; @@ -430,7 +426,7 @@ sol! { } // Named handler -- provider is injected by the macro -async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { +async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { // Full alloy Provider API -- natural .await let block_num = provider.get_block_number().await?; let balance = provider.get_balance(owner_addr).latest().await?; @@ -457,25 +453,25 @@ The SDK re-exports: - `alloy_provider::Provider` -- the full alloy Provider trait. - `alloy_rpc_types::*` -- `TransactionRequest`, `Filter`, `Block`, etc. -These are already WASM-compatible (no-std support, no system dependencies). The `HostTransport` routes all RPC calls through the single `csn::request` host function -- see doc 07 for the full design. +These are already WASM-compatible (no-std support, no system dependencies). The `HostTransport` routes all RPC calls through the `chain::request` (and batched `chain::request-batch`) host functions -- see doc 07 for the full design. -## CoW Protocol API: `CowClient` +## CoW Protocol API: `Cow` -The `cow` WIT interface (in `shepherd:cow`) exposes a generic REST passthrough to the CoW Protocol API. The `shepherd-sdk` wraps this with a typed `CowClient`: +The `cow-api` WIT interface (in `shepherd:cow`) exposes a REST passthrough to the CoW Protocol API plus a typed `submit-order` function (the two were separate interfaces, `cow` and `order`, in 0.1). The `shepherd-sdk` wraps this with a typed `Cow` client: ```rust use shepherd_sdk::prelude::*; -let cow = CowClient::new(42161); +let cow = Cow::new(42161); -// Submit an order +// Submit an order via the merged cow-api interface let uid = cow.submit_order(&OrderCreation { sell_token: sell_addr, buy_token: buy_addr, sell_amount: U256::from(1_000_000), buy_amount: U256::from(950_000), kind: OrderKind::Sell, - valid_to: block.timestamp + 300, + valid_to: (block.timestamp / 1000) + 300, // block.timestamp is ms in 0.2 ..Default::default() })?; @@ -489,11 +485,7 @@ let order = cow.get_order(&uid)?; let resp = cow.raw_request("GET", "/api/v1/auction", None)?; ``` -The `CowClient` handles JSON serialisation and routes requests through the host's `cow::request` function, which forwards to the correct CoW API base URL for the given chain. - -### Legacy: `order::submit` - -The WIT `order::submit` interface is retained for backwards compatibility. New modules should prefer `CowClient::submit_order` which provides richer types and access to the full CoW API surface (quotes, auctions, order queries). +The `Cow` client handles JSON serialisation and routes requests through the host's `cow-api::request` (REST passthrough) and `cow-api::submit-order` (order submission) functions. ## Logging Convenience @@ -524,61 +516,59 @@ nexum_sdk::info!("processing block {} on chain {}", block.number, block.chain_id ## Error Handling -The `nexum-sdk` defines a proper error type that converts to the WIT `string` error. The `shepherd-sdk` extends it with CoW-specific variants: - -### Universal (`nexum-sdk`) +In 0.2 every host function returns `result`. The SDK re-exports `HostError` and the `HostErrorKind` discriminant; module errors use the same shape so user code can `?`-propagate across host and module boundaries uniformly. ```rust -#[derive(Debug)] -pub enum Error { - Rpc(alloy_transport::TransportError), - LocalStore(String), - Identity { code: u16, message: String }, - Abi(alloy_sol_types::Error), - Serde(postcard::Error), - Custom(String), +pub struct HostError { + pub domain: String, // "chain" | "store" | "messaging" | "identity" | "cow" | + pub kind: HostErrorKind, // unsupported | unavailable | denied | rate-limited + // | timeout | invalid-input | internal + pub code: i32, // domain-specific + pub message: String, + pub data: Option, // JSON for richer context } -pub type Result = core::result::Result; - -// Auto-converts to the WIT result<_, string> via Display -impl fmt::Display for Error { ... } -``` - -### CoW Protocol (`shepherd-sdk`) +pub type Result = core::result::Result; -```rust -#[derive(Debug)] -pub enum Error { - Nexum(nexum_sdk::Error), - CowApi { status: u16, message: String }, - Order(String), +impl HostError { + /// Helper for module-defined errors. Sets `domain` to the module name, + /// `kind` to the closest match, and `code` to 0. + pub fn module(name: &str, kind: HostErrorKind, message: impl Into) -> Self { ... } } - -pub type Result = core::result::Result; ``` -Module authors use `?` naturally: +Module authors use `?` naturally, and match on `kind` for retry/backoff: ```rust // Universal module -async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { - let num = provider.get_block_number().await?; // RPC error - let decoded = MyCall::abi_decode_returns(&data)?; // ABI error - TypedState::set("last", &decoded)?; // serde/local-store error - let sig = IdentityClient::sign(&account, &data)?; // identity error +async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { + let num = provider.get_block_number().await?; // chain error -> HostError + let decoded = MyCall::abi_decode_returns(&data) + .map_err(|e| HostError::module("twap", HostErrorKind::InvalidInput, e.to_string()))?; + TypedState::set("last", &decoded)?; // store error + let sig = Signer::sign(&account, &data)?; // identity error Ok(()) } +// Inspecting the kind for retry decisions +match provider.get_block_number().await { + Ok(n) => Ok(n), + Err(e) if matches!(e.kind, HostErrorKind::Unavailable | HostErrorKind::Timeout) => retry(), + Err(e) if matches!(e.kind, HostErrorKind::RateLimited) => backoff(), + Err(e) => Err(e), +} + // CoW module -async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { - let num = provider.get_block_number().await?; // RPC error - TypedState::set("last", &num)?; // local-store error - CowClient::new(block.chain_id).submit_order(&order)?; // CoW API error +async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { + let num = provider.get_block_number().await?; // chain error + TypedState::set("last", &num)?; // store error + Cow::new(block.chain_id).submit_order(&order)?; // cow-api error Ok(()) } ``` +See the [migration guide §2](migration/0.1-to-0.2.md#2-error-model-unification-both) for the full taxonomy and the embedder-side mapping of backend signals (HTTP codes, transport errors, wallet rejections) to `host-error-kind`. + ## Testing Framework ### Universal: `nexum-sdk` Mock Host @@ -606,11 +596,11 @@ fn test_monitor_processes_block() { .set("last_block", &19_000_000u64.to_le_bytes()); // Dispatch a block event - let result = host.dispatch(Event::Block(BlockData { + let result = host.dispatch(Event::Block(Block { chain_id: 42161, number: 19_000_001, hash: vec![0; 32], - timestamp: 1700000000, + timestamp: 1_700_000_000_000, // ms since epoch })); assert!(result.is_ok()); @@ -644,7 +634,7 @@ fn test_module_signs_data() { Ok(vec![0u8; 65]) }); - let result = host.dispatch(Event::Block(BlockData { + let result = host.dispatch(Event::Block(Block { chain_id: 1, number: 19_000_001, hash: vec![0; 32], @@ -671,11 +661,11 @@ fn test_twap_monitor_submits_order() { host.chain(42161).block_number(19_000_001); - let result = host.dispatch(Event::Block(BlockData { + let result = host.dispatch(Event::Block(Block { chain_id: 42161, number: 19_000_001, hash: vec![0; 32], - timestamp: 1700000000, + timestamp: 1_700_000_000_000, })); assert!(result.is_ok()); @@ -737,11 +727,11 @@ fn test_module_as_component() { harness.mock_chain(42161).block_number(100); harness.call_init(vec![("api_url".into(), "mock".into())]).unwrap(); - let result = harness.call_on_event(Event::Block(BlockData { + let result = harness.call_on_event(Event::Block(Block { chain_id: 42161, number: 100, hash: vec![0; 32], - timestamp: 1700000000, + timestamp: 1_700_000_000_000, })); assert!(result.is_ok()); } @@ -769,7 +759,7 @@ my-module/ └── lib.rs # minimal module skeleton ``` -#### Universal module (targeting `web3:runtime/headless-module`) +#### Universal module (targeting `nexum:runtime/event-module`) `Cargo.toml`: ```toml @@ -782,7 +772,7 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] -nexum-sdk = "0.1" +nexum-sdk = "0.2" [package.metadata.component] package = "my:module" @@ -801,25 +791,25 @@ impl MyModule { Ok(()) } - async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { let block_num = provider.get_block_number().await?; info!("block {} on chain {}", block_num, block.chain_id); Ok(()) } - async fn on_logs(logs: Vec, provider: &RootProvider) -> Result<()> { + async fn on_logs(logs: Vec, provider: &RootProvider) -> Result<()> { info!("received {} logs", logs.len()); Ok(()) } - fn on_timer(timestamp: u64) -> Result<()> { - info!("timer fired at {timestamp}"); + fn on_tick(tick: Tick) -> Result<()> { + info!("tick fired at {} ms UTC", tick.fired_at); Ok(()) } } ``` -#### CoW Protocol module (targeting `shepherd:cow/shepherd-module`) +#### CoW Protocol module (targeting `shepherd:cow/shepherd`) `Cargo.toml`: ```toml @@ -832,7 +822,7 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] -shepherd-sdk = "0.1" +shepherd-sdk = "0.2" [package.metadata.component] package = "my:module" @@ -851,19 +841,19 @@ impl MyCowModule { Ok(()) } - async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { let block_num = provider.get_block_number().await?; info!("block {} on chain {}", block_num, block.chain_id); Ok(()) } - async fn on_logs(logs: Vec, provider: &RootProvider) -> Result<()> { + async fn on_logs(logs: Vec, provider: &RootProvider) -> Result<()> { info!("received {} logs", logs.len()); Ok(()) } - fn on_timer(timestamp: u64) -> Result<()> { - info!("timer fired at {timestamp}"); + fn on_tick(tick: Tick) -> Result<()> { + info!("tick fired at {} ms UTC", tick.fired_at); Ok(()) } } @@ -876,7 +866,7 @@ name = "my-module" version = "0.1.0" description = "" authors = [] -wasm = "sha256:TODO" +component = "sha256:TODO" [module.resources] max_memory_bytes = 10_485_760 @@ -890,6 +880,10 @@ max_consecutive_failures = 10 required = [] optional = [] +[capabilities] +required = ["chain", "local-store", "logging"] +optional = [] + [config] ``` @@ -916,31 +910,33 @@ cargo nexum publish --swarm http://localhost:1633 --batch-id ## SDK / Runtime Version Compatibility -The WIT definition is versioned (`web3:runtime@0.1.0`). The SDK pins this version. When the WIT evolves: +The WIT definition is versioned (`nexum:runtime@0.2.0`). The SDK pins this version. When the WIT evolves: -- **Patch** (0.1.x): backwards-compatible additions (new host functions). Old modules continue to work. +- **Patch** (0.2.x): backwards-compatible additions (new host functions, new manifest fields, new SDK helpers). Old modules continue to work. - **Minor** (0.x.0): may add new required exports. Old modules need recompilation. - **Major** (x.0.0): breaking changes. Runtime supports multiple world versions during transition. -The `bindgen!` macro on the host side uses wasmtime's **semver-aware resolution** -- a host implementing `@0.1.1` satisfies a guest compiled against `@0.1.0`. +The `bindgen!` macro on the host side uses wasmtime's **semver-aware resolution** -- a host implementing `@0.2.1` satisfies a guest compiled against `@0.2.0`. + +0.2 is the coordinated breaking-change window relative to 0.1. The 0.2.0 contracts (WIT package name, interface names, the `host-error` shape, the `nexum.toml` schema, the `#[nexum::module]` macro surface) are stable starting at 0.2.0 -- see the [migration guide §10](migration/0.1-to-0.2.md#10-deprecation-policy-going-forward-both) for the full deprecation policy. ## Summary | SDK Layer | Provides | |-----------|----------| -| `#[nexum::module]` | Eliminates WIT boilerplate; named event handlers (`on_block`, `on_logs`, `on_timer`); `async fn` + provider injection (universal) | -| `#[shepherd::module]` | Same as above, targeting CoW Protocol's `shepherd:cow/shepherd-module` world | -| `provider(chain_id)` | Full alloy `Provider` backed by host RPC via `HostTransport` | -| `IdentityClient` | Typed identity client for accounts, signing, and EIP-712 (nexum-sdk) | -| `CowClient` | Typed CoW Protocol API client backed by host `cow` interface (shepherd-sdk only) | +| `#[nexum::module]` | Eliminates WIT boilerplate; named event handlers (`on_block`, `on_logs`, `on_tick`, `on_message`); `async fn` + provider injection (universal) | +| `#[shepherd::module]` | Same as above, targeting CoW Protocol's `shepherd:cow/shepherd` world | +| `provider(chain_id)` | Full alloy `Provider` backed by host RPC via `HostTransport` (including 0.2's `chain::request-batch` for real wire-level batching) | +| `Signer` | Typed identity client for accounts, signing, and EIP-712 (nexum-sdk) | +| `Cow` | Typed CoW Protocol API client backed by host `cow-api` interface (shepherd-sdk only) | | `nexum_sdk::prelude::*` | Universal types, interfaces, alloy re-exports in one import | | `shepherd_sdk::prelude::*` | Universal prelude + CoW-specific types and interfaces | | `TypedState` | Serde-based typed local-store over raw bytes | | `sol!` | Compile-time Ethereum ABI codec (alloy-sol-types) | | `log::{info!, ...}` | Formatted logging macros | -| `Error` / `Result` | Proper error type with `?` support (extended by shepherd-sdk for CoW) | +| `HostError` / `HostErrorKind` / `Result` | Unified error type with `?` support and `kind`-based matching | | `nexum_sdk::testing::MockHost` | Native-Rust unit tests with universal mock host (includes identity mocking) | | `shepherd_sdk::testing::MockHost` | Extends universal mock with CoW-specific assertions | | `testing::MockProvider` | alloy `Provider` mock for RPC-level testing | | `testing::WasmTestHarness` | Integration tests against real wasmtime | -| `cargo nexum` | new / build / package / publish CLI | +| `cargo nexum` | new / build / package / publish / check / migrate CLI | diff --git a/docs/06-production-hardening.md b/docs/06-production-hardening.md index b4c5dd4..f3c25ef 100755 --- a/docs/06-production-hardening.md +++ b/docs/06-production-hardening.md @@ -80,7 +80,7 @@ Local-store quota (`max_state_bytes`) enforced in the `local-store::set` host fu | CPU (deterministic) | Fuel | Trap -> rollback -> restart | | CPU (wall-clock) | Epoch interruption | Yield -> resume or trap | | Memory | `ResourceLimiter` | `memory.grow` returns -1 | -| Storage | Host-side byte tracking | `local-store::set` returns `Err` | +| Storage | Host-side byte tracking | `local-store::set` returns `host-error { domain: "store", kind: invalid-input }` | ## Crash Handling & Restart Policy @@ -149,13 +149,13 @@ A restart creates a fresh `Store` (clean WASM memory) but reuses the `InstancePr ## RPC Resilience -All RPC I/O flows through alloy providers configured by the runtime operator. The `csn::request` host function (see doc 07) forwards to the provider, which is wrapped with resilience layers using alloy's tower-based middleware. +All RPC I/O flows through alloy providers configured by the runtime operator. The `chain::request` host function (see doc 07) forwards to the provider, which is wrapped with resilience layers using alloy's tower-based middleware. The additive `chain::request-batch` (0.2) routes alloy's `RequestPacket::Batch` to actually batch on the wire. ### Provider Stack ```mermaid flowchart TD - A["Module calls csn::request\n(via alloy Provider in SDK)"] --> B["Host csn::request impl\n-> alloy Provider"] + A["Module calls chain::request\n(via alloy Provider in SDK)"] --> B["Host chain::request impl\n-> alloy Provider"] B --> C["Timeout\n(10s default)"] C --> D["Retry\n(3 attempts, exponential\nbackoff + jitter)"] D --> E["Rate Limit\n(per-endpoint)"] @@ -177,11 +177,11 @@ flowchart TD chain_id = 42161 name = "arbitrum" -[[chains.csn]] +[[chains.endpoints]] url = "wss://arb-mainnet.g.alchemy.com/v2/KEY" priority = 1 -[[chains.csn]] +[[chains.endpoints]] url = "https://arb1.arbitrum.io/rpc" priority = 2 # fallback @@ -231,7 +231,7 @@ Every log line includes: |-------|--------| | `module` | Module name from manifest | | `chain_id` | Chain the event originated from | -| `event_type` | `block` / `logs` / `timer` | +| `event_type` | `block` / `logs` / `tick` / `message` | | `block_number` | For block/log events | | `level` | trace / debug / info / warn / error | | `timestamp` | ISO 8601 | diff --git a/docs/07-rpc-namespace-design.md b/docs/07-rpc-namespace-design.md index 7004c4d..9328d93 100755 --- a/docs/07-rpc-namespace-design.md +++ b/docs/07-rpc-namespace-design.md @@ -1,8 +1,14 @@ # RPC Namespace Design: Generic JSON-RPC Passthrough +> **Naming note (0.2):** This document describes the `chain` interface in the +> `nexum:runtime` WIT package. In the 0.1 design history it was called `chain` +> (short for "consensus"); 0.2 renamed it to `chain` because `chain.request(...)` +> reads itself at the call site. The function signatures below are the 0.2 shape, +> returning `host-error` rather than the 0.1-era `json-rpc-error`. + ## Problem Statement -The current WIT `blockchain` interface defines individual functions for each Ethereum RPC method: +The 0.1 design started with a `blockchain` interface that defined individual functions for each Ethereum RPC method: ```wit interface blockchain { @@ -40,12 +46,12 @@ flowchart TD provider.get_logs(&filter)"] -->|full alloy Provider API| B B["HostTransport (SDK) - implements alloy Transport trait"] -->|"csn::request(chain_id, "eth_blockNumber", "[]")"| C + implements alloy Transport trait"] -->|"chain::request(chain_id, "eth_blockNumber", "[]")"| C C["WIT boundary single generic function"] --> D - D["Host csn::request impl + D["Host chain::request impl forwards to alloy provider"] -->|"provider.raw_request_dyn(method, params)"| E E["Alloy provider stack @@ -54,20 +60,13 @@ flowchart TD ## Updated WIT Interface -Replace the `blockchain` interface with `csn`: +Replace the `blockchain` interface with `chain`: ```wit -package web3:runtime@0.1.0; - -interface csn { - use types.{chain-id}; +package nexum:runtime@0.2.0; - /// JSON-RPC error returned by the provider or the host. - record json-rpc-error { - code: s64, - message: string, - data: option, - } +interface chain { + use types.{chain-id, host-error}; /// Execute a JSON-RPC request against the specified chain. /// @@ -80,56 +79,60 @@ interface csn { /// the JSON-RPC specification. The host handles id/jsonrpc framing; the /// guest only provides method + params and receives the `result` field. request: func(chain-id: chain-id, method: string, params: string) - -> result; + -> result; + + /// 0.2 additive: batched JSON-RPC. alloy's HostTransport routes + /// RequestPacket::Batch through this, so provider.multicall(...) actually + /// batches on the wire (it silently fanned-out single requests in 0.1). + request-batch: func(chain-id: chain-id, calls: list>) + -> result>, host-error>; } ``` -The `types` interface is unchanged. The `local-store`, `remote-store`, `msg`, `order`, and `logging` interfaces are unchanged. +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: ```wit interface identity { - record identity-error { - code: u16, - message: string, - } + use types.{host-error}; /// Get available signing accounts (20-byte Ethereum addresses). - accounts: func() -> result>, identity-error>; + accounts: func() -> result>, host-error>; /// Sign raw bytes with the specified account. /// Returns a 65-byte ECDSA secp256k1 signature (r ‖ s ‖ v). - sign: func(account: list, data: list) -> result, identity-error>; + sign: func(account: list, data: list) -> result, host-error>; /// Sign EIP-712 typed data with the specified account. - sign-typed-data: func(account: list, typed-data: string) -> result, identity-error>; + sign-typed-data: func(account: list, typed-data: string) -> result, host-error>; } ``` -The universal `headless-module` world (in `web3:runtime`) contains the platform-agnostic interfaces: +The universal `event-module` world (in `nexum:runtime`) contains the platform-agnostic interfaces — six imports in 0.2: ```wit -world headless-module { - import csn; // replaces `import blockchain;` +world event-module { + import chain; // replaces `import blockchain;` from the early 0.1 sketch import identity; // cryptographic identity (key management, signing) import local-store; import remote-store; - import msg; + import messaging; import logging; - export init: func(config: types.config) -> result<_, string>; - export on-event: func(event: types.event) -> result<_, string>; + export init: func(config: types.config) -> result<_, host-error>; + export on-event: func(event: types.event) -> result<_, host-error>; } ``` -The CoW-specific `shepherd-module` world (in `shepherd:cow`) extends it with domain interfaces: +The CoW-specific `shepherd` world (in `shepherd:cow`) extends it with the merged `cow-api` interface: ```wit -world shepherd-module { - include web3:runtime/headless-module; - import cow; - import order; +world shepherd { + include nexum:runtime/event-module; + import cow-api; } ``` @@ -137,12 +140,12 @@ world shepherd-module { | Before (per-method) | After (generic) | |---|---| -| `blockchain::eth-call(chain-id, to, data)` | `csn::request(chain-id, "eth_call", params_json)` | -| `blockchain::eth-get-logs(filter)` | `csn::request(chain-id, "eth_getLogs", params_json)` | -| `blockchain::eth-block-number(chain-id)` | `csn::request(chain-id, "eth_blockNumber", "[]")` | -| *n/a — not exposed* | `csn::request(chain-id, "eth_getBalance", params_json)` | -| *n/a — not exposed* | `csn::request(chain-id, "eth_getCode", params_json)` | -| *n/a — not exposed* | `csn::request(chain-id, "eth_getStorageAt", params_json)` | +| `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 | ### Why JSON Strings (Not `list`) @@ -159,13 +162,13 @@ The host implementation is minimal — one function handles the entire `eth_` na ```rust use serde_json::value::RawValue; -impl web3::runtime::csn::Host for NexumHostState { +impl nexum::runtime::chain::Host for NexumHostState { async fn request( &mut self, chain_id: u64, method: String, params: String, - ) -> wasmtime::Result> { + ) -> wasmtime::Result> { // 1. Check if this is a signing method that requires identity delegation if self.is_signing_method(&method) { return self.dispatch_signing(chain_id, &method, ¶ms).await; @@ -173,7 +176,9 @@ impl web3::runtime::csn::Host for NexumHostState { // 2. Method allowlisting for read-only methods if !self.is_read_method_allowed(&method) { - return Ok(Err(JsonRpcError { + return Ok(Err(HostError { + domain: "chain".into(), + kind: HostErrorKind::Denied, code: -32601, message: format!("method not allowed: {method}"), data: None, @@ -182,7 +187,9 @@ impl web3::runtime::csn::Host for NexumHostState { // 3. Resolve the provider for this chain let provider = self.provider_for(chain_id).map_err(|e| { - JsonRpcError { + HostError { + domain: "chain".into(), + kind: HostErrorKind::Unsupported, code: -32002, message: format!("unknown chain: {chain_id}"), data: None, @@ -195,7 +202,7 @@ impl web3::runtime::csn::Host for NexumHostState { match provider.raw_request_dyn(method.into(), &raw_params).await { Ok(result) => Ok(Ok(result.get().to_string())), - Err(e) => Ok(Err(e.into())), // TransportError -> JsonRpcError + Err(e) => Ok(Err(HostError::from_transport("chain", e))), } } } @@ -242,17 +249,17 @@ impl NexumHostState { This could be made configurable per-module via `nexum.toml`: ```toml -[module.csn] +[module.chain] # Additional methods beyond the default read-only set. # Use with caution — write methods can have side-effects. extra_allowed_methods = ["eth_createAccessList"] ``` -The allowlist is runtime-enforced (string matching), not compile-time. This is an acceptable trade-off: the Component Model already provides structural sandboxing (modules can only call `csn::request`, not arbitrary network I/O), and the allowlist adds defence-in-depth for method-level granularity. +The allowlist is runtime-enforced (string matching), not compile-time. This is an acceptable trade-off: the Component Model already provides structural sandboxing (modules can only call `chain::request`, not arbitrary network I/O), and the allowlist adds defence-in-depth for method-level granularity. #### Signing Methods (Identity Delegation) -When a module calls `csn::request` with a signing method, the host does **not** forward the request to the RPC provider. Instead, it delegates to the `identity` backend for signing, then broadcasts the signed result via RPC. +When a module calls `chain::request` with a signing method, the host does **not** forward the request to the RPC provider. Instead, it delegates to the `identity` backend for signing, then broadcasts the signed result via RPC. ```rust impl NexumHostState { @@ -271,7 +278,7 @@ These methods are deliberately **not** in the read-only allowlist. They follow a ### Identity Delegation Flow -When a module calls a signing method through `csn::request`, the host intercepts it and delegates to the `Identity` trait: +When a module calls a signing method through `chain::request`, the host intercepts it and delegates to the `Identity` trait: ```mermaid sequenceDiagram @@ -280,7 +287,7 @@ sequenceDiagram participant I as Identity backend participant R as RPC provider - M->>C: csn::request(1, "eth_sendTransaction", params) + M->>C: chain::request(1, "eth_sendTransaction", params) C->>C: is_signing_method("eth_sendTransaction") → true C->>C: Parse transaction from params C->>I: sign(account, tx_hash) @@ -302,47 +309,49 @@ This pattern applies to all signing methods: | `eth_signTypedData_v4` | Signs EIP-712 typed data via `Identity::sign_typed_data()` | | `personal_sign` | Signs the message via `Identity::sign()` (with EIP-191 prefix) | -### Identity Trait and CsnHost +### Identity Trait and ChainHost -The host's `csn` implementation is generic over an `Identity` trait. This allows different identity backends (hardware wallet, KMS, in-memory test keys, etc.): +The host's `chain` implementation is generic over an `Identity` trait. This allows different identity backends (hardware wallet, KMS, in-memory test keys, etc.): ```rust /// Trait for identity backends that provide signing capabilities. /// -/// The host's csn implementation delegates signing methods to this trait. +/// The host's chain implementation delegates signing methods to this trait. /// Implementations can back onto hardware wallets, cloud KMS, in-memory /// test keys, or any other signing infrastructure. pub trait Identity: Send + Sync { /// Get available signing accounts (20-byte Ethereum addresses). - fn accounts(&self) -> Result>, IdentityError>; + fn accounts(&self) -> Result>, IdentityBackendError>; /// Sign raw bytes with the specified account. /// Returns a 65-byte ECDSA secp256k1 signature (r ‖ s ‖ v). - fn sign(&self, account: &[u8], data: &[u8]) -> Result, IdentityError>; + fn sign(&self, account: &[u8], data: &[u8]) -> Result, IdentityBackendError>; /// Sign EIP-712 typed data with the specified account. - fn sign_typed_data(&self, account: &[u8], typed_data: &str) -> Result, IdentityError>; + fn sign_typed_data(&self, account: &[u8], typed_data: &str) -> Result, IdentityBackendError>; } /// The host state is generic over the identity backend. -pub struct CsnHost { +pub struct ChainHost { providers: HashMap, identity: I, } -impl web3::runtime::csn::Host for CsnHost { +impl nexum::runtime::chain::Host for ChainHost { async fn request( &mut self, chain_id: u64, method: String, params: String, - ) -> wasmtime::Result> { + ) -> wasmtime::Result> { if self.is_signing_method(&method) { return self.dispatch_signing(chain_id, &method, ¶ms).await; } if !self.is_read_method_allowed(&method) { - return Ok(Err(JsonRpcError { + return Ok(Err(HostError { + domain: "chain".into(), + kind: HostErrorKind::Denied, code: -32601, message: format!("method not allowed: {method}"), data: None, @@ -355,22 +364,24 @@ impl web3::runtime::csn::Host for CsnHost { match provider.raw_request_dyn(method.into(), &raw_params).await { Ok(result) => Ok(Ok(result.get().to_string())), - Err(e) => Ok(Err(e.into())), + Err(e) => Ok(Err(HostError::from_transport("chain", e))), } } } -impl CsnHost { +impl ChainHost { /// Dispatch signing methods to the identity backend. async fn dispatch_signing( &self, chain_id: u64, method: &str, params: &str, - ) -> wasmtime::Result> { + ) -> wasmtime::Result> { match method { "eth_accounts" => { - let accounts = self.identity.accounts().map_err(|e| JsonRpcError { + let accounts = self.identity.accounts().map_err(|e| HostError { + domain: "identity".into(), + kind: HostErrorKind::Internal, code: -32000, message: e.message, data: None, @@ -396,9 +407,11 @@ impl CsnHost { // Hash the transaction and sign it let tx_hash = filled_tx.signing_hash(); let signature = self.identity.sign(&from, tx_hash.as_ref()) - .map_err(|e| JsonRpcError { + .map_err(|e| HostError { + domain: "identity".into(), + kind: HostErrorKind::Internal, code: -32000, - message: e.message, + message: e.to_string(), data: None, })?; @@ -410,7 +423,7 @@ impl CsnHost { let raw_params_box: Box = RawValue::from_string(raw_params)?; match provider.raw_request_dyn("eth_sendRawTransaction".into(), &raw_params_box).await { Ok(result) => Ok(Ok(result.get().to_string())), - Err(e) => Ok(Err(e.into())), + Err(e) => Ok(Err(HostError::from_transport("chain", e))), } } @@ -420,9 +433,11 @@ impl CsnHost { let typed_data = params_arr[1].to_string(); let signature = self.identity.sign_typed_data(&account, &typed_data) - .map_err(|e| JsonRpcError { + .map_err(|e| HostError { + domain: "identity".into(), + kind: HostErrorKind::Internal, code: -32000, - message: e.message, + message: e.to_string(), data: None, })?; Ok(Ok(format!("\"0x{}\"", hex::encode(&signature)))) @@ -440,15 +455,19 @@ impl CsnHost { let hash = keccak256(&msg); let signature = self.identity.sign(&account, &hash) - .map_err(|e| JsonRpcError { + .map_err(|e| HostError { + domain: "identity".into(), + kind: HostErrorKind::Internal, code: -32000, - message: e.message, + message: e.to_string(), data: None, })?; Ok(Ok(format!("\"0x{}\"", hex::encode(&signature)))) } - _ => Ok(Err(JsonRpcError { + _ => Ok(Err(HostError { + domain: "chain".into(), + kind: HostErrorKind::InvalidInput, code: -32601, message: format!("unknown signing method: {method}"), data: None, @@ -458,35 +477,42 @@ impl CsnHost { } ``` -The `CsnHost` also implements `web3::runtime::identity::Host` directly, delegating to the same `Identity` trait so modules can use the identity WIT interface for raw signing: +The `ChainHost` also implements `nexum::runtime::identity::Host` directly, delegating to the same `Identity` trait so modules can use the identity WIT interface for raw signing (errors map to `host-error` with `domain = "identity"`): ```rust -impl web3::runtime::identity::Host for CsnHost { - fn accounts(&mut self) -> wasmtime::Result>, IdentityError>> { - Ok(self.identity.accounts()) +impl nexum::runtime::identity::Host for ChainHost { + fn accounts(&mut self) -> wasmtime::Result>, HostError>> { + Ok(self.identity.accounts().map_err(|e| HostError { + domain: "identity".into(), + kind: e.kind(), // backend chooses unavailable/denied/internal + code: 0, + message: e.to_string(), + data: None, + })) } fn sign( &mut self, account: Vec, data: Vec, - ) -> wasmtime::Result, IdentityError>> { - Ok(self.identity.sign(&account, &data)) + ) -> wasmtime::Result, HostError>> { + Ok(self.identity.sign(&account, &data).map_err(|e| e.into_host_error("identity"))) } fn sign_typed_data( &mut self, account: Vec, typed_data: String, - ) -> wasmtime::Result, IdentityError>> { - Ok(self.identity.sign_typed_data(&account, &typed_data)) + ) -> wasmtime::Result, HostError>> { + Ok(self.identity.sign_typed_data(&account, &typed_data) + .map_err(|e| e.into_host_error("identity"))) } } ``` ## Guest SDK: `HostTransport` -The key SDK addition is a `HostTransport` struct that implements alloy's `Transport` trait by routing through the WIT `csn::request` host function. +The key SDK addition is a `HostTransport` struct that implements alloy's `Transport` trait by routing through the WIT `chain::request` host function. ### Transport Implementation @@ -532,11 +558,18 @@ impl Service for HostTransport { Ok(ResponsePacket::Single(resp)) } RequestPacket::Batch(reqs) => { - let resps: Result, _> = reqs - .iter() - .map(|r| dispatch_single(chain_id, r)) + // 0.2: route batches through chain::request-batch so the + // host actually pipelines them on the wire. + let calls: Vec<(String, String)> = reqs.iter() + .map(|r| (r.method().to_string(), + r.params().map(|p| p.get()).unwrap_or("[]").to_string())) + .collect(); + let results = chain::request_batch(chain_id, &calls) + .map_err(|e| TransportError::from_host(e))?; + let resps: Vec<_> = reqs.iter().zip(results.into_iter()) + .map(|(req, result)| build_response(req, result)) .collect(); - Ok(ResponsePacket::Batch(resps?)) + Ok(ResponsePacket::Batch(resps)) } } }) @@ -563,7 +596,7 @@ fn dispatch_single( // This calls the WIT-imported host function. Synchronous from the guest's // perspective — the host executes the RPC call asynchronously and returns // the result when ready. - match csn::request(chain_id, method, params_json) { + match chain::request(chain_id, method, params_json) { Ok(result_json) => { let payload: Box = RawValue::from_string(result_json) .map_err(|e| TransportError::deser_err(e, "host response"))?; @@ -573,14 +606,17 @@ fn dispatch_single( }) } Err(e) => { - // Return a JSON-RPC error response rather than a transport error, - // so alloy can surface the RPC error code/message to the caller. + // Map the host-error onto an alloy error payload, encoding the + // kind/domain into `data` so the caller can recover the + // discriminant via HostError::from_response. Ok(Response { id: req.id().clone(), payload: ResponsePayload::Failure(ErrorPayload { - code: e.code, + code: e.code as i64, message: e.message, - data: e.data.and_then(|d| RawValue::from_string(d).ok()), + data: Some(RawValue::from_string( + serde_json::to_string(&HostErrorWire::from(e)).unwrap() + ).unwrap()), }), }) } @@ -590,7 +626,7 @@ 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 `csn::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: @@ -645,7 +681,7 @@ This is verbose and obscures the actual logic. But we can't reimplement every `P 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`, and/or `on_timer`. The macro generates the `on_event` match. +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. @@ -656,20 +692,20 @@ The proc macro (see doc 05) already generates the WIT export boilerplate. We ext struct MyModule; impl MyModule { - async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { let block_num = provider.get_block_number().await?; // natural .await let balance = provider.get_balance(addr).latest().await?; // no block_on Ok(()) } - async fn on_logs(logs: Vec, provider: &RootProvider) -> Result<()> { + async fn on_logs(logs: Vec, provider: &RootProvider) -> Result<()> { for log in &logs { // ... } Ok(()) } - // on_timer not defined -> timer events silently ignored + // on_tick / on_message not defined -> those events are silently ignored } ``` @@ -680,8 +716,8 @@ impl MyModule { struct MyModule; impl MyModule { - async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { - let cow = CowClient::new(block.chain_id); + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { + let cow = Cow::new(block.chain_id); let block_num = provider.get_block_number().await?; cow.submit_order(&order)?; Ok(()) @@ -693,7 +729,7 @@ impl MyModule { ```rust impl Guest for MyModule { - fn on_event(event: types::Event) -> Result<(), String> { + fn on_event(event: types::Event) -> Result<(), HostError> { nexum_sdk::block_on(async { match event { Event::Block(block) => { @@ -704,9 +740,10 @@ impl Guest for MyModule { let provider = nexum_sdk::provider(logs[0].chain_id); MyModule::on_logs(logs, &provider).await } - Event::Timer(_) => Ok(()), // no handler defined + Event::Tick(_) => Ok(()), // no handler defined + Event::Message(_) => Ok(()), // no handler defined } - }).map_err(|e| e.to_string()) + }) } } ``` @@ -717,9 +754,10 @@ The generated code calls `block_on` exactly once — at the top-level export bou | Handler | Payload | Optional injectable context | |---|---|---| -| `on_block(block)` | `BlockData` | `provider: &RootProvider` (from `block.chain_id`) | -| `on_logs(logs)` | `Vec` | `provider: &RootProvider` (from `logs[0].chain_id`) | -| `on_timer(timestamp)` | `u64` | None (no chain context) | +| `on_block(block)` | `Block` | `provider: &RootProvider` (from `block.chain_id`) | +| `on_logs(logs)` | `Vec` | `provider: &RootProvider` (from `logs[0].chain_id`) | +| `on_tick(tick)` | `Tick` (`tick.fired_at` is ms UTC) | None (no chain context) | +| `on_message(message)` | `Message` | None | The macro inspects each handler's signature: - **Second parameter is `&RootProvider`** -> inject `nexum_sdk::provider(chain_id)` @@ -740,7 +778,7 @@ The macro inspects each handler's signature: 4. **Composability.** Module authors can use alloy's builder patterns naturally inside any handler: ```rust - async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { // EthCall builder — .latest() and .await both work let result = provider.call(tx).latest().await?; @@ -758,8 +796,8 @@ The macro inspects each handler's signature: 5. **Sync handlers still work.** Handlers that don't need RPC can be plain `fn`: ```rust - fn on_timer(timestamp: u64) -> Result<()> { - info!("timer fired at {timestamp}"); + fn on_tick(tick: Tick) -> Result<()> { + info!("tick fired at {} ms UTC", tick.fired_at); Ok(()) } ``` @@ -826,7 +864,7 @@ struct MyModule; impl MyModule { // Named handler — macro generates the match dispatch + provider injection - async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { + async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { // 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?; @@ -856,27 +894,23 @@ impl MyModule { } // Only implement handlers for event types you care about. - // No on_logs or on_timer -> those events are no-ops. + // No on_logs, on_tick, or on_message -> those events are no-ops. } ``` Every alloy `Provider` method works. No WIT changes. No host-side per-method code. No `block_on`. No `match event { ... }`. No manual provider construction. -## The `cow_` Namespace +## The `cow-api` Namespace CoW Protocol's API is REST-based, not JSON-RPC. Two options: -### Option A: Separate REST Interface (Recommended) +### Option A: Separate REST Interface (Recommended — chosen for 0.2) -```wit -interface cow { - use web3:runtime/types.{chain-id}; +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: - record api-error { - status: u16, - message: string, - body: option, - } +```wit +interface cow-api { + use nexum:runtime/types.{chain-id, host-error}; /// HTTP-style request to the CoW Protocol API. /// @@ -894,29 +928,32 @@ interface cow { method: string, path: string, body: option, - ) -> result; + ) -> result; + + /// Submit a serialised order. (Merged in from the 0.1 `order::submit`.) + submit-order: func(chain-id: chain-id, order-data: list) + -> result; } ``` ```wit -world shepherd-module { - include web3:runtime/headless-module; - import cow; // CoW Protocol API access - import order; // kept for backwards compat; could merge into cow +world shepherd { + include nexum:runtime/event-module; + import cow-api; } ``` The host implementation is similarly minimal: ```rust -impl shepherd::cow::cow::Host for NexumHostState { +impl shepherd::cow::cow_api::Host for NexumHostState { async fn request( &mut self, chain_id: u64, method: String, path: String, body: Option, - ) -> wasmtime::Result> { + ) -> wasmtime::Result> { let base_url = self.cow_api_url_for(chain_id)?; let url = format!("{base_url}{path}"); @@ -926,91 +963,110 @@ impl shepherd::cow::cow::Host for NexumHostState { None => req, }; - let resp = req.send().await?; + let resp = req.send().await + .map_err(|e| HostError::module("cow", HostErrorKind::Unavailable, e.to_string()))?; let status = resp.status().as_u16(); if status >= 400 { + let kind = match status { + 429 => HostErrorKind::RateLimited, + 401 | 403 => HostErrorKind::Denied, + 500..=599 => HostErrorKind::Unavailable, + _ => HostErrorKind::InvalidInput, + }; let body = resp.text().await.ok(); - return Ok(Err(ApiError { status, message: "request failed".into(), body })); + return Ok(Err(HostError { + domain: "cow".into(), + kind, + code: status as i32, + message: "request failed".into(), + data: body, + })); } - Ok(Ok(resp.text().await?)) + Ok(Ok(resp.text().await.unwrap_or_default())) } } ``` ### Option B: JSON-RPC Style (Unified) -Route `cow_*` methods through the same `csn::request` function: +Route `cow_*` methods through the same `chain::request` function: ```rust -// Guest usage: +// Guest usage (illustrative): let order_uid: String = block_on(provider.raw_request( "cow_submitOrder".into(), serde_json::json!({ "sellToken": "0x...", "buyToken": "0x...", ... }), ))?; ``` -The host dispatches by method prefix: +The host would dispatch by method prefix: ```rust async fn request(&mut self, chain_id: u64, method: String, params: String) - -> wasmtime::Result> + -> wasmtime::Result> { if method.starts_with("eth_") || method.starts_with("net_") { self.dispatch_rpc(chain_id, &method, ¶ms).await } else if method.starts_with("cow_") { self.dispatch_cow(chain_id, &method, ¶ms).await } else { - Ok(Err(JsonRpcError { code: -32601, message: "unknown namespace".into(), data: None })) + Ok(Err(HostError { + domain: "chain".into(), + kind: HostErrorKind::InvalidInput, + code: -32601, + message: "unknown namespace".into(), + data: None, + })) } } ``` -**Option A is recommended.** The CoW API is REST, not JSON-RPC — forcing it into JSON-RPC semantics adds a translation layer on both sides. A separate `cow` interface keeps the contract explicit and makes it clear in the WIT world what capabilities a module has. It also allows independent evolution — the `csn` 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: `CowClient` +### SDK: `Cow` ```rust /// Typed client for the CoW Protocol API, backed by the host runtime. -pub struct CowClient { +pub struct Cow { chain_id: u64, } -impl CowClient { +impl Cow { pub fn new(chain_id: u64) -> Self { Self { chain_id } } - /// Submit an order to the CoW Protocol API. + /// Submit an order via the typed cow-api::submit-order function. pub fn submit_order(&self, order: &OrderCreation) -> Result { - let body = serde_json::to_string(order)?; - let resp = cow::request(self.chain_id, "POST", "/api/v1/orders", Some(&body))?; - Ok(serde_json::from_str(&resp)?) + let bytes = postcard::to_allocvec(order)?; + let uid = cow_api::submit_order(self.chain_id, &bytes)?; + Ok(uid.parse()?) } /// Get an order by UID. pub fn get_order(&self, uid: &OrderUid) -> Result { - let resp = cow::request(self.chain_id, "GET", &format!("/api/v1/orders/{uid}"), None)?; + let resp = cow_api::request(self.chain_id, "GET", &format!("/api/v1/orders/{uid}"), None)?; Ok(serde_json::from_str(&resp)?) } /// Get the current auction. pub fn get_auction(&self) -> Result { - let resp = cow::request(self.chain_id, "GET", "/api/v1/auction", None)?; + let resp = cow_api::request(self.chain_id, "GET", "/api/v1/auction", None)?; Ok(serde_json::from_str(&resp)?) } /// Get a quote for a potential order. pub fn get_quote(&self, params: &OrderQuoteRequest) -> Result { let body = serde_json::to_string(params)?; - let resp = cow::request(self.chain_id, "POST", "/api/v1/quote", Some(&body))?; + let resp = cow_api::request(self.chain_id, "POST", "/api/v1/quote", Some(&body))?; Ok(serde_json::from_str(&resp)?) } /// Raw request for endpoints not yet wrapped. pub fn raw_request(&self, method: &str, path: &str, body: Option<&str>) -> Result { - Ok(cow::request(self.chain_id, method, path, body)?) + Ok(cow_api::request(self.chain_id, method, path, body)?) } } ``` @@ -1018,8 +1074,8 @@ impl CowClient { Usage in a module: ```rust -async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { - let cow = CowClient::new(block.chain_id); +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 let block_num = provider.get_block_number().await?; @@ -1030,8 +1086,9 @@ async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { buy_token: weth, sell_amount: U256::from(1_000_000_000), kind: OrderKind::Sell, - valid_to: provider.get_block(block_num.into(), false).await? - .unwrap().header.timestamp + 300, + // 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() })?; @@ -1045,14 +1102,14 @@ async fn on_block(block: BlockData, provider: &RootProvider) -> Result<()> { nexum-sdk/ ├── Cargo.toml ├── src/ -│ ├── lib.rs # re-exports, prelude, provider() constructor +│ ├── lib.rs # re-exports, prelude, provider() constructor │ ├── bindings.rs # generated WIT bindings -│ ├── transport.rs # HostTransport (alloy Transport impl) +│ ├── transport.rs # HostTransport (alloy Transport impl, batches via chain::request-batch) │ ├── local_store.rs # TypedState helpers (serde over local-store) -│ ├── identity.rs # IdentityClient (typed identity helpers) +│ ├── signer.rs # Signer (typed identity helpers) │ ├── abi.rs # alloy-sol-types integration │ ├── log.rs # logging macros -│ ├── error.rs # error types +│ ├── error.rs # HostError / HostErrorKind │ └── testing.rs # mock host, test harness └── macros/ └── src/ @@ -1062,8 +1119,7 @@ shepherd-sdk/ ├── Cargo.toml # depends on nexum-sdk, re-exports it ├── src/ │ ├── lib.rs # re-exports nexum-sdk + CoW additions -│ ├── cow.rs # CowClient typed wrapper -│ └── order.rs # order submission helpers +│ └── cow.rs # Cow typed wrapper (submit + REST passthrough) └── macros/ └── src/ └── lib.rs # #[shepherd::module] proc macro (extends nexum::module) @@ -1092,19 +1148,19 @@ All alloy crates with `default-features = false` to avoid pulling in reqwest, to ```rust // nexum_sdk::prelude -pub use crate::bindings::web3::runtime::types::*; -pub use crate::bindings::web3::runtime::csn; -pub use crate::bindings::web3::runtime::identity; -pub use crate::bindings::web3::runtime::local_store; -pub use crate::bindings::web3::runtime::remote_store; -pub use crate::bindings::web3::runtime::msg; -pub use crate::bindings::web3::runtime::logging; +pub use crate::bindings::nexum::runtime::types::*; +pub use crate::bindings::nexum::runtime::chain; +pub use crate::bindings::nexum::runtime::identity; +pub use crate::bindings::nexum::runtime::local_store; +pub use crate::bindings::nexum::runtime::remote_store; +pub use crate::bindings::nexum::runtime::messaging; +pub use crate::bindings::nexum::runtime::logging; pub use crate::log::{trace, debug, info, warn, error}; pub use crate::local_store::TypedState; -pub use crate::identity::IdentityClient; +pub use crate::signer::Signer; pub use crate::transport::HostTransport; -pub use crate::{provider, block_on}; -pub use crate::error::{Result, Error}; +pub use crate::provider; +pub use crate::error::{Result, HostError, HostErrorKind}; // Re-export alloy essentials so modules don't need direct alloy dependencies pub use alloy_primitives::{Address, B256, U256, Bytes}; @@ -1116,9 +1172,8 @@ pub use alloy_provider::Provider; ```rust // shepherd_sdk::prelude (re-exports nexum_sdk::prelude + CoW additions) pub use nexum_sdk::prelude::*; -pub use crate::bindings::shepherd::cow::cow; -pub use crate::bindings::shepherd::cow::order; -pub use crate::cow::CowClient; +pub use crate::bindings::shepherd::cow::cow_api; +pub use crate::cow::Cow; ``` ## Testing @@ -1152,14 +1207,14 @@ fn test_reads_balance() { Note: `block_on` is still available and useful in test code where `#[test]` functions are synchronous. In module code, prefer `async fn on_event` with `.await` instead. -### MockCowClient for Unit Tests +### MockCow for Unit Tests ```rust -use shepherd_sdk::testing::MockCowClient; +use shepherd_sdk::testing::MockCow; #[test] fn test_submits_order() { - let mut mock_cow = MockCowClient::new(42161); + let mut mock_cow = MockCow::new(42161); mock_cow.on_submit(|order| { assert_eq!(order.sell_token, usdc); Ok(OrderUid::from([0x42; 56])) @@ -1189,26 +1244,19 @@ The primary trade-off is **type safety at the WIT boundary**: JSON strings vs. s 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. -The compile-time guarantee that a module can only call methods in the WIT is traded for a runtime allowlist. Given that the Component Model already provides structural sandboxing (the module can only call `csn::request`, not arbitrary network I/O), and the allowlist is enforced at the host boundary before any RPC call is made, this is a sound trade-off. +The compile-time guarantee that a module can only call methods in the WIT is traded for a runtime allowlist. Given that the Component Model already provides structural sandboxing (the module can only call `chain::request`, not arbitrary network I/O), and the allowlist is enforced at the host boundary before any RPC call is made, this is a sound trade-off. ## Migration Path -If the current `blockchain` interface has already been implemented: - -1. Add `csn` interface alongside `blockchain` (both in WIT world). -2. SDK defaults to `csn`-backed `provider()`. Raw `blockchain::*` functions still work. -3. Deprecation cycle: mark `blockchain` functions as deprecated in SDK docs. -4. Remove `blockchain` interface in the next WIT minor version bump. - -If starting from scratch (recommended): implement `csn` only. Skip `blockchain` entirely. +For modules and embedders moving from 0.1 to 0.2, follow the [Migration Guide](migration/0.1-to-0.2.md). In summary: the early 0.1 `blockchain` sketch was replaced by `csn` later in 0.1 and is now `chain` in 0.2; the SDK's `block_on` is now hidden behind the `#[nexum::module]` macro; and every host function returns `host-error` rather than a per-protocol error type. ## Summary -| Component | What Changes | +| Component | What 0.2 ships | |---|---| -| **WIT** | Replace `blockchain` with `csn` (1 function). Add `identity` interface (accounts, sign, sign-typed-data). Add `cow` interface in `shepherd:cow`. `headless-module` imports 6 interfaces: csn, identity, local-store, remote-store, msg, logging. | -| **Host** | `CsnHost` — one `csn::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. One `identity::Host` impl delegating to the same backend. One `cow::request` impl forwarding to HTTP client. | -| **SDK** | `nexum-sdk`: `HostTransport` (alloy `Transport` impl), `provider()` constructor, `block_on()`, `IdentityClient` (typed identity wrapper). `shepherd-sdk`: `CowClient`, order helpers (extends `nexum-sdk`). | -| **`#[nexum::module]` / `#[shepherd::module]` macros** | Named event handlers (`on_block`, `on_logs`, `on_timer`) 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 `IdentityClient` or transparently through `csn::request` signing methods. Full CoW API via `CowClient`. No match boilerplate. No `block_on`. No manual ABI wrangling for RPC calls. | +| **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`. | +| **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. | diff --git a/docs/08-platform-generalisation.md b/docs/08-platform-generalisation.md index e1d526e..e57f656 100755 --- a/docs/08-platform-generalisation.md +++ b/docs/08-platform-generalisation.md @@ -1,17 +1,19 @@ # 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. + ## 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 can 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** — the current design (Rust/Tokio/wasmtime). Headless automation: blockchain event monitoring, order submission, background computation. -2. **Mobile app (Flutter/Dart)** — 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** — 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** — 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. -This document defines the layered architecture that enables this generalisation and specifies the universal interface set. +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:runtime/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. ## Primitive Taxonomy @@ -19,27 +21,29 @@ Before diving into WIT definitions, the universal runtime is built on six primit | Primitive | Interface | Backed by | Purpose | |-----------|-----------|-----------|---------| -| **Consensus** | `csn` | JSON-RPC (eth_*) | Read/write blockchain consensus state | +| **Chain** | `chain` | JSON-RPC (eth_*) | Read/write blockchain consensus state | | **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** | `msg` | Waku | Decentralised pub/sub messaging | +| **Messaging** | `messaging` | Waku | Decentralised pub/sub messaging | | **Logging** | `logging` | tracing / console | Diagnostic output | These six primitives are orthogonal: -- **Consensus** is the source of truth — the blockchain. 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 `csn` host implementation depends on `identity` internally — signing RPC methods (e.g. `eth_sendTransaction`) delegate to `identity` for the actual signature. +- **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 (consensus), cryptographic agency (identity), local scratch (local-store), shared content (remote-store), real-time coordination (msg), and diagnostics (logging). +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. ## Architectural Principle: Layered WIT Worlds -The current `shepherd-module` world conflates universal blockchain runtime capabilities with CoW Protocol domain-specific interfaces. To enable reuse across platforms and domains, the WIT is split into layers: +The current `shepherd` world conflates universal blockchain runtime capabilities with CoW Protocol domain-specific interfaces. To enable reuse across platforms and domains, the WIT is split into layers: ```mermaid graph TD @@ -54,11 +58,11 @@ graph TD end subgraph L1["Layer 1: Universal Runtime Interfaces"] - CSN["csn — consensus access (JSON-RPC passthrough)"] + 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["msg — decentralised pub/sub messaging"] + MSG["messaging — decentralised pub/sub messaging"] LOG["logging — structured logging"] EXP["Exports: init(config) + on-event(event)"] end @@ -73,19 +77,13 @@ Each layer builds on the one below via WIT `include`. A module compiled against These six interfaces form the universal runtime contract. Any platform — server, mobile, WebView, desktop — can implement them. -### `csn` — 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. 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 csn { - type chain-id = u64; - - record json-rpc-error { - code: s64, - message: string, - data: option, - } +interface chain { + use types.{chain-id, host-error}; /// Execute a JSON-RPC request against the specified chain. /// @@ -97,46 +95,47 @@ interface csn { /// `method` includes the namespace prefix (e.g. "eth_call"). /// `params` and the success value are JSON-encoded strings. request: func(chain-id: chain-id, method: string, params: string) - -> result; + -> result; + + /// Additive 0.2 method: batched JSON-RPC. + request-batch: func(chain-id: chain-id, calls: list>) + -> result>, host-error>; } ``` **Platform implementations:** -| Platform | `csn::request` backed by | +| Platform | `chain::request` backed by | |----------|--------------------------| | Server (Nexum) | alloy provider with tower middleware (timeout, retry, rate-limit, fallback) | | Mobile (Flutter) | HTTP client (reqwest via FFI, or Dart `http` package) to configured RPC endpoint | | 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 `csn::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 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 `csn` 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 { - record identity-error { - code: u16, - message: string, - } + use types.{host-error}; /// List available accounts (public keys or addresses). /// Returns a list of account identifiers (e.g. 20-byte Ethereum addresses). - accounts: func() -> result>, identity-error>; + accounts: func() -> result>, host-error>; /// Sign arbitrary data with the specified account's private key. /// Returns the signature bytes (e.g. 65-byte ECDSA signature with recovery id). - sign: func(account: list, data: list) -> result, identity-error>; + sign: func(account: list, data: list) -> result, host-error>; /// Sign EIP-712 typed structured data. /// `typed-data` is the JSON-encoded EIP-712 typed data structure. /// Returns the signature bytes. - sign-typed-data: func(account: list, typed-data: string) -> result, identity-error>; + sign-typed-data: func(account: list, typed-data: string) -> result, host-error>; } ``` @@ -149,15 +148,15 @@ interface identity { | WebView | window.ethereum (wallet extension) or native bridge to keychain | | Super app | Device keychain + per-module permission grants | -**Relationship with `csn`:** +**Relationship with `chain`:** -The `csn` host implementation uses `identity` internally when it encounters signing methods. For example, when a module calls `csn::request` with `eth_sendTransaction`, the host: +The `chain` host implementation uses `identity` internally when it encounters signing methods. For example, when a module calls `chain::request` with `eth_sendTransaction`, the host: 1. Constructs the transaction from the JSON-RPC params. 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 — `csn` 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 @@ -165,18 +164,20 @@ The module's private scratchpad. **Local to the device/process** — does not re ```wit interface local-store { + use types.{host-error}; + /// Get a value by key. Returns None if the key does not exist. - get: func(key: string) -> result>, string>; + get: func(key: string) -> result>, host-error>; /// Set a key-value pair. Overwrites any existing value. - /// The host MAY enforce a size quota; if exceeded, returns Err. - set: func(key: string, value: list) -> result<_, string>; + /// The host MAY enforce a size quota; if exceeded, returns Err with kind=invalid-input. + set: func(key: string, value: list) -> result<_, host-error>; /// Delete a key. No-op if the key does not exist. - delete: func(key: string) -> result<_, string>; + delete: func(key: string) -> result<_, host-error>; /// List all keys matching a prefix. Empty prefix returns all keys. - list-keys: func(prefix: string) -> result, string>; + list-keys: func(prefix: string) -> result, host-error>; } ``` @@ -201,10 +202,7 @@ Swarm is both the distribution mechanism (modules are fetched from Swarm) and a ```wit interface remote-store { - record store-error { - code: u16, - message: string, - } + use types.{host-error}; /// Upload raw data to the decentralised store. /// Returns the 32-byte content reference (Swarm address). @@ -212,14 +210,14 @@ interface remote-store { /// The host routes to its configured Bee node. Postage batch /// management is the host's responsibility — the module only /// provides data and gets back a reference. - upload: func(data: list) -> result, store-error>; + upload: func(data: list) -> result, host-error>; /// Download raw data by 32-byte content reference. /// /// The host fetches from its Bee node or a public gateway. /// Returns the raw bytes. The caller is responsible for /// interpreting the content (JSON, protobuf, WASM, etc.). - download: func(reference: list) -> result, store-error>; + download: func(reference: list) -> result, host-error>; /// Read the latest value from a mutable feed. /// @@ -228,10 +226,10 @@ interface remote-store { /// `topic`: 32-byte topic hash. /// /// Returns None if the feed has no updates. - feed-get: func( + read-feed: func( owner: list, topic: list, - ) -> result>, store-error>; + ) -> result>, host-error>; /// Update a mutable feed with new data. /// @@ -243,10 +241,10 @@ interface remote-store { /// `data`: the payload to publish. /// /// Returns the 32-byte reference of the new chunk. - feed-set: func( + write-feed: func( topic: list, data: list, - ) -> result, store-error>; + ) -> result, host-error>; } ``` @@ -263,24 +261,21 @@ interface remote-store { - **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 `feed-set`, the other reads via `feed-get`. +- **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. -### `msg` — 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), `msg` 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 msg { - record msg-error { - code: u16, - message: string, - } +interface messaging { + use types.{host-error}; record message { content-topic: string, payload: list, - timestamp: u64, + timestamp: u64, // milliseconds since Unix epoch, UTC /// Optional sender identity (protocol-dependent). sender: option>, } @@ -293,7 +288,7 @@ interface msg { /// /// Content topics follow the format: //// /// e.g. "/nexum/1/twap-updates/proto" - publish: func(content-topic: string, payload: list) -> result<_, msg-error>; + publish: func(content-topic: string, payload: list) -> result<_, host-error>; /// Query historical messages from the Waku store protocol. /// @@ -305,41 +300,41 @@ interface msg { start-time: option, end-time: option, limit: option, - ) -> result, msg-error>; + ) -> result, host-error>; } ``` -**Receiving messages** is handled through the event system, not the `msg` interface. Modules declare message subscriptions in their manifest, and the host delivers them as events: +**Receiving messages** is handled through the event system, not the `messaging` interface. Modules declare message subscriptions in their manifest, and the host delivers them as events: ```toml -[[subscribe]] -type = "message" +[[subscription]] +kind = "message" content_topic = "/nexum/1/twap-updates/proto" ``` -The event variant is extended to include message events: +The event variant in 0.2 carries `message` as a first-class variant: ```wit -record message-data { +record message { content-topic: string, payload: list, - timestamp: u64, + timestamp: u64, // milliseconds since Unix epoch, UTC sender: option>, } variant event { - block(block-data), - logs(list), - timer(u64), - message(message-data), + block(block), + logs(list), + tick(tick), + message(message), } ``` -This follows the same pattern as all other event sources: sending uses the import interface (`msg::publish`), receiving uses the declarative subscription + `on-event` dispatch. +This follows the same pattern as all other event sources: sending uses the import interface (`messaging::publish`), receiving uses the declarative subscription + `on-event` dispatch. **Platform implementations:** -| Platform | `msg` backed by | +| Platform | `messaging` backed by | |----------|-----------------| | Server (Nexum) | Waku node (nwaku or go-waku) via JSON-RPC or REST API | | Mobile (Flutter) | Waku light client via FFI (libwaku) or HTTP to remote Waku node | @@ -352,7 +347,7 @@ This follows the same pattern as all other event sources: sending uses the impor - **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. - **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). `msg` is for ephemeral signals (notifications, coordination, real-time feeds). Together they cover the full persistence spectrum. +- **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 @@ -373,63 +368,87 @@ Every platform implements this trivially. On server: `tracing` crate. On mobile: ### Universal World Definition ```wit -package web3:runtime@0.1.0; +package nexum:runtime@0.2.0; interface types { type chain-id = u64; - record block-data { + record block { chain-id: chain-id, number: u64, hash: list, - timestamp: u64, + timestamp: u64, // ms since Unix epoch, UTC } - record log-entry { + record log { chain-id: chain-id, address: list, topics: list>, data: list, block-number: u64, - tx-hash: list, + transaction-hash: list, log-index: u32, } - record message-data { + record tick { + fired-at: u64, // ms since Unix epoch, UTC + } + + record message { content-topic: string, payload: list, - timestamp: u64, + timestamp: u64, // ms since Unix epoch, UTC sender: option>, } variant event { - block(block-data), - logs(list), - timer(u64), - message(message-data), + block(block), + logs(list), + tick(tick), + message(message), + } + + type config = list>; + + variant config-value { + string(string), + integer(s64), + boolean(bool), + list(list), + } + + record host-error { + domain: string, + kind: host-error-kind, + code: s32, + message: string, + data: option, } - type config = list>; + variant host-error-kind { + unsupported, unavailable, denied, rate-limited, + timeout, invalid-input, internal, + } } -// ... csn, identity, local-store, remote-store, msg, logging interfaces as above ... +// ... chain, identity, local-store, remote-store, messaging, logging interfaces as above ... -/// Headless module — automation, background processing. -/// No UI capabilities. Runs on any conforming host. -world headless-module { - import csn; +/// Event-driven module — automation, background processing. +/// No UI capabilities. Runs on any conforming host. Six imports in 0.2. +world event-module { + import chain; import identity; import local-store; import remote-store; - import msg; + import messaging; import logging; - export init: func(config: types.config) -> result<_, string>; - export on-event: func(event: types.event) -> result<_, string>; + export init: func(config: types.config) -> result<_, host-error>; + export on-event: func(event: types.event) -> result<_, host-error>; } ``` -A module compiled against `web3:runtime/headless-module` is the **maximally portable** artifact. It runs on server, mobile, and WebView hosts without modification. +A module compiled against `nexum:runtime/event-module` is the **maximally portable** artifact. In 0.2 it runs on the server reference runtime; mobile and WebView hosts are planned (see the status banner at the top of this doc). ## Layer 2: UI Interface @@ -496,7 +515,7 @@ Interactive modules export additional lifecycle hooks beyond `init` and `on-even ```wit /// Interactive module — has a UI presence. world app-module { - include headless-module; + include event-module; import ui; /// Called when the module's UI surface is first displayed. @@ -526,7 +545,7 @@ flowchart TD D --> E["Host calls on-interact(element, action, data)"] E --> F["Module processes interaction"] F --> G["module calls ui::render(target, new-content) to update UI"] - F --> H["module calls csn::request to read chain state"] + F --> H["module calls chain::request to read chain state"] F --> I["module calls local-store::set to persist"] G --> C ``` @@ -554,36 +573,25 @@ The host loads `index.html` into a WebView and injects the bridge JavaScript tha Domain-specific interfaces extend the universal layer for particular use cases. The pattern: ```wit -package shepherd:cow@0.1.0; +package shepherd:cow@0.2.0; -interface cow { - use web3:runtime/types.{chain-id}; - - record api-error { - status: u16, - message: string, - body: option, - } +interface cow-api { + use nexum:runtime/types.{chain-id, host-error}; request: func( chain-id: chain-id, method: string, path: string, body: option, - ) -> result; -} - -interface order { - use web3:runtime/types.{chain-id}; + ) -> result; - submit: func(chain-id: chain-id, order-data: list) - -> result; + submit-order: func(chain-id: chain-id, order-data: list) + -> result; } -world shepherd-module { - include web3:runtime/headless-module; - import cow; - import order; +world shepherd { + include nexum:runtime/event-module; + import cow-api; } ``` @@ -597,37 +605,40 @@ interface vault { /* ... */ } interface strategy { /* ... */ } world yield-module { - include web3:runtime/headless-module; + include nexum:runtime/event-module; import vault; import strategy; } ``` -The `include` mechanism ensures that any domain-specific module inherits the full universal interface set. A `shepherd-module` can call `csn::request`, `identity::sign`, `local-store::get`, `remote-store::upload`, `msg::publish`, and `logging::log` — plus the CoW-specific `cow::request` and `order::submit`. +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 ``` wit/ -├── web3-runtime/ -│ ├── types.wit # chain-id, block-data, log-entry, message-data, event, config -│ ├── csn.wit # csn interface (consensus access) +├── nexum-runtime/ +│ ├── types.wit # chain-id, block, log, tick, message, event, config, host-error +│ ├── chain.wit # chain interface (consensus access + request-batch) │ ├── identity.wit # identity interface (key management, signing) │ ├── local-store.wit # local-store interface │ ├── remote-store.wit # remote-store interface (Swarm) -│ ├── msg.wit # msg interface (Waku) +│ ├── messaging.wit # messaging interface (Waku) │ ├── logging.wit # logging interface -│ ├── ui.wit # ui interface + host-capabilities -│ ├── headless-module.wit # headless-module world -│ └── app-module.wit # app-module world (includes ui) +│ ├── clock.wit # additive: clock (now-ms, monotonic-ns) +│ ├── random.wit # additive: random (CSPRNG fill) +│ ├── http.wit # additive: http (allowlisted fetch) +│ ├── 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 │ └── shepherd-cow/ - ├── cow.wit # cow interface - ├── order.wit # order interface - └── shepherd-module.wit # shepherd-module world (includes headless-module + cow + order) + ├── cow-api.wit # merged cow-api interface (request + submit-order) + └── shepherd.wit # shepherd world (includes event-module + cow-api) ``` -The `web3-runtime` package is domain-agnostic and reusable. The `shepherd-cow` package is the CoW Protocol extension. New domains add new packages without touching the universal layer. +The `nexum-runtime` package is domain-agnostic and reusable. The `shepherd-cow` package is the CoW Protocol extension. New domains add new packages without touching the universal layer. ## Platform Targets @@ -637,22 +648,21 @@ This is the current design (docs 01-07), adapted for the layered WIT. Shepherd i | Interface | Implementation | |-----------|---------------| -| `csn` | alloy provider with tower middleware (timeout, retry, rate-limit, fallback) | +| `chain` | alloy provider with tower middleware (timeout, retry, rate-limit, fallback) | | `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 | -| `msg` | Waku node (nwaku) via JSON-RPC or REST API | +| `messaging` | Waku node (nwaku) via JSON-RPC or REST API | | `logging` | `tracing` crate -> JSON structured logs | -| `cow` | reqwest HTTP client -> CoW Protocol API | -| `order` | CoW API order submission (permissionless) | +| `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 41.x (Component Model, fuel, epoch metering) | +| WASM engine | wasmtime 45.x (Component Model, fuel, epoch metering) | -The `local-store` interface is renamed from `state`. The `remote-store`, `identity`, and `msg` interfaces are new. Event sources gain a fourth type: `message` (Waku content topic subscriptions). Everything else is as designed. +### Mobile App (Flutter/Dart) — Planned -### Mobile App (Flutter/Dart) +> **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. -A Flutter application embeds a WASM runtime and provides the universal interfaces via Dart implementations: +A Flutter application would embed a WASM runtime and provide the universal interfaces via Dart implementations: ```mermaid flowchart TD @@ -664,11 +674,11 @@ flowchart TD end subgraph HostAdapter["Host Adapter (Dart)"] - HA_CSN["csn -> HTTP client to RPC endpoint"] + HA_CHAIN["chain -> HTTP client to RPC endpoint"] HA_ID["identity -> device keychain (Keystore/Keychain) or wallet SDK"] HA_LS["local-store -> SQLite (sqflite)"] HA_RS["remote-store -> HTTP to Bee gateway"] - HA_MSG["msg -> libwaku via FFI"] + HA_MSG["messaging -> libwaku via FFI"] HA_LOG["logging -> platform logger"] end @@ -699,29 +709,31 @@ 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) +### WebView (Browser Engine + Injected Host Functions) — Planned -A WebView host runs inside a native app (or standalone browser). The WASM module executes in the browser's native WASM engine. Host functions are injected via a JavaScript bridge. +> **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. + +A WebView host would run inside a native app (or standalone browser). The WASM module executes in the browser's native WASM engine. Host functions are injected via a JavaScript bridge. ```mermaid flowchart TD subgraph NativeApp["Native App Shell"] subgraph WebView["WebView"] - WASMModule["WASM Module (browser's WASM engine)\nCalls imported functions:\ncsn.request(...)\nidentity.sign(...)\nlocalStore.get(...)\nremoteStore.download(...)\nmsg.publish(...)\nlogging.log(...)"] + WASMModule["WASM Module (browser's WASM engine)\nCalls imported functions:\nchain.request(...)\nidentity.sign(...)\nlocalStore.get(...)\nremoteStore.download(...)\nmessaging.publish(...)\nlogging.log(...)"] subgraph JSBridge["JavaScript Bridge (injected)"] - JS["window.web3runtime = {\n csn: { request: (c, m, p) =>\n nativeBridge.call('csn', ...) },\n identity: { accounts: () =>\n nativeBridge.call('identity', ...) },\n localStore: { get: (k) =>\n nativeBridge.call('store', ..) },\n remoteStore: { download: (ref) =>\n nativeBridge.call('store', ..) },\n msg: { publish: (t, p) =>\n nativeBridge.call('msg', ...) },\n logging: { log: (l, m) =>\n console.log(${`[l] m`}) }\n}"] + JS["window.nexumRuntime = {\n chain: { request: (c, m, p) =>\n nativeBridge.call('chain', ...) },\n identity: { accounts: () =>\n nativeBridge.call('identity', ...) },\n localStore: { get: (k) =>\n nativeBridge.call('store', ..) },\n remoteStore: { download: (ref) =>\n nativeBridge.call('store', ..) },\n messaging: { publish: (t, p) =>\n nativeBridge.call('messaging', ...) },\n logging: { log: (l, m) =>\n console.log(${`[l] m`}) }\n}"] end WASMModule --> JSBridge end subgraph NativeHost["Native Host Adapter"] - NH_CSN["csn -> HTTP to RPC / wallet bridge"] + NH_CHAIN["chain -> HTTP to RPC / wallet bridge"] NH_ID["identity -> window.ethereum / native keychain"] NH_LS["local-store -> SQLite / IndexedDB"] NH_RS["remote-store -> HTTP to Bee gateway"] - NH_MSG["msg -> Waku node / js-waku"] + NH_MSG["messaging -> Waku node / js-waku"] NH_LOG["logging -> native logger"] end @@ -741,18 +753,18 @@ Approach 1 is preferred — it preserves the single-artifact property (one `.was **WebView-specific capability: `window.ethereum`** -In a browser context, the user may have a wallet extension (MetaMask, Rabby, etc.) that injects `window.ethereum`. The `csn::request` host function can optionally route through this: +In a browser context, the user may have a wallet extension (MetaMask, Rabby, etc.) that injects `window.ethereum`. The `chain::request` host function can optionally route through this: ```javascript // In the JS bridge -csn: { +chain: { request: async (chainId, method, params) => { if (window.ethereum && useWalletProvider) { // Route through user's wallet (gets signing capabilities too) return await window.ethereum.request({ method, params: JSON.parse(params) }); } else { // Route through native bridge to configured RPC endpoint - return await nativeBridge.call('csn', { chainId, method, params }); + return await nativeBridge.call('chain', { chainId, method, params }); } } } @@ -764,18 +776,20 @@ Similarly, the `identity` interface in a WebView context can delegate to `window **WebView-specific capability: `js-waku`** -For messaging in the browser, `js-waku` provides a pure JavaScript Waku client. The `msg` 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 +> **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: +The super app is the convergence of all targets. A native shell (Flutter) that would: -1. **Discovers modules** via ENS (doc 03) — the same discovery mechanism as the server runtime. -2. **Fetches modules** from Swarm/IPFS — the same content-addressed distribution. -3. **Runs headless modules** in an embedded WASM runtime (automation, background tasks). -4. **Runs interactive modules** in WebViews (UI, dashboards, transaction builders). -5. **Provides the universal interfaces** to all modules (csn, identity, local-store, remote-store, msg, logging). -6. **Provides the UI interface** to interactive modules. +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). +6. **Provide the UI interface** to interactive modules. ```mermaid flowchart TD @@ -797,11 +811,11 @@ flowchart TD end subgraph HostLayer["Host Adapter Layer"] - HL_CSN["csn -> HTTP to RPC endpoints"] + HL_CHAIN["chain -> HTTP to RPC endpoints"] HL_ID["identity -> device keychain + per-module grants"] HL_LS["local-store -> SQLite"] HL_RS["remote-store -> Bee light node / gateway"] - HL_MSG["msg -> Waku light client"] + HL_MSG["messaging -> Waku light client"] HL_LOG["logging -> app logger + optional cloud"] HL_UI["ui -> WebView bridge (interactive modules)"] end @@ -838,19 +852,18 @@ The super app adds a capability-grant layer on top of the WIT world. When a modu ``` "TWAP Monitor" requests: - ✓ csn — read blockchain state (chains: 42161) + ✓ 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 - ✓ msg — send/receive messages (topics: /nexum/1/twap-*) - ✗ ui — (not requested — headless module) - ✓ cow — interact with CoW Protocol API - ✓ order — submit orders to CoW Protocol + ✓ 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 `msg` 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 @@ -858,10 +871,12 @@ Any platform that wants to run modules must implement the **Host Adapter** — t ### Required Behaviours -**`csn::request`** (Consensus) +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 `json-rpc-error` for provider errors, method-not-found, and transport failures. +- 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. @@ -869,7 +884,7 @@ Any platform that wants to run modules must implement the **Host Adapter** — t - `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 `identity-error` if the account is unknown, the user rejects the signing request, or the backend is unavailable. +- 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). - SHOULD NOT expose private key material to the module. The module sends data in, gets a signature out. @@ -877,22 +892,22 @@ Any platform that wants to run modules must implement the **Host Adapter** — t - MUST provide per-module isolation (module A cannot read module B's state). - MUST persist across module restarts within the same host process/session. - SHOULD persist across host process restarts (platform-dependent). -- MAY enforce size quotas. If exceeded, `set` returns `Err` (not a trap). +- MAY enforce size quotas. If exceeded, `set` returns `host-error { domain: "store", kind: invalid-input }` (not a trap). - MAY provide transactional semantics. Modules SHOULD NOT rely on this across platforms. -**`remote-store::upload/download/feed-get/feed-set`** +**`remote-store::upload/download/read-feed/write-feed`** - MUST route to a Swarm-compatible node or gateway. - `upload` MUST return the 32-byte content reference of the stored data. -- `download` MUST return the raw bytes for a valid reference, or error for missing/unreachable content. -- `feed-set` signs with the host's identity. The owner is implicit. -- MAY return errors for unavailable connectivity (offline, no node configured). +- `download` MUST return the raw bytes for a valid reference, or `host-error` (`kind: unavailable`) for missing/unreachable content. +- `write-feed` signs with the host's identity. The owner is implicit. +- MAY return `host-error { kind: unavailable }` for offline / no-node-configured. -**`msg::publish/query`** +**`messaging::publish/query`** - MUST route `publish` to a Waku-compatible node. - `publish` MUST deliver the message to the content topic's relay network on a best-effort basis. - `query` SHOULD return historical messages if the host's Waku node supports the store protocol. -- `query` MAY return an empty list or error if store is unavailable. -- MAY apply rate limits to prevent message spam. +- `query` MAY return an empty list or `host-error { kind: unsupported }` if store is unavailable. +- MAY apply rate limits (returning `kind: rate-limited`) to prevent message spam. **`logging::log`** - MUST accept log calls without blocking or erroring. @@ -902,7 +917,7 @@ Any platform that wants to run modules must implement the **Host Adapter** — t **Event dispatch (`on-event`)** - MUST call `init(config)` exactly once before any `on-event` calls. - MUST call `on-event` for each subscribed event (per manifest). -- MUST support all four event types: `block`, `logs`, `timer`, `message`. +- MUST support all four event variants: `block`, `logs`, `tick`, `message`. - SHOULD guarantee in-order delivery within a single module. - MAY dispatch events concurrently across modules. - SHOULD handle panics/traps gracefully (restart module, not crash host). @@ -951,40 +966,34 @@ The SDK mirrors the WIT layering: ```mermaid graph TD subgraph ShepherdSDK["shepherd-sdk (Domain-specific: CoW Protocol)"] - COW_ITEMS["CowClient, order helpers,\n#[shepherd::module] macro\n(imports cow + order)"] + COW_ITEMS["Cow client,\n#[shepherd::module] macro\n(imports cow-api)"] end subgraph NexumSDK["nexum-sdk (Universal: any blockchain app)"] - NEXUM_ITEMS["HostTransport, provider(),\nTypedState, RemoteStore,\nMsgClient, Identity,\nlogging macros,\nerror types,\n#[nexum::module] macro\n(imports csn + identity\n+ local-store\n+ remote-store + msg\n+ logging)"] + NEXUM_ITEMS["HostTransport, provider(),\nTypedState, RemoteStore,\nMessaging, Signer,\nlogging macros,\nHostError / HostErrorKind,\n#[nexum::module] macro\n(imports chain + identity\n+ local-store\n+ remote-store + messaging\n+ logging)"] end ShepherdSDK -->|"extends"| NexumSDK ``` -- **`nexum-sdk`** — the universal Rust SDK for any module targeting `web3:runtime/headless-module`. Provides `HostTransport` (alloy `Transport` trait over `csn::request`), `provider(chain_id)`, `TypedState` (serde over `local-store`), `RemoteStore` (typed wrapper over `remote-store`), `MsgClient` (typed wrapper over `msg`), `Identity` (typed wrapper over `identity`), logging macros, error types. Any module author — CoW, DeFi, gaming, whatever — uses this. +- **`nexum-sdk`** — the universal Rust SDK for any module targeting `nexum:runtime/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 CoW-specific wrappers: `CowClient`, order submission helpers, the `#[shepherd::module]` proc macro (which generates `cow` and `order` imports 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. -## Migration from Current Design - -The changes from the current docs (01-07) are additive, not breaking: +## Migration from 0.1 -| Change | Impact | -|--------|--------| -| Rename `state` -> `local-store` | WIT interface rename. SDK wrapper updated. Module source uses `local_store::get()` instead of `state::get()`. | -| Rename `swarm` -> `remote-store` | Abstracts the storage backend. WIT interface and SDK wrapper renamed. Swarm is the initial implementation; the interface name is backend-agnostic. | -| Add `identity` interface | New WIT interface for key management and signing. Host gains new implementation requirement. `csn` uses `identity` internally for signing RPC methods. Modules that don't import it directly are unaffected. | -| Add `msg` interface | New WIT interface backed by Waku. Host gains new implementation requirement. Modules that don't import it are unaffected. | -| Add `message` event variant | Extends the `event` type with `message(message-data)`. Existing handlers for `block`, `logs`, `timer` are unaffected. | -| Add `ui` interface + `app-module` world | New WIT interface and world. Headless modules are unaffected. Only interactive modules import this. | -| Split WIT package: `web3:runtime` + `shepherd:cow` | Namespace change. The `shepherd-module` world now `include`s `headless-module` from `web3:runtime`. Module source is unchanged (bindgen generates the same Rust types). | -| Split SDK: `nexum-sdk` + `shepherd-sdk` | Crate restructure. `shepherd-sdk` depends on and re-exports `nexum-sdk`. Module authors using `nexum_sdk::prelude::*` see no change. | +For the full 0.1 → 0.2 rename and behaviour change list, see the [Migration Guide](migration/0.1-to-0.2.md). The main themes: -The Nexum server runtime requires three new host implementations: `identity` (keystore/KMS), `remote-store` (Bee API — already needed for content distribution), and `msg` (Waku node). Event sources gain a fourth type: `message` (Waku content topic subscriptions). +- WIT package `web3:runtime` → `nexum:runtime`; interfaces `csn` → `chain` and `msg` → `messaging`; worlds `headless-module` → `event-module` and `shepherd-module` → `shepherd`. +- CoW `cow` + `order` interfaces merged into `cow-api`. +- All host functions return the unified `host-error` (with `host-error-kind` discriminant) instead of five per-protocol error types. +- The `event-module` world imports the six primitives the docs always claimed (0.1's WIT was missing `identity` from the world definition). +- Manifest: `wasm = ...` → `component = ...`; `[[subscribe]]` → `[[subscription]]` with `kind` instead of `type`; new `[capabilities]` section drives optional/required imports; `[config]` values are now typed. +- Additive WIT: `clock`, `random`, `http`, `chain::request-batch`, and the experimental `query-module` world. ## Summary @@ -992,24 +1001,25 @@ The Nexum server runtime requires three new host implementations: `identity` (ke | Primitive | Interface | Implementation | Persistence | Scope | |-----------|-----------|---------------|-------------|-------| -| Consensus | `csn` | JSON-RPC (eth_*) | Blockchain | Global (chain) | +| Chain | `chain` | JSON-RPC (eth_*) | Blockchain | Global (chain) | | Identity | `identity` | Keystore / KMS / HSM | Key material | Per-account | | Local Store | `local-store` | redb / SQLite / IndexedDB | Device-local | Per-module | | Remote Store | `remote-store` | Ethereum Swarm | Decentralised | Global (content-addressed) | -| Messaging | `msg` | Waku | Ephemeral | Topic-based pub/sub | +| Messaging | `messaging` | Waku | Ephemeral | Topic-based pub/sub | | Logging | `logging` | tracing / console | None | Diagnostic | ### Architecture | Concept | Scope | |---------|-------| -| `web3:runtime` WIT package | Universal — any blockchain app, any platform | -| `headless-module` world | Automation modules — server, mobile, background | -| `app-module` world | Interactive modules — WebView, super app | +| `nexum:runtime` 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-module` world | CoW automation modules (includes headless-module + cow + order) | -| `nexum-sdk` crate | Universal Rust SDK (HostTransport, TypedState, RemoteStore, MsgClient, Identity) | -| `shepherd-sdk` crate | CoW Rust SDK (CowClient, order helpers, extends nexum-sdk) | +| `shepherd` world | CoW automation modules (includes event-module + cow-api) | +| `nexum-sdk` crate | Universal Rust SDK (HostTransport, TypedState, RemoteStore, Messaging, Signer, HostError) | +| `shepherd-sdk` crate | CoW Rust SDK (Cow, extends nexum-sdk) | | Content-addressed distribution | Platform-agnostic (Swarm/IPFS, ENS discovery, hash verification) | | Host Adapter | Platform-specific implementation of universal interfaces | From befe1d963ddd7eece955e59c1e51b66ae92ab4b6 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 30 May 2026 23:56:32 +0000 Subject: [PATCH 06/17] runtime: update engine + example to 0.2 WIT - Engine main.rs targets world `shepherd` (formerly `shepherd-module`), generates against nexum:runtime@0.2.0 + shepherd:cow@0.2.0. - Replaces per-domain error records (JsonRpcError, MsgError, StoreError, ApiError, bare String) with unified HostError + HostErrorKind across every host impl. - Adds Identity host stub (was missing in 0.1 despite being doc'd). - Adds chain::request_batch stub that falls back to per-call dispatch. - Renames feed_get/feed_set -> read_feed/write_feed. - Drops separate cow + order interfaces, replaced by single cow_api with request and submit_order. - Block ts in test event is now ms (1_700_000_000_000) per types.wit documented unit. - Example module targets event-module world, matches Event::Tick instead of Event::Timer, returns HostError from init/on_event. --- crates/nexum-engine/src/main.rs | 237 +++++++++++++++++++------------- modules/example/src/lib.rs | 34 +++-- 2 files changed, 168 insertions(+), 103 deletions(-) diff --git a/crates/nexum-engine/src/main.rs b/crates/nexum-engine/src/main.rs index 6771d42..cc7b689 100644 --- a/crates/nexum-engine/src/main.rs +++ b/crates/nexum-engine/src/main.rs @@ -1,15 +1,18 @@ use std::time::Instant; use wasmtime::component::{Component, Linker, ResourceTable}; +use wasmtime::error::Context as _; use wasmtime::{Engine, Store}; use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; wasmtime::component::bindgen!({ path: "../../wit/shepherd-cow", - world: "shepherd-module", + world: "shepherd", imports: { default: async }, exports: { default: async }, }); +use nexum::runtime::types::HostErrorKind; + struct HostState { wasi: WasiCtx, table: ResourceTable, @@ -24,61 +27,125 @@ impl WasiView for HostState { } } +fn unimplemented(domain: &str, detail: impl Into) -> HostError { + HostError { + domain: domain.into(), + kind: HostErrorKind::Unsupported, + code: 501, + message: detail.into(), + data: None, + } +} + // -- Stub implementations for host interfaces -- -impl web3::runtime::types::Host for HostState {} +impl nexum::runtime::types::Host for HostState {} -impl shepherd::cow::cow::Host for HostState { +impl shepherd::cow::cow_api::Host for HostState { async fn request( &mut self, _chain_id: u64, method: String, path: String, _body: Option, - ) -> Result { + ) -> Result { let start = Instant::now(); - eprintln!("[cow] {method} {path}"); - let result = Err(shepherd::cow::cow::ApiError { - status: 501, - message: "not implemented".into(), - body: None, - }); - eprintln!("[timing] cow::request: {:?}", start.elapsed()); + eprintln!("[cow-api] {method} {path}"); + let result = Err(unimplemented( + "cow-api", + format!("not implemented: {method} {path}"), + )); + eprintln!("[timing] cow-api::request: {:?}", start.elapsed()); result } -} -impl shepherd::cow::order::Host for HostState { - async fn submit(&mut self, _chain_id: u64, _order_data: Vec) -> Result { + async fn submit_order( + &mut self, + _chain_id: u64, + _order_data: Vec, + ) -> Result { let start = Instant::now(); - eprintln!("[order] submit"); - let result = Err("not implemented".into()); - eprintln!("[timing] order::submit: {:?}", start.elapsed()); + eprintln!("[cow-api] submit-order"); + let result = Err(unimplemented("cow-api", "submit-order not implemented")); + eprintln!("[timing] cow-api::submit-order: {:?}", start.elapsed()); result } } -impl web3::runtime::csn::Host for HostState { +impl nexum::runtime::chain::Host for HostState { async fn request( &mut self, _chain_id: u64, method: String, _params: String, - ) -> Result { + ) -> Result { let start = Instant::now(); - eprintln!("[csn] request: {method}"); - let result = Err(web3::runtime::csn::JsonRpcError { + eprintln!("[chain] request: {method}"); + let result = Err(HostError { + domain: "chain".into(), + kind: HostErrorKind::Unsupported, code: -32601, message: format!("method not implemented: {method}"), data: None, }); - eprintln!("[timing] csn::request: {:?}", start.elapsed()); + eprintln!("[timing] chain::request: {:?}", start.elapsed()); result } + + async fn request_batch( + &mut self, + chain_id: u64, + requests: Vec, + ) -> Result, HostError> { + let start = Instant::now(); + eprintln!("[chain] request-batch: {} calls", requests.len()); + let mut out = Vec::with_capacity(requests.len()); + for req in requests { + match self.request(chain_id, req.method, req.params).await { + Ok(s) => out.push(nexum::runtime::chain::RpcResult::Ok(s)), + Err(e) => out.push(nexum::runtime::chain::RpcResult::Err(e)), + } + } + eprintln!("[timing] chain::request-batch: {:?}", start.elapsed()); + Ok(out) + } } -impl web3::runtime::local_store::Host for HostState { - async fn get(&mut self, key: String) -> Result>, String> { +impl nexum::runtime::identity::Host for HostState { + async fn accounts(&mut self) -> Result>, HostError> { + let start = Instant::now(); + eprintln!("[identity] accounts"); + let result = Ok(vec![]); + eprintln!("[timing] identity::accounts: {:?}", start.elapsed()); + result + } + + async fn sign(&mut self, _account: Vec, _message: Vec) -> Result, HostError> { + let start = Instant::now(); + eprintln!("[identity] sign"); + let result = Err(unimplemented("identity", "sign not implemented")); + eprintln!("[timing] identity::sign: {:?}", start.elapsed()); + result + } + + async fn sign_typed_data( + &mut self, + _account: Vec, + _typed_data: String, + ) -> Result, HostError> { + let start = Instant::now(); + eprintln!("[identity] sign-typed-data"); + let result = Err(unimplemented( + "identity", + "sign-typed-data not implemented", + )); + eprintln!("[timing] identity::sign-typed-data: {:?}", start.elapsed()); + result + } +} + +impl nexum::runtime::local_store::Host for HostState { + async fn get(&mut self, key: String) -> Result>, HostError> { let start = Instant::now(); eprintln!("[local-store] get: {key}"); let result = Ok(None); @@ -86,7 +153,7 @@ impl web3::runtime::local_store::Host for HostState { result } - async fn set(&mut self, key: String, _value: Vec) -> Result<(), String> { + async fn set(&mut self, key: String, _value: Vec) -> Result<(), HostError> { let start = Instant::now(); eprintln!("[local-store] set: {key}"); let result = Ok(()); @@ -94,7 +161,7 @@ impl web3::runtime::local_store::Host for HostState { result } - async fn delete(&mut self, key: String) -> Result<(), String> { + async fn delete(&mut self, key: String) -> Result<(), HostError> { let start = Instant::now(); eprintln!("[local-store] delete: {key}"); let result = Ok(()); @@ -102,7 +169,7 @@ impl web3::runtime::local_store::Host for HostState { result } - async fn list_keys(&mut self, prefix: String) -> Result, String> { + async fn list_keys(&mut self, prefix: String) -> Result, HostError> { let start = Instant::now(); eprintln!("[local-store] list-keys: {prefix}"); let result = Ok(vec![]); @@ -111,75 +178,54 @@ impl web3::runtime::local_store::Host for HostState { } } -impl web3::runtime::remote_store::Host for HostState { - async fn upload( - &mut self, - _data: Vec, - ) -> Result, web3::runtime::remote_store::StoreError> { +impl nexum::runtime::remote_store::Host for HostState { + async fn upload(&mut self, _data: Vec) -> Result, HostError> { let start = Instant::now(); - let result = Err(web3::runtime::remote_store::StoreError { - code: 501, - message: "not implemented".into(), - }); + let result = Err(unimplemented("remote-store", "upload not implemented")); eprintln!("[timing] remote-store::upload: {:?}", start.elapsed()); result } - async fn download( - &mut self, - _reference: Vec, - ) -> Result, web3::runtime::remote_store::StoreError> { + async fn download(&mut self, _reference: Vec) -> Result, HostError> { let start = Instant::now(); - let result = Err(web3::runtime::remote_store::StoreError { - code: 501, - message: "not implemented".into(), - }); + let result = Err(unimplemented("remote-store", "download not implemented")); eprintln!("[timing] remote-store::download: {:?}", start.elapsed()); result } - async fn feed_get( + async fn read_feed( &mut self, _owner: Vec, _topic: Vec, - ) -> Result>, web3::runtime::remote_store::StoreError> { + ) -> Result>, HostError> { let start = Instant::now(); - let result = Err(web3::runtime::remote_store::StoreError { - code: 501, - message: "not implemented".into(), - }); - eprintln!("[timing] remote-store::feed-get: {:?}", start.elapsed()); + let result = Err(unimplemented("remote-store", "read-feed not implemented")); + eprintln!("[timing] remote-store::read-feed: {:?}", start.elapsed()); result } - async fn feed_set( + async fn write_feed( &mut self, _topic: Vec, _data: Vec, - ) -> Result, web3::runtime::remote_store::StoreError> { + ) -> Result, HostError> { let start = Instant::now(); - let result = Err(web3::runtime::remote_store::StoreError { - code: 501, - message: "not implemented".into(), - }); - eprintln!("[timing] remote-store::feed-set: {:?}", start.elapsed()); + let result = Err(unimplemented("remote-store", "write-feed not implemented")); + eprintln!("[timing] remote-store::write-feed: {:?}", start.elapsed()); result } } -impl web3::runtime::msg::Host for HostState { +impl nexum::runtime::messaging::Host for HostState { async fn publish( &mut self, content_topic: String, _payload: Vec, - ) -> Result<(), web3::runtime::msg::MsgError> { + ) -> Result<(), HostError> { let start = Instant::now(); - eprintln!("[msg] publish: {content_topic}"); - let result = Err(web3::runtime::msg::MsgError { - code: 501, - message: "not implemented".into(), - }); - eprintln!("[timing] msg::publish: {:?}", start.elapsed()); + eprintln!("[messaging] publish: {content_topic}"); + let result = Err(unimplemented("messaging", "publish not implemented")); + eprintln!("[timing] messaging::publish: {:?}", start.elapsed()); result } @@ -189,24 +235,24 @@ impl web3::runtime::msg::Host for HostState { _start_time: Option, _end_time: Option, _limit: Option, - ) -> Result, web3::runtime::msg::MsgError> { + ) -> Result, HostError> { let start = Instant::now(); - eprintln!("[msg] query: {content_topic}"); + eprintln!("[messaging] query: {content_topic}"); let result = Ok(vec![]); - eprintln!("[timing] msg::query: {:?}", start.elapsed()); + eprintln!("[timing] messaging::query: {:?}", start.elapsed()); result } } -impl web3::runtime::logging::Host for HostState { - async fn log(&mut self, level: web3::runtime::logging::Level, message: String) { +impl nexum::runtime::logging::Host for HostState { + async fn log(&mut self, level: nexum::runtime::logging::Level, message: String) { let start = Instant::now(); let level_str = match level { - web3::runtime::logging::Level::Trace => "TRACE", - web3::runtime::logging::Level::Debug => "DEBUG", - web3::runtime::logging::Level::Info => "INFO", - web3::runtime::logging::Level::Warn => "WARN", - web3::runtime::logging::Level::Error => "ERROR", + nexum::runtime::logging::Level::Trace => "TRACE", + nexum::runtime::logging::Level::Debug => "DEBUG", + nexum::runtime::logging::Level::Info => "INFO", + nexum::runtime::logging::Level::Warn => "WARN", + nexum::runtime::logging::Level::Error => "ERROR", }; eprintln!("[{level_str}] {message}"); eprintln!("[timing] logging::log: {:?}", start.elapsed()); @@ -217,9 +263,9 @@ impl web3::runtime::logging::Host for HostState { async fn main() -> anyhow::Result<()> { let wasm_path = std::env::args() .nth(1) - .ok_or_else(|| anyhow::anyhow!("usage: nxm-engine "))?; + .ok_or_else(|| anyhow::anyhow!("usage: nexum-engine "))?; - println!("nxm-engine: loading component from {wasm_path}"); + println!("nexum-engine: loading component from {wasm_path}"); let mut config = wasmtime::Config::new(); config.wasm_component_model(true); @@ -231,7 +277,7 @@ async fn main() -> anyhow::Result<()> { eprintln!("[timing] component load: {:?}", start.elapsed()); let mut linker = Linker::::new(&engine); - ShepherdModule::add_to_linker::>( + Shepherd::add_to_linker::>( &mut linker, |state| state, )?; @@ -248,39 +294,42 @@ async fn main() -> anyhow::Result<()> { ); let start = Instant::now(); - let bindings = ShepherdModule::instantiate_async(&mut store, &component, &linker) + let bindings = Shepherd::instantiate_async(&mut store, &component, &linker) .await .context("failed to instantiate component")?; eprintln!("[timing] component instantiate: {:?}", start.elapsed()); - // Call init with config - println!("nxm-engine: calling init..."); + println!("nexum-engine: calling init..."); let config_entries: Config = vec![("name".into(), "example".into())]; let start = Instant::now(); match bindings.call_init(&mut store, &config_entries).await? { - Ok(()) => println!("nxm-engine: init succeeded"), - Err(e) => println!("nxm-engine: init failed: {e}"), + Ok(()) => println!("nexum-engine: init succeeded"), + Err(e) => println!( + "nexum-engine: init failed: {}::{:?} {} ({})", + e.domain, e.kind, e.message, e.code + ), } eprintln!("[timing] call_init: {:?}", start.elapsed()); - // Dispatch a test block event - println!("nxm-engine: dispatching test block event..."); - let block = web3::runtime::types::BlockData { + // Dispatch a test block event (timestamps are ms since Unix epoch, UTC). + println!("nexum-engine: dispatching test block event..."); + let block = nexum::runtime::types::Block { chain_id: 1, number: 19_000_000, hash: vec![0xab; 32], - timestamp: 1_700_000_000, + timestamp: 1_700_000_000_000, }; - let event = web3::runtime::types::Event::Block(block); + let event = nexum::runtime::types::Event::Block(block); let start = Instant::now(); match bindings.call_on_event(&mut store, &event).await? { - Ok(()) => println!("nxm-engine: on-event succeeded"), - Err(e) => println!("nxm-engine: on-event failed: {e}"), + Ok(()) => println!("nexum-engine: on-event succeeded"), + Err(e) => println!( + "nexum-engine: on-event failed: {}::{:?} {} ({})", + e.domain, e.kind, e.message, e.code + ), } eprintln!("[timing] call_on_event: {:?}", start.elapsed()); - println!("nxm-engine: done"); + println!("nexum-engine: done"); Ok(()) } - -use wasmtime::error::Context as _; diff --git a/modules/example/src/lib.rs b/modules/example/src/lib.rs index 80b18a1..651450e 100644 --- a/modules/example/src/lib.rs +++ b/modules/example/src/lib.rs @@ -3,17 +3,27 @@ #![allow(clippy::too_many_arguments)] wit_bindgen::generate!({ - path: "../../wit/web3-runtime", - world: "headless-module", + path: "../../wit/nexum-runtime", + world: "event-module", }); -use web3::runtime::logging; -use web3::runtime::types; +use nexum::runtime::logging; +use nexum::runtime::types::{self, HostErrorKind}; struct ExampleModule; +fn module_err(message: impl Into) -> HostError { + HostError { + domain: "example".into(), + kind: HostErrorKind::Internal, + code: 0, + message: message.into(), + data: None, + } +} + impl Guest for ExampleModule { - fn init(config: Vec<(String, String)>) -> Result<(), String> { + fn init(config: Vec<(String, String)>) -> Result<(), HostError> { let name = config .iter() .find(|(k, _)| k == "name") @@ -23,16 +33,19 @@ impl Guest for ExampleModule { logging::Level::Info, &format!("example module init (name={name})"), ); + if name.is_empty() { + return Err(module_err("config 'name' is empty")); + } Ok(()) } - fn on_event(event: types::Event) -> Result<(), String> { + fn on_event(event: types::Event) -> Result<(), HostError> { match &event { types::Event::Block(block) => { logging::log( logging::Level::Info, &format!( - "block {} on chain {} (ts={})", + "block {} on chain {} (ts={}ms)", block.number, block.chain_id, block.timestamp ), ); @@ -43,8 +56,11 @@ impl Guest for ExampleModule { &format!("received {} log entries", logs.len()), ); } - types::Event::Timer(ts) => { - logging::log(logging::Level::Info, &format!("timer fired at {ts}")); + types::Event::Tick(tick) => { + logging::log( + logging::Level::Info, + &format!("tick fired at {}ms", tick.fired_at), + ); } types::Event::Message(msg) => { logging::log( From c792e8c6a30762f3240cea12fdfdbcf1fa8ac9bd Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 00:50:33 +0000 Subject: [PATCH 07/17] example: drop empty-name guard from init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard was inconsistent: missing 'name' key silently defaulted to 'unknown' and returned Ok, but an explicit empty string returned Err — and both paths logged a success-shaped 'name=...' line BEFORE the guard ran. The example is a hello-world; a config-validation guard belongs in a real module, not a starter. Drop it. Resolves PR #6 finding #9. --- modules/example/src/lib.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/modules/example/src/lib.rs b/modules/example/src/lib.rs index 651450e..b71b54d 100644 --- a/modules/example/src/lib.rs +++ b/modules/example/src/lib.rs @@ -8,20 +8,10 @@ wit_bindgen::generate!({ }); use nexum::runtime::logging; -use nexum::runtime::types::{self, HostErrorKind}; +use nexum::runtime::types; struct ExampleModule; -fn module_err(message: impl Into) -> HostError { - HostError { - domain: "example".into(), - kind: HostErrorKind::Internal, - code: 0, - message: message.into(), - data: None, - } -} - impl Guest for ExampleModule { fn init(config: Vec<(String, String)>) -> Result<(), HostError> { let name = config @@ -33,9 +23,6 @@ impl Guest for ExampleModule { logging::Level::Info, &format!("example module init (name={name})"), ); - if name.is_empty() { - return Err(module_err("config 'name' is empty")); - } Ok(()) } From b6a9b9c4430d7c2b8bcdac4ec1a0d8e0ce7226d4 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 00:51:23 +0000 Subject: [PATCH 08/17] ci: add wit deps sync check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both wit/nexum-runtime/ and wit/shepherd-cow/deps/nexum-runtime/ are committed to the tree; the latter is regenerated by 'just sync-wit'. Without a CI guard, a contributor can edit one without the other and the divergence only surfaces at runtime (interface-mismatch on module instantiation) — not at build time. The new job re-runs the same copy that 'just sync-wit' does, then fails if the deps tree differs from the freshly-copied canonical. Resolves PR #6 finding #8. --- .github/workflows/ci.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99de175..1585aa3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,3 +66,26 @@ jobs: targets: wasm32-wasip2 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo build -p example --target wasm32-wasip2 --release + + wit-deps-sync: + # Verifies wit/shepherd-cow/deps/nexum-runtime/ is byte-identical to the + # canonical wit/nexum-runtime/. Both trees are committed; only `just + # sync-wit` keeps them aligned. Without this check, a contributor can + # edit one and forget the other, producing a repo where the engine + # bindgen and the guest bindgen see different WIT and module + # instantiation fails at runtime on a commit that builds clean. + name: wit deps sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Regenerate shepherd-cow deps from canonical wit/nexum-runtime + run: | + rm -rf wit/shepherd-cow/deps/nexum-runtime + cp -r wit/nexum-runtime wit/shepherd-cow/deps/nexum-runtime + - name: Fail if deps tree diverged from canonical + run: | + if ! git diff --exit-code wit/shepherd-cow/deps/nexum-runtime; then + echo "::error::wit/shepherd-cow/deps/nexum-runtime is out of sync with wit/nexum-runtime." + echo "::error::Run 'just sync-wit' locally and commit the result." + exit 1 + fi From 1ee9bfbe46096141b04a61ae3629cb9f3246bd89 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 00:52:36 +0000 Subject: [PATCH 09/17] docs/01: align identity::sign to personal_sign semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shipped wit/nexum-runtime/identity.wit defines sign() as personal_sign — host MUST prepend the EIP-191 prefix ('\x19Ethereum Signed Message:\n') before hashing. docs/01 described it as 'sign raw bytes' which is a transaction-signing footgun (a raw signer can be tricked into signing EIP-155 / EIP-712 payloads disguised as plain bytes) AND diverges from the WIT — two compliant hosts implementing different specs produce mutually unverifiable signatures. Align docs/01 to the WIT. Note that raw-bytes signing, gated by an explicit capability, is on the 0.3 roadmap. Resolves PR #6 finding #1. --- docs/01-runtime-environment.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/01-runtime-environment.md b/docs/01-runtime-environment.md index ad66765..fa1af1d 100755 --- a/docs/01-runtime-environment.md +++ b/docs/01-runtime-environment.md @@ -215,10 +215,17 @@ interface identity { /// Get available signing accounts (20-byte Ethereum addresses). accounts: func() -> result>, host-error>; - /// Sign raw bytes with the specified account. + /// 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 + /// EIP-155 transactions or EIP-712 payloads disguised as plain bytes. + /// /// Returns a 65-byte ECDSA secp256k1 signature (r || s || v). - /// Extensible to other signing schemes in future versions. - sign: func(account: list, data: list) -> result, host-error>; + /// + /// A separate raw-bytes signing primitive, gated by an explicit + /// capability, is on the 0.3 roadmap. + sign: func(account: list, message: list) -> result, host-error>; /// Sign EIP-712 typed data with the specified account. /// `typed-data` is the JSON-encoded EIP-712 TypedData structure. @@ -305,7 +312,7 @@ world shepherd { - **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 raw signing operations (sign arbitrary messages, get accounts). +- **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). From c8e03d525435f19fa822901787b99b9d07d8a74d Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 00:53:48 +0000 Subject: [PATCH 10/17] docs: align chain::request-batch and http WIT snippets to shipped types Both snippets in the migration guide and docs/01 showed list> shapes that don't match the shipped WIT. Adopters who copy the doc snippets get wit-bindgen type errors against the real interfaces. - chain::request-batch now uses the nominal record rpc-request and variant rpc-result (matching wit/nexum-runtime/chain.wit). - http now uses the nominal record header and adds the timeout-ms field (matching wit/nexum-runtime/http.wit), plus *.domain wildcard syntax in the allowlist example and the docstring on how non-2xx surfaces. Resolves PR #6 findings #2 and #3. --- docs/01-runtime-environment.md | 21 ++++++++++++--- docs/migration/0.1-to-0.2.md | 48 +++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/docs/01-runtime-environment.md b/docs/01-runtime-environment.md index fa1af1d..5da457a 100755 --- a/docs/01-runtime-environment.md +++ b/docs/01-runtime-environment.md @@ -202,11 +202,26 @@ interface chain { request: func(chain-id: chain-id, method: string, params: string) -> result; + /// A single JSON-RPC request to be executed as part of a batch. + record rpc-request { + method: string, + params: string, + } + + /// Result of a single request inside a batch. Each entry is independent; + /// one failing call does not abort the others. + variant rpc-result { + ok(string), + err(host-error), + } + /// Additive 0.2 method: batched JSON-RPC. The alloy-backed HostTransport /// routes RequestPacket::Batch through this — `provider.multicall(...)` - /// actually batches on the wire in 0.2. - request-batch: func(chain-id: chain-id, calls: list>) - -> result>, host-error>; + /// 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. + request-batch: func(chain-id: chain-id, requests: list) + -> result, host-error>; } interface identity { diff --git a/docs/migration/0.1-to-0.2.md b/docs/migration/0.1-to-0.2.md index 6bfe8ac..67f92fb 100644 --- a/docs/migration/0.1-to-0.2.md +++ b/docs/migration/0.1-to-0.2.md @@ -327,17 +327,35 @@ CSPRNG. Replaces the 0.1 workaround of "you can't, period." ```wit interface http { + use types.{host-error}; + + /// A single HTTP header. Header names are case-insensitive on the wire; + /// the host normalises them. + record header { + name: string, + value: string, + } + record request { method: string, url: string, - headers: list>, + headers: list
, body: option>, + /// Optional per-request timeout in milliseconds. The host MAY clamp + /// this to its own configured maximum. + timeout-ms: option, } + record response { status: u16, - headers: list>, + headers: list
, body: list, } + + /// Perform a single HTTP request. Transport-level failures (DNS, TLS, + /// timeout, host policy rejection) surface as `host-error`; HTTP-level + /// non-2xx responses are returned as `ok(response)` so the caller can + /// inspect headers and body. fetch: func(req: request) -> result; } ``` @@ -346,20 +364,38 @@ Requires a domain allowlist in `nexum.toml`: ```toml [capabilities.http] -allow = ["api.coingecko.com", "discord.com"] +allow = ["api.coingecko.com", "*.discord.com"] ``` -Hosts MUST enforce the allowlist. The operator sees the union of granted domains at module load. This replaces the 0.1 anti-pattern of tunnelling alerts through Waku. +Hosts MUST enforce the allowlist (exact host match or `*.domain` suffix). The operator sees the union of granted domains at module load. This replaces the 0.1 anti-pattern of tunnelling alerts through Waku. ### `chain::request-batch` ```wit interface chain { + use types.{chain-id, host-error}; + + /// A single JSON-RPC request to be executed as part of a batch. + record rpc-request { + method: string, + params: string, + } + + /// Result of a single request inside a batch. Each entry is independent; + /// one failing call does not abort the others. + variant rpc-result { + ok(string), + err(host-error), + } + request: func(chain-id: chain-id, method: string, params: string) -> result; - request-batch: func(chain-id: chain-id, calls: list>) - -> result>, host-error>; + /// 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. + request-batch: func(chain-id: chain-id, requests: list) + -> result, host-error>; } ``` From f69ccb959fc38318de05ce24b74aa32672671021 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 00:55:45 +0000 Subject: [PATCH 11/17] docs: defer typed config-value variant to 0.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shipped wit/nexum-runtime/types.wit ships 0.1's stringly-typed `type config = list>`, but five doc locations plus the migration TL;DR promised a typed `config-value` variant for 0.2 (string/integer/boolean/list). Per architectural triage, defer the typed variant to 0.3 alongside the manifest parser work — the typing story lands as one coherent feature. Updates: docs/00, docs/01, docs/02 (two places), docs/08, migration guide TL;DR row + 'Typed config' subsection. Resolves PR #6 finding #4. --- docs/01-runtime-environment.md | 14 +++------ docs/02-modules-events-packaging.md | 15 ++++------ docs/08-platform-generalisation.md | 10 ++----- docs/migration/0.1-to-0.2.md | 46 +++++++---------------------- 4 files changed, 21 insertions(+), 64 deletions(-) diff --git a/docs/01-runtime-environment.md b/docs/01-runtime-environment.md index 5da457a..2f397f4 100755 --- a/docs/01-runtime-environment.md +++ b/docs/01-runtime-environment.md @@ -146,16 +146,10 @@ interface types { message(message), } - /// Typed config from nexum.toml [config] section. - /// Each entry pairs a key with a typed value (string/integer/boolean/list). - type config = list>; - - variant config-value { - string(string), - integer(s64), - boolean(bool), - list(list), - } + /// Opaque config from nexum.toml [config] section. All TOML scalars are + /// flattened to their string form by the host. A typed `config-value` + /// variant is on the 0.3 roadmap, bundled with the manifest parser work. + type config = list>; /// Unified error type returned by every host function in 0.2. record host-error { diff --git a/docs/02-modules-events-packaging.md b/docs/02-modules-events-packaging.md index bffdff2..1b68707 100755 --- a/docs/02-modules-events-packaging.md +++ b/docs/02-modules-events-packaging.md @@ -72,7 +72,7 @@ Key design points: - **`[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). -- **`config`** is opaque to the runtime semantically but **typed on the wire in 0.2**. The 0.1 WIT flattened every value to a string (`list>`); 0.2 uses `list>` where `config-value` is a variant carrying string / integer / boolean / list. The SDK's `Config::from_host` or `#[derive(NexumConfig)]` give typed accessors. +- **`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 @@ -332,15 +332,10 @@ interface types { message(message), } - /// Typed config map from nexum.toml [config] section. - type config = list>; - - variant config-value { - string(string), - integer(s64), - boolean(bool), - list(list), - } + /// Opaque config from nexum.toml [config] section. TOML scalars are + /// flattened to strings by the host. A typed config-value variant is + /// on the 0.3 roadmap, bundled with the manifest-parser work. + type config = list>; /// Unified host error (replaces the five per-protocol errors from 0.1). record host-error { diff --git a/docs/08-platform-generalisation.md b/docs/08-platform-generalisation.md index e57f656..ac680de 100755 --- a/docs/08-platform-generalisation.md +++ b/docs/08-platform-generalisation.md @@ -408,14 +408,8 @@ interface types { message(message), } - type config = list>; - - variant config-value { - string(string), - integer(s64), - boolean(bool), - list(list), - } + /// Opaque config (typed variant deferred to 0.3). + type config = list>; record host-error { domain: string, diff --git a/docs/migration/0.1-to-0.2.md b/docs/migration/0.1-to-0.2.md index 67f92fb..af22516 100644 --- a/docs/migration/0.1-to-0.2.md +++ b/docs/migration/0.1-to-0.2.md @@ -29,7 +29,7 @@ Each section is tagged `[author]`, `[embedder]`, or `[both]`. | Manifest file | `nexum.toml` (some docs said `shepherd.toml`) | `nexum.toml` (canonical) | | Manifest field | `wasm = "sha256:..."` | `component = "sha256:..."` | | Manifest section | `[[subscribe]]` | `[[subscription]]` | -| Config type | `list>` (stringified) | `list>` (typed variant) | +| 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) | @@ -256,46 +256,20 @@ 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. -### Typed config +### Config: unchanged in 0.2 -`[config]` values are no longer flattened to strings: - -```toml -# 0.1: every value became a string at the guest -[config] -cow_api_url = "https://api.cow.fi/arbitrum" -slippage_bps = 50 -enable_alerts = true - -# 0.2: TOML types are preserved through the typed config variant -[config] -cow_api_url = "https://api.cow.fi/arbitrum" # string -slippage_bps = 50 # integer -enable_alerts = true # boolean -allow_list = ["arb1", "base"] # list of string -``` - -If you currently parse `"50"` into `u64`, that code becomes: - -```diff -- let bps: u64 = config.get("slippage_bps")?.parse()?; -+ let bps: u64 = config.get_int("slippage_bps")?; // typed accessor -``` - -Or with the derive macro: +`[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 -#[derive(NexumConfig)] -struct Config { - cow_api_url: String, - slippage_bps: u64, - enable_alerts: bool, - allow_list: Vec, -} - -let config: Config = Config::from_host(raw_config)?; +let bps: u64 = config.iter() + .find(|(k, _)| k == "slippage_bps") + .map(|(_, v)| v.parse()) + .transpose()? + .unwrap_or(50); ``` +**Deferred to 0.3.** A typed `config-value` variant (string / integer / boolean / list) and a `#[derive(NexumConfig)]` helper are on the 0.3 roadmap, bundled with the manifest-parser work (see §3) so the typing story lands as one coherent feature. + --- ## 4. New capabilities (additive) [author] From ad85a0250d02f387be9c54f62d6d8e06c8737cce Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 00:56:33 +0000 Subject: [PATCH 12/17] docs/migration: replace cargo-nexum vapor with real commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §9 verification checklist referenced 'cargo nexum check' and 'cargo nexum run --mock'; §11 promised 'cargo nexum migrate --from 0.1' as shipping with 0.2. None of these exist — there is no cargo-nexum crate in the workspace. Rewrite §9 to use real `cargo` + `just` invocations. Drop the §11 codemod promise; the sed cheat sheet in §8 already does the mechanical work. Add a clear 'no cargo-nexum toolchain in 0.2; coming in 0.3' note so adopters set the right expectation. Resolves PR #6 finding #7. --- docs/migration/0.1-to-0.2.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/migration/0.1-to-0.2.md b/docs/migration/0.1-to-0.2.md index af22516..cbfb789 100644 --- a/docs/migration/0.1-to-0.2.md +++ b/docs/migration/0.1-to-0.2.md @@ -502,7 +502,10 @@ Things that **cannot** be sedded — do these by hand: After running the renames: -- [ ] `cargo nexum check` (new in 0.2) reports no warnings against the 0.2 WIT. +- [ ] `cargo check --workspace --all-targets` is clean (Rust + bindings). +- [ ] `cargo check --target wasm32-wasip2 -p ` is clean. +- [ ] `cargo test --workspace --no-fail-fast` passes. +- [ ] `just sync-wit && git diff --exit-code wit/` is clean if you vendor the runtime WIT under your own `deps/` tree. - [ ] `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`). @@ -511,7 +514,8 @@ After running the renames: - [ ] Error matching code uses `HostErrorKind` discriminant, not protocol-specific error codes. - [ ] If you used `chrono`/timestamp arithmetic, audited for the seconds-vs-ms change (0.2 is always ms UTC). - [ ] If you used `provider.multicall(...).await`, confirmed it now actually batches on the wire (`chain::request-batch` shows in tracing). -- [ ] Tests pass under `cargo nexum run --mock`. + +> **No `cargo nexum` toolchain in 0.2.** A `cargo-nexum` cargo subcommand (with `new`, `check`, `package`, `run --mock`, `migrate`) is on the 0.3 roadmap. Until then, use `cargo` directly and the `just` recipes in the reference repo. --- @@ -533,5 +537,5 @@ The mobile/wallet host story (`query-module` production support, C ABI, `nexum-h ## 11. Getting help - Open an issue at the repo with the `migration-0.2` label. -- The 0.1 → 0.2 codemod (a Rust binary that does the safe mechanical renames in §8 for you) ships in the 0.2 release as `cargo nexum migrate --from 0.1`. - The full 0.2 WIT lives in `wit/nexum-runtime/` (formerly `wit/web3-runtime/`). +- The §8 cheat sheet has the mechanical sed commands; a `cargo nexum migrate --from 0.1` codemod that wraps them safely is planned for 0.3 alongside the rest of the `cargo-nexum` toolchain. From a9d5e7878543e1ec1cf4c136ec4b58c2f46a0629 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 01:01:40 +0000 Subject: [PATCH 13/17] wit+engine: import clock/random/http in event-module; add host impls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new 0.2 capability interfaces (clock, random, http) shipped as standalone WIT files but were not imported by any world, so modules built against event-module / shepherd could not 'use' them. Their existence was advertised in the migration guide but was structurally unreachable. - event-module.wit: add 'import clock', 'import random', 'import http' (grouped as ambient host services after the six core primitives). shepherd world inherits via 'include event-module'. query-module remains intentionally sandboxed (no caps, no chain, no network). - Engine: add host impls for clock (SystemTime + monotonic_baseline: Instant), random (getrandom 0.4 fill), http (returns Unsupported for 0.2; real fetch is 0.3 — allowlist enforcement lands with #6). - Cargo.toml: add getrandom 0.4, plus serde 1 + toml 1 (for #6). Resolves PR #6 finding #5. --- crates/nexum-engine/Cargo.toml | 3 ++ crates/nexum-engine/src/main.rs | 53 ++++++++++++++++++- wit/nexum-runtime/event-module.wit | 9 +++- .../deps/nexum-runtime/event-module.wit | 9 +++- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/crates/nexum-engine/Cargo.toml b/crates/nexum-engine/Cargo.toml index 8e67c0d..65768c3 100644 --- a/crates/nexum-engine/Cargo.toml +++ b/crates/nexum-engine/Cargo.toml @@ -10,3 +10,6 @@ wasmtime = { version = "45", features = ["component-model"] } wasmtime-wasi = "45" anyhow = "1" tokio = { version = "1", features = ["full"] } +getrandom = "0.4" +serde = { version = "1", features = ["derive"] } +toml = "1" diff --git a/crates/nexum-engine/src/main.rs b/crates/nexum-engine/src/main.rs index cc7b689..1bb6b55 100644 --- a/crates/nexum-engine/src/main.rs +++ b/crates/nexum-engine/src/main.rs @@ -1,4 +1,4 @@ -use std::time::Instant; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; use wasmtime::component::{Component, Linker, ResourceTable}; use wasmtime::error::Context as _; use wasmtime::{Engine, Store}; @@ -16,6 +16,9 @@ use nexum::runtime::types::HostErrorKind; struct HostState { wasi: WasiCtx, table: ResourceTable, + /// Origin for `clock::monotonic-ns`. Differences between successive + /// readings are the only meaningful values. + monotonic_baseline: Instant, } impl WasiView for HostState { @@ -259,6 +262,53 @@ impl nexum::runtime::logging::Host for HostState { } } +// -- Additive 0.2 capabilities -- + +impl nexum::runtime::clock::Host for HostState { + async fn now_ms(&mut self) -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) + } + + async fn monotonic_ns(&mut self) -> u64 { + self.monotonic_baseline.elapsed().as_nanos() as u64 + } +} + +impl nexum::runtime::random::Host for HostState { + async fn fill(&mut self, len: u32) -> Vec { + let mut buf = vec![0u8; len as usize]; + // getrandom 0.4: fill() returns Result<(), Error>. CSPRNG failures + // are exceptionally rare on supported platforms; on failure we + // return zero-filled bytes — guests that need a strong-failure + // signal should use identity or chain primitives instead. + let _ = getrandom::fill(&mut buf); + buf + } +} + +impl nexum::runtime::http::Host for HostState { + async fn fetch( + &mut self, + req: nexum::runtime::http::Request, + ) -> Result { + let start = Instant::now(); + eprintln!("[http] {} {}", req.method, req.url); + // 0.2: reference runtime does not perform real HTTP yet. The + // per-module `[capabilities.http].allow` allowlist check is wired + // in the manifest-enforcement layer (fix #6) and runs before this + // method returns. Real fetch lands in 0.3. + let result = Err(unimplemented( + "http", + "fetch not implemented in 0.2 reference runtime", + )); + eprintln!("[timing] http::fetch: {:?}", start.elapsed()); + result + } +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let wasm_path = std::env::args() @@ -290,6 +340,7 @@ async fn main() -> anyhow::Result<()> { HostState { wasi, table: ResourceTable::new(), + monotonic_baseline: Instant::now(), }, ); diff --git a/wit/nexum-runtime/event-module.wit b/wit/nexum-runtime/event-module.wit index 0a69131..848fce3 100644 --- a/wit/nexum-runtime/event-module.wit +++ b/wit/nexum-runtime/event-module.wit @@ -5,13 +5,20 @@ package nexum:runtime@0.2.0; world event-module { use types.{config, event, host-error}; + // Six core primitives (always provided by a conforming host). import chain; + import identity; import local-store; import remote-store; import messaging; - import identity; import logging; + // Ambient host services (additive in 0.2). `http` is gated per-module by + // the `[capabilities.http].allow` allowlist in nexum.toml. + import clock; + import random; + import http; + export init: func(config: config) -> result<_, host-error>; export on-event: func(event: event) -> result<_, host-error>; } diff --git a/wit/shepherd-cow/deps/nexum-runtime/event-module.wit b/wit/shepherd-cow/deps/nexum-runtime/event-module.wit index 0a69131..848fce3 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/event-module.wit +++ b/wit/shepherd-cow/deps/nexum-runtime/event-module.wit @@ -5,13 +5,20 @@ package nexum:runtime@0.2.0; world event-module { use types.{config, event, host-error}; + // Six core primitives (always provided by a conforming host). import chain; + import identity; import local-store; import remote-store; import messaging; - import identity; import logging; + // Ambient host services (additive in 0.2). `http` is gated per-module by + // the `[capabilities.http].allow` allowlist in nexum.toml. + import clock; + import random; + import http; + export init: func(config: config) -> result<_, host-error>; export on-event: func(event: event) -> result<_, host-error>; } From 816c22949cd6b13b2aefcbcfe70d1c1d60a1b417 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 01:13:01 +0000 Subject: [PATCH 14/17] engine: minimal nexum.toml manifest parser with [capabilities] enforcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per architectural triage, ship minimal manifest enforcement in 0.2 and defer optional-capability trap stubs to 0.3. The migration guide §3 promised four mechanisms; this commit lands the two security-critical ones (required-capability check + http allowlist enforcement) plus the deprecation warning, and explicitly defers per-import trap stubs. Manifest schema (parsed in crates/nexum-engine/src/manifest.rs): [module] name, version, component (sha256; parsed, not yet verified) [capabilities] required (validated against KNOWN_CAPABILITIES; engine rejects unknown names) optional (parsed + logged; trap-stub fallback is 0.3) [capabilities.http] allow (exact host or *.suffix wildcard) [config] TOML scalars flattened to strings (typed variant on 0.3 roadmap) CLI: nexum-engine []. If the second arg is omitted, the engine looks for nexum.toml next to the .wasm. If neither exists, it emits the deprecation warning and proceeds with empty config and empty allowlist (= effectively denies all HTTP). http::fetch now performs the per-module allowlist check (host extracted via a stdlib URL parser, exact or *.suffix match). Allowlist denial returns HostError { kind: Denied }; real network fetch is still 0.3. Includes unit tests for extract_host and host_allowed. Resolves PR #6 finding #6. --- crates/nexum-engine/src/main.rs | 80 ++++++++- crates/nexum-engine/src/manifest.rs | 263 ++++++++++++++++++++++++++++ justfile | 6 +- modules/example/nexum.toml | 27 +++ 4 files changed, 365 insertions(+), 11 deletions(-) create mode 100644 crates/nexum-engine/src/manifest.rs create mode 100644 modules/example/nexum.toml diff --git a/crates/nexum-engine/src/main.rs b/crates/nexum-engine/src/main.rs index 1bb6b55..a0ec525 100644 --- a/crates/nexum-engine/src/main.rs +++ b/crates/nexum-engine/src/main.rs @@ -1,3 +1,6 @@ +mod manifest; + +use std::path::PathBuf; use std::time::{Instant, SystemTime, UNIX_EPOCH}; use wasmtime::component::{Component, Linker, ResourceTable}; use wasmtime::error::Context as _; @@ -19,6 +22,9 @@ struct HostState { /// Origin for `clock::monotonic-ns`. Differences between successive /// readings are the only meaningful values. monotonic_baseline: Instant, + /// Per-module `[capabilities.http].allow` allowlist (from nexum.toml). + /// Consulted by `http::fetch` before any outbound call. + http_allowlist: Vec, } impl WasiView for HostState { @@ -296,13 +302,43 @@ impl nexum::runtime::http::Host for HostState { ) -> Result { let start = Instant::now(); eprintln!("[http] {} {}", req.method, req.url); - // 0.2: reference runtime does not perform real HTTP yet. The - // per-module `[capabilities.http].allow` allowlist check is wired - // in the manifest-enforcement layer (fix #6) and runs before this - // method returns. Real fetch lands in 0.3. + + // Manifest allowlist enforcement runs before any I/O. Hosts that + // never link a manifest leave `http_allowlist` empty, which denies + // every request — matching the "no implicit network" stance. + let host = match manifest::extract_host(&req.url) { + Some(h) => h, + None => { + eprintln!("[timing] http::fetch: {:?}", start.elapsed()); + return Err(HostError { + domain: "http".into(), + kind: HostErrorKind::InvalidInput, + code: 0, + message: format!("not an http(s) URL: {}", req.url), + data: None, + }); + } + }; + if !manifest::host_allowed(host, &self.http_allowlist) { + eprintln!("[http] denied by allowlist: {host}"); + eprintln!("[timing] http::fetch: {:?}", start.elapsed()); + return Err(HostError { + domain: "http".into(), + kind: HostErrorKind::Denied, + code: 0, + message: format!( + "host {host} not in [capabilities.http].allow; \ + add it to nexum.toml to permit" + ), + data: None, + }); + } + + // 0.2: allowlist passed, but the reference runtime does not perform + // real HTTP yet. Real fetch lands in 0.3. let result = Err(unimplemented( "http", - "fetch not implemented in 0.2 reference runtime", + "fetch not implemented in 0.2 reference runtime (allowlist passed)", )); eprintln!("[timing] http::fetch: {:?}", start.elapsed()); result @@ -311,12 +347,30 @@ impl nexum::runtime::http::Host for HostState { #[tokio::main] async fn main() -> anyhow::Result<()> { - let wasm_path = std::env::args() - .nth(1) - .ok_or_else(|| anyhow::anyhow!("usage: nexum-engine "))?; + let mut args = std::env::args().skip(1); + let wasm_path = args.next().ok_or_else(|| { + anyhow::anyhow!("usage: nexum-engine []") + })?; + let explicit_manifest = args.next().map(PathBuf::from); println!("nexum-engine: loading component from {wasm_path}"); + // Load the manifest from the explicit path if given, otherwise from + // `nexum.toml` next to the component file. Missing → fallback (with + // deprecation warning). + let manifest_path = explicit_manifest.or_else(|| { + PathBuf::from(&wasm_path) + .parent() + .map(|p| p.join("nexum.toml")) + }); + let loaded = match manifest_path.as_deref() { + Some(p) if p.exists() => { + println!("nexum-engine: loading manifest from {}", p.display()); + manifest::load(p)? + } + _ => manifest::fallback_manifest(), + }; + let mut config = wasmtime::Config::new(); config.wasm_component_model(true); let engine = Engine::new(&config)?; @@ -341,6 +395,7 @@ async fn main() -> anyhow::Result<()> { wasi, table: ResourceTable::new(), monotonic_baseline: Instant::now(), + http_allowlist: loaded.http_allowlist, }, ); @@ -351,7 +406,14 @@ async fn main() -> anyhow::Result<()> { eprintln!("[timing] component instantiate: {:?}", start.elapsed()); println!("nexum-engine: calling init..."); - let config_entries: Config = vec![("name".into(), "example".into())]; + // 0.2: [config] is stringly-typed (typed variant deferred to 0.3). + // Fall back to a single ("name", "") pair if the manifest has + // no [config] section so the example module still has something to log. + let config_entries: Config = if loaded.config.is_empty() { + vec![("name".into(), loaded.manifest.module.name.clone())] + } else { + loaded.config + }; let start = Instant::now(); match bindings.call_init(&mut store, &config_entries).await? { Ok(()) => println!("nexum-engine: init succeeded"), diff --git a/crates/nexum-engine/src/manifest.rs b/crates/nexum-engine/src/manifest.rs new file mode 100644 index 0000000..8525de5 --- /dev/null +++ b/crates/nexum-engine/src/manifest.rs @@ -0,0 +1,263 @@ +//! Minimal `nexum.toml` parser and capability-enforcement helpers (0.2 scope). +//! +//! 0.2 intentionally ships a slim subset of the manifest spec described in +//! the migration guide §3: +//! +//! - `[capabilities].required` is parsed and validated (names must be in +//! the known capability set; the 0.2 reference engine always provides +//! all of them, so this is a sanity check + future-proofing). +//! - `[capabilities].optional` is parsed and logged; trap-stub fallback +//! for absent optionals is deferred to 0.3. +//! - `[capabilities.http].allow` is parsed and consulted by the `http` +//! host impl before any outbound call. +//! - `[config]` is flattened to `Vec<(String, String)>` and passed to the +//! module's `init`. Typed `config-value` variant is deferred to 0.3. +//! +//! When the manifest file is missing or has no `[capabilities]` section, +//! a deprecation warning is emitted on stderr and the engine falls back +//! to 0.1 behaviour (treat every linked capability as required). This +//! fallback will be removed in 0.3. + +use std::collections::HashSet; +use std::path::Path; + +use serde::Deserialize; + +/// Capability names recognised by the 0.2 reference engine. Matches the +/// interfaces the `shepherd` world links into the linker. +pub const KNOWN_CAPABILITIES: &[&str] = &[ + "chain", + "identity", + "local-store", + "remote-store", + "messaging", + "logging", + "clock", + "random", + "http", + // Domain-extension caps (provided by the shepherd world only): + "cow-api", +]; + +#[derive(Debug, Deserialize, Default)] +pub struct Manifest { + #[serde(default)] + pub module: ModuleSection, + #[serde(default)] + pub capabilities: Option, + #[serde(default)] + pub config: toml::Table, +} + +#[derive(Debug, Deserialize, Default)] +#[allow(dead_code)] // version + component parsed for future 0.3 hash-verification. +pub struct ModuleSection { + #[serde(default)] + pub name: String, + #[serde(default)] + pub version: String, + #[serde(default)] + pub component: String, +} + +#[derive(Debug, Deserialize, Default)] +pub struct CapabilitiesSection { + #[serde(default)] + pub required: Vec, + #[serde(default)] + pub optional: Vec, + #[serde(default)] + pub http: Option, +} + +#[derive(Debug, Deserialize, Default)] +pub struct HttpSection { + #[serde(default)] + pub allow: Vec, +} + +/// Errors returned while loading or validating a manifest. +#[derive(Debug)] +pub enum ParseError { + Io(std::io::Error), + Toml(toml::de::Error), + UnknownCapability(String), +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(e) => write!(f, "manifest: i/o: {e}"), + Self::Toml(e) => write!(f, "manifest: parse: {e}"), + Self::UnknownCapability(name) => write!( + f, + "manifest: unknown capability {:?} in [capabilities].required (known: {})", + name, + KNOWN_CAPABILITIES.join(", ") + ), + } + } +} + +impl std::error::Error for ParseError {} + +/// Loaded + validated manifest, plus its source path for diagnostics. +pub struct LoadedManifest { + pub manifest: Manifest, + /// Hosts to allow for `http::fetch`. Each entry is either an exact + /// hostname or a `*.suffix` wildcard. + pub http_allowlist: Vec, + /// `[config]` flattened to `(key, stringified-value)` pairs ready to + /// hand to a module's `init`. TOML scalars (string, integer, float, + /// boolean) become their text form. Arrays and tables are rendered as + /// their TOML representation. + pub config: Vec<(String, String)>, +} + +/// Read `nexum.toml` from `path`, parse, validate, and emit a deprecation +/// warning if `[capabilities]` is absent (0.1-compat fallback). +pub fn load(path: &Path) -> Result { + let raw = std::fs::read_to_string(path).map_err(ParseError::Io)?; + let manifest: Manifest = toml::from_str(&raw).map_err(ParseError::Toml)?; + + let caps = manifest.capabilities.as_ref(); + if caps.is_none() { + eprintln!( + "[deprecation] no [capabilities] section in nexum.toml — \ + defaulting to all-required (0.1 behaviour). This default \ + will be removed in 0.3; add an explicit [capabilities] block \ + now." + ); + } + + if let Some(c) = caps { + let known: HashSet<&str> = KNOWN_CAPABILITIES.iter().copied().collect(); + for name in c.required.iter().chain(c.optional.iter()) { + if !known.contains(name.as_str()) { + return Err(ParseError::UnknownCapability(name.clone())); + } + } + if !c.required.is_empty() { + eprintln!("[manifest] required capabilities: {}", c.required.join(", ")); + } + if !c.optional.is_empty() { + eprintln!( + "[manifest] optional capabilities (advisory in 0.2; trap-stub fallback \ + ships in 0.3): {}", + c.optional.join(", ") + ); + } + } + + let http_allowlist = caps + .and_then(|c| c.http.as_ref()) + .map(|h| h.allow.clone()) + .unwrap_or_default(); + if !http_allowlist.is_empty() { + eprintln!( + "[manifest] http allowlist: {}", + http_allowlist.join(", ") + ); + } + + let config = manifest + .config + .iter() + .map(|(k, v)| (k.clone(), stringify_toml_value(v))) + .collect(); + + Ok(LoadedManifest { + manifest, + http_allowlist, + config, + }) +} + +/// Synthesise a "0.1 fallback" manifest for when no `nexum.toml` is found. +/// Emits the same deprecation warning as a missing-section manifest. +pub fn fallback_manifest() -> LoadedManifest { + eprintln!( + "[deprecation] no nexum.toml found — defaulting to all-required \ + (0.1 behaviour). This default will be removed in 0.3; ship a \ + nexum.toml alongside your component." + ); + LoadedManifest { + manifest: Manifest::default(), + http_allowlist: Vec::new(), + config: Vec::new(), + } +} + +/// Check whether `host` matches any pattern in the allowlist. Patterns are +/// either exact (`api.example.com`) or `*.suffix` wildcards which match +/// any subdomain of `suffix` (but not `suffix` itself). +pub fn host_allowed(host: &str, allowlist: &[String]) -> bool { + let host = host.to_ascii_lowercase(); + allowlist.iter().any(|pat| { + let pat = pat.to_ascii_lowercase(); + if let Some(suffix) = pat.strip_prefix("*.") { + host.ends_with(&format!(".{suffix}")) + } else { + host == pat + } + }) +} + +/// Extract the host component from a URL. Returns `None` for non-http(s) +/// schemes or malformed input. Intentionally simple — adds no `url` +/// crate dependency. +pub fn extract_host(url: &str) -> Option<&str> { + let after_scheme = url + .strip_prefix("https://") + .or_else(|| url.strip_prefix("http://"))?; + let host_end = after_scheme + .find('/') + .or_else(|| after_scheme.find('?')) + .unwrap_or(after_scheme.len()); + let host = &after_scheme[..host_end]; + // strip optional user-info and port. + let host = host.rsplit('@').next().unwrap_or(host); + let host = host.split(':').next().unwrap_or(host); + if host.is_empty() { + None + } else { + Some(host) + } +} + +fn stringify_toml_value(v: &toml::Value) -> String { + match v { + toml::Value::String(s) => s.clone(), + toml::Value::Integer(i) => i.to_string(), + toml::Value::Float(f) => f.to_string(), + toml::Value::Boolean(b) => b.to_string(), + toml::Value::Datetime(d) => d.to_string(), + toml::Value::Array(_) | toml::Value::Table(_) => v.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_host_handles_common_shapes() { + assert_eq!(extract_host("https://api.example.com/v1/x"), Some("api.example.com")); + assert_eq!(extract_host("http://example.com"), Some("example.com")); + assert_eq!(extract_host("https://user:pw@host.example.com:8443/x"), Some("host.example.com")); + assert_eq!(extract_host("https://example.com?q=1"), Some("example.com")); + assert_eq!(extract_host("ftp://example.com"), None); + assert_eq!(extract_host("not a url"), None); + } + + #[test] + fn host_allowed_exact_and_wildcard() { + let allow = vec!["api.cow.fi".to_string(), "*.discord.com".to_string()]; + assert!(host_allowed("api.cow.fi", &allow)); + assert!(!host_allowed("evil.api.cow.fi", &allow)); + assert!(host_allowed("foo.discord.com", &allow)); + assert!(host_allowed("a.b.discord.com", &allow)); + assert!(!host_allowed("discord.com", &allow)); + assert!(!host_allowed("nope.example", &allow)); + } +} diff --git a/justfile b/justfile index cc11906..0d6d4e1 100644 --- a/justfile +++ b/justfile @@ -14,9 +14,11 @@ build-module: # Build everything build: build-runtime build-module -# Build the module then run the runtime with it +# Build the module then run the runtime with it. The second argument is the +# module's nexum.toml — without it the engine prints the 0.1-compat +# deprecation warning and proceeds with empty capabilities/config. run: build-module build-runtime - cargo run -p nexum-engine -- target/wasm32-wasip2/release/example.wasm + cargo run -p nexum-engine -- target/wasm32-wasip2/release/example.wasm modules/example/nexum.toml # Check the entire workspace check: sync-wit diff --git a/modules/example/nexum.toml b/modules/example/nexum.toml new file mode 100644 index 0000000..e17a547 --- /dev/null +++ b/modules/example/nexum.toml @@ -0,0 +1,27 @@ +# Example module manifest — exercises the 0.2 manifest schema end-to-end. + +[module] +name = "example" +version = "0.1.0" +# Placeholder content hash. 0.2 parses but does not verify this; 0.3 will +# compare it against the sha256 of the loaded component bytes. +component = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + +[capabilities] +# 0.2 reference engine provides all listed capabilities; this list is a +# sanity check + future-proofing. +required = ["logging"] + +# Capabilities the module would use opportunistically. In 0.2 these are +# parsed and logged; trap-stub fallback for absent optionals ships in 0.3. +optional = [] + +[capabilities.http] +# Per-module HTTP allowlist. Empty list = no outbound HTTP permitted. +# Entries are exact hostnames or *.domain wildcards. +allow = [] + +[config] +# Stringly-typed in 0.2 (typed variant on 0.3 roadmap). Numbers and +# booleans are flattened to their text form by the host on the way through. +name = "example" From c1cdb68029439fd1725ce1cce7ec17a8f914eaf3 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 06:24:54 +0000 Subject: [PATCH 15/17] style: cargo fmt (nightly) CI rustfmt (nightly) collapses several multi-line signatures that the hand-formatted code had as multi-line. --- crates/nexum-engine/src/main.rs | 17 +++-------------- crates/nexum-engine/src/manifest.rs | 26 ++++++++++++++------------ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/crates/nexum-engine/src/main.rs b/crates/nexum-engine/src/main.rs index a0ec525..3addb06 100644 --- a/crates/nexum-engine/src/main.rs +++ b/crates/nexum-engine/src/main.rs @@ -144,10 +144,7 @@ impl nexum::runtime::identity::Host for HostState { ) -> Result, HostError> { let start = Instant::now(); eprintln!("[identity] sign-typed-data"); - let result = Err(unimplemented( - "identity", - "sign-typed-data not implemented", - )); + let result = Err(unimplemented("identity", "sign-typed-data not implemented")); eprintln!("[timing] identity::sign-typed-data: {:?}", start.elapsed()); result } @@ -213,11 +210,7 @@ impl nexum::runtime::remote_store::Host for HostState { result } - async fn write_feed( - &mut self, - _topic: Vec, - _data: Vec, - ) -> Result, HostError> { + async fn write_feed(&mut self, _topic: Vec, _data: Vec) -> Result, HostError> { let start = Instant::now(); let result = Err(unimplemented("remote-store", "write-feed not implemented")); eprintln!("[timing] remote-store::write-feed: {:?}", start.elapsed()); @@ -226,11 +219,7 @@ impl nexum::runtime::remote_store::Host for HostState { } impl nexum::runtime::messaging::Host for HostState { - async fn publish( - &mut self, - content_topic: String, - _payload: Vec, - ) -> Result<(), HostError> { + async fn publish(&mut self, content_topic: String, _payload: Vec) -> Result<(), HostError> { let start = Instant::now(); eprintln!("[messaging] publish: {content_topic}"); let result = Err(unimplemented("messaging", "publish not implemented")); diff --git a/crates/nexum-engine/src/manifest.rs b/crates/nexum-engine/src/manifest.rs index 8525de5..522e168 100644 --- a/crates/nexum-engine/src/manifest.rs +++ b/crates/nexum-engine/src/manifest.rs @@ -138,7 +138,10 @@ pub fn load(path: &Path) -> Result { } } if !c.required.is_empty() { - eprintln!("[manifest] required capabilities: {}", c.required.join(", ")); + eprintln!( + "[manifest] required capabilities: {}", + c.required.join(", ") + ); } if !c.optional.is_empty() { eprintln!( @@ -154,10 +157,7 @@ pub fn load(path: &Path) -> Result { .map(|h| h.allow.clone()) .unwrap_or_default(); if !http_allowlist.is_empty() { - eprintln!( - "[manifest] http allowlist: {}", - http_allowlist.join(", ") - ); + eprintln!("[manifest] http allowlist: {}", http_allowlist.join(", ")); } let config = manifest @@ -218,11 +218,7 @@ pub fn extract_host(url: &str) -> Option<&str> { // strip optional user-info and port. let host = host.rsplit('@').next().unwrap_or(host); let host = host.split(':').next().unwrap_or(host); - if host.is_empty() { - None - } else { - Some(host) - } + if host.is_empty() { None } else { Some(host) } } fn stringify_toml_value(v: &toml::Value) -> String { @@ -242,9 +238,15 @@ mod tests { #[test] fn extract_host_handles_common_shapes() { - assert_eq!(extract_host("https://api.example.com/v1/x"), Some("api.example.com")); + assert_eq!( + extract_host("https://api.example.com/v1/x"), + Some("api.example.com") + ); assert_eq!(extract_host("http://example.com"), Some("example.com")); - assert_eq!(extract_host("https://user:pw@host.example.com:8443/x"), Some("host.example.com")); + assert_eq!( + extract_host("https://user:pw@host.example.com:8443/x"), + Some("host.example.com") + ); assert_eq!(extract_host("https://example.com?q=1"), Some("example.com")); assert_eq!(extract_host("ftp://example.com"), None); assert_eq!(extract_host("not a url"), None); From 76d6829453577f3f07ef39552a0414a277072023 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 07:22:00 +0000 Subject: [PATCH 16/17] rename WIT package nexum:runtime -> nexum:host (resolve engine/runtime naming overload) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0.2 release shipped two similarly-named things — `nexum-engine` (the Rust crate hosting WASM components) and `nexum:runtime` (the WIT package). Both contain 'runtime' or a synonym, but they're at different layers: engine = implementation, runtime = contract. Worse, README.md described the engine crate as 'Host runtime — wasmtime-based component loader' — explicitly calling the *engine* 'runtime' while a sibling directory was literally named `nexum-runtime`. The word 'runtime' overwhelmingly means 'the thing that runs code' in programming usage; putting it on the contract side inverted the convention. Rename the WIT package to `nexum:host` — the host-imports surface a guest sees. Distinction becomes self-documenting: engine (nexum-engine) = a concrete implementation host (nexum:host) = the WIT contract every engine implements Touchpoints: - wit/nexum-runtime/ -> wit/nexum-host/ (12 files renamed via mv) - wit/shepherd-cow/deps/nexum-runtime/ -> wit/shepherd-cow/deps/nexum-host/ (regenerated by 'just sync-wit'; also covered by CI guard) - Every 'package nexum:runtime@0.2.0' -> 'package nexum:host@0.2.0' (12 files) - Every 'use nexum:runtime/...' -> 'use nexum:host/...' (shepherd-cow WIT) - Every 'nexum::runtime::...' -> 'nexum::host::...' (engine + example Rust) - justfile sync-wit paths - .github/workflows/ci.yml wit-deps-sync paths - All design docs (00..08), migration guide - README.md + docs/00 add an explicit 'Engine vs. host' callout so the distinction is directly apparent to a new reader. - A few stray 'host runtime' prose mentions in docs/05 and docs/07 changed to 'host engine' for consistency. Migration guide updated so a 0.1 -> 0.2 reader never encounters the mid-development 'nexum:runtime' name — the new package is `nexum:host` end-to-end. Build + run smoke clean; nightly rustfmt clean. --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/workflows/ci.yml | 14 ++--- README.md | 12 +++-- crates/nexum-engine/src/main.rs | 52 +++++++++---------- docs/00-overview.md | 29 +++++++---- docs/01-runtime-environment.md | 30 +++++------ docs/02-modules-events-packaging.md | 14 ++--- docs/04-state-store.md | 2 +- docs/05-sdk-design.md | 30 +++++------ docs/07-rpc-namespace-design.md | 40 +++++++------- docs/08-platform-generalisation.md | 22 ++++---- docs/migration/0.1-to-0.2.md | 14 ++--- justfile | 6 +-- modules/example/src/lib.rs | 6 +-- wit/{nexum-runtime => nexum-host}/chain.wit | 2 +- .../nexum-runtime => nexum-host}/clock.wit | 2 +- .../event-module.wit | 2 +- .../nexum-runtime => nexum-host}/http.wit | 2 +- .../identity.wit | 2 +- .../local-store.wit | 2 +- wit/{nexum-runtime => nexum-host}/logging.wit | 2 +- .../messaging.wit | 2 +- .../query-module.wit | 2 +- wit/{nexum-runtime => nexum-host}/random.wit | 2 +- .../remote-store.wit | 2 +- wit/{nexum-runtime => nexum-host}/types.wit | 2 +- wit/shepherd-cow/cow-api.wit | 2 +- .../{nexum-runtime => nexum-host}/chain.wit | 2 +- .../deps/nexum-host}/clock.wit | 2 +- .../deps/nexum-host}/event-module.wit | 2 +- .../deps/nexum-host}/http.wit | 2 +- .../identity.wit | 2 +- .../deps/nexum-host}/local-store.wit | 2 +- .../{nexum-runtime => nexum-host}/logging.wit | 2 +- .../messaging.wit | 2 +- .../deps/nexum-host}/query-module.wit | 2 +- .../{nexum-runtime => nexum-host}/random.wit | 2 +- .../remote-store.wit | 2 +- .../{nexum-runtime => nexum-host}/types.wit | 2 +- wit/shepherd-cow/shepherd.wit | 2 +- 40 files changed, 169 insertions(+), 156 deletions(-) rename wit/{nexum-runtime => nexum-host}/chain.wit (98%) rename wit/{shepherd-cow/deps/nexum-runtime => nexum-host}/clock.wit (94%) rename wit/{shepherd-cow/deps/nexum-runtime => nexum-host}/event-module.wit (96%) rename wit/{shepherd-cow/deps/nexum-runtime => nexum-host}/http.wit (97%) rename wit/{nexum-runtime => nexum-host}/identity.wit (97%) rename wit/{shepherd-cow/deps/nexum-runtime => nexum-host}/local-store.wit (95%) rename wit/{nexum-runtime => nexum-host}/logging.wit (90%) rename wit/{nexum-runtime => nexum-host}/messaging.wit (95%) rename wit/{shepherd-cow/deps/nexum-runtime => nexum-host}/query-module.wit (97%) rename wit/{nexum-runtime => nexum-host}/random.wit (86%) rename wit/{nexum-runtime => nexum-host}/remote-store.wit (97%) rename wit/{nexum-runtime => nexum-host}/types.wit (98%) rename wit/shepherd-cow/deps/{nexum-runtime => nexum-host}/chain.wit (98%) rename wit/{nexum-runtime => shepherd-cow/deps/nexum-host}/clock.wit (94%) rename wit/{nexum-runtime => shepherd-cow/deps/nexum-host}/event-module.wit (96%) rename wit/{nexum-runtime => shepherd-cow/deps/nexum-host}/http.wit (97%) rename wit/shepherd-cow/deps/{nexum-runtime => nexum-host}/identity.wit (97%) rename wit/{nexum-runtime => shepherd-cow/deps/nexum-host}/local-store.wit (95%) rename wit/shepherd-cow/deps/{nexum-runtime => nexum-host}/logging.wit (90%) rename wit/shepherd-cow/deps/{nexum-runtime => nexum-host}/messaging.wit (95%) rename wit/{nexum-runtime => shepherd-cow/deps/nexum-host}/query-module.wit (97%) rename wit/shepherd-cow/deps/{nexum-runtime => nexum-host}/random.wit (86%) rename wit/shepherd-cow/deps/{nexum-runtime => nexum-host}/remote-store.wit (97%) rename wit/shepherd-cow/deps/{nexum-runtime => nexum-host}/types.wit (98%) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 636c5e8..98ec03f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -24,7 +24,7 @@ body: id: scope attributes: label: Scope - description: Universal `nexum:runtime` change, domain extension (e.g. `shepherd:cow`), runtime-only, or SDK-only? + description: Universal `nexum:host` change, domain extension (e.g. `shepherd:cow`), runtime-only, or SDK-only? - type: textarea id: extra attributes: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1585aa3..a3a4956 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,8 +68,8 @@ jobs: - run: cargo build -p example --target wasm32-wasip2 --release wit-deps-sync: - # Verifies wit/shepherd-cow/deps/nexum-runtime/ is byte-identical to the - # canonical wit/nexum-runtime/. Both trees are committed; only `just + # Verifies wit/shepherd-cow/deps/nexum-host/ is byte-identical to the + # canonical wit/nexum-host/. Both trees are committed; only `just # sync-wit` keeps them aligned. Without this check, a contributor can # edit one and forget the other, producing a repo where the engine # bindgen and the guest bindgen see different WIT and module @@ -78,14 +78,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Regenerate shepherd-cow deps from canonical wit/nexum-runtime + - name: Regenerate shepherd-cow deps from canonical wit/nexum-host run: | - rm -rf wit/shepherd-cow/deps/nexum-runtime - cp -r wit/nexum-runtime wit/shepherd-cow/deps/nexum-runtime + rm -rf wit/shepherd-cow/deps/nexum-host + cp -r wit/nexum-host wit/shepherd-cow/deps/nexum-host - name: Fail if deps tree diverged from canonical run: | - if ! git diff --exit-code wit/shepherd-cow/deps/nexum-runtime; then - echo "::error::wit/shepherd-cow/deps/nexum-runtime is out of sync with wit/nexum-runtime." + if ! git diff --exit-code wit/shepherd-cow/deps/nexum-host; then + echo "::error::wit/shepherd-cow/deps/nexum-host is out of sync with wit/nexum-host." echo "::error::Run 'just sync-wit' locally and commit the result." exit 1 fi diff --git a/README.md b/README.md index c44821d..d146082 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Shepherd is the [CoW Protocol](https://cow.fi) distribution of **Nexum**, a WebAssembly Component Model runtime for secure, sandboxed execution of capability-scoped modules. -A module compiled against the universal `nexum:runtime/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. +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. > **Upgrading from 0.1?** See the [Migration Guide](docs/migration/0.1-to-0.2.md) for the full rename table, the new `host-error` model, and the manifest-driven capability negotiation introduced in 0.2. @@ -22,12 +22,14 @@ A module compiled against the universal `nexum:runtime/event-module` world runs | Path | Purpose | | --- | --- | -| `crates/nexum-engine/` | Host runtime — wasmtime-based component loader and host implementations. | +| `crates/nexum-engine/` | The **engine** — a wasmtime-based host *implementation* of the `nexum:host` contract. The reference server runtime. | +| `wit/nexum-host/` | The **`nexum:host` WIT package** — the host/guest *contract* (interfaces, types, worlds) that every engine implements and every module imports. | +| `wit/shepherd-cow/` | `shepherd:cow` WIT package — CoW Protocol-specific extensions on top of `nexum:host`. | | `modules/example/` | Reference guest module demonstrating the module ABI. | -| `wit/nexum-runtime/` | Universal `nexum:runtime` WIT package (chain, identity, local-store, remote-store, messaging, logging). | -| `wit/shepherd-cow/` | `shepherd:cow` WIT package — CoW Protocol-specific extensions. | | `docs/` | Architecture, design notes, and the universal primitive taxonomy. Start with [`docs/00-overview.md`](docs/00-overview.md). | +> **Engine vs. host.** "Engine" is a concrete implementation that runs WASM components (today: `nexum-engine`, a wasmtime-based daemon). The `nexum:host` WIT package is the *contract* — the host-imports surface a guest sees. Other engines (mobile, browser) can implement the same `nexum:host` contract; modules built against the contract run on any compliant engine. + ## Building Shepherd uses [Nix](https://nixos.org/) flakes to pin the toolchain and [just](https://github.com/casey/just) as a task runner. @@ -53,7 +55,7 @@ Without Nix, you need: Rust (edition 2024, see `rust-toolchain.toml` if present) The `docs/` directory contains the design corpus: - [`00-overview.md`](docs/00-overview.md) — architecture, primitives, WIT worlds -- [`01-runtime-environment.md`](docs/01-runtime-environment.md) — host runtime +- [`01-runtime-environment.md`](docs/01-runtime-environment.md) — engine internals (wasmtime, fuel, epoch, ResourceLimiter) - [`02-modules-events-packaging.md`](docs/02-modules-events-packaging.md) — module ABI, events, packaging - [`03-module-discovery.md`](docs/03-module-discovery.md) — static / ENS / on-chain registry - [`04-state-store.md`](docs/04-state-store.md) — local + remote state diff --git a/crates/nexum-engine/src/main.rs b/crates/nexum-engine/src/main.rs index 3addb06..91a77af 100644 --- a/crates/nexum-engine/src/main.rs +++ b/crates/nexum-engine/src/main.rs @@ -14,7 +14,7 @@ wasmtime::component::bindgen!({ exports: { default: async }, }); -use nexum::runtime::types::HostErrorKind; +use nexum::host::types::HostErrorKind; struct HostState { wasi: WasiCtx, @@ -48,7 +48,7 @@ fn unimplemented(domain: &str, detail: impl Into) -> HostError { // -- Stub implementations for host interfaces -- -impl nexum::runtime::types::Host for HostState {} +impl nexum::host::types::Host for HostState {} impl shepherd::cow::cow_api::Host for HostState { async fn request( @@ -81,7 +81,7 @@ impl shepherd::cow::cow_api::Host for HostState { } } -impl nexum::runtime::chain::Host for HostState { +impl nexum::host::chain::Host for HostState { async fn request( &mut self, _chain_id: u64, @@ -104,15 +104,15 @@ impl nexum::runtime::chain::Host for HostState { async fn request_batch( &mut self, chain_id: u64, - requests: Vec, - ) -> Result, HostError> { + requests: Vec, + ) -> Result, HostError> { let start = Instant::now(); eprintln!("[chain] request-batch: {} calls", requests.len()); let mut out = Vec::with_capacity(requests.len()); for req in requests { match self.request(chain_id, req.method, req.params).await { - Ok(s) => out.push(nexum::runtime::chain::RpcResult::Ok(s)), - Err(e) => out.push(nexum::runtime::chain::RpcResult::Err(e)), + Ok(s) => out.push(nexum::host::chain::RpcResult::Ok(s)), + Err(e) => out.push(nexum::host::chain::RpcResult::Err(e)), } } eprintln!("[timing] chain::request-batch: {:?}", start.elapsed()); @@ -120,7 +120,7 @@ impl nexum::runtime::chain::Host for HostState { } } -impl nexum::runtime::identity::Host for HostState { +impl nexum::host::identity::Host for HostState { async fn accounts(&mut self) -> Result>, HostError> { let start = Instant::now(); eprintln!("[identity] accounts"); @@ -150,7 +150,7 @@ impl nexum::runtime::identity::Host for HostState { } } -impl nexum::runtime::local_store::Host for HostState { +impl nexum::host::local_store::Host for HostState { async fn get(&mut self, key: String) -> Result>, HostError> { let start = Instant::now(); eprintln!("[local-store] get: {key}"); @@ -184,7 +184,7 @@ impl nexum::runtime::local_store::Host for HostState { } } -impl nexum::runtime::remote_store::Host for HostState { +impl nexum::host::remote_store::Host for HostState { async fn upload(&mut self, _data: Vec) -> Result, HostError> { let start = Instant::now(); let result = Err(unimplemented("remote-store", "upload not implemented")); @@ -218,7 +218,7 @@ impl nexum::runtime::remote_store::Host for HostState { } } -impl nexum::runtime::messaging::Host for HostState { +impl nexum::host::messaging::Host for HostState { async fn publish(&mut self, content_topic: String, _payload: Vec) -> Result<(), HostError> { let start = Instant::now(); eprintln!("[messaging] publish: {content_topic}"); @@ -233,7 +233,7 @@ impl nexum::runtime::messaging::Host for HostState { _start_time: Option, _end_time: Option, _limit: Option, - ) -> Result, HostError> { + ) -> Result, HostError> { let start = Instant::now(); eprintln!("[messaging] query: {content_topic}"); let result = Ok(vec![]); @@ -242,15 +242,15 @@ impl nexum::runtime::messaging::Host for HostState { } } -impl nexum::runtime::logging::Host for HostState { - async fn log(&mut self, level: nexum::runtime::logging::Level, message: String) { +impl nexum::host::logging::Host for HostState { + async fn log(&mut self, level: nexum::host::logging::Level, message: String) { let start = Instant::now(); let level_str = match level { - nexum::runtime::logging::Level::Trace => "TRACE", - nexum::runtime::logging::Level::Debug => "DEBUG", - nexum::runtime::logging::Level::Info => "INFO", - nexum::runtime::logging::Level::Warn => "WARN", - nexum::runtime::logging::Level::Error => "ERROR", + nexum::host::logging::Level::Trace => "TRACE", + nexum::host::logging::Level::Debug => "DEBUG", + nexum::host::logging::Level::Info => "INFO", + nexum::host::logging::Level::Warn => "WARN", + nexum::host::logging::Level::Error => "ERROR", }; eprintln!("[{level_str}] {message}"); eprintln!("[timing] logging::log: {:?}", start.elapsed()); @@ -259,7 +259,7 @@ impl nexum::runtime::logging::Host for HostState { // -- Additive 0.2 capabilities -- -impl nexum::runtime::clock::Host for HostState { +impl nexum::host::clock::Host for HostState { async fn now_ms(&mut self) -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -272,7 +272,7 @@ impl nexum::runtime::clock::Host for HostState { } } -impl nexum::runtime::random::Host for HostState { +impl nexum::host::random::Host for HostState { async fn fill(&mut self, len: u32) -> Vec { let mut buf = vec![0u8; len as usize]; // getrandom 0.4: fill() returns Result<(), Error>. CSPRNG failures @@ -284,11 +284,11 @@ impl nexum::runtime::random::Host for HostState { } } -impl nexum::runtime::http::Host for HostState { +impl nexum::host::http::Host for HostState { async fn fetch( &mut self, - req: nexum::runtime::http::Request, - ) -> Result { + req: nexum::host::http::Request, + ) -> Result { let start = Instant::now(); eprintln!("[http] {} {}", req.method, req.url); @@ -415,13 +415,13 @@ async fn main() -> anyhow::Result<()> { // Dispatch a test block event (timestamps are ms since Unix epoch, UTC). println!("nexum-engine: dispatching test block event..."); - let block = nexum::runtime::types::Block { + let block = nexum::host::types::Block { chain_id: 1, number: 19_000_000, hash: vec![0xab; 32], timestamp: 1_700_000_000_000, }; - let event = nexum::runtime::types::Event::Block(block); + let event = nexum::host::types::Event::Block(block); let start = Instant::now(); match bindings.call_on_event(&mut store, &event).await? { Ok(()) => println!("nexum-engine: on-event succeeded"), diff --git a/docs/00-overview.md b/docs/00-overview.md index e56fb2d..a09de9b 100755 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -2,9 +2,20 @@ 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:runtime/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. -> **Upgrading from 0.1?** See the [Migration Guide](migration/0.1-to-0.2.md) for the full rename table (`web3:runtime` → `nexum:runtime`, `csn` → `chain`, `msg` → `messaging`, `headless-module` → `event-module`, etc.), the unified `host-error` model, and the manifest-driven capability negotiation introduced in 0.2. +### 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: + +| 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::*` | + +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. ## Architecture @@ -22,7 +33,7 @@ flowchart TB end subgraph host["Host API — WIT Interfaces"] - uni["nexum:runtime\nchain · identity · local-store · remote-store · messaging · logging"] + uni["nexum:host\nchain · identity · local-store · remote-store · messaging · logging"] ext["shepherd:cow\ncow-api"] end @@ -55,7 +66,7 @@ flowchart TB ## The Six Primitives -Every module has access to six orthogonal capabilities through the `nexum:runtime` WIT package: +Every module has access to six orthogonal capabilities through the `nexum:host` WIT package: | Primitive | Interface | Purpose | Scope | Backend (Server) | |-----------|-----------|---------|-------|-------------------| @@ -87,7 +98,7 @@ In addition to the six core primitives, the 0.2 WIT introduces three optional ca ## WIT Worlds -The WIT is split into layered packages. The universal layer (`nexum:runtime`) provides blockchain-agnostic capabilities. Domain extensions (e.g. `shepherd:cow`) add protocol-specific interfaces. +The WIT is split into layered packages. The universal layer (`nexum:host`) provides blockchain-agnostic capabilities. Domain extensions (e.g. `shepherd:cow`) add protocol-specific interfaces. ```mermaid graph TB @@ -97,7 +108,7 @@ graph TB end subgraph l1["Layer 1 — Universal Runtime"] - pkg["nexum:runtime"] + pkg["nexum:host"] ifaces["chain · identity · local-store · remote-store · messaging · logging"] exports["Exports: init · on-event"] end @@ -108,7 +119,7 @@ graph TB ``` // Universal layer — any platform, any blockchain app -package nexum:runtime@0.2.0 +package nexum:host@0.2.0 world event-module { import chain — consensus access (JSON-RPC passthrough) @@ -145,7 +156,7 @@ 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:runtime@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 | — | | Ethereum RPC | alloy | 1.5.x | @@ -348,7 +359,7 @@ nexum/ │ ├── twap-monitor/ TWAP order monitoring module │ └── ethflow-watcher/ Ethflow order monitoring module ├── wit/ -│ ├── nexum-runtime/ Universal WIT package (chain, identity, local-store, remote-store, messaging, logging) +│ ├── nexum-host/ Universal WIT package (chain, identity, local-store, remote-store, messaging, logging) │ └── shepherd-cow/ CoW Protocol WIT package (cow-api, shepherd) ├── docker/ │ └── Dockerfile diff --git a/docs/01-runtime-environment.md b/docs/01-runtime-environment.md index 2f397f4..3f72ab0 100755 --- a/docs/01-runtime-environment.md +++ b/docs/01-runtime-environment.md @@ -34,7 +34,7 @@ The Component Model is **production-viable in wasmtime 45** and gives us critica 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:runtime` world with exactly our host APIs. Zero WASI imports means zero implicit capabilities. +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. 6. **Acceptable overhead.** The canonical ABI adds marshalling for strings/lists (memory copy across boundary), but for a plugin system with coarse-grained calls this is negligible. `InstancePre` front-loads validation costs. @@ -99,14 +99,14 @@ let bindings = EventModule::instantiate_pre(&mut store, &pre)?; ## WIT Worlds: Universal and CoW-Specific -Nexum uses a two-layer WIT architecture. The **universal** package `nexum:runtime` defines platform-agnostic interfaces and the `event-module` world. The **CoW-specific** package `shepherd:cow` extends it with CoW Protocol interfaces and the `shepherd` world. +Nexum uses a two-layer WIT architecture. The **universal** package `nexum:host` defines platform-agnostic interfaces and the `event-module` world. The **CoW-specific** package `shepherd:cow` extends it with CoW Protocol interfaces and the `shepherd` world. -### Universal Package: `nexum:runtime@0.2.0` +### Universal Package: `nexum:host@0.2.0` -The `nexum:runtime` package is the single source of truth for the universal host-guest contract. It defines a custom world with **no WASI imports**: +The `nexum:host` package is the single source of truth for the universal host-guest contract. It defines a custom world with **no WASI imports**: ```wit -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface types { type chain-id = u64; @@ -286,7 +286,7 @@ The `shepherd:cow` package extends the universal world with CoW Protocol interfa package shepherd:cow@0.2.0; interface cow-api { - use nexum:runtime/types.{chain-id, host-error}; + use nexum:host/types.{chain-id, host-error}; /// HTTP-style request to the CoW Protocol API. /// @@ -310,7 +310,7 @@ interface cow-api { /// CoW Protocol module world. Extends the universal event-module /// with CoW-specific imports. world shepherd { - include nexum:runtime/event-module; + include nexum:host/event-module; import cow-api; } @@ -325,16 +325,16 @@ world shepherd { - **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:runtime/event-module` for platform-agnostic modules; `shepherd:cow/shepherd` for CoW Protocol modules that need the `cow-api` import. The experimental `nexum:runtime/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 -The host uses `wasmtime::component::bindgen!` to generate Rust traits from the WIT. For universal interfaces, the generated traits live under `nexum::runtime::`. For CoW-specific interfaces, they live under `shepherd::cow::`. +The host uses `wasmtime::component::bindgen!` to generate Rust traits from the WIT. For universal interfaces, the generated traits live under `nexum::host::`. For CoW-specific interfaces, they live under `shepherd::cow::`. ```rust // Universal event-module world wasmtime::component::bindgen!({ - path: "wit/nexum-runtime", + path: "wit/nexum-host", world: "event-module", async: true, }); @@ -364,7 +364,7 @@ trait Identity { The `chain` host implementation depends on `Identity` internally. When a module calls a signing RPC method through `chain::request` (e.g. `eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`), the host intercepts the call and delegates to the identity backend instead of forwarding to the RPC provider: ```rust -impl nexum::runtime::chain::Host for NexumHostState { +impl nexum::host::chain::Host for NexumHostState { async fn request( &mut self, chain_id: u64, @@ -424,7 +424,7 @@ impl nexum::runtime::chain::Host for NexumHostState { The `identity::Host` implementation delegates to the platform-specific `Identity` trait. Errors map to the unified `HostError`: ```rust -impl nexum::runtime::identity::Host for NexumHostState { +impl nexum::host::identity::Host for NexumHostState { async fn accounts(&mut self) -> Result>, HostError>> { match self.identity.accounts() { Ok(addrs) => Ok(Ok(addrs.into_iter().map(|a| a.to_vec()).collect())), @@ -470,7 +470,7 @@ impl nexum::runtime::identity::Host for NexumHostState { ### Local Store Host Implementation ```rust -impl nexum::runtime::local_store::Host for NexumHostState { +impl nexum::host::local_store::Host for NexumHostState { async fn get(&mut self, key: String) -> Result>, HostError>> { // Read from the in-flight WriteTransaction (not a new ReadTransaction) // so the module sees its own uncommitted writes within a single on_event. @@ -584,7 +584,7 @@ See doc 05 for the full macro design (named handlers, provider injection, escape | **Python** | componentize-py (CPython) | Maturing | | **C#** | `wit-bindgen-csharp` | Emerging | -All produce valid components against the same WIT worlds (`nexum:runtime/event-module` for universal, `shepherd:cow/shepherd` for CoW). +All produce valid components against the same WIT worlds (`nexum:host/event-module` for universal, `shepherd:cow/shepherd` for CoW). ## Execution Metering @@ -635,7 +635,7 @@ All RPC and CoW API I/O is async (alloy / reqwest on the host). wasmtime bridges | Nexum Concept | wasmtime Primitive | |------------------|--------------------| | Runtime process | `Engine` (one, shared) | -| Universal API contract | WIT world (`nexum:runtime/event-module`) | +| Universal API contract | WIT world (`nexum:host/event-module`) | | CoW API contract | WIT world (`shepherd:cow/shepherd`) | | Compiled module | `Component` (cached, thread-safe) | | Pre-validated module | `InstancePre` (linker + component) | diff --git a/docs/02-modules-events-packaging.md b/docs/02-modules-events-packaging.md index 1b68707..bebdb96 100755 --- a/docs/02-modules-events-packaging.md +++ b/docs/02-modules-events-packaging.md @@ -177,7 +177,7 @@ stateDiagram-v2 | State | Description | |-------|-------------| | **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:runtime/event-module` or `shepherd:cow/shepherd`). Installs trap stubs for capabilities the manifest declares `optional` but the host does not provide. 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). | | **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). | @@ -287,12 +287,12 @@ The runtime serialises event data via the canonical ABI (handled automatically b ## Updated WIT Worlds -The initial WIT in `01-runtime-environment.md` is extended to support the lifecycle and config. The architecture uses two packages: `nexum:runtime` for universal interfaces and `shepherd:cow` for CoW Protocol extensions. +The initial WIT in `01-runtime-environment.md` is extended to support the lifecycle and config. The architecture uses two packages: `nexum:host` for universal interfaces and `shepherd:cow` for CoW Protocol extensions. -### Universal Package: `nexum:runtime@0.2.0` +### Universal Package: `nexum:host@0.2.0` ```wit -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface types { type chain-id = u64; @@ -409,7 +409,7 @@ world event-module { package shepherd:cow@0.2.0; interface cow-api { - use nexum:runtime/types.{chain-id, host-error}; + use nexum:host/types.{chain-id, host-error}; /// HTTP-style request to the CoW Protocol API. request: func( @@ -426,7 +426,7 @@ interface cow-api { /// CoW Protocol module world — extends event-module with cow-api. world shepherd { - include nexum:runtime/event-module; + include nexum:host/event-module; import cow-api; } @@ -448,7 +448,7 @@ Operator deploys a module: 3. Runtime compiles Component, creates InstancePre: - Validates component satisfies target world - (nexum:runtime/event-module or shepherd:cow/shepherd) + (nexum:host/event-module or shepherd:cow/shepherd) - Installs trap stubs for any [capabilities].optional imports the host doesn't provide - Enforces resource limits from manifest diff --git a/docs/04-state-store.md b/docs/04-state-store.md index b76a6dd..1eff78c 100755 --- a/docs/04-state-store.md +++ b/docs/04-state-store.md @@ -48,7 +48,7 @@ This per-file design ensures concurrent modules never contend on write locks (se ```wit interface local-store { - use nexum:runtime/types.{host-error}; + use nexum:host/types.{host-error}; /// Get a value by key. Returns none if key doesn't exist. get: func(key: string) -> result>, host-error>; diff --git a/docs/05-sdk-design.md b/docs/05-sdk-design.md index 374ce77..758a5d2 100755 --- a/docs/05-sdk-design.md +++ b/docs/05-sdk-design.md @@ -4,7 +4,7 @@ The SDK is split into two layers: -1. **`nexum-sdk`** -- the universal SDK for any `nexum:runtime/event-module`. It provides: +1. **`nexum-sdk`** -- the universal SDK for any `nexum:host/event-module`. It provides: - WIT bindings (re-exported, version-pinned) - A proc macro (`#[nexum::module]`) that eliminates boilerplate (supports `async fn` for natural `.await`) - A full alloy `Provider` backed by the host's RPC stack (`HostTransport`) @@ -54,12 +54,12 @@ shepherd-sdk/ └── lib.rs # #[shepherd::module] proc macro (CoW variant) ``` -The workspace root `wit/nexum-runtime/` is the **universal WIT definition**. The `wit/shepherd-cow/` directory extends it with CoW Protocol interfaces. The SDKs reference these via path (not a copy) to prevent drift: +The workspace root `wit/nexum-host/` is the **universal WIT definition**. The `wit/shepherd-cow/` directory extends it with CoW Protocol interfaces. The SDKs reference these via path (not a copy) to prevent drift: ```toml # nexum-sdk/Cargo.toml [package.metadata.component.target] -path = "../wit/nexum-runtime" +path = "../wit/nexum-host" ``` ```toml @@ -126,7 +126,7 @@ impl TwapMonitor { } ``` -The `#[nexum::module]` macro generates code against the `nexum:runtime/event-module` world. +The `#[nexum::module]` macro generates code against the `nexum:host/event-module` world. ### CoW Protocol: `#[shepherd::module]` @@ -185,7 +185,7 @@ impl Guest for TwapMonitor { export!(TwapMonitor); ``` -For the CoW `#[shepherd::module]`, the generated code additionally imports `shepherd:cow/cow-api` alongside the `nexum:runtime` base. +For the CoW `#[shepherd::module]`, the generated code additionally imports `shepherd:cow/cow-api` alongside the `nexum:host` base. ### Named event handlers @@ -239,13 +239,13 @@ Resolution order: ```rust // nexum_sdk::prelude -pub use crate::bindings::nexum::runtime::types::*; -pub use crate::bindings::nexum::runtime::chain; -pub use crate::bindings::nexum::runtime::identity; -pub use crate::bindings::nexum::runtime::local_store; -pub use crate::bindings::nexum::runtime::remote_store; -pub use crate::bindings::nexum::runtime::messaging; -pub use crate::bindings::nexum::runtime::logging; +pub use crate::bindings::nexum::host::types::*; +pub use crate::bindings::nexum::host::chain; +pub use crate::bindings::nexum::host::identity; +pub use crate::bindings::nexum::host::local_store; +pub use crate::bindings::nexum::host::remote_store; +pub use crate::bindings::nexum::host::messaging; +pub use crate::bindings::nexum::host::logging; pub use crate::log::{trace, debug, info, warn, error}; pub use crate::local_store::TypedState; pub use crate::signer::Signer; @@ -364,7 +364,7 @@ Implementation: ```rust /// Typed client for the identity WIT interface. /// -/// Provides cryptographic signing operations backed by the host runtime's +/// Provides cryptographic signing operations backed by the host engine's /// key management. The host manages private keys -- modules never see them. pub struct Signer; @@ -759,7 +759,7 @@ my-module/ └── lib.rs # minimal module skeleton ``` -#### Universal module (targeting `nexum:runtime/event-module`) +#### Universal module (targeting `nexum:host/event-module`) `Cargo.toml`: ```toml @@ -910,7 +910,7 @@ cargo nexum publish --swarm http://localhost:1633 --batch-id ## SDK / Runtime Version Compatibility -The WIT definition is versioned (`nexum:runtime@0.2.0`). The SDK pins this version. When the WIT evolves: +The WIT definition is versioned (`nexum:host@0.2.0`). The SDK pins this version. When the WIT evolves: - **Patch** (0.2.x): backwards-compatible additions (new host functions, new manifest fields, new SDK helpers). Old modules continue to work. - **Minor** (0.x.0): may add new required exports. Old modules need recompilation. diff --git a/docs/07-rpc-namespace-design.md b/docs/07-rpc-namespace-design.md index 9328d93..8f443a3 100755 --- a/docs/07-rpc-namespace-design.md +++ b/docs/07-rpc-namespace-design.md @@ -1,7 +1,7 @@ # RPC Namespace Design: Generic JSON-RPC Passthrough > **Naming note (0.2):** This document describes the `chain` interface in the -> `nexum:runtime` WIT package. In the 0.1 design history it was called `chain` +> `nexum:host` WIT package. In the 0.1 design history it was called `chain` > (short for "consensus"); 0.2 renamed it to `chain` because `chain.request(...)` > reads itself at the call site. The function signatures below are the 0.2 shape, > returning `host-error` rather than the 0.1-era `json-rpc-error`. @@ -63,7 +63,7 @@ flowchart TD Replace the `blockchain` interface with `chain`: ```wit -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface chain { use types.{chain-id, host-error}; @@ -111,7 +111,7 @@ interface identity { } ``` -The universal `event-module` world (in `nexum:runtime`) 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 { @@ -131,7 +131,7 @@ The CoW-specific `shepherd` world (in `shepherd:cow`) extends it with the merged ```wit world shepherd { - include nexum:runtime/event-module; + include nexum:host/event-module; import cow-api; } ``` @@ -162,7 +162,7 @@ The host implementation is minimal — one function handles the entire `eth_` na ```rust use serde_json::value::RawValue; -impl nexum::runtime::chain::Host for NexumHostState { +impl nexum::host::chain::Host for NexumHostState { async fn request( &mut self, chain_id: u64, @@ -337,7 +337,7 @@ pub struct ChainHost { identity: I, } -impl nexum::runtime::chain::Host for ChainHost { +impl nexum::host::chain::Host for ChainHost { async fn request( &mut self, chain_id: u64, @@ -477,10 +477,10 @@ impl ChainHost { } ``` -The `ChainHost` also implements `nexum::runtime::identity::Host` directly, delegating to the same `Identity` trait so modules can use the identity WIT interface for raw signing (errors map to `host-error` with `domain = "identity"`): +The `ChainHost` also implements `nexum::host::identity::Host` directly, delegating to the same `Identity` trait so modules can use the identity WIT interface for raw signing (errors map to `host-error` with `domain = "identity"`): ```rust -impl nexum::runtime::identity::Host for ChainHost { +impl nexum::host::identity::Host for ChainHost { fn accounts(&mut self) -> wasmtime::Result>, HostError>> { Ok(self.identity.accounts().map_err(|e| HostError { domain: "identity".into(), @@ -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 runtime. 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 { @@ -647,7 +647,7 @@ pub fn block_on(future: F) -> F::Output { use alloy_provider::RootProvider; use alloy_rpc_client::RpcClient; -/// Create an alloy `Provider` backed by the Nexum host runtime. +/// 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 @@ -910,7 +910,7 @@ In 0.1 this was two interfaces, `cow` (REST passthrough) and `order` (typed `sub ```wit interface cow-api { - use nexum:runtime/types.{chain-id, host-error}; + use nexum:host/types.{chain-id, host-error}; /// HTTP-style request to the CoW Protocol API. /// @@ -938,7 +938,7 @@ interface cow-api { ```wit world shepherd { - include nexum:runtime/event-module; + include nexum:host/event-module; import cow-api; } ``` @@ -1028,7 +1028,7 @@ async fn request(&mut self, chain_id: u64, method: String, params: String) ### SDK: `Cow` ```rust -/// Typed client for the CoW Protocol API, backed by the host runtime. +/// Typed client for the CoW Protocol API, backed by the host engine. pub struct Cow { chain_id: u64, } @@ -1148,13 +1148,13 @@ All alloy crates with `default-features = false` to avoid pulling in reqwest, to ```rust // nexum_sdk::prelude -pub use crate::bindings::nexum::runtime::types::*; -pub use crate::bindings::nexum::runtime::chain; -pub use crate::bindings::nexum::runtime::identity; -pub use crate::bindings::nexum::runtime::local_store; -pub use crate::bindings::nexum::runtime::remote_store; -pub use crate::bindings::nexum::runtime::messaging; -pub use crate::bindings::nexum::runtime::logging; +pub use crate::bindings::nexum::host::types::*; +pub use crate::bindings::nexum::host::chain; +pub use crate::bindings::nexum::host::identity; +pub use crate::bindings::nexum::host::local_store; +pub use crate::bindings::nexum::host::remote_store; +pub use crate::bindings::nexum::host::messaging; +pub use crate::bindings::nexum::host::logging; pub use crate::log::{trace, debug, info, warn, error}; pub use crate::local_store::TypedState; pub use crate::signer::Signer; diff --git a/docs/08-platform-generalisation.md b/docs/08-platform-generalisation.md index ac680de..afc50ab 100755 --- a/docs/08-platform-generalisation.md +++ b/docs/08-platform-generalisation.md @@ -13,7 +13,7 @@ The Nexum runtime (docs 01-07) is designed as a server-side Rust binary embeddin 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:runtime/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. +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. ## Primitive Taxonomy @@ -368,7 +368,7 @@ Every platform implements this trivially. On server: `tracing` crate. On mobile: ### Universal World Definition ```wit -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface types { type chain-id = u64; @@ -442,7 +442,7 @@ world event-module { } ``` -A module compiled against `nexum:runtime/event-module` is the **maximally portable** artifact. In 0.2 it runs on the server reference runtime; mobile and WebView hosts are planned (see the status banner at the top of this doc). +A module compiled against `nexum:host/event-module` is the **maximally portable** artifact. In 0.2 it runs on the server reference runtime; mobile and WebView hosts are planned (see the status banner at the top of this doc). ## Layer 2: UI Interface @@ -570,7 +570,7 @@ Domain-specific interfaces extend the universal layer for particular use cases. package shepherd:cow@0.2.0; interface cow-api { - use nexum:runtime/types.{chain-id, host-error}; + use nexum:host/types.{chain-id, host-error}; request: func( chain-id: chain-id, @@ -584,7 +584,7 @@ interface cow-api { } world shepherd { - include nexum:runtime/event-module; + include nexum:host/event-module; import cow-api; } ``` @@ -599,7 +599,7 @@ interface vault { /* ... */ } interface strategy { /* ... */ } world yield-module { - include nexum:runtime/event-module; + include nexum:host/event-module; import vault; import strategy; } @@ -611,7 +611,7 @@ The `include` mechanism ensures that any domain-specific module inherits the ful ``` wit/ -├── nexum-runtime/ +├── nexum-host/ │ ├── types.wit # chain-id, block, log, tick, message, event, config, host-error │ ├── chain.wit # chain interface (consensus access + request-batch) │ ├── identity.wit # identity interface (key management, signing) @@ -632,7 +632,7 @@ wit/ └── shepherd.wit # shepherd world (includes event-module + cow-api) ``` -The `nexum-runtime` package is domain-agnostic and reusable. The `shepherd-cow` package is the CoW Protocol extension. New domains add new packages without touching the universal layer. +The `nexum-host` package is domain-agnostic and reusable. The `shepherd-cow` package is the CoW Protocol extension. New domains add new packages without touching the universal layer. ## Platform Targets @@ -970,7 +970,7 @@ graph TD ShepherdSDK -->|"extends"| NexumSDK ``` -- **`nexum-sdk`** — the universal Rust SDK for any module targeting `nexum:runtime/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). @@ -982,7 +982,7 @@ For **non-Rust** module authors (JavaScript, Python, Go, C++), the SDK is unnece For the full 0.1 → 0.2 rename and behaviour change list, see the [Migration Guide](migration/0.1-to-0.2.md). The main themes: -- WIT package `web3:runtime` → `nexum:runtime`; interfaces `csn` → `chain` and `msg` → `messaging`; worlds `headless-module` → `event-module` and `shepherd-module` → `shepherd`. +- WIT package `web3:runtime` → `nexum:host`; interfaces `csn` → `chain` and `msg` → `messaging`; worlds `headless-module` → `event-module` and `shepherd-module` → `shepherd`. - CoW `cow` + `order` interfaces merged into `cow-api`. - All host functions return the unified `host-error` (with `host-error-kind` discriminant) instead of five per-protocol error types. - The `event-module` world imports the six primitives the docs always claimed (0.1's WIT was missing `identity` from the world definition). @@ -1006,7 +1006,7 @@ For the full 0.1 → 0.2 rename and behaviour change list, see the [Migration Gu | Concept | Scope | |---------|-------| -| `nexum:runtime` WIT package | Universal — any blockchain app, any platform | +| `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 | diff --git a/docs/migration/0.1-to-0.2.md b/docs/migration/0.1-to-0.2.md index cbfb789..189e0cf 100644 --- a/docs/migration/0.1-to-0.2.md +++ b/docs/migration/0.1-to-0.2.md @@ -15,7 +15,7 @@ Each section is tagged `[author]`, `[embedder]`, or `[both]`. | Area | 0.1 | 0.2 | |---|---|---| -| WIT package | `web3:runtime` | `nexum:runtime` | +| WIT package | `web3:runtime` | `nexum:host` | | Consensus interface | `csn` | `chain` | | Messaging interface | `msg` | `messaging` | | Default world | `headless-module` | `event-module` | @@ -45,8 +45,8 @@ If you only do four things: update your `nexum.toml`, run the sed cheat-sheet at ```diff - use web3:runtime/types.{config, event}; - use web3:runtime/chain.{chain-id}; -+ use nexum:runtime/types.{config, event}; -+ use nexum:runtime/chain.{chain-id}; ++ use nexum:host/types.{config, event}; ++ use nexum:host/chain.{chain-id}; ``` Why: `web3:` precommitted the engine to crypto-only branding. The package is now named after the engine; web3-specific capabilities live inside it as interfaces. @@ -406,7 +406,7 @@ If you're writing a module that fits this shape, target it now and stub the host + nexum-engine = "0.2" ``` -The 0.1 release renamed `nexum-runtime` → `nxm-engine`. 0.2 reverses that to `nexum-engine` for consistency with `nexum-sdk`, `shepherd-sdk`, `cargo-nexum`. +The 0.1 release renamed `nexum-host` → `nxm-engine`. 0.2 reverses that to `nexum-engine` for consistency with `nexum-sdk`, `shepherd-sdk`, `cargo-nexum`. ```diff - use nxm_engine::{Engine, Module}; @@ -451,7 +451,7 @@ For mechanical search/replace in your codebase. Apply in order; some replacement ```bash # WIT package -rg -l 'web3:runtime' | xargs sed -i 's/web3:runtime/nexum:runtime/g' +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) rg -l '\bcsn\b' | xargs sed -i 's/\bcsn\b/chain/g' @@ -523,7 +523,7 @@ After running the renames: 0.2 is the breaking-change window. The contracts below are stable starting at 0.2.0: -- WIT package name `nexum:runtime` and interface names within it. +- WIT package name `nexum:host` and interface names within it. - The `host-error` / `host-error-kind` shape. - The `nexum.toml` manifest schema. - The `#[nexum::module]` macro surface. @@ -537,5 +537,5 @@ The mobile/wallet host story (`query-module` production support, C ABI, `nexum-h ## 11. Getting help - Open an issue at the repo with the `migration-0.2` label. -- The full 0.2 WIT lives in `wit/nexum-runtime/` (formerly `wit/web3-runtime/`). +- The full 0.2 WIT lives in `wit/nexum-host/` (formerly `wit/web3-runtime/`). - The §8 cheat sheet has the mechanical sed commands; a `cargo nexum migrate --from 0.1` codemod that wraps them safely is planned for 0.3 alongside the rest of the `cargo-nexum` toolchain. diff --git a/justfile b/justfile index 0d6d4e1..89a66a1 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,7 @@ -# Sync WIT deps (copies nexum-runtime into shepherd-cow/deps) +# Sync WIT deps (copies nexum-host into shepherd-cow/deps) sync-wit: - rm -rf wit/shepherd-cow/deps/nexum-runtime - cp -r wit/nexum-runtime wit/shepherd-cow/deps/nexum-runtime + rm -rf wit/shepherd-cow/deps/nexum-host + cp -r wit/nexum-host wit/shepherd-cow/deps/nexum-host # Build the host runtime build-runtime: sync-wit diff --git a/modules/example/src/lib.rs b/modules/example/src/lib.rs index b71b54d..9472a5b 100644 --- a/modules/example/src/lib.rs +++ b/modules/example/src/lib.rs @@ -3,12 +3,12 @@ #![allow(clippy::too_many_arguments)] wit_bindgen::generate!({ - path: "../../wit/nexum-runtime", + path: "../../wit/nexum-host", world: "event-module", }); -use nexum::runtime::logging; -use nexum::runtime::types; +use nexum::host::logging; +use nexum::host::types; struct ExampleModule; diff --git a/wit/nexum-runtime/chain.wit b/wit/nexum-host/chain.wit similarity index 98% rename from wit/nexum-runtime/chain.wit rename to wit/nexum-host/chain.wit index 0b49a75..574368b 100644 --- a/wit/nexum-runtime/chain.wit +++ b/wit/nexum-host/chain.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface chain { use types.{chain-id, host-error}; diff --git a/wit/shepherd-cow/deps/nexum-runtime/clock.wit b/wit/nexum-host/clock.wit similarity index 94% rename from wit/shepherd-cow/deps/nexum-runtime/clock.wit rename to wit/nexum-host/clock.wit index edc18b6..b57408a 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/clock.wit +++ b/wit/nexum-host/clock.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Host-provided clock. Guest modules MUST use this rather than relying on /// WASI clocks so the host can virtualise time during replay/testing. diff --git a/wit/shepherd-cow/deps/nexum-runtime/event-module.wit b/wit/nexum-host/event-module.wit similarity index 96% rename from wit/shepherd-cow/deps/nexum-runtime/event-module.wit rename to wit/nexum-host/event-module.wit index 848fce3..30a1e99 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/event-module.wit +++ b/wit/nexum-host/event-module.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Event-driven module — automation, background processing. /// No UI capabilities. Runs on any conforming host. diff --git a/wit/shepherd-cow/deps/nexum-runtime/http.wit b/wit/nexum-host/http.wit similarity index 97% rename from wit/shepherd-cow/deps/nexum-runtime/http.wit rename to wit/nexum-host/http.wit index e529a28..0e12fd4 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/http.wit +++ b/wit/nexum-host/http.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Generic HTTP client capability. Modules that need this MUST opt in via /// their manifest; the host enforces an allow-list of destinations. diff --git a/wit/nexum-runtime/identity.wit b/wit/nexum-host/identity.wit similarity index 97% rename from wit/nexum-runtime/identity.wit rename to wit/nexum-host/identity.wit index 25234ad..6d62f7f 100644 --- a/wit/nexum-runtime/identity.wit +++ b/wit/nexum-host/identity.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Identity / signing capability. /// diff --git a/wit/shepherd-cow/deps/nexum-runtime/local-store.wit b/wit/nexum-host/local-store.wit similarity index 95% rename from wit/shepherd-cow/deps/nexum-runtime/local-store.wit rename to wit/nexum-host/local-store.wit index ba58be8..546dc0b 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/local-store.wit +++ b/wit/nexum-host/local-store.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface local-store { use types.{host-error}; diff --git a/wit/nexum-runtime/logging.wit b/wit/nexum-host/logging.wit similarity index 90% rename from wit/nexum-runtime/logging.wit rename to wit/nexum-host/logging.wit index ac4286d..37e9193 100644 --- a/wit/nexum-runtime/logging.wit +++ b/wit/nexum-host/logging.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface logging { enum level { diff --git a/wit/nexum-runtime/messaging.wit b/wit/nexum-host/messaging.wit similarity index 95% rename from wit/nexum-runtime/messaging.wit rename to wit/nexum-host/messaging.wit index f24f3d0..0334834 100644 --- a/wit/nexum-runtime/messaging.wit +++ b/wit/nexum-host/messaging.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface messaging { use types.{host-error, message}; diff --git a/wit/shepherd-cow/deps/nexum-runtime/query-module.wit b/wit/nexum-host/query-module.wit similarity index 97% rename from wit/shepherd-cow/deps/nexum-runtime/query-module.wit rename to wit/nexum-host/query-module.wit index a531a1b..f261a8b 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/query-module.wit +++ b/wit/nexum-host/query-module.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Query module — synchronous, side-effect-free evaluation. /// diff --git a/wit/nexum-runtime/random.wit b/wit/nexum-host/random.wit similarity index 86% rename from wit/nexum-runtime/random.wit rename to wit/nexum-host/random.wit index 14c8574..e37a67a 100644 --- a/wit/nexum-runtime/random.wit +++ b/wit/nexum-host/random.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Cryptographically secure randomness from the host. interface random { diff --git a/wit/nexum-runtime/remote-store.wit b/wit/nexum-host/remote-store.wit similarity index 97% rename from wit/nexum-runtime/remote-store.wit rename to wit/nexum-host/remote-store.wit index 09f793b..f68e32f 100644 --- a/wit/nexum-runtime/remote-store.wit +++ b/wit/nexum-host/remote-store.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface remote-store { use types.{host-error}; diff --git a/wit/nexum-runtime/types.wit b/wit/nexum-host/types.wit similarity index 98% rename from wit/nexum-runtime/types.wit rename to wit/nexum-host/types.wit index d4d38c3..6a6def1 100644 --- a/wit/nexum-runtime/types.wit +++ b/wit/nexum-host/types.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Common types shared across all runtime interfaces. /// diff --git a/wit/shepherd-cow/cow-api.wit b/wit/shepherd-cow/cow-api.wit index c4ec6f8..0787d01 100644 --- a/wit/shepherd-cow/cow-api.wit +++ b/wit/shepherd-cow/cow-api.wit @@ -1,7 +1,7 @@ package shepherd:cow@0.2.0; interface cow-api { - use nexum:runtime/types@0.2.0.{chain-id, host-error}; + use nexum:host/types@0.2.0.{chain-id, host-error}; /// HTTP-style request to the CoW Protocol API. /// diff --git a/wit/shepherd-cow/deps/nexum-runtime/chain.wit b/wit/shepherd-cow/deps/nexum-host/chain.wit similarity index 98% rename from wit/shepherd-cow/deps/nexum-runtime/chain.wit rename to wit/shepherd-cow/deps/nexum-host/chain.wit index 0b49a75..574368b 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/chain.wit +++ b/wit/shepherd-cow/deps/nexum-host/chain.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface chain { use types.{chain-id, host-error}; diff --git a/wit/nexum-runtime/clock.wit b/wit/shepherd-cow/deps/nexum-host/clock.wit similarity index 94% rename from wit/nexum-runtime/clock.wit rename to wit/shepherd-cow/deps/nexum-host/clock.wit index edc18b6..b57408a 100644 --- a/wit/nexum-runtime/clock.wit +++ b/wit/shepherd-cow/deps/nexum-host/clock.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Host-provided clock. Guest modules MUST use this rather than relying on /// WASI clocks so the host can virtualise time during replay/testing. diff --git a/wit/nexum-runtime/event-module.wit b/wit/shepherd-cow/deps/nexum-host/event-module.wit similarity index 96% rename from wit/nexum-runtime/event-module.wit rename to wit/shepherd-cow/deps/nexum-host/event-module.wit index 848fce3..30a1e99 100644 --- a/wit/nexum-runtime/event-module.wit +++ b/wit/shepherd-cow/deps/nexum-host/event-module.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Event-driven module — automation, background processing. /// No UI capabilities. Runs on any conforming host. diff --git a/wit/nexum-runtime/http.wit b/wit/shepherd-cow/deps/nexum-host/http.wit similarity index 97% rename from wit/nexum-runtime/http.wit rename to wit/shepherd-cow/deps/nexum-host/http.wit index e529a28..0e12fd4 100644 --- a/wit/nexum-runtime/http.wit +++ b/wit/shepherd-cow/deps/nexum-host/http.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Generic HTTP client capability. Modules that need this MUST opt in via /// their manifest; the host enforces an allow-list of destinations. diff --git a/wit/shepherd-cow/deps/nexum-runtime/identity.wit b/wit/shepherd-cow/deps/nexum-host/identity.wit similarity index 97% rename from wit/shepherd-cow/deps/nexum-runtime/identity.wit rename to wit/shepherd-cow/deps/nexum-host/identity.wit index 25234ad..6d62f7f 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/identity.wit +++ b/wit/shepherd-cow/deps/nexum-host/identity.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Identity / signing capability. /// diff --git a/wit/nexum-runtime/local-store.wit b/wit/shepherd-cow/deps/nexum-host/local-store.wit similarity index 95% rename from wit/nexum-runtime/local-store.wit rename to wit/shepherd-cow/deps/nexum-host/local-store.wit index ba58be8..546dc0b 100644 --- a/wit/nexum-runtime/local-store.wit +++ b/wit/shepherd-cow/deps/nexum-host/local-store.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface local-store { use types.{host-error}; diff --git a/wit/shepherd-cow/deps/nexum-runtime/logging.wit b/wit/shepherd-cow/deps/nexum-host/logging.wit similarity index 90% rename from wit/shepherd-cow/deps/nexum-runtime/logging.wit rename to wit/shepherd-cow/deps/nexum-host/logging.wit index ac4286d..37e9193 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/logging.wit +++ b/wit/shepherd-cow/deps/nexum-host/logging.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface logging { enum level { diff --git a/wit/shepherd-cow/deps/nexum-runtime/messaging.wit b/wit/shepherd-cow/deps/nexum-host/messaging.wit similarity index 95% rename from wit/shepherd-cow/deps/nexum-runtime/messaging.wit rename to wit/shepherd-cow/deps/nexum-host/messaging.wit index f24f3d0..0334834 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/messaging.wit +++ b/wit/shepherd-cow/deps/nexum-host/messaging.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface messaging { use types.{host-error, message}; diff --git a/wit/nexum-runtime/query-module.wit b/wit/shepherd-cow/deps/nexum-host/query-module.wit similarity index 97% rename from wit/nexum-runtime/query-module.wit rename to wit/shepherd-cow/deps/nexum-host/query-module.wit index a531a1b..f261a8b 100644 --- a/wit/nexum-runtime/query-module.wit +++ b/wit/shepherd-cow/deps/nexum-host/query-module.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Query module — synchronous, side-effect-free evaluation. /// diff --git a/wit/shepherd-cow/deps/nexum-runtime/random.wit b/wit/shepherd-cow/deps/nexum-host/random.wit similarity index 86% rename from wit/shepherd-cow/deps/nexum-runtime/random.wit rename to wit/shepherd-cow/deps/nexum-host/random.wit index 14c8574..e37a67a 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/random.wit +++ b/wit/shepherd-cow/deps/nexum-host/random.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Cryptographically secure randomness from the host. interface random { diff --git a/wit/shepherd-cow/deps/nexum-runtime/remote-store.wit b/wit/shepherd-cow/deps/nexum-host/remote-store.wit similarity index 97% rename from wit/shepherd-cow/deps/nexum-runtime/remote-store.wit rename to wit/shepherd-cow/deps/nexum-host/remote-store.wit index 09f793b..f68e32f 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/remote-store.wit +++ b/wit/shepherd-cow/deps/nexum-host/remote-store.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; interface remote-store { use types.{host-error}; diff --git a/wit/shepherd-cow/deps/nexum-runtime/types.wit b/wit/shepherd-cow/deps/nexum-host/types.wit similarity index 98% rename from wit/shepherd-cow/deps/nexum-runtime/types.wit rename to wit/shepherd-cow/deps/nexum-host/types.wit index d4d38c3..6a6def1 100644 --- a/wit/shepherd-cow/deps/nexum-runtime/types.wit +++ b/wit/shepherd-cow/deps/nexum-host/types.wit @@ -1,4 +1,4 @@ -package nexum:runtime@0.2.0; +package nexum:host@0.2.0; /// Common types shared across all runtime interfaces. /// diff --git a/wit/shepherd-cow/shepherd.wit b/wit/shepherd-cow/shepherd.wit index be1f386..88aff14 100644 --- a/wit/shepherd-cow/shepherd.wit +++ b/wit/shepherd-cow/shepherd.wit @@ -2,6 +2,6 @@ package shepherd:cow@0.2.0; /// Shepherd module — event-driven Nexum module with CoW Protocol extensions. world shepherd { - include nexum:runtime/event-module@0.2.0; + include nexum:host/event-module@0.2.0; import cow-api; } From f392af651f2bd72b33443e3fe31020ffca8fa1a3 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sun, 31 May 2026 10:24:30 +0000 Subject: [PATCH 17/17] wit: drop deps/ vendoring; list both packages explicitly in bindgen The 0.2 release vendored a copy of wit/nexum-host/ under wit/shepherd-cow/deps/nexum-host/ because the engine's bindgen target (shepherd:cow/shepherd) imports from nexum:host via 'include', and wit-parser's default cross-package resolution looks at /deps/. Maintained by 'just sync-wit' + a CI guard to catch drift. That whole pipeline was treating the symptom rather than the cause. Both bindgen macros (wasmtime::component::bindgen! and wit_bindgen::generate!) accept 'path' as an array of dirs, each holding one package. Listing both packages explicitly resolves the cross-package reference natively with no vendored copy. Changes: - crates/nexum-engine/src/main.rs: bindgen path is now ["../../wit/nexum-host", "../../wit/shepherd-cow"]; world fully qualified as "shepherd:cow/shepherd". - modules/example/src/lib.rs: world fully qualified as "nexum:host/event-module". Path stays single (the example doesn't import shepherd:cow). - wit/shepherd-cow/deps/ deleted entirely (12 files). - justfile: sync-wit recipe removed; build-runtime renamed to build-engine (clearer; matches the engine vs host vocabulary established earlier); check + run no longer depend on sync-wit. - .github/workflows/ci.yml: wit-deps-sync job removed (nothing to sync, nothing to drift). - docs/migration/0.1-to-0.2.md: checklist item about vendored deps rewritten to point at the new bindgen pattern. Result: 1 source of truth per WIT package, zero ceremony to keep them aligned, ~360 fewer lines of vendored bytes in the repo. --- .github/workflows/ci.yml | 23 ----- crates/nexum-engine/src/main.rs | 7 +- docs/migration/0.1-to-0.2.md | 2 +- justfile | 17 ++-- modules/example/src/lib.rs | 2 +- wit/shepherd-cow/deps/nexum-host/chain.wit | 37 --------- wit/shepherd-cow/deps/nexum-host/clock.wit | 13 --- .../deps/nexum-host/event-module.wit | 24 ------ wit/shepherd-cow/deps/nexum-host/http.wit | 39 --------- wit/shepherd-cow/deps/nexum-host/identity.wit | 24 ------ .../deps/nexum-host/local-store.wit | 18 ---- wit/shepherd-cow/deps/nexum-host/logging.wit | 15 ---- .../deps/nexum-host/messaging.wit | 19 ----- .../deps/nexum-host/query-module.wit | 25 ------ wit/shepherd-cow/deps/nexum-host/random.wit | 7 -- .../deps/nexum-host/remote-store.wit | 33 -------- wit/shepherd-cow/deps/nexum-host/types.wit | 83 ------------------- 17 files changed, 13 insertions(+), 375 deletions(-) delete mode 100644 wit/shepherd-cow/deps/nexum-host/chain.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/clock.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/event-module.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/http.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/identity.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/local-store.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/logging.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/messaging.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/query-module.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/random.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/remote-store.wit delete mode 100644 wit/shepherd-cow/deps/nexum-host/types.wit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3a4956..99de175 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,26 +66,3 @@ jobs: targets: wasm32-wasip2 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo build -p example --target wasm32-wasip2 --release - - wit-deps-sync: - # Verifies wit/shepherd-cow/deps/nexum-host/ is byte-identical to the - # canonical wit/nexum-host/. Both trees are committed; only `just - # sync-wit` keeps them aligned. Without this check, a contributor can - # edit one and forget the other, producing a repo where the engine - # bindgen and the guest bindgen see different WIT and module - # instantiation fails at runtime on a commit that builds clean. - name: wit deps sync - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Regenerate shepherd-cow deps from canonical wit/nexum-host - run: | - rm -rf wit/shepherd-cow/deps/nexum-host - cp -r wit/nexum-host wit/shepherd-cow/deps/nexum-host - - name: Fail if deps tree diverged from canonical - run: | - if ! git diff --exit-code wit/shepherd-cow/deps/nexum-host; then - echo "::error::wit/shepherd-cow/deps/nexum-host is out of sync with wit/nexum-host." - echo "::error::Run 'just sync-wit' locally and commit the result." - exit 1 - fi diff --git a/crates/nexum-engine/src/main.rs b/crates/nexum-engine/src/main.rs index 91a77af..f013f22 100644 --- a/crates/nexum-engine/src/main.rs +++ b/crates/nexum-engine/src/main.rs @@ -7,9 +7,12 @@ use wasmtime::error::Context as _; use wasmtime::{Engine, Store}; use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; +// Both packages are listed explicitly so wit-parser can resolve the +// cross-package reference natively — no vendored deps/ tree needed. +// World name is fully qualified. wasmtime::component::bindgen!({ - path: "../../wit/shepherd-cow", - world: "shepherd", + path: ["../../wit/nexum-host", "../../wit/shepherd-cow"], + world: "shepherd:cow/shepherd", imports: { default: async }, exports: { default: async }, }); diff --git a/docs/migration/0.1-to-0.2.md b/docs/migration/0.1-to-0.2.md index 189e0cf..8a036ab 100644 --- a/docs/migration/0.1-to-0.2.md +++ b/docs/migration/0.1-to-0.2.md @@ -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. -- [ ] `just sync-wit && git diff --exit-code wit/` is clean if you vendor the runtime WIT under your own `deps/` tree. +- [ ] 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/justfile b/justfile index 89a66a1..3e01212 100644 --- a/justfile +++ b/justfile @@ -1,10 +1,5 @@ -# Sync WIT deps (copies nexum-host into shepherd-cow/deps) -sync-wit: - rm -rf wit/shepherd-cow/deps/nexum-host - cp -r wit/nexum-host wit/shepherd-cow/deps/nexum-host - -# Build the host runtime -build-runtime: sync-wit +# Build the host engine +build-engine: cargo build -p nexum-engine # Build the example WASM module @@ -12,15 +7,15 @@ build-module: cargo build --target wasm32-wasip2 --release -p example # Build everything -build: build-runtime build-module +build: build-engine build-module -# Build the module then run the runtime with it. The second argument is the +# Build the module then run the engine with it. The second argument is the # module's nexum.toml — without it the engine prints the 0.1-compat # deprecation warning and proceeds with empty capabilities/config. -run: build-module build-runtime +run: build-module build-engine cargo run -p nexum-engine -- target/wasm32-wasip2/release/example.wasm modules/example/nexum.toml # Check the entire workspace -check: sync-wit +check: cargo check --target wasm32-wasip2 -p example cargo check -p nexum-engine diff --git a/modules/example/src/lib.rs b/modules/example/src/lib.rs index 9472a5b..a008a3d 100644 --- a/modules/example/src/lib.rs +++ b/modules/example/src/lib.rs @@ -4,7 +4,7 @@ wit_bindgen::generate!({ path: "../../wit/nexum-host", - world: "event-module", + world: "nexum:host/event-module", }); use nexum::host::logging; diff --git a/wit/shepherd-cow/deps/nexum-host/chain.wit b/wit/shepherd-cow/deps/nexum-host/chain.wit deleted file mode 100644 index 574368b..0000000 --- a/wit/shepherd-cow/deps/nexum-host/chain.wit +++ /dev/null @@ -1,37 +0,0 @@ -package nexum:host@0.2.0; - -interface chain { - use types.{chain-id, host-error}; - - /// A single JSON-RPC request to be executed as part of a batch. - record rpc-request { - method: string, - params: string, - } - - /// Result of a single request inside a batch. Each entry is independent; - /// one failing call does not abort the others. - variant rpc-result { - ok(string), - err(host-error), - } - - /// Execute a JSON-RPC request against the specified chain. - /// - /// The host routes to its configured provider for the given chain, - /// applying whatever middleware is appropriate for the platform - /// (timeout, retry, rate-limit, fallback on server; simple HTTP - /// on mobile; window.ethereum or injected provider in WebView). - /// - /// `method` includes the namespace prefix (e.g. "eth_call"). - /// `params` and the success value are JSON-encoded strings. - request: func(chain-id: chain-id, method: string, params: string) - -> result; - - /// Execute several JSON-RPC requests against the same chain in a single - /// round trip where the host transport supports it. 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. - request-batch: func(chain-id: chain-id, requests: list) - -> result, host-error>; -} diff --git a/wit/shepherd-cow/deps/nexum-host/clock.wit b/wit/shepherd-cow/deps/nexum-host/clock.wit deleted file mode 100644 index b57408a..0000000 --- a/wit/shepherd-cow/deps/nexum-host/clock.wit +++ /dev/null @@ -1,13 +0,0 @@ -package nexum:host@0.2.0; - -/// Host-provided clock. Guest modules MUST use this rather than relying on -/// WASI clocks so the host can virtualise time during replay/testing. -interface clock { - /// Wall-clock time in milliseconds since the Unix epoch, UTC. - now-ms: func() -> u64; - - /// Monotonic timer in nanoseconds. The origin is unspecified; only - /// differences between successive calls are meaningful. Suitable for - /// measuring elapsed time without exposure to wall-clock jumps. - monotonic-ns: func() -> u64; -} diff --git a/wit/shepherd-cow/deps/nexum-host/event-module.wit b/wit/shepherd-cow/deps/nexum-host/event-module.wit deleted file mode 100644 index 30a1e99..0000000 --- a/wit/shepherd-cow/deps/nexum-host/event-module.wit +++ /dev/null @@ -1,24 +0,0 @@ -package nexum:host@0.2.0; - -/// Event-driven module — automation, background processing. -/// No UI capabilities. Runs on any conforming host. -world event-module { - use types.{config, event, host-error}; - - // Six core primitives (always provided by a conforming host). - import chain; - import identity; - import local-store; - import remote-store; - import messaging; - import logging; - - // Ambient host services (additive in 0.2). `http` is gated per-module by - // the `[capabilities.http].allow` allowlist in nexum.toml. - import clock; - import random; - import http; - - export init: func(config: config) -> result<_, host-error>; - export on-event: func(event: event) -> result<_, host-error>; -} diff --git a/wit/shepherd-cow/deps/nexum-host/http.wit b/wit/shepherd-cow/deps/nexum-host/http.wit deleted file mode 100644 index 0e12fd4..0000000 --- a/wit/shepherd-cow/deps/nexum-host/http.wit +++ /dev/null @@ -1,39 +0,0 @@ -package nexum:host@0.2.0; - -/// Generic HTTP client capability. Modules that need this MUST opt in via -/// their manifest; the host enforces an allow-list of destinations. -interface http { - use types.{host-error}; - - /// A single HTTP header. Header names are case-insensitive on the wire; - /// the host normalises them. - record header { - name: string, - value: string, - } - - record request { - /// HTTP method, e.g. "GET", "POST". - method: string, - /// Absolute URL (scheme + host + path + query). - url: string, - headers: list
, - /// Optional request body. Empty for methods like GET. - body: option>, - /// Optional per-request timeout in milliseconds. The host MAY clamp - /// this to its own configured maximum. - timeout-ms: option, - } - - record response { - status: u16, - headers: list
, - body: list, - } - - /// Perform a single HTTP request. Transport-level failures (DNS, TLS, - /// timeout, host policy rejection) surface as `host-error`; HTTP-level - /// non-2xx responses are returned as an `ok(response)` with the status - /// set accordingly so the caller can inspect headers/body. - fetch: func(req: request) -> result; -} diff --git a/wit/shepherd-cow/deps/nexum-host/identity.wit b/wit/shepherd-cow/deps/nexum-host/identity.wit deleted file mode 100644 index 6d62f7f..0000000 --- a/wit/shepherd-cow/deps/nexum-host/identity.wit +++ /dev/null @@ -1,24 +0,0 @@ -package nexum:host@0.2.0; - -/// Identity / signing capability. -/// -/// 0.2 ships a single, minimal interface. A future release (0.4+) is -/// expected to split this into `identity-read` and `identity-sign` and to -/// introduce a richer `signing-result` variant; for 0.2 the simple shape is -/// sufficient because user rejection can already be expressed via -/// `host-error { kind: denied, .. }`. -interface identity { - use types.{host-error}; - - /// Return the list of account addresses (20-byte EVM addresses) the host - /// is willing to sign for. Empty list means no signing capability. - accounts: func() -> result>, host-error>; - - /// Sign an arbitrary message with personal_sign semantics (prepends the - /// "\x19Ethereum Signed Message:\n" prefix). Returns a 65-byte signature. - sign: func(account: list, message: list) -> result, host-error>; - - /// Sign EIP-712 typed data. `typed-data` is a JSON-encoded EIP-712 payload. - /// Returns a 65-byte signature. - sign-typed-data: func(account: list, typed-data: string) -> result, host-error>; -} diff --git a/wit/shepherd-cow/deps/nexum-host/local-store.wit b/wit/shepherd-cow/deps/nexum-host/local-store.wit deleted file mode 100644 index 546dc0b..0000000 --- a/wit/shepherd-cow/deps/nexum-host/local-store.wit +++ /dev/null @@ -1,18 +0,0 @@ -package nexum:host@0.2.0; - -interface local-store { - use types.{host-error}; - - /// Get a value by key. Returns none if the key does not exist. - get: func(key: string) -> result>, host-error>; - - /// Set a key-value pair. Overwrites any existing value. - /// The host may enforce a size quota; if exceeded, returns err. - set: func(key: string, value: list) -> result<_, host-error>; - - /// Delete a key. No-op if the key does not exist. - delete: func(key: string) -> result<_, host-error>; - - /// List all keys matching a prefix. Empty prefix returns all keys. - list-keys: func(prefix: string) -> result, host-error>; -} diff --git a/wit/shepherd-cow/deps/nexum-host/logging.wit b/wit/shepherd-cow/deps/nexum-host/logging.wit deleted file mode 100644 index 37e9193..0000000 --- a/wit/shepherd-cow/deps/nexum-host/logging.wit +++ /dev/null @@ -1,15 +0,0 @@ -package nexum:host@0.2.0; - -interface logging { - enum level { - trace, - debug, - info, - warn, - error, - } - - /// Emit a structured log message. - /// The host decides how to handle it (stdout, file, discard). - log: func(level: level, message: string); -} diff --git a/wit/shepherd-cow/deps/nexum-host/messaging.wit b/wit/shepherd-cow/deps/nexum-host/messaging.wit deleted file mode 100644 index 0334834..0000000 --- a/wit/shepherd-cow/deps/nexum-host/messaging.wit +++ /dev/null @@ -1,19 +0,0 @@ -package nexum:host@0.2.0; - -interface messaging { - use types.{host-error, message}; - - /// Publish a message to a content topic. - /// - /// Content topics follow the format: //// - /// e.g. "/nexum/1/twap-updates/proto" - publish: func(content-topic: string, payload: list) -> result<_, host-error>; - - /// Query historical messages from the Waku store protocol. - query: func( - content-topic: string, - start-time: option, - end-time: option, - limit: option, - ) -> result, host-error>; -} diff --git a/wit/shepherd-cow/deps/nexum-host/query-module.wit b/wit/shepherd-cow/deps/nexum-host/query-module.wit deleted file mode 100644 index f261a8b..0000000 --- a/wit/shepherd-cow/deps/nexum-host/query-module.wit +++ /dev/null @@ -1,25 +0,0 @@ -package nexum:host@0.2.0; - -/// Query module — synchronous, side-effect-free evaluation. -/// -/// EXPERIMENTAL (0.2): the shape of this world is provisional and may -/// change in a future minor release without a major bump. Hosts and SDKs -/// should expect breakage here until the world is stabilised. -/// -/// A query module exposes a single pure `evaluate` entry point. It is given -/// read-only access to the local store (for cached/derived state) and to -/// logging; everything else (chain access, network, messaging, signing) is -/// deliberately excluded so the host can run queries inside a tight -/// deterministic sandbox. -world query-module { - use types.{config, host-error}; - - import local-store; - import logging; - - export init: func(config: config) -> result<_, host-error>; - - /// Evaluate the query. `input` and the returned bytes are opaque to the - /// host; the module and its caller agree on the encoding. - export evaluate: func(input: list) -> result, host-error>; -} diff --git a/wit/shepherd-cow/deps/nexum-host/random.wit b/wit/shepherd-cow/deps/nexum-host/random.wit deleted file mode 100644 index e37a67a..0000000 --- a/wit/shepherd-cow/deps/nexum-host/random.wit +++ /dev/null @@ -1,7 +0,0 @@ -package nexum:host@0.2.0; - -/// Cryptographically secure randomness from the host. -interface random { - /// Return `len` bytes of cryptographically secure random data. - fill: func(len: u32) -> list; -} diff --git a/wit/shepherd-cow/deps/nexum-host/remote-store.wit b/wit/shepherd-cow/deps/nexum-host/remote-store.wit deleted file mode 100644 index f68e32f..0000000 --- a/wit/shepherd-cow/deps/nexum-host/remote-store.wit +++ /dev/null @@ -1,33 +0,0 @@ -package nexum:host@0.2.0; - -interface remote-store { - use types.{host-error}; - - /// Upload raw data to the decentralised store. - /// Returns the 32-byte content reference (Swarm address). - upload: func(data: list) -> result, host-error>; - - /// Download raw data by 32-byte content reference. - download: func(reference: list) -> result, host-error>; - - /// Read the latest value from a mutable feed. - /// - /// Feeds are mutable pointers: (owner, topic) -> latest chunk. - /// `owner`: 20-byte Ethereum address of the feed owner. - /// `topic`: 32-byte topic hash. - read-feed: func( - owner: list, - topic: list, - ) -> result>, host-error>; - - /// Update a mutable feed with new data. - /// - /// The host signs the feed update with its configured identity. - /// `topic`: 32-byte topic hash. - /// `data`: the payload to publish. - /// Returns the 32-byte reference of the new chunk. - write-feed: func( - topic: list, - data: list, - ) -> result, host-error>; -} diff --git a/wit/shepherd-cow/deps/nexum-host/types.wit b/wit/shepherd-cow/deps/nexum-host/types.wit deleted file mode 100644 index 6a6def1..0000000 --- a/wit/shepherd-cow/deps/nexum-host/types.wit +++ /dev/null @@ -1,83 +0,0 @@ -package nexum:host@0.2.0; - -/// Common types shared across all runtime interfaces. -/// -/// All `u64` timestamps in this package are milliseconds since the Unix -/// epoch, UTC, unless otherwise noted (e.g. `clock::monotonic-ns` is -/// nanoseconds from an arbitrary monotonic origin). -interface types { - type chain-id = u64; - - record block { - chain-id: chain-id, - number: u64, - hash: list, - timestamp: u64, - } - - record log { - chain-id: chain-id, - address: list, - topics: list>, - data: list, - block-number: u64, - transaction-hash: list, - log-index: u32, - } - - /// A message delivered over the messaging interface. Defined here (rather - /// than only in `messaging.wit`) so the `event` variant can reference it - /// without a cross-interface use clause. - record message { - content-topic: string, - payload: list, - timestamp: u64, - /// Optional sender identity (protocol-dependent). - sender: option>, - } - - /// Fired by the host on a configured cadence. `fired-at` is the host's - /// wall-clock time (ms since Unix epoch, UTC) at which the tick was - /// generated. - record tick { - fired-at: u64, - } - - variant event { - block(block), - logs(list), - tick(tick), - message(message), - } - - /// Opaque config from nexum.toml [config] section. - type config = list>; - - /// Coarse categorisation of host-side failures. The kind is suitable for - /// programmatic dispatch by guests; `message` carries a human-readable - /// detail and `code` carries a domain-specific numeric (e.g. a JSON-RPC - /// error code, HTTP status, etc.). - variant host-error-kind { - unsupported, - unavailable, - denied, - rate-limited, - timeout, - invalid-input, - internal, - } - - /// Unified error returned by every host-imported function. - /// - /// `domain` is a short identifier for the originating subsystem - /// (e.g. "chain", "local-store", "remote-store", "messaging", - /// "identity", "http"). `data` is an optional opaque payload (often a - /// JSON-encoded blob). - record host-error { - domain: string, - kind: host-error-kind, - code: s32, - message: string, - data: option, - } -}