Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions crates/nexum-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ use quote::quote;
use syn::{ImplItem, ItemImpl, Type};

/// The handler names recognised on a `#[module]` impl. Any method not in
/// this set is left untouched on the type; any handler in the set that is
/// absent is treated as a no-op in the generated `on-event` dispatch.
/// this set is left untouched on the type, except that names starting
/// with `on_` are rejected at compile time (a typo'd handler would
/// otherwise silently never fire); any handler in the set that is absent
/// is treated as a no-op in the generated `on-event` dispatch.
const HANDLERS: [&str; 5] = ["init", "on_block", "on_chain_logs", "on_tick", "on_message"];

/// Generate the per-cdylib glue for a nexum module.
Expand All @@ -34,7 +36,12 @@ const HANDLERS: [&str; 5] = ["init", "on_block", "on_chain_logs", "on_tick", "on
/// (`Guest`, `Fault`, the `nexum::host::*` modules) lands at the module
/// crate root, so the emitted glue and the handler bodies resolve those
/// names there; the WIT directory is located by walking up from
/// `CARGO_MANIFEST_DIR`.
/// `CARGO_MANIFEST_DIR`. Two corollaries: the consuming crate must
/// declare `wit-bindgen` as a direct dependency (the emitted
/// `wit_bindgen::generate!` call resolves against the consumer's
/// namespace), and the crate root must not shadow std prelude names
/// such as `Result`, `Vec`, or `Ok` (wit-bindgen's generated `Guest`
/// trait refers to them unqualified).
#[proc_macro_attribute]
Comment on lines +42 to 45

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second corollary may be inaccurate: the macro's own quote! output already uses ::core::result::Result, ::std::vec::Vec, and ::core::result::Result::Ok — fully qualified paths — so shadowing those names in the crate root would not break the macro-emitted code. The constraint, if real, comes from the wit_bindgen::generate! expansion itself, not from "wit-bindgen's generated Guest trait" broadly. Worth verifying which names generate! actually emits unqualified before documenting this as a constraint; if it turns out the expansion is also hygienic, the corollary can be dropped.

Suggested change
/// namespace), and the crate root must not shadow std prelude names
/// such as `Result`, `Vec`, or `Ok` (wit-bindgen's generated `Guest`
/// trait refers to them unqualified).
#[proc_macro_attribute]
/// `CARGO_MANIFEST_DIR`. One corollary: the consuming crate must
/// declare `wit-bindgen` as a direct dependency (the emitted
/// `wit_bindgen::generate!` call resolves against the consumer's
/// namespace, not `nexum-macros`'s).

pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
if !attr.is_empty() {
Expand All @@ -57,6 +64,42 @@ pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
.to_compile_error()
.into();
}
if let Some((_, trait_path, _)) = &input.trait_ {
return syn::Error::new_spanned(
trait_path,
"#[nexum_sdk::module] must be applied to an inherent impl, not a trait impl",
)
.to_compile_error()
.into();
}
if !input.generics.params.is_empty() {
return syn::Error::new_spanned(
&input.generics,
"#[nexum_sdk::module] must be applied to a non-generic impl",
)
.to_compile_error()
.into();
}

// A typo'd handler (`on_blocks`, `on_chainlogs`, ...) would otherwise
// compile as an ordinary helper while its event silently no-ops, so
// reserve the `on_` prefix for the recognised handler set.
for item in &input.items {
if let ImplItem::Fn(f) = item {
let name = f.sig.ident.to_string();
if name.starts_with("on_") && !HANDLERS.contains(&name.as_str()) {
return syn::Error::new_spanned(
&f.sig.ident,
format!(
"`{name}` is not a recognised #[nexum_sdk::module] handler; expected one \
of {HANDLERS:?} (rename helpers so they do not start with `on_`)"
),
Comment on lines +93 to +96

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error tells the user to rename, but the lower-friction fix is to move the helper to a separate impl block — the macro only inspects the single attributed impl. A user with a well-named on_retry_cooldown_expired helper would prefer that over renaming.

Suggested change
format!(
"`{name}` is not a recognised #[nexum_sdk::module] handler; expected one \
of {HANDLERS:?} (rename helpers so they do not start with `on_`)"
),
format!(
"`{name}` is not a recognised #[nexum_sdk::module] handler; \
expected one of {HANDLERS:?}. Move helpers that start with \
`on_` to a separate `impl` block, or rename them."
),

)
.to_compile_error()
.into();
}
}
}

let present: Vec<&str> = input
.items
Expand All @@ -69,6 +112,15 @@ pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
_ => None,
})
.collect();
if present.is_empty() {
return syn::Error::new_spanned(
self_ty,
"#[nexum_sdk::module] found no recognised handlers on this impl; define at least one \
of `init`, `on_block`, `on_chain_logs`, `on_tick`, `on_message`",
)
.to_compile_error()
.into();
}
let has = |name: &str| present.contains(&name);

let (nexum_wit, shepherd_wit) = match locate_wit() {
Expand Down
28 changes: 18 additions & 10 deletions docs/05-sdk-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ things from the SDK:
`nexum:host/event-module` (or the CoW-extended `shepherd:cow/shepherd`
world): react to blocks, chain logs, ticks, or messages; read and
write local state; submit orders. This persona is served today by
`nexum-sdk` (+ the `#[nexum::module]` macro) and, for CoW-specific
modules, `shepherd-sdk` on top.
`nexum-sdk` (+ the `#[nexum::module]` macro, spelled
`#[nexum_sdk::module]` in code) and, for CoW-specific modules,
`shepherd-sdk` on top.

2. **Venue adapter author.** Writes an adapter that exposes a trading
venue (CoW Protocol, a DEX, a lending market, ...) to modules
Expand Down Expand Up @@ -61,7 +62,8 @@ nexum-sdk/
├── config.rs # (key, value) config-table lookups, decimal scaling
├── address.rs # EVM address parsing with typed errors
├── http.rs # Fetch trait seam, WasiFetch, FetchError (wasi:http)
└── tracing.rs # guest tracing facade + panic hook over a LogSink seam
├── tracing.rs # guest tracing facade + panic hook over a LogSink seam
└── proptests.rs # cfg(test) property tests (not part of the public surface)

nexum-macros/
├── Cargo.toml # proc-macro = true
Expand All @@ -74,8 +76,9 @@ shepherd-sdk/
├── lib.rs # crate docs; no re-export of nexum-sdk
├── prelude.rs # cowprotocol order/signing/orderbook re-exports
├── wit_bindgen_macro.rs # bind_cow_host_via_wit_bindgen! - layers CowApiHost onto WitBindgenHost
└── cow/ # CowApiHost trait, gpv2_to_order_data, PollOutcome, decode_revert,
# RetryAction classifiers, run() (poll -> gate/journal/submit)
├── cow/ # CowApiHost trait, gpv2_to_order_data, PollOutcome, decode_revert,
│ # RetryAction classifiers, run() (poll -> gate/journal/submit)
└── proptests.rs # cfg(test) property tests (not part of the public surface)
```

`nexum-sdk` is host-neutral and domain-free: any module targeting the
Expand Down Expand Up @@ -116,7 +119,8 @@ pub trait Host: ChainHost + LocalStoreHost + LoggingHost {}
impl<T: ChainHost + LocalStoreHost + LoggingHost> Host for T {}
```

`shepherd-sdk` adds a fourth trait, `CowApiHost` (`submit_order`), and
`shepherd-sdk` adds a fourth trait, `CowApiHost` (`submit_order`,
`cow_api_request`), and
its own supertrait `CowHost: Host + CowApiHost`. Strategy code takes
`&impl Host` (or a narrower `<H: ChainHost + LocalStoreHost>` bound
when it only needs part of the surface) so tests inject
Expand Down Expand Up @@ -223,9 +227,13 @@ in `nexum-sdk-test`; `shepherd-sdk-test` adds a `cow_api` field on the
per-call-scriptable `MockVenue` via `MockHost::with_venue()`), each
recording calls and letting tests program responses. Tests run as
plain native Rust against the traits - no `wasm32-wasip2` target, no
wasmtime instance, no network round-trip. This is the whole testing
story today; there is no `WasmTestHarness` or component-level
integration harness in the tree.
wasmtime instance, no network round-trip. This is the whole SDK-side
testing story today. (The runtime crate separately ships a
feature-gated component-level harness - `nexum-runtime`'s
`test_utils::TestRuntime`, behind the `test-utils` feature - that
loads a compiled `.wasm` plus manifest under real wasmtime and
dispatches events to it; that is runtime-internal tooling, not part
of the module-author SDK contract.)

## Venue-adapter persona (planned)

Expand Down Expand Up @@ -287,7 +295,7 @@ of it, not a requirement.
error model (`Fault`, `ChainError`, `CowApiError`) the host traits
return.
- [Migration guide §7](migration/0.1-to-0.2.md#7-sdk-changes-author) -
the 0.1 -> 0.2 SDK rename table.
what changed in the SDK surface between 0.1 and 0.2.
- [doc 07](07-rpc-namespace-design.md) - the `chain` RPC passthrough
design and why module authors call `host.request` directly rather
than through an injected provider.
46 changes: 29 additions & 17 deletions docs/migration/0.1-to-0.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,23 +404,35 @@ The Rust API surface is otherwise unchanged in 0.2. The C ABI and `nexum-host` e

### 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) | `Fault` + `HostFault` / `ChainError` | Per-interface typed errors over the shared vocabulary; 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`.
0.1 had no Rust SDK crate: modules called the wit-bindgen-generated
imports directly and hand-wrote the per-cdylib `Guest` / `export!`
glue. 0.2 ships `nexum-sdk` (host-neutral helpers) with
`shepherd-sdk` layering the CoW Protocol surface on top. What that
means for a module you are porting:

- **Host traits.** Strategy code is written against the
`ChainHost` / `LocalStoreHost` / `LoggingHost` traits (plus
`CowApiHost` in `shepherd-sdk`) instead of the raw wit-bindgen
import shims; the `bind_host_via_wit_bindgen!` /
`bind_cow_host_via_wit_bindgen!` macros generate the adapter at
the cdylib boundary, and the `nexum-sdk-test` /
`shepherd-sdk-test` mocks slot in for native unit tests.
- **Typed errors.** Handlers and host traits use the `Fault` /
`ChainError` / `CowApiError` vocabulary from §2 in place of 0.1's
bare strings.
- **One attribute macro.** `#[nexum_sdk::module]` (a re-export of
`nexum_macros::module`) replaces the hand-written glue: applied to
an inherent `impl` block of named handlers (`init`, `on_block`,
`on_chain_logs`, `on_tick`, `on_message`), it emits the
`wit_bindgen::generate!` call, the host adapter, the `Guest` impl
dispatching to the handlers present, and `export!`. Handlers are
synchronous `fn`s; there is no `async` support, no `block_on`, and
no separate `#[shepherd::module]`.

Earlier drafts of this section described a richer 0.2 surface

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Earlier drafts" is ambiguous — it reads as earlier commits of this file (stale docs) rather than features that were planned and cut. A reader who wrote code against those APIs gets a different message from each reading.

Suggested change
Earlier drafts of this section described a richer 0.2 surface
This section previously described a richer 0.2 surface

(`provider()`, `Signer`, `Messaging`, `RemoteStore`, a hidden
`HostTransport`); none of that shipped. See
[doc 05](../05-sdk-design.md) for the SDK that exists.

### Non-Rust SDKs

Expand Down
25 changes: 20 additions & 5 deletions docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,34 @@

`nexum-sdk` is the guest-side library every module consumes: typed
primitives, ABI helpers, an effect-trait seam for testing, the
per-module adapter macro, and a `prelude` that keeps boilerplate out
of module crates. `shepherd-sdk` layers the CoW Protocol surface on
top; modules that touch the orderbook depend on both crates and
import each directly (nothing is re-exported between them).
`#[nexum_sdk::module]` attribute macro and per-module adapter macro,
and a `prelude` that keeps boilerplate out of module crates.
`shepherd-sdk` layers the CoW Protocol surface on top; modules that
touch the orderbook depend on both crates and import each directly
(nothing is re-exported between them).

This page is the entry point. The full API reference is the rustdoc
site under `target/doc/nexum_sdk/` and `target/doc/shepherd_sdk/`,
generated by:

```sh
RUSTDOCFLAGS="-D warnings -D missing-docs" cargo doc -p shepherd-sdk -p nexum-sdk --no-deps --open
RUSTDOCFLAGS="-D warnings -D missing-docs" cargo doc -p shepherd-sdk -p nexum-sdk -p nexum-macros --no-deps --open
```

## Authoring a module

Modules are authored with the `#[nexum_sdk::module]` attribute
(re-exported from `nexum-macros`). Apply it to an inherent `impl`
block whose associated functions are the named event handlers -
`init`, `on_block`, `on_chain_logs`, `on_tick`, `on_message` - each
taking its wit-bindgen payload and returning `Result<(), Fault>`.
The macro generates the rest of the per-cdylib glue: the
`wit_bindgen::generate!` call, the `bind_host_via_wit_bindgen!()`
adapter, a `Guest` impl whose `on_event` dispatches to whichever
handlers are present (absent handlers no-op), and `export!`. See
[doc 05](05-sdk-design.md#the-nexummodule-macro) for a worked
example and the `nexum-macros` rustdoc for the fine print.

## Supported host capabilities

The SDK is host-neutral - it does not call wit-bindgen-generated
Expand Down
Loading