feat(venue-sdk): add nexum-venue-sdk crate#251
Conversation
be70b63 to
0a3398a
Compare
0a3398a to
6f2bdbb
Compare
6f2bdbb to
6806824
Compare
6806824 to
0e4c79b
Compare
0e4c79b to
233e9fc
Compare
233e9fc to
7593566
Compare
7593566 to
ce4648a
Compare
ce4648a to
d074cf6
Compare
d074cf6 to
de679c3
Compare
de679c3 to
9de7450
Compare
9de7450 to
eaebfaf
Compare
eaebfaf to
aa15304
Compare
aa15304 to
9b7a402
Compare
9b7a402 to
f1f8871
Compare
f1f8871 to
349d31b
Compare
560688d to
e19251f
Compare
e19251f to
d2fd2b1
Compare
d2fd2b1 to
be53811
Compare
be53811 to
2cf2cf9
Compare
2cf2cf9 to
24bc04f
Compare
24bc04f to
77688fc
Compare
77688fc to
aa0304e
Compare
aa0304e to
6db2785
Compare
6db2785 to
f757151
Compare
f757151 to
ca3b167
Compare
e7a8560 to
b75198e
Compare
2ef07d6 to
52775b4
Compare
52775b4 to
1d9b27a
Compare
lgahdl
left a comment
There was a problem hiding this comment.
Clean SDK design and good test coverage overall. Three items below.
| /// 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 |
There was a problem hiding this comment.
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.
| /// 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>; |
| /// The pool or the venue behind it failed the call. The payload is | ||
| /// the wire `venue-error`, which carries no `Display`; format via | ||
| /// `Debug`. |
There was a problem hiding this comment.
"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.
| /// 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), |
| 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(_) | ||
| )); | ||
| } |
There was a problem hiding this comment.
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:
| 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(_) | |
| )); | |
| } |
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.
1d9b27a to
aaaa9e1
Compare
Summary
Ships
crates/nexum-venue-sdk(single commit34077c1), the venue-side SDKthat generates the
nexum:adapter/venue-adapterworld bindings oncein-crate and layers the
VenueAdaptertrait, the borshIntentBodycodec, and the typed intent client core on top.
Closes #230
Changes
crates/nexum-venue-sdk(new workspace member): generates thenexum:adapter/venue-adapterworld bindings once, inbindings.rs, viawit_bindgen::generate!withpub_export_macro: trueand anadditional
PartialEqderive.VenueAdaptertrait (adapter.rs) mirroring the world's exportface (
init,derive_header,submit,status,cancel), plusexport_venue_adapter!which turns an impl into the component's exportglue for the adapter's cdylib.
IntentBodycodec: theIntentBodytrait and typedBodyErrorinbody.rs(Empty,UnknownVersion { version },Malformed { version, detail }), with the wire form being the borshenum layout (one-byte tag = declaration index, then the borsh payload).
#[derive(IntentBody)]incrates/nexum-macros(intent_body.rs,wired into
lib.rs), re-exported at the SDK root, generating thetag/version handling so an unknown tag decodes as the typed
BodyError::UnknownVersionrather than a stringly borsh error.client.rs):IntentPoolas thebyte-level seam and
IntentClient<P>binding one venue, encodingrequest/response bodies through
IntentBody.transport.rs,faults.rs):From<host::Fault> for Fault,From<host::Fault> for VenueError, andFrom<nexum_sdk::http::FetchError> for VenueError.tests/adapter.rs).Cargo.toml: add thecrates/nexum-venue-sdkmember and hoistborshto a workspace dependency;Cargo.lockupdated accordingly.Test plan
Independent verification of
feat/m1-venue-sdk(single commit34077c1,stacked on
origin/feat/m1-pool-router), all gates run insidenix develop(rustc 1.94.0), warm target,--locked. Nowit/changes inrange, so the wasmtime bindgen smoke-compile requirement does not apply.
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspaceNotes for reviewer
Adversarial red-team of this PR found no blocking or fixable defects:
every generated type matches the WIT contracts field-for-field, fold arms
are exhaustive where claimed (with
non_exhaustivewildcards only whererequired), and the derive's tag/version handling is sound, including the
256-variant boundary.
Stacked on feat/m1-pool-router; merges bottom-up after the fault train
lands; retarget to develop then.