From deadc1c43cb442a564f857737e67db647810955f Mon Sep 17 00:00:00 2001 From: mfw78 Date: Tue, 7 Jul 2026 00:08:27 +0000 Subject: [PATCH] feat(wit): author the nexum:intent package with the pool interface Add the venue-neutral intent ontology at 0.1.0 over the value-flow vocabulary: the intent-header (gives/wants/valid-until/settlement/ authorisation), the auth-scheme variant (eip712, eip1271, presign, offchain-sig, unsigned), the intent-status lifecycle with a structured fail-reason, the opaque receipt alias, and a self-contained venue-error (deliberately not embedding the host fault type, so the package's freeze cadence is not pinned to nexum:host versioning). The pool interface routes submit/status/cancel by a venue string with the body as opaque bytes. submit returns a submit-outcome variant from day one: accepted(receipt), or requires-signing(unsigned-tx) for venues whose settlement is a host-signed on-chain call. The unsigned-tx record only describes the call (chain, to, value, input); the host routes it through the guard's host-signed class and fills gas and fees, so adapters still structurally cannot move value. Identifier hygiene follows the value-flow rule (internal-error rather than internal, a Swift declaration keyword). A wasmtime bindgen smoke, compiled under test in the host crate through a throwaway world that imports the pool interface, names every generated type, case, and field by its plain Rust spelling and pins the three pool function signatures with a dummy host impl. --- crates/nexum-runtime/src/bindings.rs | 98 ++++++++++++++++++++ wit/nexum-intent/pool.wit | 26 ++++++ wit/nexum-intent/types.wit | 132 +++++++++++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 wit/nexum-intent/pool.wit create mode 100644 wit/nexum-intent/types.wit diff --git a/crates/nexum-runtime/src/bindings.rs b/crates/nexum-runtime/src/bindings.rs index 008ed56b..23d8776b 100644 --- a/crates/nexum-runtime/src/bindings.rs +++ b/crates/nexum-runtime/src/bindings.rs @@ -66,3 +66,101 @@ mod value_flow_smoke { assert!(amount.amount.is_empty()); } } + +/// Bindgen smoke for the `nexum:intent` package, mirroring the value-flow +/// smoke above: no host consumer exists yet (the pool router lands later), +/// so the package compiles under test only, through a throwaway world that +/// imports the pool interface and, transitively, the types interface and +/// its value-flow dependency. The test names every generated type, case, +/// and field by its plain Rust spelling, and a dummy `pool` host impl pins +/// the three function signatures, so a keyword collision or an accidental +/// signature change fails this build rather than a downstream binding. +#[cfg(test)] +mod intent_smoke { + wasmtime::component::bindgen!({ + inline: " + package nexum:intent-smoke; + world smoke { + import nexum:intent/pool@0.1.0; + } + ", + path: ["../../wit/nexum-value-flow", "../../wit/nexum-intent"], + }); + + use nexum::intent::types::{ + AuthScheme, FailReason, IntentHeader, IntentStatus, SubmitOutcome, UnsignedTx, VenueError, + }; + use nexum::value_flow::types::Settlement; + + struct DummyPool; + + impl nexum::intent::pool::Host for DummyPool { + fn submit(&mut self, _venue: String, _body: Vec) -> Result { + Err(VenueError::UnknownVenue) + } + + fn status( + &mut self, + _venue: String, + _receipt: Vec, + ) -> Result { + Err(VenueError::UnknownVenue) + } + + fn cancel(&mut self, _venue: String, _receipt: Vec) -> Result<(), VenueError> { + Err(VenueError::UnknownVenue) + } + } + + #[test] + fn identifiers_bind_unescaped() { + use nexum::intent::pool::Host; + + let _ = AuthScheme::Eip712; + let _ = AuthScheme::Eip1271; + let _ = AuthScheme::Presign; + let _ = AuthScheme::OffchainSig; + let _ = AuthScheme::Unsigned; + + let header = IntentHeader { + gives: Vec::new(), + wants: Vec::new(), + valid_until: None, + settlement: Settlement::EvmChain(1), + authorisation: AuthScheme::Eip712, + }; + assert!(header.gives.is_empty() && header.wants.is_empty()); + + let _ = IntentStatus::Pending; + let _ = IntentStatus::Open; + let _ = IntentStatus::Settled(None); + let _ = IntentStatus::Failed(FailReason { + code: String::new(), + detail: String::new(), + }); + let _ = IntentStatus::Expired; + let _ = IntentStatus::Cancelled; + + let tx = UnsignedTx { + chain_id: 1, + to: Vec::new(), + value: Vec::new(), + input: Vec::new(), + }; + let _ = SubmitOutcome::Accepted(Vec::new()); + let _ = SubmitOutcome::RequiresSigning(tx); + + let _ = VenueError::InvalidBody(String::new()); + let _ = VenueError::InvalidReceipt; + let _ = VenueError::Rejected(String::new()); + let _ = VenueError::Denied(String::new()); + let _ = VenueError::Unsupported(String::new()); + let _ = VenueError::Unavailable(String::new()); + let _ = VenueError::InternalError(String::new()); + + let mut pool = DummyPool; + assert!(pool.submit(String::new(), Vec::new()).is_err()); + assert!(pool.status(String::new(), Vec::new()).is_err()); + assert!(pool.cancel(String::new(), Vec::new()).is_err()); + } +} diff --git a/wit/nexum-intent/pool.wit b/wit/nexum-intent/pool.wit new file mode 100644 index 00000000..7655292a --- /dev/null +++ b/wit/nexum-intent/pool.wit @@ -0,0 +1,26 @@ +package nexum:intent@0.1.0; + +/// The strategy-module face of the intent core. The host is a router plus +/// a policy checkpoint: it resolves the venue id to the installed adapter, +/// has the adapter derive the header, runs guard policy on it, and only +/// then forwards the call. Bodies are opaque bytes at this boundary; typing +/// is recovered guest-side by venue SDK crates and host-side by the +/// adapter's header derivation. Bodies carry their own routing: there is no +/// chain parameter, a multichain venue's body schema names the chain and +/// the derived header's settlement field exposes the choice to policy. +interface pool { + use types.{intent-status, receipt, submit-outcome, venue-error}; + + /// Submit an opaque intent body to the named venue. Success is either + /// the venue's receipt or requires-signing: a transaction the host + /// must sign and send before the intent exists. + submit: func(venue: string, body: list) -> result; + + /// Report where a previously submitted intent is in its life. + status: func(venue: string, receipt: receipt) -> result; + + /// Ask the venue to withdraw an intent. Success means the venue + /// accepted the cancellation, not that settlement can no longer + /// happen: an already in-flight settlement may still win the race. + cancel: func(venue: string, receipt: receipt) -> result<_, venue-error>; +} diff --git a/wit/nexum-intent/types.wit b/wit/nexum-intent/types.wit new file mode 100644 index 00000000..22472e7b --- /dev/null +++ b/wit/nexum-intent/types.wit @@ -0,0 +1,132 @@ +package nexum:intent@0.1.0; + +/// The venue-neutral intent ontology: what a deal gives and wants, how the +/// venue authorizes it, and how its life at the venue is reported. Built on +/// the nexum:value-flow vocabulary so a submission is described the same +/// way to strategy modules, venue adapters, and guard policy. The package +/// deliberately does not depend on nexum:host: embedding the host fault +/// type in venue-error would pin this contract's freeze cadence to host +/// versioning, so venue-error carries its own transport cases. +/// +/// Identifier hygiene follows the value-flow rule: every id is checked +/// against WIT keywords and the reserved words of the nine binding-target +/// languages, preferring a two-word kebab id wherever a single word is a +/// keyword anywhere (`unsigned` not `none`, a Python keyword; +/// `internal-error` not `internal`, a Swift declaration keyword). +interface types { + use nexum:value-flow/types@0.1.0.{asset-amount, settlement}; + + /// How an intent is authorized at its venue. + variant auth-scheme { + /// An EIP-712 typed-data signature by host-held keys. The only + /// scheme that reaches the identity checkpoint at submit time. + eip712, + /// An EIP-1271 contract signature: consent happened on-chain when + /// the commitment was created, so the host signs nothing here. + eip1271, + /// A pre-signed authorization recorded at the settlement contract + /// ahead of submission. + presign, + /// A venue-defined off-chain signature scheme. + offchain-sig, + /// No authorization travels with the body. + unsigned, + } + + /// The adapter-derived description of an intent body: the stable + /// ontology guard policy runs on. Policy has teeth on `gives` (what + /// leaves the user's control); `wants` is display-grade because the + /// host can rarely verify the counterparty's obligation and must not + /// pretend to. + record intent-header { + /// Value leaving the user's control. + gives: list, + /// Value expected in return. Display-grade, not host-verified. + wants: list, + /// Expiry in milliseconds since the Unix epoch, UTC; absent means + /// the venue's own default lifetime applies. + valid-until: option, + /// Where the deal settles. + settlement: settlement, + /// How the venue authorizes the intent. + authorisation: auth-scheme, + } + + /// Venue-scoped stable identifier for a submitted intent (for CoW, + /// the 56-byte order UID). Opaque to the host and to policy. + type receipt = list; + + /// Why an intent failed terminally, as reported by the venue. + record fail-reason { + /// Venue-scoped machine-readable code, stable enough for a + /// module to match on. + code: string, + /// Human-readable detail for logs and the consent surface. + detail: string, + } + + /// Where an intent is in its life at the venue. + variant intent-status { + /// Accepted for processing but not yet live at the venue. + pending, + /// Live at the venue and eligible for settlement. + open, + /// Settled. The payload is venue-defined settlement proof (for an + /// EVM venue, typically the settlement transaction hash). + settled(option>), + /// Terminally failed. + failed(fail-reason), + /// Reached its expiry without settling. + expired, + /// Withdrawn before settlement. + cancelled, + } + + /// An EVM call the host must sign and send for the intent to exist + /// on-chain. The adapter only describes the call: the host routes it + /// through the guard's host-signed class, fills the gas and fee + /// fields, and signs, so adapters still structurally cannot move + /// value. Always a call to existing code; adapters cannot deploy. + record unsigned-tx { + /// Chain the transaction must land on. + chain-id: u64, + /// 20-byte address of the contract to call. + to: list, + /// Native value, big-endian unsigned; an empty list is zero. + value: list, + /// ABI-encoded calldata. + input: list, + } + + /// What a successful submit produced. A variant from day one: an + /// on-chain-settlement venue (an ethflow-style order) has no receipt + /// to give until a transaction is signed, and bolting that on later + /// would break every deployed module. + variant submit-outcome { + /// The venue holds the intent; the receipt is its stable id. + accepted(receipt), + /// Settlement requires a host-signed on-chain transaction first. + requires-signing(unsigned-tx), + } + + /// Failure of a pool or adapter call. + variant venue-error { + /// No installed adapter answers to the venue id. + unknown-venue, + /// Body bytes failed to decode against the venue's published + /// schema: malformed bytes or an unknown outer version. + invalid-body(string), + /// The receipt was not issued by this venue or is malformed. + invalid-receipt, + /// The venue refused the intent under its own rules. + rejected(string), + /// Guard policy refused the egress before it reached the venue. + denied(string), + /// The venue or adapter does not support the operation. + unsupported(string), + /// Transport or venue infrastructure failure; retry later. + unavailable(string), + /// Adapter or router failure that is not the caller's fault. + internal-error(string), + } +}