Skip to content

feat(venue-sdk): add nexum-venue-sdk crate#251

Merged
mfw78 merged 2 commits into
dev/m1from
feat/m1-venue-sdk
Jul 15, 2026
Merged

feat(venue-sdk): add nexum-venue-sdk crate#251
mfw78 merged 2 commits into
dev/m1from
feat/m1-venue-sdk

Conversation

@mfw78

@mfw78 mfw78 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

Adds crates/nexum-venue-sdk, the venue-side SDK that generates the
nexum:adapter/venue-adapter world bindings once in-crate (bindings.rs,
via wit_bindgen::generate! with pub_export_macro: true and an
additional PartialEq derive) and layers the following on top:

  • The VenueAdapter trait (adapter.rs), mirroring the world's export
    face (init, derive_header, submit, status, cancel), plus
    export_venue_adapter! to turn an impl into the component's export
    glue for the adapter's cdylib.
  • The borsh IntentBody codec (body.rs): the IntentBody trait and
    typed BodyError (Empty, UnknownVersion { version },
    Malformed { version, detail }), wire form is the borsh enum layout
    (one-byte tag = declaration index, then the borsh payload).
  • #[derive(IntentBody)] in crates/nexum-macros (intent_body.rs,
    wired into lib.rs), re-exported at the SDK root, generating the
    tag/version handling so an unknown tag decodes as the typed
    BodyError::UnknownVersion rather than a stringly borsh error.
  • The typed intent client core (client.rs): IntentPool as the
    byte-level seam and IntentClient<P> binding one venue, encoding
    request/response bodies through IntentBody.
  • Typed transport wrappers and fault conversions (transport.rs,
    faults.rs): From<host::Fault> for Fault, From<host::Fault> for VenueError, and From<nexum_sdk::http::FetchError> for VenueError.
  • Crate-level integration tests (tests/adapter.rs).
  • Root Cargo.toml: adds the crates/nexum-venue-sdk member, hoists
    borsh to a workspace dependency; Cargo.lock updated accordingly.

Why

Venues need a typed, borsh-backed intent codec and a thin adapter trait
over the nexum:adapter/venue-adapter world so integrations get
compile-time contract checking against the WIT definitions instead of
stringly decode errors, and an unknown intent-body version fails
cleanly as a typed BodyError rather than an opaque borsh error.

Testing

All gates run inside nix develop (rustc 1.94.0), --locked. No wit/
changes in range, so the wasmtime bindgen smoke-compile requirement does
not apply.

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace

Adversarial red-team review found no blocking or fixable defects: every
generated type matches the WIT contracts field-for-field, fold arms are
exhaustive where claimed (with non_exhaustive wildcards only where
required), and the derive's tag/version handling is sound, including the
256-variant boundary.

AI Assistance

Claude (Opus) used for the implementation.

Closes #230

@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from be70b63 to 0a3398a Compare July 7, 2026 23:12
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 34077c1 to 503ea26 Compare July 7, 2026 23:12
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 0a3398a to 6f2bdbb Compare July 8, 2026 00:42
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 503ea26 to 9b37071 Compare July 8, 2026 00:42
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 6f2bdbb to 6806824 Compare July 8, 2026 01:19
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 9b37071 to 51eb32c Compare July 8, 2026 01:19
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 6806824 to 0e4c79b Compare July 8, 2026 01:44
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 51eb32c to e519e54 Compare July 8, 2026 01:44
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 0e4c79b to 233e9fc Compare July 8, 2026 02:11
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from e519e54 to 3fbb66b Compare July 8, 2026 02:11
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 233e9fc to 7593566 Compare July 8, 2026 02:20
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 3fbb66b to 277be91 Compare July 8, 2026 02:20
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 7593566 to ce4648a Compare July 8, 2026 02:23
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 277be91 to 6235e9a Compare July 8, 2026 02:23
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from ce4648a to d074cf6 Compare July 8, 2026 03:03
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 6235e9a to ac6cecf Compare July 8, 2026 03:03
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from d074cf6 to de679c3 Compare July 8, 2026 04:59
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from ac6cecf to 7b9e72d Compare July 8, 2026 04:59
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from de679c3 to 9de7450 Compare July 8, 2026 05:31
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 7b9e72d to c2ecccb Compare July 8, 2026 05:31
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 9de7450 to eaebfaf Compare July 8, 2026 06:21
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from c2ecccb to a13db1d Compare July 8, 2026 06:21
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from eaebfaf to aa15304 Compare July 8, 2026 06:40
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from a13db1d to b272899 Compare July 8, 2026 06:40
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from aa15304 to 9b7a402 Compare July 8, 2026 07:07
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from b272899 to 5725d00 Compare July 8, 2026 07:07
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 9b7a402 to f1f8871 Compare July 8, 2026 07:21
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 5725d00 to 9176d60 Compare July 8, 2026 07:21
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from f1f8871 to 349d31b Compare July 8, 2026 07:36
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 9176d60 to 23e6e3c Compare July 8, 2026 07:36
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 59a0937 to bde0251 Compare July 8, 2026 08:55
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from e19251f to d2fd2b1 Compare July 8, 2026 09:02
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from bde0251 to 28da180 Compare July 8, 2026 09:02
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from d2fd2b1 to be53811 Compare July 8, 2026 09:19
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 28da180 to 44a33a6 Compare July 8, 2026 09:19
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from be53811 to 2cf2cf9 Compare July 8, 2026 12:10
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 44a33a6 to 6a63dad Compare July 8, 2026 12:10
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 2cf2cf9 to 24bc04f Compare July 8, 2026 12:20
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 6a63dad to 26e4eac Compare July 8, 2026 12:20
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 24bc04f to 77688fc Compare July 8, 2026 12:50
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 26e4eac to 6da73e3 Compare July 8, 2026 12:50
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 77688fc to aa0304e Compare July 8, 2026 13:41
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from 6da73e3 to b1204a5 Compare July 8, 2026 13:41
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from aa0304e to 6db2785 Compare July 8, 2026 13:59
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from f76b9dc to b272be9 Compare July 8, 2026 13:59
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 6db2785 to f757151 Compare July 8, 2026 14:09
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from b272be9 to b9e0061 Compare July 8, 2026 14:09
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from f757151 to ca3b167 Compare July 8, 2026 22:33
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch 3 times, most recently from e7a8560 to b75198e Compare July 8, 2026 23:24
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 2ef07d6 to 52775b4 Compare July 8, 2026 23:24
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from b75198e to ac9227f Compare July 8, 2026 23:29
@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 52775b4 to 1d9b27a Compare July 8, 2026 23:29

@lgahdl lgahdl left a comment

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.

Clean SDK design and good test coverage overall. Three items below.

Comment on lines +24 to +26
/// Project an opaque intent body onto the stable header guard
/// policy runs on. Must be a pure derivation: no transport, no side
/// effects, so the host can inspect a header before deciding to

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.

Same purity-claim gap as PR #249 — this doc says "Must be a pure derivation: no transport, no side effects" but the venue-adapter world unconditionally imports nexum:host/chain and nexum:host/messaging, so nothing in the WIT or the generated export shim prevents derive_header from calling them. The host router calls derive_header before running the guard, so any side effects happen before policy runs.

Suggested change
/// Project an opaque intent body onto the stable header guard
/// policy runs on. Must be a pure derivation: no transport, no side
/// effects, so the host can inspect a header before deciding to
/// Project an opaque intent body onto the stable header guard
/// policy runs on. By convention a pure derivation — no transport, no
/// side effects — so the host can inspect a header before deciding to
/// submit; the WIT cannot enforce this, as chain and messaging imports
/// remain available inside `derive-header`.
fn derive_header(body: Vec<u8>) -> Result<IntentHeader, VenueError>;

Comment on lines +88 to +90
/// The pool or the venue behind it failed the call. The payload is
/// the wire `venue-error`, which carries no `Display`; format via
/// `Debug`.

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.

"carries no Display" is inaccurate. VenueError appears in the error slot of all four nexum:intent/adapter results, so wit-bindgen 0.58 sets TypeInfo::error = true and generates impl Display for VenueError (delegates to Debug). The {0:?} format is still correct — the generated Display and Debug produce the same output — but the stated reason is wrong.

Suggested change
/// The pool or the venue behind it failed the call. The payload is
/// the wire `venue-error`, which carries no `Display`; format via
/// `Debug`.
/// The pool or the venue behind it failed the call. The wire
/// `venue-error`'s generated `Display` delegates to `Debug`, so
/// `{0:?}` gives the richer output.
#[error("venue error: {0:?}")]
Venue(VenueError),

Comment on lines +117 to +130
fn transport_fault_folds_to_venue_error_by_shape() {
assert_eq!(
VenueError::from(host::Fault::Denied("nope".into())),
VenueError::Denied("nope".into()),
);
assert!(matches!(
VenueError::from(host::Fault::Timeout),
VenueError::Unavailable(_)
));
assert!(matches!(
VenueError::from(host::Fault::InvalidInput("bug".into())),
VenueError::InternalError(_)
));
}

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 RateLimited arm in From<host::Fault> for VenueError silently drops retry_after_ms — the caller loses the backoff hint and gets a plain string. That's the only arm where structured data is destroyed rather than threaded through, making it the most likely regression target. Adding it to this test would catch a future refactor that accidentally routes it elsewhere:

Suggested change
fn transport_fault_folds_to_venue_error_by_shape() {
assert_eq!(
VenueError::from(host::Fault::Denied("nope".into())),
VenueError::Denied("nope".into()),
);
assert!(matches!(
VenueError::from(host::Fault::Timeout),
VenueError::Unavailable(_)
));
assert!(matches!(
VenueError::from(host::Fault::InvalidInput("bug".into())),
VenueError::InternalError(_)
));
}
#[test]
fn transport_fault_folds_to_venue_error_by_shape() {
assert_eq!(
VenueError::from(host::Fault::Denied("nope".into())),
VenueError::Denied("nope".into()),
);
assert!(matches!(
VenueError::from(host::Fault::Timeout),
VenueError::Unavailable(_)
));
assert!(matches!(
VenueError::from(host::Fault::RateLimited(host::RateLimit { retry_after_ms: Some(500) })),
VenueError::Unavailable(_) // retry_after_ms is intentionally dropped here
));
assert!(matches!(
VenueError::from(host::Fault::InvalidInput("bug".into())),
VenueError::InternalError(_)
));
}

@mfw78
mfw78 force-pushed the feat/m1-pool-router branch from 1d9b27a to aaaa9e1 Compare July 9, 2026 02:09
@mfw78
mfw78 force-pushed the feat/m1-venue-sdk branch from ac9227f to 959404f Compare July 9, 2026 02:09
mfw78 and others added 2 commits July 15, 2026 00:35
Introduce the venue-author persona crate: the VenueAdapter trait and
export macro over an in-crate bindgen of the adapter world, the borsh
IntentBody derive whose outer version enum fails unknown tags typedly,
the typed intent client core over the byte-level pool seam, and typed
wrappers over the scoped chain, messaging, and wasi:http imports. The
derive expansion lands in nexum-macros and re-exports from the SDK;
borsh enters the workspace dependency table.
The SDK RpcError.data is Bytes; the wit-bindgen chain-error carries
Vec<u8>, so wrap it (Vec -> Bytes is O(1)) when reprojecting.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-train Part of the current stacked PR train; lands in sequence, do not merge out of order.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants