diff --git a/crates/cow-venue/src/client.rs b/crates/cow-venue/src/client.rs
index 81bd4e5c..174bb4e8 100644
--- a/crates/cow-venue/src/client.rs
+++ b/crates/cow-venue/src/client.rs
@@ -1,19 +1,19 @@
//! The typed CoW intent client.
//!
-//! [`CowClient`] binds the strategy-facing [`IntentClient`] to the CoW
+//! [`CowClient`] binds the keeper-facing [`IntentClient`] to the CoW
//! venue id and speaks the venue's own [`CowIntentBody`] over it, so
-//! strategy code submits a typed CoW body without naming the venue on
+//! keeper code submits a typed CoW body without naming the venue on
//! every call or handling wire bytes. The classification API
//! ([`classify`](crate::classification::classify)) travels in the same
//! slice so the client that submits an order and the table that
//! classifies its rejection version together.
-use nexum_venue_sdk::client::{ClientError, IntentClient, IntentPool};
+use nexum_venue_sdk::client::{ClientError, IntentClient, VenueClient};
use nexum_venue_sdk::{IntentStatus, SubmitOutcome};
use crate::body::CowIntentBody;
-/// The venue id the CoW adapter registers under and the router resolves.
+/// The venue id the CoW adapter registers under and the registry resolves.
/// Every [`CowClient`] call routes here.
pub const VENUE: &str = "cow";
@@ -25,11 +25,11 @@ pub struct CowClient
{
inner: IntentClient
,
}
-impl CowClient {
- /// Bind a pool handle to the CoW venue.
- pub fn new(pool: P) -> Self {
+impl CowClient {
+ /// Bind a client handle to the CoW venue.
+ pub fn new(venues: P) -> Self {
Self {
- inner: IntentClient::new(pool, VENUE),
+ inner: IntentClient::new(venues, VENUE),
}
}
@@ -66,13 +66,13 @@ mod tests {
/// Records the venue every call routed to and the bytes submitted.
/// Cloneable over a shared log so the test can inspect it after the
- /// pool moves into the client.
+ /// handle moves into the client.
#[derive(Clone, Default)]
- struct SpyPool {
+ struct SpyClient {
submitted: SubmitLog,
}
- impl IntentPool for SpyPool {
+ impl VenueClient for SpyClient {
fn submit(&self, venue: &str, body: Vec) -> Result {
self.submitted
.borrow_mut()
@@ -112,15 +112,15 @@ mod tests {
fn submit_routes_to_the_cow_venue_with_encoded_body() {
use nexum_venue_sdk::IntentBody;
- let pool = SpyPool::default();
+ let spy = SpyClient::default();
let body = sample_body();
let expected = body.to_bytes().expect("body encodes");
- let client = CowClient::new(pool.clone());
+ let client = CowClient::new(spy.clone());
assert_eq!(client.venue(), VENUE);
client.submit(&body).expect("submit succeeds");
- let calls = pool.submitted.borrow();
+ let calls = spy.submitted.borrow();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, VENUE);
assert_eq!(calls[0].1, expected);
diff --git a/crates/nexum-macros/src/lib.rs b/crates/nexum-macros/src/lib.rs
index c760b37f..d9566925 100644
--- a/crates/nexum-macros/src/lib.rs
+++ b/crates/nexum-macros/src/lib.rs
@@ -9,7 +9,7 @@
//!
//! [`venue`] is the adapter counterpart: it emits the same per-cdylib
//! wit-bindgen and `export!`, but for a per-component venue-adapter
-//! world exporting the `nexum:intent/adapter` face and importing only
+//! world exporting the `videre:venue/adapter` face and importing only
//! the manifest's declared scoped transport.
//!
//! [`derive@IntentBody`] implements the venue SDK's versioned body codec
@@ -271,7 +271,7 @@ pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
.into()
}
-/// The associated functions the `nexum:intent/adapter` face mandates. A
+/// The associated functions the `videre:venue/adapter` face mandates. A
/// venue adapter must define all four; `init` is separate (a no-op when
/// absent, exactly as in a module).
const VENUE_EXPORTS: [&str; 4] = ["derive_header", "submit", "status", "cancel"];
@@ -280,7 +280,7 @@ const VENUE_EXPORTS: [&str; 4] = ["derive_header", "submit", "status", "cancel"]
///
/// Apply to an inherent `impl` block whose associated functions are the
/// adapter face: `derive_header`, `submit`, `status`, `cancel` (all
-/// required, from `nexum:intent/adapter`), plus an optional `init`
+/// required, from `videre:venue/adapter`), plus an optional `init`
/// (absent means a no-op). Each takes and returns the per-cdylib
/// wit-bindgen payloads for its signature. The macro reads the crate's
/// `module.toml`, synthesizes a per-component world exporting the
@@ -298,7 +298,7 @@ const VENUE_EXPORTS: [&str; 4] = ["derive_header", "submit", "status", "cancel"]
///
/// The same crate-root resolution invariants as [`macro@module`] apply:
/// the wit-bindgen output lands at the module crate root (so the emitted
-/// glue resolves `Guest`, `Fault`, and the `nexum::*` type modules
+/// glue resolves `Guest`, `Fault`, and the `nexum::*`/`videre::*` type modules
/// there), the consuming crate must declare `wit-bindgen` as a direct
/// dependency, and the crate root must not shadow std prelude names.
#[proc_macro_attribute]
@@ -423,12 +423,12 @@ pub fn venue(attr: TokenStream, item: TokenStream) -> TokenStream {
#init_impl
}
- impl exports::nexum::intent::adapter::Guest for __NexumVenueAdapterExport {
+ impl exports::videre::venue::adapter::Guest for __NexumVenueAdapterExport {
fn derive_header(
body: ::std::vec::Vec,
) -> ::core::result::Result<
- nexum::intent::types::IntentHeader,
- nexum::intent::types::VenueError,
+ videre::types::types::IntentHeader,
+ videre::types::types::VenueError,
> {
<#self_ty>::derive_header(body)
}
@@ -436,8 +436,8 @@ pub fn venue(attr: TokenStream, item: TokenStream) -> TokenStream {
fn submit(
body: ::std::vec::Vec,
) -> ::core::result::Result<
- nexum::intent::types::SubmitOutcome,
- nexum::intent::types::VenueError,
+ videre::types::types::SubmitOutcome,
+ videre::types::types::VenueError,
> {
<#self_ty>::submit(body)
}
@@ -445,15 +445,15 @@ pub fn venue(attr: TokenStream, item: TokenStream) -> TokenStream {
fn status(
receipt: ::std::vec::Vec,
) -> ::core::result::Result<
- nexum::intent::types::IntentStatus,
- nexum::intent::types::VenueError,
+ videre::types::types::IntentStatus,
+ videre::types::types::VenueError,
> {
<#self_ty>::status(receipt)
}
fn cancel(
receipt: ::std::vec::Vec,
- ) -> ::core::result::Result<(), nexum::intent::types::VenueError> {
+ ) -> ::core::result::Result<(), videre::types::types::VenueError> {
<#self_ty>::cancel(receipt)
}
}
diff --git a/crates/nexum-macros/src/world.rs b/crates/nexum-macros/src/world.rs
index 95614893..3fcf3818 100644
--- a/crates/nexum-macros/src/world.rs
+++ b/crates/nexum-macros/src/world.rs
@@ -32,7 +32,7 @@ struct Capability {
/// Every capability the macro recognises, in emission order. Mirrors
/// the runtime's core registry plus the extension namespaces the
-/// workspace ships (`nexum:intent/pool`, `shepherd:cow/cow-api`).
+/// workspace ships (`videre:venue/client`, `shepherd:cow/cow-api`).
const KNOWN: &[Capability] = &[
Capability {
name: "chain",
@@ -71,9 +71,9 @@ const KNOWN: &[Capability] = &[
adapter: Some("logging"),
},
Capability {
- name: "pool",
- import: Some("nexum:intent/pool@0.1.0"),
- packages: &["nexum-value-flow", "nexum-intent"],
+ name: "client",
+ import: Some("videre:venue/client@0.1.0"),
+ packages: &["videre-value-flow", "videre-types", "videre-venue"],
adapter: None,
},
Capability {
@@ -149,7 +149,7 @@ const VENUE_CAPABILITIES: &[&str] = &["chain", "messaging", "http"];
/// Build the per-component venue-adapter world from the declared
/// capability names. The world exports `init` and the
-/// `nexum:intent/adapter` face and imports exactly the declared scoped
+/// `videre:venue/adapter` face and imports exactly the declared scoped
/// transport, so a macro-built adapter's imports equal its declarations
/// by construction. A capability outside the venue-permitted set is a
/// compile error: an adapter that reaches for host key material or
@@ -167,11 +167,16 @@ pub fn synthesize_venue(declared: &[String]) -> Result {
}
let mut imports = String::new();
- // The export face (`nexum:intent/adapter`, its types, and the
- // value-flow vocabulary they are expressed in) needs the intent
+ // The export face (`videre:venue/adapter`, its types, and the
+ // value-flow vocabulary they are expressed in) needs the videre
// packages on the resolve path beyond the leaf host package, in
// dependency order: a package precedes its dependants.
- let mut packages = vec!["nexum-value-flow", "nexum-intent", "nexum-host"];
+ let mut packages = vec![
+ "videre-value-flow",
+ "videre-types",
+ "nexum-host",
+ "videre-venue",
+ ];
for cap in KNOWN {
if !declared.iter().any(|d| d == cap.name) {
continue;
@@ -198,7 +203,7 @@ pub fn synthesize_venue(declared: &[String]) -> Result {
wit.push_str(&imports);
wit.push_str(
"\n export init: func(config: config) -> result<_, fault>;\n \
- export nexum:intent/adapter@0.1.0;\n}\n",
+ export videre:venue/adapter@0.1.0;\n}\n",
);
Ok(ModuleWorld {
@@ -278,8 +283,13 @@ mod tests {
const MODULE_PACKAGES: [&str; 1] = ["nexum-host"];
/// The package set every venue world resolves against: the exported
- /// adapter face pulls the intent vocabulary, in dependency order.
- const VENUE_PACKAGES: [&str; 3] = ["nexum-value-flow", "nexum-intent", "nexum-host"];
+ /// adapter face pulls the videre vocabulary, in dependency order.
+ const VENUE_PACKAGES: [&str; 4] = [
+ "videre-value-flow",
+ "videre-types",
+ "nexum-host",
+ "videre-venue",
+ ];
#[test]
fn logging_only_world_imports_logging_alone() {
@@ -299,12 +309,17 @@ mod tests {
}
#[test]
- fn pool_pulls_the_intent_packages() {
- let world = synthesize(&["pool".to_string()]).unwrap();
- assert!(world.wit.contains("import nexum:intent/pool@0.1.0;"));
+ fn client_pulls_the_videre_packages() {
+ let world = synthesize(&["client".to_string()]).unwrap();
+ assert!(world.wit.contains("import videre:venue/client@0.1.0;"));
assert_eq!(
world.packages,
- vec!["nexum-host", "nexum-value-flow", "nexum-intent"]
+ vec![
+ "nexum-host",
+ "videre-value-flow",
+ "videre-types",
+ "videre-venue"
+ ]
);
assert!(world.adapters.is_empty());
}
@@ -340,7 +355,7 @@ mod tests {
.wit
.contains("export init: func(config: config) -> result<_, fault>;")
);
- assert!(world.wit.contains("export nexum:intent/adapter@0.1.0;"));
+ assert!(world.wit.contains("export videre:venue/adapter@0.1.0;"));
assert_eq!(world.packages, VENUE_PACKAGES);
assert!(world.adapters.is_empty());
}
@@ -368,12 +383,18 @@ mod tests {
fn venue_world_with_no_capabilities_imports_nothing() {
let world = synthesize_venue(&[]).unwrap();
assert!(!world.wit.contains("import"));
- assert!(world.wit.contains("export nexum:intent/adapter@0.1.0;"));
+ assert!(world.wit.contains("export videre:venue/adapter@0.1.0;"));
}
#[test]
fn venue_world_refuses_non_transport_capabilities() {
- for cap in ["local-store", "remote-store", "identity", "logging", "pool"] {
+ for cap in [
+ "local-store",
+ "remote-store",
+ "identity",
+ "logging",
+ "client",
+ ] {
let err = synthesize_venue(&[cap.to_string()]).unwrap_err();
assert!(err.contains(cap), "message was: {err}");
assert!(err.contains("venue adapter"), "message was: {err}");
diff --git a/crates/nexum-runtime/src/bindings.rs b/crates/nexum-runtime/src/bindings.rs
index 52e9a3fa..f587536a 100644
--- a/crates/nexum-runtime/src/bindings.rs
+++ b/crates/nexum-runtime/src/bindings.rs
@@ -11,11 +11,11 @@
//!
//! `nexum:host` is a leaf package: the host `event` variant carries an
//! intent-status transition as opaque bytes, so the core world resolves
-//! against `wit/nexum-host` alone. The intent and value-flow vocabulary
-//! generates in the adapter bindgen below, and the pool bindgen remaps
-//! onto it with `with`, so one Rust type serves the router and the
-//! adapter face alike. `PartialEq` is derived so the router can compare
-//! a polled status against the last delivered one.
+//! against `wit/nexum-host` alone. The videre vocabulary generates in the
+//! adapter bindgen below, and the client bindgen remaps onto it with
+//! `with`, so one Rust type serves the registry and the adapter face
+//! alike. `PartialEq` is derived so the registry can compare a polled
+//! status against the last delivered one.
wasmtime::component::bindgen!({
path: ["../../wit/nexum-host"],
@@ -26,25 +26,25 @@ wasmtime::component::bindgen!({
});
/// WIT bindings for the second component kind: the
-/// `nexum:adapter/venue-adapter` world. An adapter imports only the scoped
+/// `videre:venue/venue-adapter` world. An adapter imports only the scoped
/// transport it needs (chain and messaging; outbound HTTP is wasi:http,
/// linked and allowlisted separately as for event-module) and exports the
-/// `nexum:intent/adapter` face plus `init`. The shared `nexum:host`
+/// `videre:venue/adapter` face plus `init`. The shared `nexum:host`
/// interfaces are reused from the `event-module` bindings above via
/// `with`, so the `chain`/`messaging` `Host` impls and the `fault` type
/// an adapter sees are the very ones the core host constructs. The
-/// `nexum:intent` and `nexum:value-flow` types generate here (the leaf
-/// host world no longer reaches them) and the pool bindgen below remaps
+/// `videre:types` and `videre:value-flow` types generate here (the leaf
+/// host world no longer reaches them) and the client bindgen below remaps
/// onto them.
mod venue_adapter {
wasmtime::component::bindgen!({
path: [
- "../../wit/nexum-value-flow",
- "../../wit/nexum-intent",
+ "../../wit/videre-value-flow",
+ "../../wit/videre-types",
"../../wit/nexum-host",
- "../../wit/nexum-adapter",
+ "../../wit/videre-venue",
],
- world: "nexum:adapter/venue-adapter",
+ world: "videre:venue/venue-adapter",
imports: { default: async },
exports: { default: async },
with: {
@@ -58,86 +58,77 @@ mod venue_adapter {
pub use venue_adapter::VenueAdapter;
-/// The strategy-facing `nexum:intent/pool` import bound host-side. The pool
-/// world imports the interface a module calls; the intent and value-flow
-/// types it uses are reused from the adapter bindings above via `with`, so
-/// the `SubmitOutcome` and `VenueError` the router hands back to a module
+/// The keeper-facing `videre:venue/client` import bound host-side. The
+/// world imports the interface a module calls; the videre types it uses
+/// are reused from the adapter bindings above via `with`, so the
+/// `SubmitOutcome` and `VenueError` the registry hands back to a module
/// are the very ones an adapter's `submit` produced - no lift between two
/// structurally identical copies. Async, because the `Host` impl awaits the
/// per-adapter mutex and the adapter's own async guest calls.
-mod pool_host {
+mod client_host {
wasmtime::component::bindgen!({
inline: "
- package nexum:pool-host;
- world pool-host {
- import nexum:intent/pool@0.1.0;
+ package videre:client-host;
+ world client-host {
+ import videre:venue/client@0.1.0;
}
",
- path: ["../../wit/nexum-value-flow", "../../wit/nexum-intent"],
+ path: [
+ "../../wit/videre-value-flow",
+ "../../wit/videre-types",
+ "../../wit/nexum-host",
+ "../../wit/videre-venue",
+ ],
imports: { default: async },
with: {
- "nexum:value-flow/types": super::venue_adapter::nexum::value_flow::types,
- "nexum:intent/types": super::venue_adapter::nexum::intent::types,
+ "videre:value-flow/types": super::venue_adapter::videre::value_flow::types,
+ "videre:types/types": super::venue_adapter::videre::types::types,
},
});
}
-/// The router-observed status transition delivered through the `event`
-/// variant, re-exported at the plain spelling the router names.
+/// The host-bound client interface: the `Host` trait the registry implements
+/// and the `add_to_linker` the module linker calls.
+pub use client_host::videre::venue::client;
+/// The registry-observed status transition delivered through the `event`
+/// variant, re-exported at the plain spelling the registry names.
pub use nexum::host::types::IntentStatusUpdate;
-/// The host-bound pool interface: the `Host` trait the router implements and
-/// the `add_to_linker` the module linker calls.
-pub use pool_host::nexum::intent::pool;
-/// The shared intent ontology, re-exported at the plain spellings the router
-/// and the `pool::Host` impl name.
-pub use venue_adapter::nexum::intent::types::{
- AuthScheme, FailReason, IntentHeader, IntentStatus, SubmitOutcome, UnsignedTx, VenueError,
+/// The shared intent ontology, re-exported at the plain spellings the
+/// registry and the `client::Host` impl name.
+pub use venue_adapter::videre::types::types::{
+ AuthScheme, IntentHeader, IntentStatus, RateLimit, Settlement, SubmitOutcome, UnsignedTx,
+ VenueError,
};
/// The value-flow vocabulary the header is expressed in.
-pub use venue_adapter::nexum::value_flow::types as value_flow;
-
-/// Bindgen smoke for the `nexum:value-flow` types package. The package has
-/// no host consumer yet (the intent router that will bind it lands later),
-/// so this compiles it under test only, through a throwaway world that
-/// imports the interface. Its value is the identifier-hygiene gate: the
-/// test names every generated type, variant, and field by its plain Rust
-/// spelling, so a WIT id that collided with a Rust keyword would surface as
-/// an `r#` escape and fail to compile here rather than in a downstream
-/// binding.
+pub use venue_adapter::videre::value_flow::types as value_flow;
+
+/// Bindgen smoke for the `videre:value-flow` types package, compiled under
+/// test through a throwaway world that imports the interface. Its value is
+/// the identifier-hygiene gate: the test names every generated type,
+/// variant, and field by its plain Rust spelling, so a WIT id that collided
+/// with a Rust keyword would surface as an `r#` escape and fail to compile
+/// here rather than in a downstream binding.
#[cfg(test)]
mod value_flow_smoke {
wasmtime::component::bindgen!({
inline: "
- package nexum:value-flow-smoke;
+ package videre:value-flow-smoke;
world smoke {
- import nexum:value-flow/types@0.1.0;
+ import videre:value-flow/types@0.1.0;
}
",
- path: ["../../wit/nexum-value-flow"],
+ path: ["../../wit/videre-value-flow"],
});
#[test]
fn identifiers_bind_unescaped() {
- use nexum::value_flow::types::{Asset, AssetAmount, OffchainDesc, ServiceDesc, Settlement};
+ use videre::value_flow::types::{Asset, AssetAmount, Erc20};
- let _ = Settlement::EvmChain(1);
- let _ = Settlement::Offchain(String::new());
-
- let service = ServiceDesc {
- kind: String::new(),
- summary: String::new(),
- };
- let offchain = OffchainDesc {
- domain: String::new(),
- summary: String::new(),
+ let erc20 = Erc20 {
+ token: vec![0u8; 20],
};
-
- let _ = Asset::NativeToken(Settlement::EvmChain(1));
- let _ = Asset::Erc20((1, Vec::new()));
- let _ = Asset::Erc721((1, Vec::new(), Vec::new()));
- let _ = Asset::Erc1155((1, Vec::new(), Vec::new()));
- let _ = Asset::Service(service);
- let asset = Asset::Offchain(offchain);
+ let _ = Asset::Native;
+ let asset = Asset::Erc20(erc20);
let amount = AssetAmount {
asset,
@@ -147,34 +138,39 @@ mod value_flow_smoke {
}
}
-/// 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
+/// Bindgen smoke for the `videre:types` and `videre:venue` packages,
+/// mirroring the value-flow smoke above: a throwaway world imports the
+/// client 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 `client` 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 {
+mod client_smoke {
wasmtime::component::bindgen!({
inline: "
- package nexum:intent-smoke;
+ package videre:client-smoke;
world smoke {
- import nexum:intent/pool@0.1.0;
+ import videre:venue/client@0.1.0;
}
",
- path: ["../../wit/nexum-value-flow", "../../wit/nexum-intent"],
+ path: [
+ "../../wit/videre-value-flow",
+ "../../wit/videre-types",
+ "../../wit/nexum-host",
+ "../../wit/videre-venue",
+ ],
});
- use nexum::intent::types::{
- AuthScheme, FailReason, IntentHeader, IntentStatus, SubmitOutcome, UnsignedTx, VenueError,
+ use videre::types::types::{
+ AuthScheme, IntentHeader, IntentStatus, RateLimit, Settlement, SubmitOutcome, UnsignedTx,
+ VenueError,
};
- use nexum::value_flow::types::Settlement;
+ use videre::value_flow::types::{Asset, AssetAmount};
- struct DummyPool;
+ struct DummyClient;
- impl nexum::intent::pool::Host for DummyPool {
+ impl videre::venue::client::Host for DummyClient {
fn submit(&mut self, _venue: String, _body: Vec) -> Result {
Err(VenueError::UnknownVenue)
}
@@ -192,55 +188,56 @@ mod intent_smoke {
}
}
+ fn amount(bytes: Vec) -> AssetAmount {
+ AssetAmount {
+ asset: Asset::Native,
+ amount: bytes,
+ }
+ }
+
#[test]
fn identifiers_bind_unescaped() {
- use nexum::intent::pool::Host;
+ use videre::venue::client::Host;
- let _ = AuthScheme::Eip712;
let _ = AuthScheme::Eip1271;
- let _ = AuthScheme::Presign;
- let _ = AuthScheme::OffchainSig;
- let _ = AuthScheme::Unsigned;
+ let _ = AuthScheme::Eip712;
let header = IntentHeader {
- gives: Vec::new(),
- wants: Vec::new(),
- valid_until: None,
- settlement: Settlement::EvmChain(1),
+ gives: amount(vec![1]),
+ wants: amount(Vec::new()),
+ settlement: Settlement { chain: 1 },
authorisation: AuthScheme::Eip712,
};
- assert!(header.gives.is_empty() && header.wants.is_empty());
+ assert!(header.wants.amount.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::Fulfilled;
let _ = IntentStatus::Cancelled;
+ let _ = IntentStatus::Expired;
let tx = UnsignedTx {
- chain_id: 1,
+ chain: 1,
to: Vec::new(),
value: Vec::new(),
- input: Vec::new(),
+ data: Vec::new(),
};
let _ = SubmitOutcome::Accepted(Vec::new());
let _ = SubmitOutcome::RequiresSigning(tx);
+ let _ = VenueError::UnknownVenue;
let _ = VenueError::InvalidBody(String::new());
- let _ = VenueError::InvalidReceipt;
- let _ = VenueError::Rejected(String::new());
+ let _ = VenueError::Unsupported;
let _ = VenueError::Denied(String::new());
- let _ = VenueError::Unsupported(String::new());
+ let _ = VenueError::RateLimited(RateLimit {
+ retry_after_ms: Some(250),
+ });
let _ = VenueError::Unavailable(String::new());
- let _ = VenueError::InternalError(String::new());
+ let _ = VenueError::Timeout;
- 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());
+ let mut client = DummyClient;
+ assert!(client.submit(String::new(), Vec::new()).is_err());
+ assert!(client.status(String::new(), Vec::new()).is_err());
+ assert!(client.cancel(String::new(), Vec::new()).is_err());
}
}
diff --git a/crates/nexum-runtime/src/builder.rs b/crates/nexum-runtime/src/builder.rs
index 262e5939..34ad617b 100644
--- a/crates/nexum-runtime/src/builder.rs
+++ b/crates/nexum-runtime/src/builder.rs
@@ -268,7 +268,7 @@ impl LaunchRuntime for AssembledRuntime<'_, T> {
// will see: at least one intent-status subscriber and at least one
// installed adapter to poll.
let poll_statuses = supervisor.has_intent_status_subscribers()
- && supervisor.pool_router().venue_count() > 0;
+ && supervisor.venue_registry().venue_count() > 0;
// No subscriptions: nothing to drive. Return a handle whose event loop
// is already complete so `wait` resolves immediately.
@@ -308,7 +308,7 @@ impl LaunchRuntime for AssembledRuntime<'_, T> {
);
let intent_status_stream = poll_statuses.then(|| {
event_loop::open_intent_status_stream(
- supervisor.pool_router(),
+ supervisor.venue_registry(),
engine_cfg.limits.status_poll_interval(),
&executor,
&mut reconnect_tasks,
diff --git a/crates/nexum-runtime/src/engine_config.rs b/crates/nexum-runtime/src/engine_config.rs
index ea278de3..06cbc08a 100644
--- a/crates/nexum-runtime/src/engine_config.rs
+++ b/crates/nexum-runtime/src/engine_config.rs
@@ -26,7 +26,7 @@ use strum::IntoStaticStr;
use thiserror::Error;
use tracing::{info, warn};
-use crate::host::pool_router::{DEFAULT_QUOTA_MAX_CHARGES, DEFAULT_QUOTA_WINDOW, PoolQuota};
+use crate::host::venue_registry::{DEFAULT_QUOTA_MAX_CHARGES, DEFAULT_QUOTA_WINDOW, SubmitQuota};
use crate::runtime::dispatch_rate::{
DEFAULT_DISPATCH_BURST, DEFAULT_DISPATCH_REFILL_PER_SEC, DispatchRatePolicy,
};
@@ -284,7 +284,7 @@ const DEFAULT_LOG_BYTES_PER_RUN: usize = 256 * 1024;
/// history for diagnosis without unbounded growth.
const DEFAULT_LOG_RUNS_RETAINED: usize = 16;
-/// Default cadence for router-driven intent status polling (5 s). Fast
+/// Default cadence for registry-driven intent status polling (5 s). Fast
/// enough that a settling intent is observed within a block time or two,
/// slow enough that per-receipt venue calls stay negligible.
const DEFAULT_STATUS_POLL_INTERVAL: Duration = Duration::from_secs(5);
@@ -488,11 +488,11 @@ impl ModuleLimits {
}
/// Resolved per-caller submission quota (overrides or defaults). A zero
- /// `max_charges` is saturated up to 1 by the router builder, so a
+ /// `max_charges` is saturated up to 1 by the registry builder, so a
/// misconfigured budget still admits one submission rather than bricking
/// every venue.
- pub fn quota(&self) -> PoolQuota {
- PoolQuota::new(
+ pub fn quota(&self) -> SubmitQuota {
+ SubmitQuota::new(
self.quota.max_charges.unwrap_or(DEFAULT_QUOTA_MAX_CHARGES),
self.quota
.window_secs
@@ -588,7 +588,7 @@ pub struct PoisonLimitsSection {
}
/// `[limits.quota]` per-caller intent submission budget. Both optional;
-/// omitted values resolve to the router defaults via [`ModuleLimits::quota`].
+/// omitted values resolve to the registry defaults via [`ModuleLimits::quota`].
///
/// A caller (a strategy module, keyed by its namespace) may accrue at most
/// `max_charges` submissions within a sliding `window_secs`; a decode failure
@@ -607,7 +607,7 @@ pub struct QuotaLimitsSection {
/// omitted value resolves to the built-in default and a degenerate zero
/// saturates up to 1 ms via [`ModuleLimits::status_poll_interval`].
///
-/// The cadence is how often the router polls each installed adapter's
+/// The cadence is how often the registry polls each installed adapter's
/// `status` export for the receipts it watches; only observed transitions
/// fan out as `intent-status` events.
#[derive(Debug, Default, Deserialize)]
diff --git a/crates/nexum-runtime/src/host/impls/mod.rs b/crates/nexum-runtime/src/host/impls/mod.rs
index a5b80c14..40b65ade 100644
--- a/crates/nexum-runtime/src/host/impls/mod.rs
+++ b/crates/nexum-runtime/src/host/impls/mod.rs
@@ -11,6 +11,6 @@ mod identity;
mod local_store;
mod logging;
mod messaging;
-mod pool;
mod remote_store;
mod types;
+mod venue_client;
diff --git a/crates/nexum-runtime/src/host/impls/pool.rs b/crates/nexum-runtime/src/host/impls/pool.rs
deleted file mode 100644
index d02e29e5..00000000
--- a/crates/nexum-runtime/src/host/impls/pool.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-//! `nexum:intent/pool`: the strategy-facing intent import. Every method is a
-//! thin delegation to the shared [`PoolRouter`](crate::host::pool_router)
-//! carried in the store; the router owns the venue resolution, per-adapter
-//! serialisation, guard seam, and quota. The caller identity the router meters
-//! against is this store's module namespace.
-
-use crate::bindings::pool::Host;
-use crate::bindings::{IntentStatus, SubmitOutcome, VenueError};
-use crate::host::component::RuntimeTypes;
-use crate::host::state::HostState;
-
-impl Host for HostState {
- async fn submit(&mut self, venue: String, body: Vec) -> Result {
- self.pool_router
- .submit(&self.run.module, &venue, body)
- .await
- }
-
- async fn status(
- &mut self,
- venue: String,
- receipt: Vec,
- ) -> Result {
- self.pool_router.status(&venue, receipt).await
- }
-
- async fn cancel(&mut self, venue: String, receipt: Vec) -> Result<(), VenueError> {
- self.pool_router.cancel(&venue, receipt).await
- }
-}
diff --git a/crates/nexum-runtime/src/host/impls/venue_client.rs b/crates/nexum-runtime/src/host/impls/venue_client.rs
new file mode 100644
index 00000000..26e76f59
--- /dev/null
+++ b/crates/nexum-runtime/src/host/impls/venue_client.rs
@@ -0,0 +1,36 @@
+//! `videre:venue/client`: the keeper-facing venue import. Every method is a
+//! thin delegation to the shared
+//! [`VenueRegistry`](crate::host::venue_registry) carried in the store; the
+//! registry owns the venue resolution, per-adapter serialisation, guard
+//! seam, and quota. The caller identity the registry meters against is this
+//! store's module namespace.
+
+use crate::bindings::client::Host;
+use crate::bindings::{IntentStatus, SubmitOutcome, VenueError};
+use crate::host::component::RuntimeTypes;
+use crate::host::state::HostState;
+use crate::host::venue_registry::VenueId;
+
+impl Host for HostState {
+ async fn submit(&mut self, venue: String, body: Vec) -> Result {
+ self.venue_registry
+ .submit(&self.run.module, &VenueId::from(venue), body)
+ .await
+ }
+
+ async fn status(
+ &mut self,
+ venue: String,
+ receipt: Vec,
+ ) -> Result {
+ self.venue_registry
+ .status(&VenueId::from(venue), receipt)
+ .await
+ }
+
+ async fn cancel(&mut self, venue: String, receipt: Vec) -> Result<(), VenueError> {
+ self.venue_registry
+ .cancel(&VenueId::from(venue), receipt)
+ .await
+ }
+}
diff --git a/crates/nexum-runtime/src/host/mod.rs b/crates/nexum-runtime/src/host/mod.rs
index 5c41e467..1cf10f5d 100644
--- a/crates/nexum-runtime/src/host/mod.rs
+++ b/crates/nexum-runtime/src/host/mod.rs
@@ -31,6 +31,6 @@ pub mod http;
mod impls;
pub mod local_store_redb;
pub mod logs;
-pub mod pool_router;
pub mod provider_pool;
pub mod state;
+pub mod venue_registry;
diff --git a/crates/nexum-runtime/src/host/state.rs b/crates/nexum-runtime/src/host/state.rs
index d299ccd0..5dd07d85 100644
--- a/crates/nexum-runtime/src/host/state.rs
+++ b/crates/nexum-runtime/src/host/state.rs
@@ -13,7 +13,7 @@ use wasmtime_wasi_http::WasiHttpCtx;
use super::component::{Handle, RuntimeTypes};
use super::http::HttpGate;
use super::logs::{LogRouter, RunId};
-use super::pool_router::PoolRouter;
+use super::venue_registry::VenueRegistry;
/// Per-module host state, generic over the [`RuntimeTypes`] lattice
/// binding the backend seams. The composition root supplies the
@@ -51,10 +51,10 @@ pub struct HostState {
/// `local-store` backend - per-module handle with pre-computed
/// keccak256 namespace prefix.
pub store: Handle,
- /// The intent pool router the `nexum:intent/pool` import dispatches to.
+ /// The venue registry the `videre:venue/client` import dispatches to.
/// Every module store carries the same shared handle; an adapter store,
- /// which cannot call pool, carries an empty one.
- pub pool_router: PoolRouter,
+ /// which cannot call the client face, carries an empty one.
+ pub venue_registry: VenueRegistry,
}
// `WasiView: Send`, so the backends must be `Send` too; the lattice
diff --git a/crates/nexum-runtime/src/host/pool_router.rs b/crates/nexum-runtime/src/host/venue_registry.rs
similarity index 67%
rename from crates/nexum-runtime/src/host/pool_router.rs
rename to crates/nexum-runtime/src/host/venue_registry.rs
index 6353cf8a..dd4637b7 100644
--- a/crates/nexum-runtime/src/host/pool_router.rs
+++ b/crates/nexum-runtime/src/host/venue_registry.rs
@@ -1,16 +1,16 @@
-//! The intent pool router: the strategy-facing `nexum:intent/pool` import
+//! The venue registry: the keeper-facing `videre:venue/client` import
//! resolved to installed venue adapters.
//!
-//! A module's `pool::submit(venue, body)` reaches the host here. The router
-//! resolves the venue id to the one installed adapter that answers for it,
-//! then drives a fixed sequence against that adapter: derive the header,
-//! run the guard interposition seam on it, and only then submit. Status and
-//! cancel are pass-throughs; they are not submissions, so they skip the
-//! header, the guard, and the quota.
+//! A module's `client::submit(venue, body)` reaches the host here. The
+//! registry resolves the venue id to the one installed adapter that answers
+//! for it, then drives a fixed sequence against that adapter: derive the
+//! header, run the guard interposition seam on it, and only then submit.
+//! Status and cancel are pass-throughs; they are not submissions, so they
+//! skip the header, the guard, and the quota.
//!
//! Invocation is serialised per adapter. A wasmtime `Store` is not `Sync`,
-//! so each adapter sits behind its own async mutex: concurrent pool calls to
-//! the same venue queue on that mutex, while calls to different venues run
+//! so each adapter sits behind its own async mutex: concurrent client calls
+//! to the same venue queue on that mutex, while calls to different venues run
//! in parallel. The lock is held across the guest await, which is the whole
//! point - it is the actor boundary that keeps one adapter store
//! single-threaded.
@@ -23,6 +23,7 @@
//! own budget rather than the adapter's.
use std::collections::{HashMap, VecDeque};
+use std::fmt;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
@@ -33,7 +34,8 @@ use tracing::warn;
use wasmtime::Store;
use crate::bindings::{
- IntentHeader, IntentStatus, IntentStatusUpdate, SubmitOutcome, VenueAdapter, VenueError,
+ IntentHeader, IntentStatus, IntentStatusUpdate, RateLimit, SubmitOutcome, VenueAdapter,
+ VenueError,
};
use crate::host::component::RuntimeTypes;
use crate::host::state::HostState;
@@ -43,18 +45,48 @@ pub const DEFAULT_QUOTA_MAX_CHARGES: u32 = 256;
/// Default sliding window the per-caller submission budget is counted over.
pub const DEFAULT_QUOTA_WINDOW: Duration = Duration::from_secs(60);
+/// Venue identifier: the id an adapter registers under and a submission
+/// names. Opaque beyond equality.
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+pub struct VenueId(String);
+
+impl VenueId {
+ /// The id at its wire spelling.
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+impl From for VenueId {
+ fn from(id: String) -> Self {
+ Self(id)
+ }
+}
+
+impl From<&str> for VenueId {
+ fn from(id: &str) -> Self {
+ Self(id.to_owned())
+ }
+}
+
+impl fmt::Display for VenueId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&self.0)
+ }
+}
+
/// Per-caller submission quota. Both a forwarded submission and a charged
/// decode failure consume one unit; the window slides so a caller's budget
/// refills as old charges age out.
#[derive(Debug, Clone, Copy)]
-pub struct PoolQuota {
+pub struct SubmitQuota {
/// Maximum charges a single caller may accrue within `window`.
pub max_charges: u32,
/// Sliding window the charges are counted across.
pub window: Duration,
}
-impl PoolQuota {
+impl SubmitQuota {
/// Pair a budget with the window it is counted over.
pub const fn new(max_charges: u32, window: Duration) -> Self {
Self {
@@ -64,30 +96,37 @@ impl PoolQuota {
}
}
-impl Default for PoolQuota {
+impl Default for SubmitQuota {
fn default() -> Self {
Self::new(DEFAULT_QUOTA_MAX_CHARGES, DEFAULT_QUOTA_WINDOW)
}
}
-/// The guard interposition seam. The router runs this on the adapter-derived
-/// header after `derive-header` and before `submit`. The shipped policy is a
-/// no-op that allows every egress; the egress-guard epic replaces the
-/// installed policy with the real facts-plus-analysers pipeline without the
-/// router changing shape.
-pub trait GuardPolicy: Send + Sync {
+/// The guard interposition seam. The registry runs this on the
+/// adapter-derived header after `derive-header` and before `submit`. The
+/// shipped policy is the unit guard, which allows every egress; the
+/// egress-guard epic replaces the installed policy with the real
+/// facts-plus-analysers pipeline without the registry changing shape.
+pub trait EgressGuard: Send + Sync {
/// Decide whether the derived header may proceed to the adapter's submit.
fn check(&self, ctx: &GuardContext<'_>) -> GuardVerdict;
}
+/// The unit guard: allow every egress.
+impl EgressGuard for () {
+ fn check(&self, _ctx: &GuardContext<'_>) -> GuardVerdict {
+ GuardVerdict::Allow
+ }
+}
+
/// What the guard sees: who is submitting, to which venue, and the header the
/// adapter derived from the opaque body. The header is the stable ontology
/// policy has teeth on; the raw body never reaches the guard.
pub struct GuardContext<'a> {
/// Namespace of the calling module.
pub caller: &'a str,
- /// Venue id the submission is routed to.
- pub venue: &'a str,
+ /// Venue the submission is routed to.
+ pub venue: &'a VenueId,
/// Adapter-derived header for the body.
pub header: &'a IntentHeader,
}
@@ -100,24 +139,15 @@ pub enum GuardVerdict {
Deny(String),
}
-/// The shipped no-op policy: allow every egress. Named so the composition
-/// root reads plainly and the egress-guard epic has an obvious thing to swap.
-pub struct AllowAllGuard;
-
-impl GuardPolicy for AllowAllGuard {
- fn check(&self, _ctx: &GuardContext<'_>) -> GuardVerdict {
- GuardVerdict::Allow
- }
-}
-
/// The per-adapter invocation seam. One installed adapter answers for exactly
-/// one venue; the router owns the adapter's `Store` behind an async mutex and
-/// reaches it only through this trait, so the router's sequencing and quota
-/// logic is testable against a stub that never spins up a wasmtime store.
+/// one venue; the registry owns the adapter's `Store` behind an async mutex
+/// and reaches it only through this trait, so the registry's sequencing and
+/// quota logic is testable against a stub that never spins up a wasmtime
+/// store.
///
-/// The futures are boxed so the router can hold heterogeneous adapters behind
-/// one `dyn` slot without the whole router turning generic over an adapter
-/// type it never names.
+/// The futures are boxed so the registry can hold heterogeneous adapters
+/// behind one `dyn` slot without the whole registry turning generic over an
+/// adapter type it never names.
pub trait VenueInvoker: Send {
/// Project the opaque body onto the stable header the guard runs on.
fn derive_header<'a>(
@@ -139,16 +169,16 @@ pub trait VenueInvoker: Send {
/// The live adapter: a supervised wasmtime `Store` plus the `venue-adapter`
/// bindings, refuelled before each guest call. A trap is projected onto
-/// `internal-error` rather than propagated: a misbehaving adapter must not be
-/// the caller's fault, and it must not unwind through the router into the
+/// `unavailable` rather than propagated: a misbehaving adapter must not be
+/// the caller's fault, and it must not unwind through the registry into the
/// calling module's store.
-pub struct AdapterActor {
+pub struct VenueActor {
store: Store>,
bindings: VenueAdapter,
fuel_per_call: u64,
}
-impl AdapterActor {
+impl VenueActor {
/// Wrap an instantiated adapter store for routing.
pub fn new(store: Store>, bindings: VenueAdapter, fuel_per_call: u64) -> Self {
Self {
@@ -163,7 +193,7 @@ impl AdapterActor {
fn refuel(&mut self) -> Result<(), VenueError> {
self.store
.set_fuel(self.fuel_per_call)
- .map_err(|e| VenueError::InternalError(format!("adapter refuel failed: {e}")))
+ .map_err(|e| VenueError::Unavailable(format!("adapter refuel failed: {e}")))
}
}
@@ -171,10 +201,10 @@ impl AdapterActor {
/// carried so an operator sees why the adapter died without the wasm frame
/// list leaking to the calling module.
fn trap_to_venue_error(trap: wasmtime::Error) -> VenueError {
- VenueError::InternalError(format!("adapter trapped: {}", trap.root_cause()))
+ VenueError::Unavailable(format!("adapter trapped: {}", trap.root_cause()))
}
-impl VenueInvoker for AdapterActor {
+impl VenueInvoker for VenueActor {
fn derive_header<'a>(
&'a mut self,
body: &'a [u8],
@@ -183,7 +213,7 @@ impl VenueInvoker for AdapterActor {
self.refuel()?;
match self
.bindings
- .nexum_intent_adapter()
+ .videre_venue_adapter()
.call_derive_header(&mut self.store, body)
.await
{
@@ -201,7 +231,7 @@ impl VenueInvoker for AdapterActor {
self.refuel()?;
match self
.bindings
- .nexum_intent_adapter()
+ .videre_venue_adapter()
.call_submit(&mut self.store, body)
.await
{
@@ -216,7 +246,7 @@ impl VenueInvoker for AdapterActor {
self.refuel()?;
match self
.bindings
- .nexum_intent_adapter()
+ .videre_venue_adapter()
.call_status(&mut self.store, &receipt)
.await
{
@@ -231,7 +261,7 @@ impl VenueInvoker for AdapterActor {
self.refuel()?;
match self
.bindings
- .nexum_intent_adapter()
+ .videre_venue_adapter()
.call_cancel(&mut self.store, &receipt)
.await
{
@@ -251,86 +281,74 @@ struct QuotaLedger {
per_caller: HashMap>,
}
-/// One receipt the router polls for status transitions. `last` starts
+/// One receipt the registry polls for status transitions. `last` starts
/// `None` so the first successful poll always reports, giving a
/// subscriber the intent's current state without waiting for a change.
struct WatchedIntent {
- venue: String,
+ venue: VenueId,
receipt: Vec,
last: Option,
}
/// A polled status is terminal when the intent can never change again:
-/// the router stops watching the receipt after reporting it.
-fn is_terminal(status: &IntentStatus) -> bool {
+/// the registry stops watching the receipt after reporting it.
+fn is_terminal(status: IntentStatus) -> bool {
matches!(
status,
- IntentStatus::Settled(_)
- | IntentStatus::Failed(_)
- | IntentStatus::Expired
- | IntentStatus::Cancelled
+ IntentStatus::Fulfilled | IntentStatus::Cancelled | IntentStatus::Expired
)
}
-/// Lower an adapter-reported status onto the opaque status body the host
-/// `event` stream carries: `settled` is `fulfilled` plus its proof, and
-/// `failed` is `cancelled` plus its reason (the body's lifecycle enum has
-/// no failed case).
-fn status_body(status: &IntentStatus) -> StatusBody {
+/// Lower a polled status onto the opaque status body the host `event`
+/// stream carries. The registry attests the lifecycle state alone; proof
+/// and failure reason ride the body only when the venue supplies them.
+fn status_body(status: IntentStatus) -> StatusBody {
use nexum_status_body::IntentStatus as Lifecycle;
- let (status, proof, reason) = match status {
- IntentStatus::Pending => (Lifecycle::Pending, None, None),
- IntentStatus::Open => (Lifecycle::Open, None, None),
- IntentStatus::Settled(proof) => (Lifecycle::Fulfilled, proof.clone(), None),
- IntentStatus::Failed(reason) => (
- Lifecycle::Cancelled,
- None,
- Some(nexum_status_body::FailReason {
- code: reason.code.clone(),
- detail: reason.detail.clone(),
- }),
- ),
- IntentStatus::Expired => (Lifecycle::Expired, None, None),
- IntentStatus::Cancelled => (Lifecycle::Cancelled, None, None),
+ let status = match status {
+ IntentStatus::Pending => Lifecycle::Pending,
+ IntentStatus::Open => Lifecycle::Open,
+ IntentStatus::Fulfilled => Lifecycle::Fulfilled,
+ IntentStatus::Cancelled => Lifecycle::Cancelled,
+ IntentStatus::Expired => Lifecycle::Expired,
};
StatusBody {
status,
- proof,
- reason,
+ proof: None,
+ reason: None,
}
}
-/// The shared router state. Cloning a [`PoolRouter`] is an `Arc` bump; every
-/// module store carries the same handle, so a submission from any module
-/// reaches the same adapters and the same quota ledger.
-struct PoolRouterInner {
- adapters: HashMap,
- guard: Arc,
- quota: PoolQuota,
+/// The shared registry state. Cloning a [`VenueRegistry`] is an `Arc` bump;
+/// every module store carries the same handle, so a submission from any
+/// module reaches the same adapters and the same quota ledger.
+struct VenueRegistryInner {
+ adapters: HashMap,
+ guard: Arc,
+ quota: SubmitQuota,
ledger: Mutex,
/// Receipts under status watch, appended by accepted submissions and
/// pruned as they reach a terminal status.
watched: Mutex>,
}
-/// The strategy-facing pool router, cheap to clone and shared across every
+/// The keeper-facing venue registry, cheap to clone and shared across every
/// module store.
#[derive(Clone)]
-pub struct PoolRouter {
- inner: Arc,
+pub struct VenueRegistry {
+ inner: Arc,
}
-impl PoolRouter {
- /// An empty router: no adapters, the no-op guard, the default quota. This
- /// is what an adapter store (which cannot call pool) and the single-module
- /// `just run` path carry.
+impl VenueRegistry {
+ /// An empty registry: no adapters, the unit guard, the default quota.
+ /// This is what an adapter store (which cannot call the client face) and
+ /// the single-module `just run` path carry.
pub fn empty() -> Self {
- PoolRouterBuilder::new(PoolQuota::default()).build()
+ VenueRegistryBuilder::new(SubmitQuota::default()).build()
}
/// Resolve a venue id to its installed adapter slot.
- fn resolve(&self, venue: &str) -> Result {
+ fn resolve(&self, venue: &VenueId) -> Result {
self.inner
.adapters
.get(venue)
@@ -372,16 +390,17 @@ impl PoolRouter {
pub async fn submit(
&self,
caller: &str,
- venue: &str,
+ venue: &VenueId,
body: Vec,
) -> Result {
let slot = self.resolve(venue)?;
// Gate before touching the adapter so a quota-exhausted caller never
- // reaches the adapter store or its mutex.
+ // reaches the adapter store or its mutex. Exhaustion is retryable
+ // once the window slides, so it is rate-limited, never denied.
if !self.quota_admits(caller) {
- return Err(VenueError::Denied(format!(
- "submission quota exhausted for caller {caller}"
- )));
+ return Err(VenueError::RateLimited(RateLimit {
+ retry_after_ms: Some(window_ms(self.inner.quota.window)),
+ }));
}
let mut adapter = slot.lock().await;
let header = match adapter.derive_header(&body).await {
@@ -416,16 +435,16 @@ impl PoolRouter {
/// Put a `(venue, receipt)` pair under status watch. Idempotent: a
/// re-submitted receipt keeps its existing watch entry.
- fn watch(&self, venue: &str, receipt: Vec) {
+ fn watch(&self, venue: &VenueId, receipt: Vec) {
let mut watched = self.inner.watched.lock().expect("watch list poisoned");
if watched
.iter()
- .any(|w| w.venue == venue && w.receipt == receipt)
+ .any(|w| w.venue == *venue && w.receipt == receipt)
{
return;
}
watched.push(WatchedIntent {
- venue: venue.to_owned(),
+ venue: venue.clone(),
receipt,
last: None,
});
@@ -444,12 +463,11 @@ impl PoolRouter {
/// return the transitions: statuses that differ from the last one
/// reported for that receipt (the first successful poll always
/// reports). A terminal status is reported once and the receipt is
- /// dropped from the watch; a transport failure leaves the entry
- /// untouched for the next cadence, except `invalid-receipt`, which
- /// means the venue disowns the receipt, so watching is pointless.
+ /// dropped from the watch; a failure leaves the entry untouched for
+ /// the next cadence.
pub async fn poll_status_transitions(&self) -> Vec {
// Snapshot so the std mutex is never held across the guest await.
- let snapshot: Vec<(String, Vec)> = {
+ let snapshot: Vec<(VenueId, Vec)> = {
let watched = self.inner.watched.lock().expect("watch list poisoned");
watched
.iter()
@@ -458,7 +476,7 @@ impl PoolRouter {
};
let mut updates = Vec::new();
for (venue, receipt) in snapshot {
- // Installed adapters never leave the router, so a resolve
+ // Installed adapters never leave the registry, so a resolve
// failure here is unreachable; skip defensively regardless.
let Ok(slot) = self.resolve(&venue) else {
continue;
@@ -473,10 +491,6 @@ impl PoolRouter {
updates.push(update);
}
}
- Err(VenueError::InvalidReceipt) => {
- warn!(venue = %venue, "venue disowns a watched receipt - dropping it");
- self.unwatch(&venue, &receipt);
- }
Err(err) => {
warn!(
venue = %venue,
@@ -497,19 +511,19 @@ impl PoolRouter {
/// cadence).
fn record_polled_status(
&self,
- venue: &str,
+ venue: &VenueId,
receipt: &[u8],
status: IntentStatus,
) -> Option {
let mut watched = self.inner.watched.lock().expect("watch list poisoned");
let pos = watched
.iter()
- .position(|w| w.venue == venue && w.receipt == receipt)?;
- let changed = watched[pos].last.as_ref() != Some(&status);
+ .position(|w| w.venue == *venue && w.receipt == receipt)?;
+ let changed = watched[pos].last != Some(status);
let update = if changed {
- match status_body(&status).encode() {
+ match status_body(status).encode() {
Ok(body) => Some(IntentStatusUpdate {
- venue: venue.to_owned(),
+ venue: venue.as_str().to_owned(),
receipt: receipt.to_vec(),
status: body,
}),
@@ -525,7 +539,7 @@ impl PoolRouter {
} else {
None
};
- if is_terminal(&status) {
+ if is_terminal(status) {
watched.remove(pos);
} else {
watched[pos].last = Some(status);
@@ -533,15 +547,13 @@ impl PoolRouter {
update
}
- /// Drop a `(venue, receipt)` pair from the status watch.
- fn unwatch(&self, venue: &str, receipt: &[u8]) {
- let mut watched = self.inner.watched.lock().expect("watch list poisoned");
- watched.retain(|w| !(w.venue == venue && w.receipt == receipt));
- }
-
/// Report where a previously submitted intent is in its life. Not a
/// submission: no header, no guard, no quota, just the serialised call.
- pub async fn status(&self, venue: &str, receipt: Vec) -> Result {
+ pub async fn status(
+ &self,
+ venue: &VenueId,
+ receipt: Vec,
+ ) -> Result {
let slot = self.resolve(venue)?;
let mut adapter = slot.lock().await;
adapter.status(receipt).await
@@ -549,7 +561,7 @@ impl PoolRouter {
/// Ask the venue to withdraw an intent. Not a submission, so it skips the
/// header, guard, and quota like `status`.
- pub async fn cancel(&self, venue: &str, receipt: Vec) -> Result<(), VenueError> {
+ pub async fn cancel(&self, venue: &VenueId, receipt: Vec) -> Result<(), VenueError> {
let slot = self.resolve(venue)?;
let mut adapter = slot.lock().await;
adapter.cancel(receipt).await
@@ -561,6 +573,11 @@ impl PoolRouter {
}
}
+/// A quota window as whole milliseconds, saturating at `u64::MAX`.
+fn window_ms(window: Duration) -> u64 {
+ u64::try_from(window.as_millis()).unwrap_or(u64::MAX)
+}
+
/// Drop charge timestamps that have aged out of the window.
fn prune(history: &mut VecDeque, window: Duration) {
let now = Instant::now();
@@ -573,29 +590,29 @@ fn prune(history: &mut VecDeque, window: Duration) {
}
}
-/// Assembles a [`PoolRouter`]: adapters install first (at supervisor boot,
-/// before any module store carries the built router), then the router
-/// freezes. The guard defaults to the no-op [`AllowAllGuard`]; the
-/// egress-guard epic overrides it here.
-pub struct PoolRouterBuilder {
- adapters: HashMap,
- guard: Arc,
- quota: PoolQuota,
+/// Assembles a [`VenueRegistry`]: adapters install first (at supervisor
+/// boot, before any module store carries the built registry), then the
+/// registry freezes. The guard defaults to the unit guard; the egress-guard
+/// epic overrides it here.
+pub struct VenueRegistryBuilder {
+ adapters: HashMap,
+ guard: Arc,
+ quota: SubmitQuota,
}
-impl PoolRouterBuilder {
- /// Start an empty builder with the given quota and the no-op guard.
- pub fn new(quota: PoolQuota) -> Self {
+impl VenueRegistryBuilder {
+ /// Start an empty builder with the given quota and the unit guard.
+ pub fn new(quota: SubmitQuota) -> Self {
Self {
adapters: HashMap::new(),
- guard: Arc::new(AllowAllGuard),
+ guard: Arc::new(()),
quota,
}
}
/// Override the guard policy. The egress-guard epic wires the real
/// pipeline through here; tests inject a denying policy to prove the seam.
- pub fn with_guard(mut self, guard: Arc) -> Self {
+ pub fn with_guard(mut self, guard: Arc) -> Self {
self.guard = guard;
self
}
@@ -605,7 +622,7 @@ impl PoolRouterBuilder {
/// which is a config error worth failing boot over.
pub fn install(
&mut self,
- venue: String,
+ venue: VenueId,
invoker: impl VenueInvoker + 'static,
) -> Result<(), DuplicateVenue> {
if self.adapters.contains_key(&venue) {
@@ -616,17 +633,17 @@ impl PoolRouterBuilder {
Ok(())
}
- /// Freeze the builder into a shared router.
- pub fn build(self) -> PoolRouter {
+ /// Freeze the builder into a shared registry.
+ pub fn build(self) -> VenueRegistry {
if self.quota.max_charges == 0 {
- // A zero budget would deny every submission; saturate up to one so
- // a misconfigured quota still admits a single submission rather
+ // A zero budget would refuse every submission; saturate up to one
+ // so a misconfigured quota still admits a single submission rather
// than bricking every venue. Mirrors the poison-policy clamp.
- warn!("pool submission quota max_charges is 0; clamping to 1");
+ warn!("submission quota max_charges is 0; clamping to 1");
}
- let quota = PoolQuota::new(self.quota.max_charges.max(1), self.quota.window);
- PoolRouter {
- inner: Arc::new(PoolRouterInner {
+ let quota = SubmitQuota::new(self.quota.max_charges.max(1), self.quota.window);
+ VenueRegistry {
+ inner: Arc::new(VenueRegistryInner {
adapters: self.adapters,
guard: self.guard,
quota,
@@ -639,10 +656,10 @@ impl PoolRouterBuilder {
/// Two installed adapters claimed the same venue id.
#[derive(Debug, thiserror::Error)]
-#[error("venue id {venue:?} is claimed by more than one installed adapter")]
+#[error("venue id {venue} is claimed by more than one installed adapter")]
pub struct DuplicateVenue {
/// The colliding venue id.
- pub venue: String,
+ pub venue: VenueId,
}
#[cfg(test)]
@@ -651,11 +668,16 @@ mod tests {
use nexum_status_body::IntentStatus as Lifecycle;
- use crate::bindings::value_flow::Settlement;
- use crate::bindings::{AuthScheme, FailReason, IntentHeader, UnsignedTx};
+ use crate::bindings::value_flow::{Asset, AssetAmount};
+ use crate::bindings::{AuthScheme, IntentHeader, Settlement, UnsignedTx};
use super::*;
+ /// The venue id every test installs its stub adapter under.
+ fn cow() -> VenueId {
+ VenueId::from("cow")
+ }
+
/// Decode an update's opaque status body.
fn decoded(update: &IntentStatusUpdate) -> StatusBody {
StatusBody::decode(&update.status).expect("status body decodes")
@@ -671,8 +693,8 @@ mod tests {
}
/// A programmable adapter that records call counts and returns canned
- /// outcomes, so the router's sequencing, guard seam, and quota are tested
- /// without a wasmtime store.
+ /// outcomes, so the registry's sequencing, guard seam, and quota are
+ /// tested without a wasmtime store.
#[derive(Default)]
struct StubCalls {
derive: AtomicUsize,
@@ -774,7 +796,7 @@ mod tests {
/// A guard that refuses every egress with a fixed reason.
struct DenyGuard;
- impl GuardPolicy for DenyGuard {
+ impl EgressGuard for DenyGuard {
fn check(&self, _ctx: &GuardContext<'_>) -> GuardVerdict {
GuardVerdict::Deny("blocked by test policy".to_owned())
}
@@ -782,36 +804,43 @@ mod tests {
fn header() -> IntentHeader {
IntentHeader {
- gives: Vec::new(),
- wants: Vec::new(),
- valid_until: None,
- settlement: Settlement::EvmChain(1),
- authorisation: AuthScheme::Unsigned,
+ gives: AssetAmount {
+ asset: Asset::Native,
+ amount: vec![1],
+ },
+ wants: AssetAmount {
+ asset: Asset::Native,
+ amount: Vec::new(),
+ },
+ settlement: Settlement { chain: 1 },
+ authorisation: AuthScheme::Eip712,
}
}
- fn router_with(
- quota: PoolQuota,
- guard: Option>,
+ fn registry_with(
+ quota: SubmitQuota,
+ guard: Option>,
adapter: StubAdapter,
- ) -> PoolRouter {
- let mut builder = PoolRouterBuilder::new(quota);
+ ) -> VenueRegistry {
+ let mut builder = VenueRegistryBuilder::new(quota);
if let Some(guard) = guard {
builder = builder.with_guard(guard);
}
- builder
- .install("cow".to_owned(), adapter)
- .expect("install adapter");
+ builder.install(cow(), adapter).expect("install adapter");
builder.build()
}
#[tokio::test]
async fn submit_round_trips_through_derive_guard_submit() {
let calls = Arc::new(StubCalls::default());
- let router = router_with(PoolQuota::default(), None, StubAdapter::new(calls.clone()));
+ let registry = registry_with(
+ SubmitQuota::default(),
+ None,
+ StubAdapter::new(calls.clone()),
+ );
- let outcome = router
- .submit("mod-a", "cow", b"body".to_vec())
+ let outcome = registry
+ .submit("mod-a", &cow(), b"body".to_vec())
.await
.expect("submit succeeds");
@@ -823,10 +852,14 @@ mod tests {
#[tokio::test]
async fn unknown_venue_is_rejected_without_touching_an_adapter() {
let calls = Arc::new(StubCalls::default());
- let router = router_with(PoolQuota::default(), None, StubAdapter::new(calls.clone()));
+ let registry = registry_with(
+ SubmitQuota::default(),
+ None,
+ StubAdapter::new(calls.clone()),
+ );
- let err = router
- .submit("mod-a", "unlisted", b"body".to_vec())
+ let err = registry
+ .submit("mod-a", &VenueId::from("unlisted"), b"body".to_vec())
.await
.expect_err("unknown venue rejected");
@@ -838,14 +871,14 @@ mod tests {
#[tokio::test]
async fn guard_deny_blocks_submit_after_deriving_the_header() {
let calls = Arc::new(StubCalls::default());
- let router = router_with(
- PoolQuota::default(),
+ let registry = registry_with(
+ SubmitQuota::default(),
Some(Arc::new(DenyGuard)),
StubAdapter::new(calls.clone()),
);
- let err = router
- .submit("mod-a", "cow", b"body".to_vec())
+ let err = registry
+ .submit("mod-a", &cow(), b"body".to_vec())
.await
.expect_err("guard denies");
@@ -857,19 +890,34 @@ mod tests {
}
#[tokio::test]
- async fn submission_quota_denies_once_the_budget_is_spent() {
+ async fn submission_quota_rate_limits_once_the_budget_is_spent() {
let calls = Arc::new(StubCalls::default());
- let quota = PoolQuota::new(2, Duration::from_secs(3600));
- let router = router_with(quota, None, StubAdapter::new(calls.clone()));
+ let quota = SubmitQuota::new(2, Duration::from_secs(3600));
+ let registry = registry_with(quota, None, StubAdapter::new(calls.clone()));
- assert!(router.submit("mod-a", "cow", b"b".to_vec()).await.is_ok());
- assert!(router.submit("mod-a", "cow", b"b".to_vec()).await.is_ok());
- let err = router
- .submit("mod-a", "cow", b"b".to_vec())
+ assert!(
+ registry
+ .submit("mod-a", &cow(), b"b".to_vec())
+ .await
+ .is_ok()
+ );
+ assert!(
+ registry
+ .submit("mod-a", &cow(), b"b".to_vec())
+ .await
+ .is_ok()
+ );
+ let err = registry
+ .submit("mod-a", &cow(), b"b".to_vec())
.await
.expect_err("third submit over quota");
- assert!(matches!(err, VenueError::Denied(reason) if reason.contains("quota")));
+ // Exhaustion is retryable once the window slides: rate-limited
+ // carrying the window, never denied.
+ assert!(matches!(
+ err,
+ VenueError::RateLimited(rl) if rl.retry_after_ms == Some(3_600_000)
+ ));
// The over-quota call is stopped at the gate, so the adapter saw only
// the two admitted submits.
assert_eq!(calls.submit.load(Ordering::SeqCst), 2);
@@ -878,17 +926,28 @@ mod tests {
#[tokio::test]
async fn quota_is_per_caller() {
let calls = Arc::new(StubCalls::default());
- let quota = PoolQuota::new(1, Duration::from_secs(3600));
- let router = router_with(quota, None, StubAdapter::new(calls.clone()));
+ let quota = SubmitQuota::new(1, Duration::from_secs(3600));
+ let registry = registry_with(quota, None, StubAdapter::new(calls.clone()));
- assert!(router.submit("mod-a", "cow", b"b".to_vec()).await.is_ok());
assert!(
- router.submit("mod-a", "cow", b"b".to_vec()).await.is_err(),
+ registry
+ .submit("mod-a", &cow(), b"b".to_vec())
+ .await
+ .is_ok()
+ );
+ assert!(
+ registry
+ .submit("mod-a", &cow(), b"b".to_vec())
+ .await
+ .is_err(),
"mod-a is over its own budget"
);
// A different caller has its own budget.
assert!(
- router.submit("mod-b", "cow", b"b".to_vec()).await.is_ok(),
+ registry
+ .submit("mod-b", &cow(), b"b".to_vec())
+ .await
+ .is_ok(),
"mod-b has an independent budget"
);
}
@@ -896,18 +955,18 @@ mod tests {
#[tokio::test]
async fn decode_failures_are_charged_and_stop_re_invoking_the_adapter() {
let calls = Arc::new(StubCalls::default());
- let quota = PoolQuota::new(1, Duration::from_secs(3600));
+ let quota = SubmitQuota::new(1, Duration::from_secs(3600));
let adapter =
StubAdapter::new(calls.clone()).with_derive(Err(VenueError::InvalidBody("bad".into())));
- let router = router_with(quota, None, adapter);
+ let registry = registry_with(quota, None, adapter);
// First garbage body: derive fails, the failure is charged.
- let first = router.submit("mod-a", "cow", b"junk".to_vec()).await;
+ let first = registry.submit("mod-a", &cow(), b"junk".to_vec()).await;
assert!(matches!(first, Err(VenueError::InvalidBody(_))));
// Second: the charge from the decode failure exhausts the budget, so
// the caller is stopped at the gate and the adapter is not re-invoked.
- let second = router.submit("mod-a", "cow", b"junk".to_vec()).await;
- assert!(matches!(second, Err(VenueError::Denied(_))));
+ let second = registry.submit("mod-a", &cow(), b"junk".to_vec()).await;
+ assert!(matches!(second, Err(VenueError::RateLimited(_))));
assert_eq!(
calls.derive.load(Ordering::SeqCst),
1,
@@ -918,19 +977,19 @@ mod tests {
#[tokio::test]
async fn non_decode_venue_errors_are_not_charged() {
let calls = Arc::new(StubCalls::default());
- let quota = PoolQuota::new(1, Duration::from_secs(3600));
+ let quota = SubmitQuota::new(1, Duration::from_secs(3600));
let adapter = StubAdapter::new(calls.clone())
.with_derive(Err(VenueError::Unavailable("rpc down".into())));
- let router = router_with(quota, None, adapter);
+ let registry = registry_with(quota, None, adapter);
assert!(matches!(
- router.submit("mod-a", "cow", b"b".to_vec()).await,
+ registry.submit("mod-a", &cow(), b"b".to_vec()).await,
Err(VenueError::Unavailable(_))
));
// A venue-side failure did not spend the caller's budget: it may try
// again, so derive is reached a second time.
assert!(matches!(
- router.submit("mod-a", "cow", b"b".to_vec()).await,
+ registry.submit("mod-a", &cow(), b"b".to_vec()).await,
Err(VenueError::Unavailable(_))
));
assert_eq!(calls.derive.load(Ordering::SeqCst), 2);
@@ -941,14 +1000,14 @@ mod tests {
let calls = Arc::new(StubCalls::default());
// A spent budget must not block reads: status and cancel are not
// submissions.
- let quota = PoolQuota::new(1, Duration::from_secs(3600));
- let router = router_with(quota, None, StubAdapter::new(calls.clone()));
+ let quota = SubmitQuota::new(1, Duration::from_secs(3600));
+ let registry = registry_with(quota, None, StubAdapter::new(calls.clone()));
assert!(matches!(
- router.status("cow", b"r".to_vec()).await,
+ registry.status(&cow(), b"r".to_vec()).await,
Ok(IntentStatus::Open)
));
- assert!(router.cancel("cow", b"r".to_vec()).await.is_ok());
+ assert!(registry.cancel(&cow(), b"r".to_vec()).await.is_ok());
assert_eq!(calls.status.load(Ordering::SeqCst), 1);
assert_eq!(calls.cancel.load(Ordering::SeqCst), 1);
}
@@ -956,14 +1015,14 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn concurrent_calls_to_one_adapter_are_serialised() {
let calls = Arc::new(StubCalls::default());
- let quota = PoolQuota::new(1000, Duration::from_secs(3600));
- let router = router_with(quota, None, StubAdapter::new(calls.clone()));
+ let quota = SubmitQuota::new(1000, Duration::from_secs(3600));
+ let registry = registry_with(quota, None, StubAdapter::new(calls.clone()));
let mut handles = Vec::new();
for _ in 0..8 {
- let router = router.clone();
+ let registry = registry.clone();
handles.push(tokio::spawn(async move {
- let _ = router.submit("mod-a", "cow", b"b".to_vec()).await;
+ let _ = registry.submit("mod-a", &cow(), b"b".to_vec()).await;
}));
}
for h in handles {
@@ -976,22 +1035,23 @@ mod tests {
#[test]
fn duplicate_venue_id_is_rejected() {
- let mut builder = PoolRouterBuilder::new(PoolQuota::default());
+ let mut builder = VenueRegistryBuilder::new(SubmitQuota::default());
let a = Arc::new(StubCalls::default());
let b = Arc::new(StubCalls::default());
builder
- .install("cow".to_owned(), StubAdapter::new(a))
+ .install(cow(), StubAdapter::new(a))
.expect("first install");
let err = builder
- .install("cow".to_owned(), StubAdapter::new(b))
+ .install(cow(), StubAdapter::new(b))
.expect_err("second install collides");
- assert_eq!(err.venue, "cow");
+ assert_eq!(err.venue, cow());
}
#[test]
fn zero_quota_saturates_to_one() {
- let router = PoolRouterBuilder::new(PoolQuota::new(0, Duration::from_secs(60))).build();
- assert_eq!(router.inner.quota.max_charges, 1);
+ let registry =
+ VenueRegistryBuilder::new(SubmitQuota::new(0, Duration::from_secs(60))).build();
+ assert_eq!(registry.inner.quota.max_charges, 1);
}
// ── status watch + polling ────────────────────────────────────────
@@ -999,21 +1059,21 @@ mod tests {
#[tokio::test]
async fn accepted_submission_goes_under_status_watch() {
let calls = Arc::new(StubCalls::default());
- let router = router_with(PoolQuota::default(), None, StubAdapter::new(calls));
+ let registry = registry_with(SubmitQuota::default(), None, StubAdapter::new(calls));
- assert_eq!(router.watched_count(), 0);
- router
- .submit("mod-a", "cow", b"body".to_vec())
+ assert_eq!(registry.watched_count(), 0);
+ registry
+ .submit("mod-a", &cow(), b"body".to_vec())
.await
.expect("submit succeeds");
- assert_eq!(router.watched_count(), 1);
+ assert_eq!(registry.watched_count(), 1);
// Re-submitting the same receipt does not double-watch it.
- router
- .submit("mod-a", "cow", b"body".to_vec())
+ registry
+ .submit("mod-a", &cow(), b"body".to_vec())
.await
.expect("submit succeeds");
- assert_eq!(router.watched_count(), 1);
+ assert_eq!(registry.watched_count(), 1);
}
#[tokio::test]
@@ -1021,42 +1081,46 @@ mod tests {
let calls = Arc::new(StubCalls::default());
let adapter =
StubAdapter::new(calls).with_submit(Ok(SubmitOutcome::RequiresSigning(UnsignedTx {
- chain_id: 1,
+ chain: 1,
to: vec![0u8; 20],
value: Vec::new(),
- input: Vec::new(),
+ data: Vec::new(),
})));
- let router = router_with(PoolQuota::default(), None, adapter);
+ let registry = registry_with(SubmitQuota::default(), None, adapter);
- router
- .submit("mod-a", "cow", b"body".to_vec())
+ registry
+ .submit("mod-a", &cow(), b"body".to_vec())
.await
.expect("submit succeeds");
// No receipt exists yet, so there is nothing to poll.
- assert_eq!(router.watched_count(), 0);
- assert!(router.poll_status_transitions().await.is_empty());
+ assert_eq!(registry.watched_count(), 0);
+ assert!(registry.poll_status_transitions().await.is_empty());
}
#[tokio::test]
async fn poll_reports_the_first_status_then_dedupes_repeats() {
let calls = Arc::new(StubCalls::default());
- let router = router_with(PoolQuota::default(), None, StubAdapter::new(calls.clone()));
- router
- .submit("mod-a", "cow", b"body".to_vec())
+ let registry = registry_with(
+ SubmitQuota::default(),
+ None,
+ StubAdapter::new(calls.clone()),
+ );
+ registry
+ .submit("mod-a", &cow(), b"body".to_vec())
.await
.expect("submit succeeds");
// First poll: `last` is unset, so the current status reports.
- let first = router.poll_status_transitions().await;
+ let first = registry.poll_status_transitions().await;
assert_eq!(first.len(), 1);
assert_eq!(first[0].venue, "cow");
assert_eq!(first[0].receipt, b"receipt");
assert_eq!(decoded(&first[0]), plain(Lifecycle::Open));
// Second poll: same status, nothing to report.
- assert!(router.poll_status_transitions().await.is_empty());
+ assert!(registry.poll_status_transitions().await.is_empty());
assert_eq!(calls.status.load(Ordering::SeqCst), 2);
- assert_eq!(router.watched_count(), 1, "open is not terminal");
+ assert_eq!(registry.watched_count(), 1, "open is not terminal");
}
#[tokio::test]
@@ -1066,17 +1130,17 @@ mod tests {
Ok(IntentStatus::Pending),
Ok(IntentStatus::Pending),
Ok(IntentStatus::Open),
- Ok(IntentStatus::Settled(Some(b"tx".to_vec()))),
+ Ok(IntentStatus::Fulfilled),
]);
- let router = router_with(PoolQuota::default(), None, adapter);
- router
- .submit("mod-a", "cow", b"body".to_vec())
+ let registry = registry_with(SubmitQuota::default(), None, adapter);
+ registry
+ .submit("mod-a", &cow(), b"body".to_vec())
.await
.expect("submit succeeds");
let mut seen = Vec::new();
for _ in 0..4 {
- seen.extend(router.poll_status_transitions().await);
+ seen.extend(registry.poll_status_transitions().await);
}
let statuses: Vec = seen.iter().map(decoded).collect();
assert_eq!(
@@ -1084,17 +1148,13 @@ mod tests {
vec![
plain(Lifecycle::Pending),
plain(Lifecycle::Open),
- StatusBody {
- status: Lifecycle::Fulfilled,
- proof: Some(b"tx".to_vec()),
- reason: None,
- },
+ plain(Lifecycle::Fulfilled),
],
"the repeated pending is deduplicated; each transition reports once",
);
- assert_eq!(router.watched_count(), 0, "settled prunes the watch");
+ assert_eq!(registry.watched_count(), 0, "fulfilled prunes the watch");
// A further poll has nothing left to ask the adapter about.
- assert!(router.poll_status_transitions().await.is_empty());
+ assert!(registry.poll_status_transitions().await.is_empty());
}
#[tokio::test]
@@ -1102,59 +1162,35 @@ mod tests {
let calls = Arc::new(StubCalls::default());
let adapter = StubAdapter::new(calls)
.with_status_script([Err(VenueError::Unavailable("venue down".into()))]);
- let router = router_with(PoolQuota::default(), None, adapter);
- router
- .submit("mod-a", "cow", b"body".to_vec())
+ let registry = registry_with(SubmitQuota::default(), None, adapter);
+ registry
+ .submit("mod-a", &cow(), b"body".to_vec())
.await
.expect("submit succeeds");
- assert!(router.poll_status_transitions().await.is_empty());
+ assert!(registry.poll_status_transitions().await.is_empty());
assert_eq!(
- router.watched_count(),
+ registry.watched_count(),
1,
"transient failure keeps the entry"
);
// The venue recovered: the next poll reports the current status.
- let updates = router.poll_status_transitions().await;
+ let updates = registry.poll_status_transitions().await;
assert_eq!(updates.len(), 1);
assert_eq!(decoded(&updates[0]), plain(Lifecycle::Open));
}
#[test]
- fn failed_lowers_to_cancelled_plus_reason() {
- let body = status_body(&IntentStatus::Failed(FailReason {
- code: "oc".into(),
- detail: "od".into(),
- }));
- assert_eq!(
- body,
- StatusBody {
- status: Lifecycle::Cancelled,
- proof: None,
- reason: Some(nexum_status_body::FailReason {
- code: "oc".into(),
- detail: "od".into(),
- }),
- },
- );
- }
-
- #[tokio::test]
- async fn disowned_receipt_is_dropped_from_the_watch() {
- let calls = Arc::new(StubCalls::default());
- let adapter = StubAdapter::new(calls).with_status_script([Err(VenueError::InvalidReceipt)]);
- let router = router_with(PoolQuota::default(), None, adapter);
- router
- .submit("mod-a", "cow", b"body".to_vec())
- .await
- .expect("submit succeeds");
-
- assert!(router.poll_status_transitions().await.is_empty());
- assert_eq!(
- router.watched_count(),
- 0,
- "a receipt the venue disowns is never polled again",
- );
+ fn every_lifecycle_state_lowers_onto_the_status_body() {
+ for (wire, lowered) in [
+ (IntentStatus::Pending, Lifecycle::Pending),
+ (IntentStatus::Open, Lifecycle::Open),
+ (IntentStatus::Fulfilled, Lifecycle::Fulfilled),
+ (IntentStatus::Cancelled, Lifecycle::Cancelled),
+ (IntentStatus::Expired, Lifecycle::Expired),
+ ] {
+ assert_eq!(status_body(wire), plain(lowered));
+ }
}
}
diff --git a/crates/nexum-runtime/src/manifest/capabilities.rs b/crates/nexum-runtime/src/manifest/capabilities.rs
index 50108786..ef00fcef 100644
--- a/crates/nexum-runtime/src/manifest/capabilities.rs
+++ b/crates/nexum-runtime/src/manifest/capabilities.rs
@@ -39,17 +39,18 @@ pub const CORE_NAMESPACE: NamespaceCaps = NamespaceCaps {
ifaces: CORE_CAPABILITIES,
};
-/// Capability names under the `nexum:intent/` package a module may import.
-/// Only the strategy-facing `pool` interface is a capability; the `types`
-/// package is type-only and needs no declaration.
-pub const INTENT_CAPABILITIES: &[&str] = &["pool"];
-
-/// The intent namespace: the `nexum:intent/pool` import is linked into every
-/// module linker, so a module that submits intents declares the `pool`
-/// capability the same way it declares a `nexum:host/` one.
-pub const INTENT_NAMESPACE: NamespaceCaps = NamespaceCaps {
- prefix: "nexum:intent/",
- ifaces: INTENT_CAPABILITIES,
+/// Capability names under the `videre:venue/` package a module may import.
+/// Only the keeper-facing `client` interface is a capability; the
+/// `videre:types` and `videre:value-flow` packages are type-only and need
+/// no declaration.
+pub const VENUE_CAPABILITIES: &[&str] = &["client"];
+
+/// The venue namespace: the `videre:venue/client` import is linked into
+/// every module linker, so a module that submits intents declares the
+/// `client` capability the same way it declares a `nexum:host/` one.
+pub const VENUE_NAMESPACE: NamespaceCaps = NamespaceCaps {
+ prefix: "videre:venue/",
+ ifaces: VENUE_CAPABILITIES,
};
/// The interfaces a `venue-adapter` world links: the scoped transport
@@ -127,10 +128,10 @@ impl Default for CapabilityRegistry {
impl CapabilityRegistry {
/// The registry with the core `nexum:host/` namespace plus the
- /// strategy-facing `nexum:intent/pool` import every module linker carries.
+ /// keeper-facing `videre:venue/client` import every module linker carries.
pub fn core() -> Self {
Self {
- namespaces: vec![CORE_NAMESPACE, INTENT_NAMESPACE],
+ namespaces: vec![CORE_NAMESPACE, VENUE_NAMESPACE],
}
}
@@ -327,12 +328,17 @@ mod tests {
}
#[test]
- fn intent_pool_is_a_core_capability_but_intent_types_is_not() {
+ fn venue_client_is_a_core_capability_but_videre_types_is_not() {
let r = CapabilityRegistry::core();
- assert_eq!(r.wit_import_to_cap("nexum:intent/pool@0.1.0"), Some("pool"));
- assert!(r.is_known("pool"));
- // The type-only interface is not a capability and needs no declaration.
- assert_eq!(r.wit_import_to_cap("nexum:intent/types@0.1.0"), None);
+ assert_eq!(
+ r.wit_import_to_cap("videre:venue/client@0.1.0"),
+ Some("client")
+ );
+ assert!(r.is_known("client"));
+ // The type-only interfaces are not capabilities and need no
+ // declaration.
+ assert_eq!(r.wit_import_to_cap("videre:types/types@0.1.0"), None);
+ assert_eq!(r.wit_import_to_cap("videre:value-flow/types@0.1.0"), None);
}
#[test]
diff --git a/crates/nexum-runtime/src/runtime/event_loop.rs b/crates/nexum-runtime/src/runtime/event_loop.rs
index dd6fa885..a63526df 100644
--- a/crates/nexum-runtime/src/runtime/event_loop.rs
+++ b/crates/nexum-runtime/src/runtime/event_loop.rs
@@ -40,8 +40,8 @@ use tracing::{info, warn};
use crate::bindings::nexum;
use crate::host::component::{ChainProvider, RuntimeTypes};
-use crate::host::pool_router::PoolRouter;
use crate::host::provider_pool::ProviderError;
+use crate::host::venue_registry::VenueRegistry;
use crate::runtime::restart_policy::backoff_for;
use crate::supervisor::{ChainLogSub, Supervisor};
use nexum_tasks::{TaskExecutor, TaskExit, TaskSet};
@@ -147,32 +147,32 @@ where
streams
}
-/// Router-driven intent status polling: one task that, on every cadence
+/// Registry-driven intent status polling: one task that, on every cadence
/// tick, polls each installed adapter's status export through the shared
-/// [`PoolRouter`] and forwards the observed transitions. The task is
+/// [`VenueRegistry`] and forwards the observed transitions. The task is
/// spawned via `executor` into `tasks` like the reconnect tasks and exits
/// cleanly when the loop's receiver drops.
pub fn open_intent_status_stream(
- router: PoolRouter,
+ registry: VenueRegistry,
cadence: Duration,
executor: &TaskExecutor,
tasks: &mut TaskSet,
) -> IntentStatusStream {
let (tx, rx) = mpsc::channel::(RECONNECT_CHANNEL_BUF);
- tasks.push(executor.spawn(Box::pin(status_poll_task(router, cadence, tx))));
+ tasks.push(executor.spawn(Box::pin(status_poll_task(registry, cadence, tx))));
Box::pin(receiver_stream(rx))
}
/// Poll loop behind [`open_intent_status_stream`]. Sleeps the cadence
/// first so the engine's boot dispatch settles before the first poll.
async fn status_poll_task(
- router: PoolRouter,
+ registry: VenueRegistry,
cadence: Duration,
tx: mpsc::Sender,
) -> TaskExit {
loop {
tokio::time::sleep(cadence).await;
- for update in router.poll_status_transitions().await {
+ for update in registry.poll_status_transitions().await {
if tx.send(update).await.is_err() {
// Receiver dropped -> engine shutting down.
return TaskExit::ReceiverGone;
diff --git a/crates/nexum-runtime/src/supervisor.rs b/crates/nexum-runtime/src/supervisor.rs
index 78390678..363b0cc9 100644
--- a/crates/nexum-runtime/src/supervisor.rs
+++ b/crates/nexum-runtime/src/supervisor.rs
@@ -48,10 +48,10 @@ use crate::host::http::HttpGate;
#[cfg(test)]
use crate::host::local_store_redb::LocalStore;
use crate::host::logs::{LogRecord, LogSource, RunId, StdioStream};
-use crate::host::pool_router::{AdapterActor, PoolRouter, PoolRouterBuilder};
#[cfg(test)]
use crate::host::provider_pool::ProviderPool;
use crate::host::state::HostState;
+use crate::host::venue_registry::{VenueActor, VenueId, VenueRegistry, VenueRegistryBuilder};
use crate::manifest::{
self, CapabilityRegistry, LoadedManifest, ModuleKind, ResourceSection, Subscription,
};
@@ -61,13 +61,13 @@ use crate::manifest::{
/// the component seam backends.
pub struct Supervisor {
modules: Vec>,
- /// The intent pool router: every installed venue adapter's serialising
+ /// The venue registry: every installed venue adapter's serialising
/// store, keyed by venue id. Cached so a module restart rebuilds a store
/// carrying the same shared handle. Adapters boot through the same store,
/// fuel, and memory machinery as modules but carry no subscriptions:
- /// modules reach them through this router, not through dispatch. Folding
+ /// modules reach them through this registry, not through dispatch. Folding
/// adapters into the restart and poison sweeps is still a later change.
- pool_router: PoolRouter,
+ venue_registry: VenueRegistry,
/// Venue adapters loaded at boot, whether or not `init` succeeded.
adapters_total: usize,
/// Adapters whose `init` succeeded and that are installed for routing.
@@ -254,16 +254,16 @@ struct LoadedModule {
}
/// A venue adapter instantiated into a supervised store, ready to install in
-/// the pool router. It boots through the same store, fuel, and memory
+/// the venue registry. It boots through the same store, fuel, and memory
/// machinery as a module but carries no subscriptions: modules reach it
-/// through the router, not through dispatch. Adapter restart and poison
+/// through the registry, not through dispatch. Adapter restart and poison
/// handling are still a later change; an `init` failure leaves `alive` false
/// so the adapter is loaded but not routable.
struct LoadedAdapter {
/// Venue id the adapter answers for (its manifest name).
- venue_id: String,
- /// The refuelable adapter store, ready to serialise behind a router mutex.
- actor: AdapterActor,
+ venue_id: VenueId,
+ /// The refuelable adapter store, ready to serialise behind a registry mutex.
+ actor: VenueActor,
/// Whether `init` succeeded; a failed adapter is not installed for routing.
alive: bool,
}
@@ -281,14 +281,15 @@ impl Supervisor {
clocks: Option,
) -> Result {
let registry = capability_registry(extensions);
- // Adapters instantiate first: the pool router must contain them before
- // any module store (which carries the built router) is built. Adapters
- // link only their scoped transport, against a dedicated linker built
- // from the same core backends, and their own stores carry an empty
- // router since an adapter cannot call pool.
+ // Adapters instantiate first: the venue registry must contain them
+ // before any module store (which carries the built registry) is
+ // built. Adapters link only their scoped transport, against a
+ // dedicated linker built from the same core backends, and their own
+ // stores carry an empty registry since an adapter cannot call the
+ // client face.
let adapter_linker = build_adapter_linker::(engine)?;
let adapter_registry = CapabilityRegistry::adapter();
- let mut router_builder = PoolRouterBuilder::new(engine_cfg.limits.quota());
+ let mut registry_builder = VenueRegistryBuilder::new(engine_cfg.limits.quota());
let adapters_total = engine_cfg.adapters.len();
let mut adapters_alive = 0;
for entry in &engine_cfg.adapters {
@@ -305,7 +306,7 @@ impl Supervisor {
.with_context(|| format!("load adapter {}", entry.path.display()))?;
if loaded.alive {
adapters_alive += 1;
- router_builder
+ registry_builder
.install(loaded.venue_id.clone(), loaded.actor)
.with_context(|| format!("install adapter {}", loaded.venue_id))?;
} else {
@@ -315,7 +316,7 @@ impl Supervisor {
);
}
}
- let pool_router = router_builder.build();
+ let venue_registry = registry_builder.build();
let mut modules = Vec::with_capacity(engine_cfg.modules.len());
for entry in &engine_cfg.modules {
@@ -327,7 +328,7 @@ impl Supervisor {
&engine_cfg.limits,
®istry,
clocks.as_ref(),
- pool_router.clone(),
+ venue_registry.clone(),
)
.await
.with_context(|| format!("load module {}", entry.path.display()))?;
@@ -343,7 +344,7 @@ impl Supervisor {
);
Ok(Self {
modules,
- pool_router,
+ venue_registry,
adapters_total,
adapters_alive,
engine: engine.clone(),
@@ -377,9 +378,9 @@ impl Supervisor {
manifest: manifest.map(Path::to_path_buf),
};
// The single-module override path serves `just run`; adapters are
- // configured through `engine.toml`, so the router is empty here and
- // every pool call resolves to `unknown-venue`.
- let pool_router = PoolRouter::empty();
+ // configured through `engine.toml`, so the registry is empty here and
+ // every client call resolves to `unknown-venue`.
+ let venue_registry = VenueRegistry::empty();
let loaded = Self::load_one(
engine,
linker,
@@ -388,12 +389,12 @@ impl Supervisor {
limits,
®istry,
clocks.as_ref(),
- pool_router.clone(),
+ venue_registry.clone(),
)
.await?;
Ok(Self {
modules: vec![loaded],
- pool_router,
+ venue_registry,
adapters_total: 0,
adapters_alive: 0,
engine: engine.clone(),
@@ -423,7 +424,7 @@ impl Supervisor {
chain_response_max_bytes: usize,
state_quota: u64,
clocks: Option<&WasiClockOverride>,
- pool_router: PoolRouter,
+ venue_registry: VenueRegistry,
) -> Result> {
let namespace: &str = &run.module;
// Capture guest stdout/stderr per store instead of inheriting the
@@ -481,7 +482,7 @@ impl Supervisor {
chain: components.chain.clone(),
chain_response_max_bytes,
store: module_store,
- pool_router,
+ venue_registry,
},
);
store.limiter(|state| &mut state.limits);
@@ -490,7 +491,7 @@ impl Supervisor {
}
// One flat argument per shared input threaded onto the store, plus the
- // pool router the module's `nexum:intent/pool` import dispatches to.
+ // venue registry the module's `videre:venue/client` import dispatches to.
#[allow(clippy::too_many_arguments)]
async fn load_one(
engine: &Engine,
@@ -500,7 +501,7 @@ impl Supervisor {
limits_cfg: &ModuleLimits,
registry: &CapabilityRegistry,
clocks: Option<&WasiClockOverride>,
- pool_router: PoolRouter,
+ venue_registry: VenueRegistry,
) -> Result> {
let manifest_path = resolve_manifest_path(&entry.path, entry.manifest.as_deref());
let loaded_manifest: LoadedManifest = match manifest_path.as_deref() {
@@ -565,7 +566,7 @@ impl Supervisor {
limits_cfg.chain_response_max_bytes(),
state_bytes,
clocks,
- pool_router,
+ venue_registry,
)?;
let bindings = EventModule::instantiate_async(&mut store, &component, linker)
.await
@@ -661,7 +662,7 @@ impl Supervisor {
/// capability set, build a supervised store carrying the operator's
/// HTTP and messaging grants, instantiate the `VenueAdapter` bindings
/// against the adapter linker, and run `init`. Nothing dispatches to
- /// the result yet; it boots so the router can later reach it.
+ /// the result yet; it boots so the registry can later reach it.
async fn load_adapter(
engine: &Engine,
linker: &Linker>,
@@ -732,9 +733,10 @@ impl Supervisor {
);
let run = RunId::new(adapter_namespace.clone(), 0);
- // An adapter store cannot call pool, so it carries an empty router;
- // this also keeps the real router out of the adapter's `HostState`,
- // so there is no reference cycle back into the router that owns it.
+ // An adapter store cannot call the client face, so it carries an
+ // empty registry; this also keeps the real registry out of the
+ // adapter's `HostState`, so there is no reference cycle back into
+ // the registry that owns it.
let mut store = Self::build_store(
engine,
components,
@@ -747,7 +749,7 @@ impl Supervisor {
limits_cfg.chain_response_max_bytes(),
limits_cfg.state_bytes(),
clocks,
- PoolRouter::empty(),
+ VenueRegistry::empty(),
)?;
let bindings = VenueAdapter::instantiate_async(&mut store, &component, linker)
.await
@@ -782,8 +784,8 @@ impl Supervisor {
store.set_fuel(limits_cfg.fuel())?;
Ok(LoadedAdapter {
- venue_id: adapter_namespace,
- actor: AdapterActor::new(store, bindings, limits_cfg.fuel()),
+ venue_id: VenueId::from(adapter_namespace),
+ actor: VenueActor::new(store, bindings, limits_cfg.fuel()),
alive: init_succeeded,
})
}
@@ -799,7 +801,7 @@ impl Supervisor {
}
/// Number of adapters whose `init` succeeded and that are installed in the
- /// pool router for routing.
+ /// venue registry for routing.
#[cfg_attr(not(test), allow(dead_code))]
pub fn adapter_alive_count(&self) -> usize {
self.adapters_alive
@@ -914,10 +916,10 @@ impl Supervisor {
let linker = build_linker::(&self.engine, &self.extensions)?;
// Borrowed before the `&mut self.modules[idx]` reborrow so the restart
- // path applies the same clock override and the same shared pool router
+ // path applies the same clock override and the same shared registry
// as the initial boot.
let clocks = self.clocks.clone();
- let pool_router = self.pool_router.clone();
+ let venue_registry = self.venue_registry.clone();
let module = &mut self.modules[idx];
// A restart is a new run: bump the sequence so its logs key
// apart from the dead run's, which stays readable until evicted.
@@ -934,7 +936,7 @@ impl Supervisor {
module.chain_response_max_bytes,
module.local_store_bytes,
clocks.as_ref(),
- pool_router,
+ venue_registry,
)?;
let bindings = EventModule::instantiate_async(&mut store, &module.component, &linker)
.await
@@ -1126,7 +1128,7 @@ impl Supervisor {
ok
}
- /// Dispatch a router-observed intent status transition to every module
+ /// Dispatch a registry-observed intent status transition to every module
/// subscribed to `intent-status` events whose venue filter admits the
/// update's venue. Returns the number of modules invoked. Mirrors
/// `dispatch_block`: dead modules past their backoff are restarted
@@ -1187,9 +1189,9 @@ impl Supervisor {
})
}
- /// The shared intent pool router carried by every module store.
- pub fn pool_router(&self) -> PoolRouter {
- self.pool_router.clone()
+ /// The shared venue registry carried by every module store.
+ pub fn venue_registry(&self) -> VenueRegistry {
+ self.venue_registry.clone()
}
/// Shared per-module dispatch path: refuel, call `on_event`, and
@@ -1423,10 +1425,10 @@ pub fn build_linker(
) -> anyhow::Result>> {
let mut linker = Linker::>::new(engine);
EventModule::add_to_linker::, HasSelf>>(&mut linker, |state| state)?;
- // The intent pool import is linked into every module linker; it dispatches
- // to the shared router carried in each store's `HostState`. Modules that do
- // not import it are unaffected.
- crate::bindings::pool::add_to_linker::, HasSelf>>(
+ // The venue client import is linked into every module linker; it
+ // dispatches to the shared registry carried in each store's `HostState`.
+ // Modules that do not import it are unaffected.
+ crate::bindings::client::add_to_linker::, HasSelf>>(
&mut linker,
|state| state,
)?;
diff --git a/crates/nexum-runtime/src/supervisor/tests.rs b/crates/nexum-runtime/src/supervisor/tests.rs
index ec5da908..c71f43db 100644
--- a/crates/nexum-runtime/src/supervisor/tests.rs
+++ b/crates/nexum-runtime/src/supervisor/tests.rs
@@ -542,7 +542,7 @@ chain_id = 1
// ── intent-status subscription E2E ────────────────────────────────────
-/// A scripted venue adapter for the router: accepts every submission with
+/// A scripted venue adapter for the registry: accepts every submission with
/// a fixed receipt and serves statuses front-first from a script, falling
/// back to `open` once drained.
struct ScriptedAdapter {
@@ -557,7 +557,7 @@ impl ScriptedAdapter {
}
}
-impl crate::host::pool_router::VenueInvoker for ScriptedAdapter {
+impl crate::host::venue_registry::VenueInvoker for ScriptedAdapter {
fn derive_header<'a>(
&'a mut self,
_body: &'a [u8],
@@ -567,11 +567,16 @@ impl crate::host::pool_router::VenueInvoker for ScriptedAdapter {
> {
Box::pin(async move {
Ok(crate::bindings::IntentHeader {
- gives: Vec::new(),
- wants: Vec::new(),
- valid_until: None,
- settlement: crate::bindings::value_flow::Settlement::EvmChain(1),
- authorisation: crate::bindings::AuthScheme::Unsigned,
+ gives: crate::bindings::value_flow::AssetAmount {
+ asset: crate::bindings::value_flow::Asset::Native,
+ amount: vec![1],
+ },
+ wants: crate::bindings::value_flow::AssetAmount {
+ asset: crate::bindings::value_flow::Asset::Native,
+ amount: Vec::new(),
+ },
+ settlement: crate::bindings::Settlement { chain: 1 },
+ authorisation: crate::bindings::AuthScheme::Eip712,
})
})
}
@@ -613,12 +618,14 @@ impl crate::host::pool_router::VenueInvoker for ScriptedAdapter {
}
}
-/// Build a router with one scripted adapter installed under `cow`.
-fn scripted_router(adapter: ScriptedAdapter) -> crate::host::pool_router::PoolRouter {
- let mut builder = crate::host::pool_router::PoolRouterBuilder::new(
- crate::host::pool_router::PoolQuota::default(),
+/// Build a registry with one scripted adapter installed under `cow`.
+fn scripted_registry(adapter: ScriptedAdapter) -> crate::host::venue_registry::VenueRegistry {
+ let mut builder = crate::host::venue_registry::VenueRegistryBuilder::new(
+ crate::host::venue_registry::SubmitQuota::default(),
);
- builder.install("cow".to_owned(), adapter).expect("install");
+ builder
+ .install(crate::host::venue_registry::VenueId::from("cow"), adapter)
+ .expect("install");
builder.build()
}
@@ -645,7 +652,7 @@ venue = "cow"
}
/// The acceptance path: a module subscribed to `intent-status` receives
-/// the transitions the router observed by polling the adapter's status
+/// the transitions the registry observed by polling the adapter's status
/// export, and a transition from a venue outside its filter is not
/// delivered.
#[tokio::test]
@@ -678,24 +685,28 @@ async fn e2e_intent_status_subscription_receives_polled_transitions() {
.expect("boot_single");
assert!(supervisor.has_intent_status_subscribers());
- // The router watches the receipt of an accepted submission and polls
+ // The registry watches the receipt of an accepted submission and polls
// the adapter's status export; each poll here observes a transition.
- let router = scripted_router(ScriptedAdapter::new([
+ let registry = scripted_registry(ScriptedAdapter::new([
IntentStatus::Pending,
- IntentStatus::Settled(None),
+ IntentStatus::Fulfilled,
]));
- router
- .submit("test-caller", "cow", b"body".to_vec())
+ registry
+ .submit(
+ "test-caller",
+ &crate::host::venue_registry::VenueId::from("cow"),
+ b"body".to_vec(),
+ )
.await
.expect("submit");
let mut delivered = 0;
for _ in 0..2 {
- for update in router.poll_status_transitions().await {
+ for update in registry.poll_status_transitions().await {
delivered += supervisor.dispatch_intent_status(update).await;
}
}
- assert_eq!(delivered, 2, "pending then settled, one subscriber each");
+ assert_eq!(delivered, 2, "pending then fulfilled, one subscriber each");
assert_eq!(supervisor.alive_count(), 1, "module must remain alive");
// A venue outside the module's filter is not delivered.
@@ -747,9 +758,13 @@ async fn e2e_intent_status_flows_through_the_event_loop() {
.await
.expect("boot_single");
- let router = scripted_router(ScriptedAdapter::new([]));
- router
- .submit("test-caller", "cow", b"body".to_vec())
+ let registry = scripted_registry(ScriptedAdapter::new([]));
+ registry
+ .submit(
+ "test-caller",
+ &crate::host::venue_registry::VenueId::from("cow"),
+ b"body".to_vec(),
+ )
.await
.expect("submit");
@@ -757,7 +772,7 @@ async fn e2e_intent_status_flows_through_the_event_loop() {
let executor = manager.executor();
let mut tasks = TaskSet::new();
let stream = crate::runtime::event_loop::open_intent_status_stream(
- router,
+ registry,
Duration::from_millis(10),
&executor,
&mut tasks,
@@ -789,13 +804,14 @@ async fn e2e_intent_status_flows_through_the_event_loop() {
}
/// The first-train acceptance path, end to end over two real components:
-/// the echo-client module submits through `nexum:intent/pool`, the host
-/// router forwards to the installed echo-venue adapter, and the module
-/// receives the settled `intent-status` the router polls back. Proves the
-/// intent core round-trips module -> host router -> venue adapter with no
+/// the echo-client module submits through `videre:venue/client`, the host
+/// registry forwards to the installed echo-venue adapter, and the module
+/// receives the fulfilled `intent-status` the registry polls back. Proves
+/// the intent core round-trips module -> host registry -> venue adapter
+/// with no
/// scripted stand-ins on either side.
#[tokio::test]
-async fn e2e_echo_module_router_adapter_round_trip() {
+async fn e2e_echo_module_registry_adapter_round_trip() {
use crate::engine_config::{AdapterEntry, EngineConfig, ModuleEntry};
use crate::host::component::ChainMethod;
use crate::test_utils::{MockChainProvider, MockStateStore, MockTypes};
@@ -843,7 +859,7 @@ async fn e2e_echo_module_router_adapter_round_trip() {
assert!(supervisor.has_intent_status_subscribers());
// A block drives the module's on_block, which submits to the echo venue
- // through the shared pool router; the router watches the accepted receipt.
+ // through the shared registry; the registry watches the accepted receipt.
let block = nexum::host::types::Block {
chain_id: 1,
number: 19_000_000,
@@ -852,13 +868,13 @@ async fn e2e_echo_module_router_adapter_round_trip() {
};
assert_eq!(supervisor.dispatch_block(block).await, 1);
- // Poll the router the module submitted through and fan its transitions
+ // Poll the registry the module submitted through and fan its transitions
// back to the module. echo-venue settles instantly, so the first poll
// reports a terminal status and the watch is pruned.
- let router = supervisor.pool_router();
+ let registry = supervisor.venue_registry();
let mut delivered = 0;
for _ in 0..2 {
- for update in router.poll_status_transitions().await {
+ for update in registry.poll_status_transitions().await {
assert_eq!(update.venue, "echo-venue");
let body =
nexum_status_body::StatusBody::decode(&update.status).expect("status body decodes");
@@ -886,7 +902,7 @@ async fn e2e_echo_module_router_adapter_round_trip() {
messages
.iter()
.any(|m| m.contains("submitted") && m.contains("echo-venue")),
- "module submitted through the pool; records were: {messages:?}",
+ "module submitted through the client face; records were: {messages:?}",
);
assert!(
messages
diff --git a/crates/nexum-venue-sdk/src/adapter.rs b/crates/nexum-venue-sdk/src/adapter.rs
index 58fc8ffc..f711ea96 100644
--- a/crates/nexum-venue-sdk/src/adapter.rs
+++ b/crates/nexum-venue-sdk/src/adapter.rs
@@ -2,7 +2,7 @@
//! it into the component's `venue-adapter` world surface.
//!
//! The trait mirrors the world's export face one to one: `init` from the
-//! world itself, the four intent functions from `nexum:intent/adapter`.
+//! world itself, the four intent functions from `videre:venue/adapter`.
//! Functions are associated (no `self`): the component model instantiates
//! one adapter per venue and calls exports statically, so adapter state
//! lives in the adapter's own statics, exactly as in event modules.
@@ -62,7 +62,7 @@ macro_rules! export_venue_adapter {
}
}
- impl $crate::bindings::exports::nexum::intent::adapter::Guest
+ impl $crate::bindings::exports::videre::venue::adapter::Guest
for __NexumVenueAdapterExport
{
fn derive_header(
diff --git a/crates/nexum-venue-sdk/src/bindings.rs b/crates/nexum-venue-sdk/src/bindings.rs
index 3e88fff2..f40e1585 100644
--- a/crates/nexum-venue-sdk/src/bindings.rs
+++ b/crates/nexum-venue-sdk/src/bindings.rs
@@ -1,4 +1,4 @@
-//! Guest bindings for the `nexum:adapter/venue-adapter` world.
+//! Guest bindings for the `videre:venue/venue-adapter` world.
//!
//! Unlike event modules, which run `wit_bindgen::generate!` per cdylib,
//! the venue SDK generates the adapter world's bindings once, here: the
@@ -7,17 +7,17 @@
//! types, and [`export_venue_adapter!`](crate::export_venue_adapter)
//! emits the component export glue into the adapter's own cdylib via the
//! generated (hidden) export macro. Downstream bindgens wanting type
-//! identity with this crate remap `nexum:intent/types` and
-//! `nexum:value-flow/types` onto these modules with `with`.
+//! identity with this crate remap `videre:types/types` and
+//! `videre:value-flow/types` onto these modules with `with`.
wit_bindgen::generate!({
path: [
- "../../wit/nexum-value-flow",
- "../../wit/nexum-intent",
+ "../../wit/videre-value-flow",
+ "../../wit/videre-types",
"../../wit/nexum-host",
- "../../wit/nexum-adapter",
+ "../../wit/videre-venue",
],
- world: "nexum:adapter/venue-adapter",
+ world: "videre:venue/venue-adapter",
generate_all,
pub_export_macro: true,
export_macro_name: "__export_venue_adapter_world",
diff --git a/crates/nexum-venue-sdk/src/body.rs b/crates/nexum-venue-sdk/src/body.rs
index d542aca2..1f202ce2 100644
--- a/crates/nexum-venue-sdk/src/body.rs
+++ b/crates/nexum-venue-sdk/src/body.rs
@@ -72,16 +72,15 @@ pub enum BodyError {
}
/// Fold a codec failure into the wire error an adapter returns: decode
-/// failures are the caller's malformed body (`invalid-body`, whose WIT
-/// contract names exactly these two causes), an encode failure is the
-/// adapter's own bug (`internal-error`).
+/// failures are the caller's malformed body (`invalid-body`); an encode
+/// failure is the adapter's own bug, reported retryable (`unavailable`).
impl From for VenueError {
fn from(err: BodyError) -> Self {
match err {
BodyError::Empty | BodyError::UnknownVersion { .. } | BodyError::Malformed { .. } => {
VenueError::InvalidBody(err.to_string())
}
- BodyError::Encode { .. } => VenueError::InternalError(err.to_string()),
+ BodyError::Encode { .. } => VenueError::Unavailable(err.to_string()),
}
}
}
diff --git a/crates/nexum-venue-sdk/src/client.rs b/crates/nexum-venue-sdk/src/client.rs
index edfc8dd7..5048eda1 100644
--- a/crates/nexum-venue-sdk/src/client.rs
+++ b/crates/nexum-venue-sdk/src/client.rs
@@ -1,12 +1,12 @@
//! The typed intent client core: [`IntentClient`] over the byte-level
-//! [`IntentPool`] seam.
+//! [`VenueClient`] seam.
//!
-//! The pool boundary carries opaque bodies; this module is where a
+//! The client boundary carries opaque bodies; this module is where a
//! typed body meets it. [`IntentClient`] binds one venue and encodes
-//! through [`IntentBody`] before submission, so strategy code never
+//! through [`IntentBody`] before submission, so keeper code never
//! handles wire bytes. The seam is byte-level on purpose: the
-//! strategy-module SDK implements [`IntentPool`] over its own
-//! `nexum:intent/pool` import shims, tests implement it in memory
+//! strategy-module SDK implements [`VenueClient`] over its own
+//! `videre:venue/client` import shims, tests implement it in memory
//! (an in-process adapter works directly), and the typed layer above is
//! shared by both.
@@ -14,9 +14,9 @@ use strum::IntoStaticStr;
use crate::{BodyError, IntentBody, IntentStatus, SubmitOutcome, VenueError};
-/// Byte-level access to the strategy-facing `nexum:intent/pool`
+/// Byte-level access to the keeper-facing `videre:venue/client`
/// interface, venue named per call as on the wire.
-pub trait IntentPool {
+pub trait VenueClient {
/// Submit an opaque intent body to the named venue.
fn submit(&self, venue: &str, body: Vec) -> Result;
@@ -30,18 +30,18 @@ pub trait IntentPool {
}
/// A typed intent client bound to one venue: encodes an [`IntentBody`]
-/// to wire bytes and forwards through the [`IntentPool`] seam.
+/// to wire bytes and forwards through the [`VenueClient`] seam.
#[derive(Clone, Debug)]
pub struct IntentClient {
- pool: P,
+ venues: P,
venue: String,
}
-impl IntentClient {
- /// Bind a pool handle to the venue id the router resolves.
- pub fn new(pool: P, venue: impl Into) -> Self {
+impl IntentClient {
+ /// Bind a client handle to the venue id the registry resolves.
+ pub fn new(venues: P, venue: impl Into) -> Self {
Self {
- pool,
+ venues,
venue: venue.into(),
}
}
@@ -54,39 +54,39 @@ impl IntentClient {
/// Encode a typed body and submit it to the bound venue.
pub fn submit(&self, body: &B) -> Result {
let bytes = body.to_bytes()?;
- self.pool
+ self.venues
.submit(&self.venue, bytes)
.map_err(ClientError::Venue)
}
/// Report where a previously submitted intent is in its life.
pub fn status(&self, receipt: &[u8]) -> Result {
- self.pool
+ self.venues
.status(&self.venue, receipt)
.map_err(ClientError::Venue)
}
/// Ask the bound venue to withdraw an intent.
pub fn cancel(&self, receipt: &[u8]) -> Result<(), ClientError> {
- self.pool
+ self.venues
.cancel(&self.venue, receipt)
.map_err(ClientError::Venue)
}
}
/// Why a typed intent call failed: before the wire (the body failed to
-/// encode) or beyond it (the pool or venue refused).
+/// encode) or beyond it (the registry or venue refused).
///
/// `IntoStaticStr` yields a snake_case label per case for log and
/// metric fields.
#[derive(Clone, Debug, PartialEq, thiserror::Error, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub enum ClientError {
- /// The typed body failed to encode; nothing reached the pool.
+ /// The typed body failed to encode; nothing crossed the wire.
#[error(transparent)]
Body(#[from] BodyError),
- /// The pool or the venue behind it failed the call. The payload is
- /// the wire `venue-error`, which carries no `Display`; format via
+ /// The registry or the venue behind it failed the call. The payload
+ /// is the wire `venue-error`, which carries no `Display`; format via
/// `Debug`.
#[error("venue error: {0:?}")]
Venue(VenueError),
diff --git a/crates/nexum-venue-sdk/src/faults.rs b/crates/nexum-venue-sdk/src/faults.rs
index 9a0e3a1e..f8e00ce1 100644
--- a/crates/nexum-venue-sdk/src/faults.rs
+++ b/crates/nexum-venue-sdk/src/faults.rs
@@ -11,7 +11,7 @@
use nexum_sdk::host;
use crate::bindings::nexum::host::types::RateLimit as WireRateLimit;
-use crate::{Fault, VenueError};
+use crate::{Fault, RateLimit, VenueError};
/// Lift the wire fault into the SDK-neutral vocabulary the transport
/// seams and `nexum-sdk` helpers speak. Exhaustive: the wire enum is
@@ -53,37 +53,39 @@ impl From for Fault {
}
/// Fold a transport fault into the venue error an intent function
-/// returns: a policy refusal stays `denied`, retryable transport states
-/// (`unavailable`, `rate-limited`, `timeout`) fold to `unavailable`,
-/// `unsupported` passes through, and the caller-shaped cases
-/// (`invalid-input`, `internal`) become `internal-error` because inside
-/// an intent function the transport's caller is the adapter itself.
+/// returns: `denied`, `rate-limited`, `timeout`, and `unsupported` map
+/// structurally; `unavailable` keeps its detail; the caller-shaped cases
+/// (`invalid-input`, `internal`) fold to retryable `unavailable` because
+/// inside an intent function the transport's caller is the adapter
+/// itself, never the module.
impl From for VenueError {
fn from(fault: host::Fault) -> Self {
match fault {
host::Fault::Denied(s) => VenueError::Denied(s),
- host::Fault::Unsupported(s) => VenueError::Unsupported(s),
- host::Fault::Unavailable(_) | host::Fault::RateLimited(_) | host::Fault::Timeout => {
- VenueError::Unavailable(fault.to_string())
- }
- other => VenueError::InternalError(other.to_string()),
+ host::Fault::Unsupported(_) => VenueError::Unsupported,
+ host::Fault::RateLimited(rl) => VenueError::RateLimited(RateLimit {
+ retry_after_ms: rl.retry_after_ms,
+ }),
+ host::Fault::Timeout => VenueError::Timeout,
+ host::Fault::Unavailable(s) => VenueError::Unavailable(s),
+ other => VenueError::Unavailable(other.to_string()),
}
}
}
/// Fold a wasi:http fetch failure into the venue error an intent
-/// function returns: an allowlist refusal stays `denied`, timeouts and
-/// transport failures are retryable `unavailable`, and a request the
-/// adapter itself malformed is `internal-error`.
+/// function returns: an allowlist refusal stays `denied`, a timeout is
+/// `timeout`, and transport failures (including a request the adapter
+/// itself malformed) are retryable `unavailable`.
impl From for VenueError {
fn from(err: nexum_sdk::http::FetchError) -> Self {
use nexum_sdk::http::FetchError;
match err {
FetchError::Denied => VenueError::Denied(err.to_string()),
- FetchError::Timeout(_) | FetchError::Transport(_) => {
+ FetchError::Timeout(_) => VenueError::Timeout,
+ FetchError::Transport(_) | FetchError::InvalidRequest(_) => {
VenueError::Unavailable(err.to_string())
}
- FetchError::InvalidRequest(_) => VenueError::InternalError(err.to_string()),
}
}
}
@@ -119,13 +121,16 @@ mod tests {
VenueError::from(host::Fault::Denied("nope".into())),
VenueError::Denied("nope".into()),
);
+ assert_eq!(VenueError::from(host::Fault::Timeout), VenueError::Timeout);
assert!(matches!(
- VenueError::from(host::Fault::Timeout),
- VenueError::Unavailable(_)
+ VenueError::from(host::Fault::RateLimited(host::RateLimit {
+ retry_after_ms: Some(250),
+ })),
+ VenueError::RateLimited(rl) if rl.retry_after_ms == Some(250)
));
assert!(matches!(
VenueError::from(host::Fault::InvalidInput("bug".into())),
- VenueError::InternalError(_)
+ VenueError::Unavailable(_)
));
}
@@ -142,7 +147,7 @@ mod tests {
));
assert!(matches!(
VenueError::from(FetchError::InvalidRequest("bad url".into())),
- VenueError::InternalError(_)
+ VenueError::Unavailable(_)
));
}
}
diff --git a/crates/nexum-venue-sdk/src/lib.rs b/crates/nexum-venue-sdk/src/lib.rs
index 45a5dc43..1c0559e3 100644
--- a/crates/nexum-venue-sdk/src/lib.rs
+++ b/crates/nexum-venue-sdk/src/lib.rs
@@ -19,7 +19,7 @@
//!
//! - [`client`] - the typed intent client core: [`IntentClient`] binds a
//! venue and encodes through [`IntentBody`] before the byte-level
-//! [`IntentPool`] seam. Lives here (not in the strategy SDK) so the
+//! [`VenueClient`] seam. Lives here (not in the strategy SDK) so the
//! codec and the client that speaks it version together.
//!
//! - [`transport`] - typed wrappers over the world's scoped imports:
@@ -41,7 +41,7 @@
//!
//! [`ChainHost`]: nexum_sdk::host::ChainHost
//! [`IntentClient`]: client::IntentClient
-//! [`IntentPool`]: client::IntentPool
+//! [`VenueClient`]: client::VenueClient
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![warn(missing_docs)]
@@ -57,7 +57,7 @@ pub mod transport;
pub use adapter::VenueAdapter;
pub use body::{BodyError, IntentBody};
-pub use client::{ClientError, IntentClient, IntentPool};
+pub use client::{ClientError, IntentClient, VenueClient};
/// Derive [`IntentBody`] on the outer per-venue version enum. See
/// [`nexum_macros::IntentBody`].
pub use nexum_macros::IntentBody;
@@ -76,11 +76,12 @@ pub use nexum_macros::venue;
/// The intent ontology at its plain spellings: the types the
/// [`VenueAdapter`] face and the client core speak.
-pub use bindings::nexum::intent::types::{
- AuthScheme, FailReason, IntentHeader, IntentStatus, SubmitOutcome, UnsignedTx, VenueError,
+pub use bindings::videre::types::types::{
+ AuthScheme, IntentHeader, IntentStatus, RateLimit, Settlement, SubmitOutcome, UnsignedTx,
+ VenueError,
};
/// The value-flow vocabulary intent headers are expressed in.
-pub use bindings::nexum::value_flow::types as value_flow;
+pub use bindings::videre::value_flow::types as value_flow;
/// The wire config table (`nexum:host/types.config`) `init` receives.
pub use bindings::nexum::host::types::Config;
diff --git a/crates/nexum-venue-sdk/tests/adapter.rs b/crates/nexum-venue-sdk/tests/adapter.rs
index c6afd9ca..699915f0 100644
--- a/crates/nexum-venue-sdk/tests/adapter.rs
+++ b/crates/nexum-venue-sdk/tests/adapter.rs
@@ -3,13 +3,13 @@
//! `export_venue_adapter!`, and round-trips a versioned body through
//! `#[derive(IntentBody)]` - including the typed unknown-version
//! failure and the typed client core driving the adapter through the
-//! [`IntentPool`] seam.
+//! [`VenueClient`] seam.
use borsh::{BorshDeserialize, BorshSerialize};
-use nexum_venue_sdk::value_flow::{Asset, AssetAmount, Settlement};
+use nexum_venue_sdk::value_flow::{Asset, AssetAmount};
use nexum_venue_sdk::{
AuthScheme, BodyError, ClientError, Config, Fault, IntentBody, IntentClient, IntentHeader,
- IntentPool, IntentStatus, SubmitOutcome, VenueAdapter, VenueError,
+ IntentStatus, Settlement, SubmitOutcome, VenueAdapter, VenueClient, VenueError,
};
/// First published body version: a fixed-price quote.
@@ -60,15 +60,17 @@ impl VenueAdapter for DemoAdapter {
}
fn derive_header(body: Vec) -> Result {
- let (amount_wei, valid_until) = Self::decode(&body)?;
+ let (amount_wei, _valid_until_ms) = Self::decode(&body)?;
Ok(IntentHeader {
- gives: vec![AssetAmount {
- asset: Asset::NativeToken(Settlement::EvmChain(1)),
+ gives: AssetAmount {
+ asset: Asset::Native,
amount: amount_wei.to_be_bytes().to_vec(),
- }],
- wants: Vec::new(),
- valid_until,
- settlement: Settlement::EvmChain(1),
+ },
+ wants: AssetAmount {
+ asset: Asset::Native,
+ amount: Vec::new(),
+ },
+ settlement: Settlement { chain: 1 },
authorisation: AuthScheme::Eip712,
})
}
@@ -82,7 +84,11 @@ impl VenueAdapter for DemoAdapter {
if receipt == RECEIPT {
Ok(IntentStatus::Open)
} else {
- Err(VenueError::InvalidReceipt)
+ // A receipt this venue never issued can never succeed, so the
+ // refusal is the non-retryable case.
+ Err(VenueError::Denied(
+ "receipt not issued by this venue".into(),
+ ))
}
}
@@ -95,11 +101,11 @@ impl VenueAdapter for DemoAdapter {
// venue-adapter world.
nexum_venue_sdk::export_venue_adapter!(DemoAdapter);
-/// In-process pool: routes the demo venue id straight into the adapter,
-/// standing in for the host router the strategy-side seam will bind.
-struct InProcessPool;
+/// In-process client: routes the demo venue id straight into the adapter,
+/// standing in for the host registry the keeper-side seam will bind.
+struct InProcessClient;
-impl IntentPool for InProcessPool {
+impl VenueClient for InProcessClient {
fn submit(&self, venue: &str, body: Vec) -> Result {
if venue != "demo" {
return Err(VenueError::UnknownVenue);
@@ -192,9 +198,9 @@ fn empty_and_malformed_bodies_fail_typedly() {
fn adapter_projects_the_header_from_a_versioned_body() {
let bytes = v2_body().to_bytes().unwrap();
let header = DemoAdapter::derive_header(bytes).unwrap();
- assert_eq!(header.gives.len(), 1);
- assert_eq!(header.gives[0].amount, 1_000_000u64.to_be_bytes().to_vec());
- assert_eq!(header.valid_until, Some(1_700_000_000_000));
+ assert_eq!(header.gives.asset, Asset::Native);
+ assert_eq!(header.gives.amount, 1_000_000u64.to_be_bytes().to_vec());
+ assert_eq!(header.settlement, Settlement { chain: 1 });
assert_eq!(header.authorisation, AuthScheme::Eip712);
}
@@ -210,8 +216,8 @@ fn adapter_reports_an_unknown_version_as_invalid_body() {
}
#[test]
-fn typed_client_round_trips_through_the_pool_seam() {
- let client = IntentClient::new(InProcessPool, "demo");
+fn typed_client_round_trips_through_the_client_seam() {
+ let client = IntentClient::new(InProcessClient, "demo");
let outcome = client.submit(&v2_body()).unwrap();
let SubmitOutcome::Accepted(receipt) = outcome else {
@@ -224,13 +230,13 @@ fn typed_client_round_trips_through_the_pool_seam() {
assert!(matches!(
client.status(&[0, 1]).unwrap_err(),
- ClientError::Venue(VenueError::InvalidReceipt)
+ ClientError::Venue(VenueError::Denied(_))
));
}
#[test]
-fn unbound_venue_is_unknown_at_the_pool() {
- let client = IntentClient::new(InProcessPool, "nowhere");
+fn unbound_venue_is_unknown_at_the_client() {
+ let client = IntentClient::new(InProcessClient, "nowhere");
assert!(matches!(
client.submit(&v2_body()).unwrap_err(),
ClientError::Venue(VenueError::UnknownVenue)
diff --git a/crates/nexum-venue-test/goldens/reference-header.json b/crates/nexum-venue-test/goldens/reference-header.json
index 980751be..ded49d81 100644
--- a/crates/nexum-venue-test/goldens/reference-header.json
+++ b/crates/nexum-venue-test/goldens/reference-header.json
@@ -5,19 +5,16 @@
"name": "v1-small",
"body": "00010000000000000002000000676d",
"header": {
- "gives": [
- {
- "asset": {
- "native-token": {
- "evm-chain": 1
- }
- },
- "amount": "01"
- }
- ],
- "wants": [],
+ "gives": {
+ "asset": "native",
+ "amount": "01"
+ },
+ "wants": {
+ "asset": "native",
+ "amount": ""
+ },
"settlement": {
- "evm-chain": 1
+ "chain": 1
},
"authorisation": "eip712"
},
@@ -27,34 +24,24 @@
"name": "v2-full",
"body": "0140420f00000000000b00000074776f20636f6666656573010068e5cf8b010000140000000102030405060708090a0b0c0d0e0f101112131401",
"header": {
- "gives": [
- {
- "asset": {
- "native-token": {
- "evm-chain": 1
- }
- },
- "amount": "0f4240"
- }
- ],
- "wants": [
- {
- "asset": {
- "erc20": {
- "chain-id": 1,
- "address": "0102030405060708090a0b0c0d0e0f1011121314"
- }
- },
- "amount": "0f4240"
- }
- ],
- "valid-until": 1700000000000,
+ "gives": {
+ "asset": "native",
+ "amount": "0f4240"
+ },
+ "wants": {
+ "asset": {
+ "erc20": {
+ "token": "0102030405060708090a0b0c0d0e0f1011121314"
+ }
+ },
+ "amount": "0f4240"
+ },
"settlement": {
- "evm-chain": 1
+ "chain": 1
},
"authorisation": "eip712"
},
- "notes": "v2 adds the expiry and an erc20 want at the recipient address"
+ "notes": "v2 adds an erc20 want at the recipient token address"
}
]
}
diff --git a/crates/nexum-venue-test/src/header.rs b/crates/nexum-venue-test/src/header.rs
index bee21583..f2908622 100644
--- a/crates/nexum-venue-test/src/header.rs
+++ b/crates/nexum-venue-test/src/header.rs
@@ -14,8 +14,8 @@
use std::fmt;
use std::path::Path;
-use nexum_venue_sdk::value_flow::{Asset, AssetAmount, Settlement};
-use nexum_venue_sdk::{AuthScheme, IntentHeader};
+use nexum_venue_sdk::value_flow::{Asset, AssetAmount};
+use nexum_venue_sdk::{AuthScheme, IntentHeader, Settlement};
use serde::{Deserialize, Serialize};
use crate::fixture::{self, FixtureError, hex_bytes};
@@ -53,12 +53,9 @@ pub struct HeaderGolden {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct GoldenHeader {
/// Value leaving the user's control.
- pub gives: Vec,
- /// Value expected in return.
- pub wants: Vec,
- /// Expiry in milliseconds since the Unix epoch, UTC.
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub valid_until: Option,
+ pub gives: GoldenAssetAmount,
+ /// Value expected in return. Display-grade, not host-verified.
+ pub wants: GoldenAssetAmount,
/// Where the deal settles.
pub settlement: GoldenSettlement,
/// How the venue authorises the intent.
@@ -66,29 +63,26 @@ pub struct GoldenHeader {
}
/// Serde mirror of the wire `asset-amount`. `amount` is big-endian
-/// unsigned, hex in the file; an empty string is zero.
+/// unsigned, minimal-length, hex in the file; an empty string is zero.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GoldenAssetAmount {
/// The asset moving.
pub asset: GoldenAsset,
- /// Big-endian unsigned amount bytes.
+ /// Big-endian minimal-length unsigned amount bytes.
#[serde(with = "hex_bytes")]
pub amount: Vec,
}
/// Serde mirror of the wire `settlement`.
-#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", deny_unknown_fields)]
-pub enum GoldenSettlement {
- /// Settles on an EVM chain, by chain id.
- EvmChain(u64),
- /// Settles off-chain in the named domain.
- Offchain(String),
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct GoldenSettlement {
+ /// EVM chain id the deal settles on.
+ pub chain: u64,
}
-/// Serde mirror of the wire `asset`. Token addresses and ids are hex
-/// in the file.
+/// Serde mirror of the wire `asset`. Token addresses are hex in the file.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(
rename_all = "kebab-case",
@@ -96,51 +90,13 @@ pub enum GoldenSettlement {
deny_unknown_fields
)]
pub enum GoldenAsset {
- /// The settlement domain's own gas token.
- NativeToken(GoldenSettlement),
- /// An ERC-20 token.
+ /// The settlement chain's gas token.
+ Native,
+ /// An ERC-20 token on the settlement chain.
Erc20 {
- /// Chain the token lives on.
- chain_id: u64,
- /// 20-byte contract address.
- #[serde(with = "hex_bytes")]
- address: Vec,
- },
- /// An ERC-721 NFT.
- Erc721 {
- /// Chain the token lives on.
- chain_id: u64,
/// 20-byte contract address.
#[serde(with = "hex_bytes")]
- address: Vec,
- /// Token id, big-endian, arbitrary width.
- #[serde(with = "hex_bytes")]
- token_id: Vec,
- },
- /// An ERC-1155 token.
- Erc1155 {
- /// Chain the token lives on.
- chain_id: u64,
- /// 20-byte contract address.
- #[serde(with = "hex_bytes")]
- address: Vec,
- /// Token id, big-endian, arbitrary width.
- #[serde(with = "hex_bytes")]
- token_id: Vec,
- },
- /// A non-token service obligation.
- Service {
- /// Namespaced service kind, e.g. `swarm:postage`.
- kind: String,
- /// Human-readable description for the consent sheet.
- summary: String,
- },
- /// A real-world asset settled off-chain.
- Offchain {
- /// Jurisdiction or registry domain.
- domain: String,
- /// Human-readable description for the consent sheet.
- summary: String,
+ token: Vec,
},
}
@@ -148,24 +104,17 @@ pub enum GoldenAsset {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum GoldenAuthScheme {
- /// EIP-712 typed-data signature by host-held keys.
- Eip712,
/// EIP-1271 contract signature.
Eip1271,
- /// Pre-signed authorisation at the settlement contract.
- Presign,
- /// Venue-defined off-chain signature scheme.
- OffchainSig,
- /// No authorisation travels with the body.
- Unsigned,
+ /// EIP-712 typed-data signature by host-held keys.
+ Eip712,
}
impl From for GoldenHeader {
fn from(header: IntentHeader) -> Self {
Self {
- gives: header.gives.into_iter().map(Into::into).collect(),
- wants: header.wants.into_iter().map(Into::into).collect(),
- valid_until: header.valid_until,
+ gives: header.gives.into(),
+ wants: header.wants.into(),
settlement: header.settlement.into(),
authorisation: header.authorisation.into(),
}
@@ -183,9 +132,8 @@ impl From for GoldenAssetAmount {
impl From for GoldenSettlement {
fn from(settlement: Settlement) -> Self {
- match settlement {
- Settlement::EvmChain(chain_id) => GoldenSettlement::EvmChain(chain_id),
- Settlement::Offchain(domain) => GoldenSettlement::Offchain(domain),
+ Self {
+ chain: settlement.chain,
}
}
}
@@ -193,26 +141,8 @@ impl From for GoldenSettlement {
impl From for GoldenAsset {
fn from(asset: Asset) -> Self {
match asset {
- Asset::NativeToken(settlement) => GoldenAsset::NativeToken(settlement.into()),
- Asset::Erc20((chain_id, address)) => GoldenAsset::Erc20 { chain_id, address },
- Asset::Erc721((chain_id, address, token_id)) => GoldenAsset::Erc721 {
- chain_id,
- address,
- token_id,
- },
- Asset::Erc1155((chain_id, address, token_id)) => GoldenAsset::Erc1155 {
- chain_id,
- address,
- token_id,
- },
- Asset::Service(desc) => GoldenAsset::Service {
- kind: desc.kind,
- summary: desc.summary,
- },
- Asset::Offchain(desc) => GoldenAsset::Offchain {
- domain: desc.domain,
- summary: desc.summary,
- },
+ Asset::Native => GoldenAsset::Native,
+ Asset::Erc20(erc20) => GoldenAsset::Erc20 { token: erc20.token },
}
}
}
@@ -220,11 +150,8 @@ impl From for GoldenAsset {
impl From for GoldenAuthScheme {
fn from(scheme: AuthScheme) -> Self {
match scheme {
- AuthScheme::Eip712 => GoldenAuthScheme::Eip712,
AuthScheme::Eip1271 => GoldenAuthScheme::Eip1271,
- AuthScheme::Presign => GoldenAuthScheme::Presign,
- AuthScheme::OffchainSig => GoldenAuthScheme::OffchainSig,
- AuthScheme::Unsigned => GoldenAuthScheme::Unsigned,
+ AuthScheme::Eip712 => GoldenAuthScheme::Eip712,
}
}
}
@@ -336,47 +263,24 @@ impl HeaderGoldens {
#[cfg(test)]
mod tests {
use nexum_venue_sdk::VenueError;
- use nexum_venue_sdk::value_flow::{OffchainDesc, ServiceDesc};
+ use nexum_venue_sdk::value_flow::Erc20;
use super::*;
fn wire_header() -> IntentHeader {
IntentHeader {
- gives: vec![
- AssetAmount {
- asset: Asset::NativeToken(Settlement::EvmChain(100)),
- amount: vec![0x0d, 0xe0, 0xb6],
- },
- AssetAmount {
- asset: Asset::Erc20((1, vec![0xAA; 20])),
- amount: vec![1, 0],
- },
- AssetAmount {
- asset: Asset::Erc721((1, vec![0xBB; 20], vec![7])),
- amount: vec![1],
- },
- AssetAmount {
- asset: Asset::Erc1155((1, vec![0xCC; 20], vec![8])),
- amount: vec![2],
- },
- AssetAmount {
- asset: Asset::Service(ServiceDesc {
- kind: "swarm:postage".to_owned(),
- summary: "storage for 30 days".to_owned(),
- }),
- amount: Vec::new(),
- },
- ],
- wants: vec![AssetAmount {
- asset: Asset::Offchain(OffchainDesc {
- domain: "iso:AU".to_owned(),
- summary: "a deed".to_owned(),
+ gives: AssetAmount {
+ asset: Asset::Native,
+ amount: vec![0x0d, 0xe0, 0xb6],
+ },
+ wants: AssetAmount {
+ asset: Asset::Erc20(Erc20 {
+ token: vec![0xAA; 20],
}),
- amount: Vec::new(),
- }],
- valid_until: Some(1_700_000_000_000),
- settlement: Settlement::Offchain("acme".to_owned()),
- authorisation: AuthScheme::OffchainSig,
+ amount: vec![1, 0],
+ },
+ settlement: Settlement { chain: 100 },
+ authorisation: AuthScheme::Eip1271,
}
}
@@ -395,12 +299,11 @@ mod tests {
let json = goldens.to_json();
assert_eq!(HeaderGoldens::from_json(&json).unwrap(), goldens);
// The wire spellings are the contract for non-Rust readers.
- assert!(json.contains("\"native-token\""));
- assert!(json.contains("\"chain-id\""));
- assert!(json.contains("\"token-id\""));
- assert!(json.contains("\"valid-until\""));
- assert!(json.contains("\"offchain-sig\""));
- assert!(json.contains("\"evm-chain\""));
+ assert!(json.contains("\"native\""));
+ assert!(json.contains("\"erc20\""));
+ assert!(json.contains("\"token\""));
+ assert!(json.contains("\"chain\""));
+ assert!(json.contains("\"eip1271\""));
}
#[test]
@@ -432,7 +335,7 @@ mod tests {
calls += 1;
if calls == 1 {
let mut header = wire_header();
- header.valid_until = None;
+ header.authorisation = AuthScheme::Eip712;
Ok(header)
} else {
Err(VenueError::InvalidBody("nope".to_owned()))
@@ -454,6 +357,6 @@ mod tests {
goldens
.record("a", vec![1], |_| Ok::<_, VenueError>(wire_header()))
.unwrap();
- goldens.assert_conforms(|_| Err::(VenueError::InvalidReceipt));
+ goldens.assert_conforms(|_| Err::(VenueError::Timeout));
}
}
diff --git a/crates/nexum-venue-test/src/reference.rs b/crates/nexum-venue-test/src/reference.rs
index 74ce00b2..be6eef02 100644
--- a/crates/nexum-venue-test/src/reference.rs
+++ b/crates/nexum-venue-test/src/reference.rs
@@ -11,8 +11,8 @@
//! must reproduce them byte for byte.
use borsh::{BorshDeserialize, BorshSerialize};
-use nexum_venue_sdk::value_flow::{Asset, AssetAmount, Settlement};
-use nexum_venue_sdk::{AuthScheme, IntentBody, IntentHeader, VenueError};
+use nexum_venue_sdk::value_flow::{Asset, AssetAmount, Erc20};
+use nexum_venue_sdk::{AuthScheme, IntentBody, IntentHeader, Settlement, VenueError};
/// The published codec vector file, verbatim.
pub const CODEC_VECTORS_JSON: &str = include_str!("../vectors/reference-body.json");
@@ -59,29 +59,36 @@ pub enum ReferenceBody {
}
/// The reference venue's pure header derivation, the subject the
-/// published goldens pin. Gives the amount as chain-1 native token,
-/// wants (for v2) the same amount as an ERC-20 at the recipient
-/// address, and authorises via EIP-712.
+/// published goldens pin. Gives the amount as the chain's native token,
+/// wants (for v2) the same amount as an ERC-20 at the recipient token
+/// address, and authorises via EIP-712. V1 wants nothing, spelled as a
+/// zero native amount.
pub fn derive_reference_header(body: Vec) -> Result {
- let (amount_wei, valid_until, wants) = match ReferenceBody::from_bytes(&body)? {
- ReferenceBody::V1(quote) => (quote.amount_wei, None, Vec::new()),
+ let (amount_wei, wants) = match ReferenceBody::from_bytes(&body)? {
+ ReferenceBody::V1(quote) => (
+ quote.amount_wei,
+ AssetAmount {
+ asset: Asset::Native,
+ amount: Vec::new(),
+ },
+ ),
ReferenceBody::V2(quote) => (
quote.amount_wei,
- quote.valid_until_ms,
- vec![AssetAmount {
- asset: Asset::Erc20((1, quote.recipient)),
+ AssetAmount {
+ asset: Asset::Erc20(Erc20 {
+ token: quote.recipient,
+ }),
amount: minimal_be(quote.amount_wei),
- }],
+ },
),
};
Ok(IntentHeader {
- gives: vec![AssetAmount {
- asset: Asset::NativeToken(Settlement::EvmChain(1)),
+ gives: AssetAmount {
+ asset: Asset::Native,
amount: minimal_be(amount_wei),
- }],
+ },
wants,
- valid_until,
- settlement: Settlement::EvmChain(1),
+ settlement: Settlement { chain: 1 },
authorisation: AuthScheme::Eip712,
})
}
@@ -227,8 +234,7 @@ mod tests {
derive_reference_header,
)
.unwrap()
- .notes =
- Some("v2 adds the expiry and an erc20 want at the recipient address".to_owned());
+ .notes = Some("v2 adds an erc20 want at the recipient token address".to_owned());
goldens
}
diff --git a/crates/nexum-venue-test/tests/conformance.rs b/crates/nexum-venue-test/tests/conformance.rs
index 290c2ef4..d6c37040 100644
--- a/crates/nexum-venue-test/tests/conformance.rs
+++ b/crates/nexum-venue-test/tests/conformance.rs
@@ -3,7 +3,7 @@
//! golden files, and a deliberately divergent adapter is caught by
//! them.
-use nexum_venue_sdk::value_flow::{Asset, AssetAmount, Settlement};
+use nexum_venue_sdk::value_flow::{Asset, AssetAmount};
use nexum_venue_sdk::{
AuthScheme, Config, Fault, IntentHeader, IntentStatus, SubmitOutcome, VenueAdapter, VenueError,
};
@@ -58,9 +58,7 @@ fn divergent_derivation_is_caught_by_the_published_goldens() {
// The classic byte-order bug: little-endian amounts.
let derive = |body: Vec| -> Result {
let mut header = derive_reference_header(body)?;
- for give in &mut header.gives {
- give.amount.reverse();
- }
+ header.gives.amount.reverse();
Ok(header)
};
let report = HeaderGoldens::from_json(HEADER_GOLDENS_JSON)
@@ -124,10 +122,7 @@ fn published_files_document_the_wire_format_in_hex() {
);
// And the expected header speaks the value-flow vocabulary.
let derived = derive_reference_header(golden.body.clone()).unwrap();
- assert_eq!(
- derived.gives[0].asset,
- Asset::NativeToken(Settlement::EvmChain(1)),
- );
+ assert_eq!(derived.gives.asset, Asset::Native);
assert_eq!(derived.authorisation, AuthScheme::Eip712);
- let _: &AssetAmount = &derived.gives[0];
+ let _: &AssetAmount = &derived.gives;
}
diff --git a/crates/shepherd-cow-host/src/ext_cow.rs b/crates/shepherd-cow-host/src/ext_cow.rs
index 018adaba..c98acb31 100644
--- a/crates/shepherd-cow-host/src/ext_cow.rs
+++ b/crates/shepherd-cow-host/src/ext_cow.rs
@@ -26,8 +26,6 @@ use crate::cow_orderbook::{CowApiError, OrderBookPool};
mod bindings {
wasmtime::component::bindgen!({
path: [
- "../../wit/nexum-value-flow",
- "../../wit/nexum-intent",
"../../wit/nexum-host",
"../../wit/shepherd-cow",
],
diff --git a/docs/adr/0013-composable-cow-structured-poll.md b/docs/adr/0013-composable-cow-structured-poll.md
index 70222401..63764ab3 100644
--- a/docs/adr/0013-composable-cow-structured-poll.md
+++ b/docs/adr/0013-composable-cow-structured-poll.md
@@ -16,7 +16,7 @@ The blocker is deployment. The fork is `abiVersion 2.0.0-dev`, `deployments/netw
## Decision
-The CoW module is a generic, handler-agnostic ComposableCoW monitor (a `ccow-monitor`), not a TWAP-specific strategy. It indexes `ConditionalOrderCreated`, polls each authorised order, and switches on `GeneratorResult.code` and nothing else: `POST` (with a signature emitted) submits the order through `nexum:intent/pool`; `WAIT_TIMESTAMP` / `WAIT_BLOCK` reschedule at `waitUntil`; `TRY_NEXT_BLOCK` re-polls; `INVALID` drops the watch; `NEEDS_INPUT` hands to the offchainInput layer or parks. It decodes no revert selectors, computes no schedule, and holds no per-handler branch. TWAP survives as a watch scope and golden vectors, not as code.
+The CoW module is a generic, handler-agnostic ComposableCoW monitor (a `ccow-monitor`), not a TWAP-specific strategy. It indexes `ConditionalOrderCreated`, polls each authorised order, and switches on `GeneratorResult.code` and nothing else: `POST` (with a signature emitted) submits the order through `videre:venue/client`; `WAIT_TIMESTAMP` / `WAIT_BLOCK` reschedule at `waitUntil`; `TRY_NEXT_BLOCK` re-polls; `INVALID` drops the watch; `NEEDS_INPUT` hands to the offchainInput layer or parks. It decodes no revert selectors, computes no schedule, and holds no per-handler branch. TWAP survives as a watch scope and golden vectors, not as code.
The chassis (ADR-0009) supplies the mechanism: the watch-set and journal stores are unchanged; the two-gate store (`next_block:` / `next_epoch:`) now holds the contract-supplied `waitUntil` / `nextPollTimestamp` slots, so its value source moves from off-chain revert-decode to the contract verdict while its shape is unchanged. The `ConditionalSource` seam returns a structured `Verdict` mirroring `GeneratorResultCode` plus the hints; it does not decode or schedule.
diff --git a/modules/ethflow-watcher/src/lib.rs b/modules/ethflow-watcher/src/lib.rs
index 0c04791b..21fd3d1c 100644
--- a/modules/ethflow-watcher/src/lib.rs
+++ b/modules/ethflow-watcher/src/lib.rs
@@ -38,8 +38,6 @@ use wit_bindgen as _;
#[cfg(target_arch = "wasm32")]
wit_bindgen::generate!({
path: [
- "../../wit/nexum-value-flow",
- "../../wit/nexum-intent",
"../../wit/nexum-host",
"../../wit/shepherd-cow",
],
diff --git a/modules/examples/echo-client/Cargo.toml b/modules/examples/echo-client/Cargo.toml
index 95e02533..f4cf47a0 100644
--- a/modules/examples/echo-client/Cargo.toml
+++ b/modules/examples/echo-client/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
-description = "Shepherd example module paired with the echo-venue adapter: submits an opaque body through nexum:intent/pool on every block and logs the intent-status transitions the router fans back."
+description = "Shepherd example module paired with the echo-venue adapter: submits an opaque body through videre:venue/client on every block and logs the intent-status transitions the registry fans back."
[lints]
workspace = true
diff --git a/modules/examples/echo-client/module.toml b/modules/examples/echo-client/module.toml
index f10708d1..4b1df513 100644
--- a/modules/examples/echo-client/module.toml
+++ b/modules/examples/echo-client/module.toml
@@ -1,7 +1,8 @@
-# echo-client module manifest - the strategy half of the echo pair. It
-# submits through nexum:intent/pool and observes intent-status, so it
-# declares the `pool` capability alongside `logging`; the per-module world
-# the macro derives imports exactly nexum:intent/pool and nexum:host/logging.
+# echo-client module manifest - the keeper half of the echo pair. It
+# submits through videre:venue/client and observes intent-status, so it
+# declares the `client` capability alongside `logging`; the per-module world
+# the macro derives imports exactly videre:venue/client and
+# nexum:host/logging.
[module]
name = "echo-client"
@@ -10,8 +11,8 @@ version = "0.1.0"
component = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
[capabilities]
-# `pool` grants the nexum:intent/pool import; `logging` the log sink.
-required = ["pool", "logging"]
+# `client` grants the videre:venue/client import; `logging` the log sink.
+required = ["client", "logging"]
optional = []
[capabilities.http]
@@ -22,7 +23,7 @@ allow = []
kind = "block"
chain_id = 1
-# Observe the status transitions the router polls from the echo-venue adapter.
+# Observe the status transitions the registry polls from the echo-venue adapter.
[[subscription]]
kind = "intent-status"
venue = "echo-venue"
diff --git a/modules/examples/echo-client/src/lib.rs b/modules/examples/echo-client/src/lib.rs
index 62407488..6a769517 100644
--- a/modules/examples/echo-client/src/lib.rs
+++ b/modules/examples/echo-client/src/lib.rs
@@ -1,15 +1,15 @@
//! # echo-client (reference Shepherd intent module)
//!
-//! The strategy half of the echo pair. On every chain-1 block it submits an
-//! opaque body through `nexum:intent/pool` to the `echo-venue` adapter and
-//! logs the receipt, and it logs each `intent-status` transition the router
-//! fans back from that venue. Paired with the echo-venue adapter it is the
-//! smallest end-to-end demonstration of the intent core: module -> host
-//! router -> venue adapter, and the status event back.
+//! The keeper half of the echo pair. On every chain-1 block it submits an
+//! opaque body through `videre:venue/client` to the `echo-venue` adapter and
+//! logs the receipt, and it logs each `intent-status` transition the
+//! registry fans back from that venue. Paired with the echo-venue adapter it
+//! is the smallest end-to-end demonstration of the intent core: module ->
+//! host registry -> venue adapter, and the status event back.
//!
-//! It declares two capabilities (`pool`, `logging`), so the built component
-//! imports `nexum:intent/pool` and `nexum:host/logging` and nothing else:
-//! the per-module world matches the manifest by construction.
+//! It declares two capabilities (`client`, `logging`), so the built
+//! component imports `videre:venue/client` and `nexum:host/logging` and
+//! nothing else: the per-module world matches the manifest by construction.
// wit_bindgen::generate! expands to host-import shims whose arity matches
// the WIT signatures, which can exceed clippy's too-many-arguments threshold.
@@ -17,8 +17,8 @@
#![allow(clippy::too_many_arguments)]
use nexum::host::{logging, types};
-use nexum::intent::pool;
-use nexum::intent::types::SubmitOutcome;
+use videre::types::types::SubmitOutcome;
+use videre::venue::client;
/// Venue id the paired echo-venue adapter answers for; the module submits
/// to and observes exactly this venue.
@@ -33,7 +33,7 @@ impl EchoClient {
// receipt, so the body content is immaterial; the block number keeps
// it non-empty and legible in the logs.
let body = block.number.to_be_bytes().to_vec();
- match pool::submit(ECHO_VENUE, &body) {
+ match client::submit(ECHO_VENUE, &body) {
Ok(SubmitOutcome::Accepted(receipt)) => logging::log(
logging::Level::Info,
&format!(
diff --git a/modules/examples/echo-venue/src/lib.rs b/modules/examples/echo-venue/src/lib.rs
index ef80f731..d99726ad 100644
--- a/modules/examples/echo-venue/src/lib.rs
+++ b/modules/examples/echo-venue/src/lib.rs
@@ -2,7 +2,7 @@
//!
//! The minimal reference venue adapter: it accepts any body, echoes it back
//! as the receipt, and settles instantly (every receipt it issued reports
-//! `settled`). It carries no real venue protocol, so it doubles as the
+//! `fulfilled`). It carries no real venue protocol, so it doubles as the
//! smallest end-to-end demonstration of `#[nexum_venue_sdk::venue]` - the
//! attribute supplies the per-cdylib wit-bindgen call for a world derived
//! from `module.toml`, the `Guest` export glue, and `export!`, leaving only
@@ -19,8 +19,10 @@
#![allow(clippy::too_many_arguments)]
use nexum::host::chain;
-use nexum::intent::types::{IntentHeader, IntentStatus, SubmitOutcome, VenueError};
-use nexum::value_flow::types::{Asset, AssetAmount, Settlement};
+use videre::types::types::{
+ AuthScheme, IntentHeader, IntentStatus, Settlement, SubmitOutcome, VenueError,
+};
+use videre::value_flow::types::{Asset, AssetAmount};
struct EchoVenue;
@@ -33,16 +35,19 @@ impl EchoVenue {
fn derive_header(body: Vec) -> Result {
// The echo venue gives back exactly the bytes handed to it, so the
// header's `gives` amount is the body length: enough to exercise
- // the value-flow vocabulary without a real schema.
+ // the value-flow vocabulary without a real schema. Wants nothing,
+ // spelled as a zero native amount.
Ok(IntentHeader {
- gives: vec![AssetAmount {
- asset: Asset::NativeToken(Settlement::EvmChain(1)),
- amount: (body.len() as u64).to_be_bytes().to_vec(),
- }],
- wants: Vec::new(),
- valid_until: None,
- settlement: Settlement::EvmChain(1),
- authorisation: nexum::intent::types::AuthScheme::Unsigned,
+ gives: AssetAmount {
+ asset: Asset::Native,
+ amount: minimal_be(body.len() as u64),
+ },
+ wants: AssetAmount {
+ asset: Asset::Native,
+ amount: Vec::new(),
+ },
+ settlement: Settlement { chain: 1 },
+ authorisation: AuthScheme::Eip1271,
})
}
@@ -55,25 +60,25 @@ impl EchoVenue {
Ok(SubmitOutcome::Accepted(body))
}
- fn status(receipt: Vec) -> Result {
- if receipt.is_empty() {
- Err(VenueError::InvalidReceipt)
- } else {
- // Settles instantly: the intent reaches a terminal state on the
- // first status poll, with no venue-side settlement proof.
- Ok(IntentStatus::Settled(None))
- }
+ fn status(_receipt: Vec) -> Result {
+ // Settles instantly: the intent reaches a terminal state on the
+ // first status poll.
+ Ok(IntentStatus::Fulfilled)
}
- fn cancel(receipt: Vec) -> Result<(), VenueError> {
- if receipt.is_empty() {
- Err(VenueError::InvalidReceipt)
- } else {
- Ok(())
- }
+ fn cancel(_receipt: Vec) -> Result<(), VenueError> {
+ Ok(())
}
}
+/// Big-endian bytes with leading zeros trimmed: the minimal `uint`
+/// spelling, where an empty list is zero.
+fn minimal_be(value: u64) -> Vec {
+ let bytes = value.to_be_bytes();
+ let first = bytes.iter().position(|byte| *byte != 0);
+ first.map_or(Vec::new(), |index| bytes[index..].to_vec())
+}
+
/// echo-venue as the `nexum-venue-test` conformance target: the adapter's
/// pure header derivation is held to a hand-written golden through the kit's
/// serde mirror types. The macro mints echo-venue's own bindgen
@@ -83,43 +88,15 @@ impl EchoVenue {
#[cfg(test)]
mod conformance {
use super::*;
- use nexum::intent::types::AuthScheme;
use nexum_venue_test::{
GoldenAsset, GoldenAssetAmount, GoldenAuthScheme, GoldenHeader, GoldenSettlement,
HeaderGolden, HeaderGoldens,
};
- fn settlement_to_golden(settlement: Settlement) -> GoldenSettlement {
- match settlement {
- Settlement::EvmChain(chain_id) => GoldenSettlement::EvmChain(chain_id),
- Settlement::Offchain(domain) => GoldenSettlement::Offchain(domain),
- }
- }
-
fn asset_to_golden(asset: Asset) -> GoldenAsset {
match asset {
- Asset::NativeToken(settlement) => {
- GoldenAsset::NativeToken(settlement_to_golden(settlement))
- }
- Asset::Erc20((chain_id, address)) => GoldenAsset::Erc20 { chain_id, address },
- Asset::Erc721((chain_id, address, token_id)) => GoldenAsset::Erc721 {
- chain_id,
- address,
- token_id,
- },
- Asset::Erc1155((chain_id, address, token_id)) => GoldenAsset::Erc1155 {
- chain_id,
- address,
- token_id,
- },
- Asset::Service(desc) => GoldenAsset::Service {
- kind: desc.kind,
- summary: desc.summary,
- },
- Asset::Offchain(desc) => GoldenAsset::Offchain {
- domain: desc.domain,
- summary: desc.summary,
- },
+ Asset::Native => GoldenAsset::Native,
+ Asset::Erc20(erc20) => GoldenAsset::Erc20 { token: erc20.token },
}
}
@@ -132,20 +109,18 @@ mod conformance {
fn auth_to_golden(scheme: AuthScheme) -> GoldenAuthScheme {
match scheme {
- AuthScheme::Eip712 => GoldenAuthScheme::Eip712,
AuthScheme::Eip1271 => GoldenAuthScheme::Eip1271,
- AuthScheme::Presign => GoldenAuthScheme::Presign,
- AuthScheme::OffchainSig => GoldenAuthScheme::OffchainSig,
- AuthScheme::Unsigned => GoldenAuthScheme::Unsigned,
+ AuthScheme::Eip712 => GoldenAuthScheme::Eip712,
}
}
fn header_to_golden(header: IntentHeader) -> GoldenHeader {
GoldenHeader {
- gives: header.gives.into_iter().map(amount_to_golden).collect(),
- wants: header.wants.into_iter().map(amount_to_golden).collect(),
- valid_until: header.valid_until,
- settlement: settlement_to_golden(header.settlement),
+ gives: amount_to_golden(header.gives),
+ wants: amount_to_golden(header.wants),
+ settlement: GoldenSettlement {
+ chain: header.settlement.chain,
+ },
authorisation: auth_to_golden(header.authorisation),
}
}
@@ -155,25 +130,32 @@ mod conformance {
EchoVenue::derive_header(body).map(header_to_golden)
}
+ fn zero_native() -> GoldenAssetAmount {
+ GoldenAssetAmount {
+ asset: GoldenAsset::Native,
+ amount: Vec::new(),
+ }
+ }
+
#[test]
fn derive_header_conforms_to_the_published_golden() {
// The echo contract: gives chain-1 native token whose amount is the
- // body length as eight big-endian bytes, wants nothing, and carries
- // no authorisation. A conforming adapter reproduces this exactly.
+ // body length in minimal big-endian bytes, wants zero native, and
+ // authorises via EIP-1271. A conforming adapter reproduces this
+ // exactly.
let golden = HeaderGolden {
name: "four-byte-body".to_owned(),
body: vec![1, 2, 3, 4],
header: GoldenHeader {
- gives: vec![GoldenAssetAmount {
- asset: GoldenAsset::NativeToken(GoldenSettlement::EvmChain(1)),
- amount: 4u64.to_be_bytes().to_vec(),
- }],
- wants: Vec::new(),
- valid_until: None,
- settlement: GoldenSettlement::EvmChain(1),
- authorisation: GoldenAuthScheme::Unsigned,
+ gives: GoldenAssetAmount {
+ asset: GoldenAsset::Native,
+ amount: vec![4],
+ },
+ wants: zero_native(),
+ settlement: GoldenSettlement { chain: 1 },
+ authorisation: GoldenAuthScheme::Eip1271,
},
- notes: Some("amount is the 8-byte big-endian body length".to_owned()),
+ notes: Some("amount is the minimal big-endian body length".to_owned()),
};
let goldens = HeaderGoldens {
venue: "echo-venue".to_owned(),
@@ -184,22 +166,21 @@ mod conformance {
#[test]
fn divergent_derivation_is_caught_by_the_golden() {
- // A little-endian amount is the classic byte-order bug; the golden
- // must reject it, proving the check has teeth on echo-venue.
+ // A non-minimal amount is the classic uint bug; the golden must
+ // reject it, proving the check has teeth on echo-venue.
let goldens = HeaderGoldens {
venue: "echo-venue".to_owned(),
goldens: vec![HeaderGolden {
name: "four-byte-body".to_owned(),
body: vec![1, 2, 3, 4],
header: GoldenHeader {
- gives: vec![GoldenAssetAmount {
- asset: GoldenAsset::NativeToken(GoldenSettlement::EvmChain(1)),
- amount: 4u64.to_le_bytes().to_vec(),
- }],
- wants: Vec::new(),
- valid_until: None,
- settlement: GoldenSettlement::EvmChain(1),
- authorisation: GoldenAuthScheme::Unsigned,
+ gives: GoldenAssetAmount {
+ asset: GoldenAsset::Native,
+ amount: 4u64.to_be_bytes().to_vec(),
+ },
+ wants: zero_native(),
+ settlement: GoldenSettlement { chain: 1 },
+ authorisation: GoldenAuthScheme::Eip1271,
},
notes: None,
}],
diff --git a/modules/examples/stop-loss/src/lib.rs b/modules/examples/stop-loss/src/lib.rs
index be6ef1b7..ff846bdf 100644
--- a/modules/examples/stop-loss/src/lib.rs
+++ b/modules/examples/stop-loss/src/lib.rs
@@ -24,8 +24,6 @@
wit_bindgen::generate!({
path: [
- "../../../wit/nexum-value-flow",
- "../../../wit/nexum-intent",
"../../../wit/nexum-host",
"../../../wit/shepherd-cow",
],
diff --git a/modules/fixtures/clock-reader/src/lib.rs b/modules/fixtures/clock-reader/src/lib.rs
index b072abd0..2f0cea8b 100644
--- a/modules/fixtures/clock-reader/src/lib.rs
+++ b/modules/fixtures/clock-reader/src/lib.rs
@@ -17,8 +17,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
wit_bindgen::generate!({
path: [
- "../../../wit/nexum-value-flow",
- "../../../wit/nexum-intent",
"../../../wit/nexum-host",
],
world: "nexum:host/event-module",
diff --git a/modules/fixtures/flaky-bomb/src/lib.rs b/modules/fixtures/flaky-bomb/src/lib.rs
index cd5c7d41..d1b9598a 100644
--- a/modules/fixtures/flaky-bomb/src/lib.rs
+++ b/modules/fixtures/flaky-bomb/src/lib.rs
@@ -22,8 +22,6 @@
wit_bindgen::generate!({
path: [
- "../../../wit/nexum-value-flow",
- "../../../wit/nexum-intent",
"../../../wit/nexum-host",
],
world: "nexum:host/event-module",
diff --git a/modules/fixtures/fuel-bomb/src/lib.rs b/modules/fixtures/fuel-bomb/src/lib.rs
index 14b55f13..3e6b6b3b 100644
--- a/modules/fixtures/fuel-bomb/src/lib.rs
+++ b/modules/fixtures/fuel-bomb/src/lib.rs
@@ -14,8 +14,6 @@
wit_bindgen::generate!({
path: [
- "../../../wit/nexum-value-flow",
- "../../../wit/nexum-intent",
"../../../wit/nexum-host",
],
world: "nexum:host/event-module",
diff --git a/modules/fixtures/memory-bomb/src/lib.rs b/modules/fixtures/memory-bomb/src/lib.rs
index cf1dd031..948b65e1 100644
--- a/modules/fixtures/memory-bomb/src/lib.rs
+++ b/modules/fixtures/memory-bomb/src/lib.rs
@@ -13,8 +13,6 @@
wit_bindgen::generate!({
path: [
- "../../../wit/nexum-value-flow",
- "../../../wit/nexum-intent",
"../../../wit/nexum-host",
],
world: "nexum:host/event-module",
diff --git a/modules/fixtures/panic-bomb/src/lib.rs b/modules/fixtures/panic-bomb/src/lib.rs
index 09fcb89c..56eaa348 100644
--- a/modules/fixtures/panic-bomb/src/lib.rs
+++ b/modules/fixtures/panic-bomb/src/lib.rs
@@ -14,8 +14,6 @@
wit_bindgen::generate!({
path: [
- "../../../wit/nexum-value-flow",
- "../../../wit/nexum-intent",
"../../../wit/nexum-host",
],
world: "nexum:host/event-module",
diff --git a/modules/fixtures/slow-host/src/lib.rs b/modules/fixtures/slow-host/src/lib.rs
index 5e490752..97b420be 100644
--- a/modules/fixtures/slow-host/src/lib.rs
+++ b/modules/fixtures/slow-host/src/lib.rs
@@ -27,8 +27,6 @@
wit_bindgen::generate!({
path: [
- "../../../wit/nexum-value-flow",
- "../../../wit/nexum-intent",
"../../../wit/nexum-host",
],
world: "nexum:host/event-module",
diff --git a/modules/twap-monitor/src/lib.rs b/modules/twap-monitor/src/lib.rs
index c23eb237..0483cb52 100644
--- a/modules/twap-monitor/src/lib.rs
+++ b/modules/twap-monitor/src/lib.rs
@@ -25,8 +25,6 @@
wit_bindgen::generate!({
path: [
- "../../wit/nexum-value-flow",
- "../../wit/nexum-intent",
"../../wit/nexum-host",
"../../wit/shepherd-cow",
],
diff --git a/wit/nexum-adapter/venue-adapter.wit b/wit/nexum-adapter/venue-adapter.wit
deleted file mode 100644
index 9656bc47..00000000
--- a/wit/nexum-adapter/venue-adapter.wit
+++ /dev/null
@@ -1,30 +0,0 @@
-package nexum:adapter@0.1.0;
-
-/// A venue adapter: the second component kind. Where an event-module
-/// automates strategy over the six core primitives, a venue adapter
-/// speaks one venue's protocol and nothing else. It imports only the
-/// scoped transport it needs to reach its venue - chain RPC and
-/// messaging - and exports the intent adapter face. It has no
-/// local-store, remote-store, identity, or logging import: an adapter
-/// structurally cannot touch host key material or persistent state, only
-/// move bytes to and from its venue. The host links exactly these
-/// imports into an adapter store, so an adapter that reaches for anything
-/// else fails to instantiate.
-world venue-adapter {
- use nexum:host/types@0.2.0.{config, fault};
-
- // Scoped transport. Outbound HTTP is wasi:http, linked separately and
- // gated per-adapter by the `[[adapters]].http_allow` allowlist in
- // engine.toml, the same way event-module treats outbound HTTP; the
- // per-adapter `[[adapters]].messaging_topics` scopes the messaging
- // content topics an adapter may reach. Time and randomness are
- // ambient wasi:clocks / wasi:random.
- import nexum:host/chain@0.2.0;
- import nexum:host/messaging@0.2.0;
-
- /// Configure the adapter from its `[config]` before any submission.
- /// Mirrors the event-module `init` so the supervisor boots both kinds
- /// through the same store, fuel, and restart machinery.
- export init: func(config: config) -> result<_, fault>;
- export nexum:intent/adapter@0.1.0;
-}
diff --git a/wit/nexum-intent/adapter.wit b/wit/nexum-intent/adapter.wit
deleted file mode 100644
index 1c0a28ce..00000000
--- a/wit/nexum-intent/adapter.wit
+++ /dev/null
@@ -1,31 +0,0 @@
-package nexum:intent@0.1.0;
-
-/// The venue face of the intent core, the mirror of the strategy-facing
-/// `pool` interface. One installed adapter answers for exactly one venue,
-/// so none of these functions carries a `venue` argument: the router
-/// resolves a venue id to its adapter and calls the adapter directly.
-/// Bodies stay opaque bytes at this boundary; the adapter recovers typing
-/// against its own venue schema. The package depends only on its own
-/// types, never on `nexum:host`, so the adapter contract's freeze cadence
-/// stays independent of host versioning.
-interface adapter {
- use types.{intent-header, intent-status, receipt, submit-outcome, venue-error};
-
- /// Project an opaque intent body onto the stable header guard policy
- /// runs on. A pure derivation: no transport, no side effects, so the
- /// host can derive and inspect a header before deciding to submit.
- derive-header: func(body: list) -> result;
-
- /// Submit an opaque intent body to this adapter's 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(body: list) -> result;
-
- /// Report where a previously submitted intent is in its life.
- status: func(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(receipt: receipt) -> result<_, venue-error>;
-}
diff --git a/wit/nexum-intent/pool.wit b/wit/nexum-intent/pool.wit
deleted file mode 100644
index 7655292a..00000000
--- a/wit/nexum-intent/pool.wit
+++ /dev/null
@@ -1,26 +0,0 @@
-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
deleted file mode 100644
index 32c2e16d..00000000
--- a/wit/nexum-intent/types.wit
+++ /dev/null
@@ -1,132 +0,0 @@
-package nexum:intent@0.1.0;
-
-/// The venue-neutral intent ontology: what a deal gives and wants, how the
-/// venue authorises 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 authorised 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 authorisation recorded at the settlement contract
- /// ahead of submission.
- presign,
- /// A venue-defined off-chain signature scheme.
- offchain-sig,
- /// No authorisation 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 authorises 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),
- }
-}
diff --git a/wit/nexum-value-flow/types.wit b/wit/nexum-value-flow/types.wit
deleted file mode 100644
index d626d409..00000000
--- a/wit/nexum-value-flow/types.wit
+++ /dev/null
@@ -1,98 +0,0 @@
-package nexum:value-flow@0.1.0;
-
-/// The egress-neutral vocabulary for value in motion: where a deal settles,
-/// what asset moves, and how much of it. Shared by intent headers,
-/// simulation balance diffs, and analyser verdict subjects so that a spend
-/// is written the same way in all three places. This is the platform's
-/// hardest-freezing contract; the package carries no dependency so it can
-/// outlive any interface built on it.
-///
-/// Identifier hygiene is a freeze gate. Every identifier below is checked
-/// against WIT keywords (including in-flight proposals -- the package is
-/// `value-flow`, not `value`, because the component model's value-imports
-/// feature is circling that word) and against the reserved words of the
-/// nine binding-target languages (Rust, Python, JS, Go, C#, Java, Kotlin,
-/// Swift, Dart). A two-word kebab id is preferred wherever a single word is
-/// a keyword anywhere: `native-token` not `native` (a Java modifier),
-/// `offchain` not `external` (a Dart keyword). WIT parses the rejected
-/// spellings today; the cost lands later, as escaped identifiers in the
-/// generated bindings for exactly the personas the SDK exists to serve.
-interface types {
- /// Where a deal comes to rest. A variant from day one: the chain-id
- /// plumbing assumes every venue settles on an EVM chain, which the
- /// off-chain marketplace target breaks.
- variant settlement {
- /// Settles on an EVM chain, identified by its chain id.
- evm-chain(u64),
- /// Settles off-chain: the payload is a jurisdiction or a
- /// venue-defined domain. Settlement here is a legal or
- /// out-of-band process, not a chain state transition.
- offchain(string),
- }
-
- /// A non-token service obligation whose worth the host cannot compute,
- /// e.g. Swarm postage: storage capacity for a duration. Policy on a
- /// service asset is adapter-attested, not host-verified; the consent
- /// surface renders `summary` and must say the host verifies nothing.
- record service-desc {
- /// Namespaced service kind the venue defines, e.g. `swarm:postage`.
- /// Stable enough for policy to match on.
- kind: string,
- /// Adapter-supplied human-readable description for the consent sheet.
- summary: string,
- }
-
- /// A real-world asset whose settlement is a legal process rather than a
- /// chain state transition: a deed, a chattel, a registry entry. The host
- /// verifies nothing about it. The case name mirrors `settlement.offchain`
- /// deliberately: the same concept on two axes -- where the asset lives,
- /// and where the deal settles.
- record offchain-desc {
- /// Jurisdiction or registry domain the asset lives in, e.g. an
- /// ISO country code or a venue-defined registry name.
- domain: string,
- /// Adapter-supplied human-readable description for the consent sheet.
- summary: string,
- }
-
- /// A kind of value that can move. The ERC cases carry a bare chain id
- /// rather than a `settlement` because an ERC token is inherently EVM;
- /// `native-token` wraps `settlement` because a chain's own gas token is
- /// the one asset that exists on every settlement domain. Each token
- /// tuple carries raw big-endian bytes: a 20-byte address, and for the
- /// NFT cases a token id of arbitrary width.
- variant asset {
- /// The settlement domain's own gas token (ETH, BZZ, ...). Only an
- /// on-chain settlement carries a gas token, so `native-token` pairs
- /// meaningfully with `settlement.evm-chain`; `native-token(offchain)`
- /// is representable but intentionally invalid -- an off-chain domain
- /// has no gas token -- so consumers MUST reject that pairing rather
- /// than ascribe a meaning to it.
- native-token(settlement),
- /// An ERC-20 token: (chain id, 20-byte contract address).
- erc20(tuple>),
- /// An ERC-721 NFT: (chain id, 20-byte contract address, token id).
- erc721(tuple, list>),
- /// An ERC-1155 token: (chain id, 20-byte contract address, token id).
- erc1155(tuple, list>),
- /// A non-token service, e.g. storage capacity for a duration.
- service(service-desc),
- /// A real-world asset settled off-chain.
- offchain(offchain-desc),
- }
-
- /// An amount of one asset. `amount` is a big-endian unsigned integer,
- /// most-significant byte first, with no fixed width; an empty list is
- /// zero. Amounts are never negative -- direction lives in whichever
- /// field holds the pair (a header's `gives` vs `wants`), not here.
- ///
- /// The canonical wire form is minimal-length: no leading zero bytes, so
- /// zero is the empty list and five is `[0x05]`, never `[0x00, 0x05]`.
- /// Encoders MUST emit the minimal form; decoders MUST compare amounts by
- /// integer value, not by byte equality, so a non-minimal encoding from a
- /// lenient peer still compares equal to its canonical twin.
- record asset-amount {
- asset: asset,
- amount: list,
- }
-}
diff --git a/wit/videre-types/types.wit b/wit/videre-types/types.wit
new file mode 100644
index 00000000..e8b69c81
--- /dev/null
+++ b/wit/videre-types/types.wit
@@ -0,0 +1,83 @@
+package videre:types@0.1.0;
+
+/// The venue-neutral intent ontology. Depends only on value-flow; never on
+/// nexum:host, so the venue-error transport cases are its own.
+interface types {
+ use videre:value-flow/types@0.1.0.{asset-amount};
+
+ /// How an intent is authorised at its venue. Non-EVM schemes are 0.2+.
+ variant auth-scheme {
+ eip1271,
+ eip712,
+ }
+
+ /// Where a deal settles. EVM-only in 0.1.
+ record settlement {
+ chain: u64,
+ }
+
+ /// Adapter-derived description of an intent body: the ontology guard policy
+ /// runs on. Policy has teeth on `gives`; `wants` is display-grade.
+ record intent-header {
+ gives: asset-amount,
+ wants: asset-amount,
+ settlement: settlement,
+ authorisation: auth-scheme,
+ }
+
+ /// Venue-scoped stable id for a submitted intent. Opaque to host and policy.
+ type receipt = list;
+
+ /// An EVM call the host must sign and send. The adapter only describes it;
+ /// the host fills gas/fee and signs, so adapters cannot move value. Always
+ /// a call to existing code.
+ record unsigned-tx {
+ chain: u64,
+ /// 20-byte contract address.
+ to: list,
+ /// Native value, big-endian minimal; empty is zero.
+ value: list,
+ /// ABI-encoded calldata.
+ data: list,
+ }
+
+ /// What a successful submit produced.
+ variant submit-outcome {
+ accepted(receipt),
+ requires-signing(unsigned-tx),
+ }
+
+ /// Lifecycle state. Coarse and portable; proof and failure reason ride the
+ /// opaque status body (docs/design/videre-wit-pinned-0.1.0.md).
+ enum intent-status {
+ pending,
+ open,
+ fulfilled,
+ cancelled,
+ expired,
+ }
+
+ /// Failure of a client or adapter call. `denied` and `rate-limited` are the
+ /// only guard/transport shapes; `denied` MUST NOT be retried.
+ variant venue-error {
+ unknown-venue,
+ invalid-body(string),
+ unsupported,
+ denied(string),
+ rate-limited(rate-limit),
+ unavailable(string),
+ timeout,
+ }
+
+ record rate-limit {
+ retry-after-ms: option,
+ }
+
+ /// An indicative quotation for a body. Firm/RFQ maker-side offers are 0.2+.
+ record quotation {
+ gives: asset-amount,
+ wants: asset-amount,
+ fee: asset-amount,
+ valid-until-ms: u64,
+ }
+}
diff --git a/wit/videre-value-flow/types.wit b/wit/videre-value-flow/types.wit
new file mode 100644
index 00000000..c3aa9de9
--- /dev/null
+++ b/wit/videre-value-flow/types.wit
@@ -0,0 +1,32 @@
+package videre:value-flow@0.1.0;
+
+/// Egress-neutral vocabulary for value in motion. Carries no dependency so it
+/// outlives any contract built on it. EVM-only in 0.1.
+interface types {
+ /// 20-byte EVM address, big-endian.
+ type address = list;
+
+ /// Unsigned integer, big-endian, minimal-length: no leading zero bytes,
+ /// zero is the empty list. Decoders MUST compare by integer value, not by
+ /// byte equality.
+ type uint = list;
+
+ /// An ERC-20 token on the intent's settlement chain.
+ record erc20 {
+ token: address,
+ }
+
+ /// A kind of value that can move. erc721/erc1155/service/offchain are 0.2+.
+ variant asset {
+ /// The settlement chain's gas token.
+ native,
+ erc20(erc20),
+ }
+
+ /// An amount of one asset. Never negative; direction lives in the field
+ /// that holds the pair (`gives` vs `wants`).
+ record asset-amount {
+ asset: asset,
+ amount: uint,
+ }
+}
diff --git a/wit/videre-venue/venue.wit b/wit/videre-venue/venue.wit
new file mode 100644
index 00000000..6516586c
--- /dev/null
+++ b/wit/videre-venue/venue.wit
@@ -0,0 +1,39 @@
+package videre:venue@0.1.0;
+
+/// Worker (keeper) face. The host holds the venue registry; the keeper names
+/// a venue by string.
+interface client {
+ use videre:types/types@0.1.0.{receipt, intent-status, submit-outcome, venue-error};
+
+ submit: func(venue: string, body: list) -> result;
+ status: func(venue: string, receipt: receipt) -> result;
+ cancel: func(venue: string, receipt: receipt) -> result<_, venue-error>;
+}
+
+/// Provider (venue) face. Mirrors `client` without the venue selector: one
+/// installed adapter answers for exactly one venue, so the registry resolves
+/// a venue id to its adapter and calls it directly.
+interface adapter {
+ use videre:types/types@0.1.0.{intent-header, receipt, intent-status, submit-outcome, venue-error};
+
+ /// Pure: derive the guard-facing header from a body. No I/O.
+ derive-header: func(body: list) -> result;
+ submit: func(body: list) -> result;
+ status: func(receipt: receipt) -> result