diff --git a/README.md b/README.md index f0b248cb..e3e7021b 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ A module built against `nexum:host` runs on any Nexum-compatible host. CoW order | `nexum/crates/nexum-sdk/` | Guest SDK: host trait seam, bind macro, chain/config/address helpers, `wasi:http` fetch, tracing facade. | | `videre/crates/videre-sdk/` | Venue-platform SDK: the `videre:venue` client and adapter contracts. | | `shepherd/crates/cow-venue/` | The bundled CoW venue adapter component. | -| `wit/nexum-host/` | The `nexum:host` WIT package: the host/guest contract. | -| `wit/videre-venue/` | The `videre:venue` WIT package: the venue-adapter contract. | -| `wit/shepherd-cow/` | `cow-events.wit`: the CoW event ABIs of record. | +| `nexum/wit/nexum-host/` | The `nexum:host` WIT package: the host/guest contract. | +| `videre/wit/videre-venue/` | The `videre:venue` WIT package: the venue-adapter contract. | +| `shepherd/wit/shepherd-cow/` | `cow-events.wit`: the CoW event ABIs of record. | | `modules/` | Guest modules: TWAP and EthFlow keepers, examples, and test fixtures. | | `docs/` | Architecture and design notes. Start with [`docs/00-overview.md`](docs/00-overview.md). | diff --git a/docs/00-overview.md b/docs/00-overview.md index aa7b76aa..d1026485 100755 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -9,7 +9,7 @@ Nexum is a WASM Component Model runtime that provides secure, sandboxed executio | Term | What it is | Where you find it | |---|---|---| | **engine** (`nexum`) | A concrete implementation that loads and runs WASM components. The 0.2 reference engine is a wasmtime-based server daemon. | `nexum/crates/nexum-runtime/`, the `nexum` binary, `cargo run -p nexum-cli` | -| **host** (`nexum:host`) | The WIT contract: the host-imported interfaces (chain, identity, local-store, ...), types, and worlds that every engine implements and every module imports. | `wit/nexum-host/`, `package nexum:host@0.1.0`, Rust path `nexum::host::*` | +| **host** (`nexum:host`) | The WIT contract: the host-imported interfaces (chain, identity, local-store, ...), types, and worlds that every engine implements and every module imports. | `nexum/wit/nexum-host/`, `package nexum:host@0.1.0`, Rust path `nexum::host::*` | An engine implements `nexum:host` so that modules built against `nexum:host` can run on it. The reference engine ships as two crates: the `nexum-runtime` library (embeddable, no CLI surface) and the `nexum` binary in `nexum/crates/nexum-cli`. A Rust embedder constructs an `EngineConfig` in code and calls `nexum_runtime::bootstrap::run_from_config`; see `nexum/crates/nexum-runtime/examples/embed.rs`. @@ -243,23 +243,18 @@ The `nexum:host` WIT contract is host-portable: any host implementing it can run ``` shepherd/ -├── crates/ -│ ├── nexum-runtime/ Core WASM host (server) library: event system, local store, bootstrap -│ ├── nexum-cli/ The `nexum` binary: clap CLI over the runtime library -│ ├── nexum-sdk/ Generic guest SDK: host-trait seam, chain/config/address helpers, wasi:http fetch -│ ├── nexum-sdk-test/ Generic mock host for module tests -│ ├── videre-sdk/ Venue + keeper SDK: venue-adapter export trait, typed venue client -│ ├── videre-host/ Host-side venue registry + status watch -│ ├── videre-test/ Venue/keeper test surface -│ └── cow-venue/ The CoW venue: order body types + IntentBody codec -├── modules/ -│ ├── twap-monitor/ TWAP order monitoring module -│ ├── ethflow-watcher/ Ethflow order monitoring module -│ └── examples/ reference modules (price-alert, balance-tracker, http-probe, echo-*) -├── wit/ -│ ├── nexum-host/ Universal WIT package (chain, identity, local-store, remote-store, messaging, logging) -│ ├── shepherd-cow/ CoW event enum (cow-events) -│ └── videre-venue/ Venue-adapter contract (client + adapter faces) +├── nexum/ L1: universal runtime +│ ├── crates/ nexum-runtime, nexum-cli, nexum-sdk, nexum-sdk-test, ... +│ ├── modules/ example modules (price-alert, balance-tracker, http-probe) + runtime fixtures +│ └── wit/ nexum-host (chain, identity, local-store, remote-store, messaging, logging) +├── videre/ L2: intent/venue platform +│ ├── crates/ videre-sdk, videre-host, videre-test, ... +│ ├── modules/ echo-* reference modules + fixtures +│ └── wit/ videre-types, videre-value-flow, videre-venue; deps/ vendored via wit-deps +├── shepherd/ L3: CoW composition +│ ├── crates/ shepherd, cow-venue, composable-cow +│ ├── modules/ twap-monitor, ethflow-watcher +│ └── wit/ shepherd-cow (cow-events); deps/ vendored via wit-deps ├── Dockerfile ├── docker-compose.yml └── docs/ diff --git a/docs/01-runtime-environment.md b/docs/01-runtime-environment.md index 250298f9..f0f887e3 100755 --- a/docs/01-runtime-environment.md +++ b/docs/01-runtime-environment.md @@ -302,7 +302,7 @@ The host uses `wasmtime::component::bindgen!` to generate Rust traits from the W ```rust wasmtime::component::bindgen!({ - path: "wit/nexum-host", + path: "nexum/wit/nexum-host", world: "event-module", async: true, }); diff --git a/docs/06-production-hardening.md b/docs/06-production-hardening.md index d4299596..ffdf5df0 100755 --- a/docs/06-production-hardening.md +++ b/docs/06-production-hardening.md @@ -48,7 +48,7 @@ Block following uses `eth_subscribe(newHeads)` on a WebSocket URL and polls `eth ## Error model -Host interfaces surface a common `fault` variant (`wit/nexum-host/types.wit`): `unsupported`, `unavailable`, `denied`, `rate-limited` (carrying `retry-after-ms` guidance), `timeout`, `invalid-input`, `internal`. A fault is a typed, recoverable return the guest can handle; a trap (fuel, memory, panic) is not, and drives the restart path above. +Host interfaces surface a common `fault` variant (`nexum/wit/nexum-host/types.wit`): `unsupported`, `unavailable`, `denied`, `rate-limited` (carrying `retry-after-ms` guidance), `timeout`, `invalid-input`, `internal`. A fault is a typed, recoverable return the guest can handle; a trap (fuel, memory, panic) is not, and drives the restart path above. ## Structured logging diff --git a/docs/07-rpc-namespace-design.md b/docs/07-rpc-namespace-design.md index 79e4ef33..0e52471b 100755 --- a/docs/07-rpc-namespace-design.md +++ b/docs/07-rpc-namespace-design.md @@ -4,7 +4,7 @@ Modules reach chain state through one host function, `chain.request`, plus a bat ## The WIT interface -`nexum:host/chain` (`wit/nexum-host/chain.wit`): +`nexum:host/chain` (`nexum/wit/nexum-host/chain.wit`): ```wit interface chain { diff --git a/docs/08-platform-generalisation.md b/docs/08-platform-generalisation.md index 2a42ee17..f76549b4 100755 --- a/docs/08-platform-generalisation.md +++ b/docs/08-platform-generalisation.md @@ -38,7 +38,7 @@ Layer 1 is the world every module compiles against. A Layer 2 capability adds an ## Layer 1: Universal Interfaces -Six interfaces form the universal runtime contract. The signatures below are the shipped `wit/nexum-host` package. +Six interfaces form the universal runtime contract. The signatures below are the shipped `nexum/wit/nexum-host` package. ### `chain` @@ -277,26 +277,31 @@ The retired predecessor to venue adapters was a Layer-3 *world* extension: a `sh ## Complete WIT Package Layout ``` -wit/ -├── nexum-host/ -│ ├── types.wit # chain-id, block, log, tick, message, custom-event, event, config, fault -│ ├── chain.wit # chain interface (request + request-batch) -│ ├── identity.wit # identity interface (accounts, signing) -│ ├── local-store.wit -│ ├── remote-store.wit # Swarm -│ ├── messaging.wit # Waku -│ ├── logging.wit -│ ├── event-module.wit # event-module world (6 imports) -│ └── query-module.wit # experimental: no host impl in 0.2 -│ +nexum/wit/ +└── nexum-host/ + ├── types.wit # chain-id, block, log, tick, message, custom-event, event, config, fault + ├── chain.wit # chain interface (request + request-batch) + ├── identity.wit # identity interface (accounts, signing) + ├── local-store.wit + ├── remote-store.wit # Swarm + ├── messaging.wit # Waku + ├── logging.wit + ├── event-module.wit # event-module world (6 imports) + └── query-module.wit # experimental: no host impl in 0.2 + +videre/wit/ ├── videre-value-flow/ │ └── types.wit # asset + asset-amount vocabulary ├── videre-types/ │ └── types.wit # intent-header, quotation, receipt, submit-outcome, intent-status, venue-error ├── videre-venue/ │ └── venue.wit # client + adapter interfaces, venue-adapter world -└── shepherd-cow/ - └── cow-events.wit # CoW event-ABI package of record (legacy package name) +└── deps/ # nexum-host, vendored via wit-deps + +shepherd/wit/ +├── shepherd-cow/ +│ └── cow-events.wit # CoW event-ABI package of record (legacy package name) +└── deps/ # nexum-host + videre-*, vendored via wit-deps ``` The `nexum-host` package is domain-agnostic. The `videre` packages are the venue-neutral intent contract. `shepherd-cow` carries only the CoW event ABIs. New domains add adapter components, not packages. diff --git a/docs/deployment/multi-chain.md b/docs/deployment/multi-chain.md index bf1c4a47..ee58cde4 100644 --- a/docs/deployment/multi-chain.md +++ b/docs/deployment/multi-chain.md @@ -114,7 +114,7 @@ chain_id = 100 ## Event topics -keccak256 of the event signatures, identical on every chain; only the EthFlow `address` changes. Package of record: `wit/shepherd-cow/cow-events.wit`. +keccak256 of the event signatures, identical on every chain; only the EthFlow `address` changes. Package of record: `shepherd/wit/shepherd-cow/cow-events.wit`. | Event | Topic-0 | |-------|---------| diff --git a/docs/design/carve-workspace.md b/docs/design/carve-workspace.md index 39b93173..1d7d6bfe 100644 --- a/docs/design/carve-workspace.md +++ b/docs/design/carve-workspace.md @@ -39,9 +39,27 @@ converges in three steps: ## WIT resolution -WIT stays in a single root `wit/` for this step. `resolve_wit_package` -(`nexum/crates/nexum-world`) walks manifest-dir ancestors to the nearest `wit/`, -so every crate resolves the shared tree regardless of depth. Splitting `wit/` into -per-group `wit/` + `wit/deps/` requires the crate-local wit-deps flip and is done -in #404/#405, not here. The hardcoded `wit_bindgen::generate!` path lists that -bypass the resolver were re-based one level deeper by the move. +Each group owns its WIT: `nexum/wit/nexum-host`, `videre/wit/videre-{types, +value-flow,venue}`, `shepherd/wit/shepherd-cow`. There is no shared root `wit/`. + +Cross-group WIT follows the same tier order as the crates and is vendored into +the consuming group's `wit/deps/` by [wit-deps]: `videre/wit/deps.toml` pulls +`nexum-host`; `shepherd/wit/deps.toml` pulls `nexum-host` plus the three +`videre-*` packages. The manifests use path sources into the owning group's +tree, and the checked-in `deps.lock` digests pin the vendored copies. After +editing an owned WIT package, re-run `wit-deps` from each consuming group root +(`videre/`, `shepherd/`) and commit the refreshed `wit/deps` + `deps.lock`. + +`resolve_wit_packages` (`nexum/crates/nexum-world`) walks manifest-dir ancestors +to the nearest `wit/` tree and resolves every package there, vendored +`wit/deps/` before owned `wit/`; it never falls through to an +outer tree, so a group cannot use WIT it has not vendored. The hardcoded +`wit_bindgen::generate!`/`bindgen!`/`include_str!` path lists point at the same +group-local trees. + +Convergence at the physical carve: the path sources in each `deps.toml` flip to +pinned git-tag tarball URLs of the owning repo (wit-deps `url` + `sha256` +sources), then post-carve to wkg/OCI per-package semver releases. Only the +manifests change; the resolver and the vendored layout stay as they are. + +[wit-deps]: https://github.com/bytecodealliance/wit-deps diff --git a/nexum/crates/nexum-runtime/src/bindings.rs b/nexum/crates/nexum-runtime/src/bindings.rs index e79803af..5789985f 100644 --- a/nexum/crates/nexum-runtime/src/bindings.rs +++ b/nexum/crates/nexum-runtime/src/bindings.rs @@ -9,7 +9,7 @@ //! so extension services can compare event payloads. wasmtime::component::bindgen!({ - path: ["../../../wit/nexum-host"], + path: ["../../wit/nexum-host"], world: "nexum:host/event-module", imports: { default: async }, exports: { default: async }, diff --git a/nexum/crates/nexum-world/src/lib.rs b/nexum/crates/nexum-world/src/lib.rs index f8595f9b..5ee94c77 100644 --- a/nexum/crates/nexum-world/src/lib.rs +++ b/nexum/crates/nexum-world/src/lib.rs @@ -477,21 +477,29 @@ pub fn synthesize(declared: &[String], extensions: &[ExtensionRow]) -> Result` before own `wit/`, else the -/// nearest ancestor `wit/` that carries it. +/// The nearest ancestor `wit/` tree is the sole authority: vendored +/// `wit/deps/` before owned `wit/`. A package missing +/// from that tree is an error; outer trees are never consulted, so a +/// group cannot leak WIT it has not vendored. pub fn resolve_wit_packages>( start: &Path, packages: &[S], ) -> Result, String> { + let wit = find_wit_tree(start).ok_or_else(|| { + format!( + "no `wit/` tree exists under {} or any ancestor", + start.display() + ) + })?; packages .iter() .map(|package| { let package = package.as_ref(); - resolve_wit_package(start, package).ok_or_else(|| { + resolve_wit_package(&wit, package).ok_or_else(|| { format!( "declared capabilities need the `{package}` WIT package, but neither \ - `wit/deps/{package}` nor `wit/{package}` exists under {} or any ancestor", - start.display() + `wit/deps/{package}` nor `wit/{package}` exists in {}", + wit.display() ) }) }) @@ -522,22 +530,28 @@ pub fn is_plain_type(ty: &syn::Type) -> bool { matches!(ty, syn::Type::Path(tp) if tp.qself.is_none()) } -/// Find one package directory: crate-local `wit/deps/` then -/// `wit/`, walking up on a miss. -fn resolve_wit_package(start: &Path, package: &str) -> Option { +/// The nearest ancestor `wit/` directory of `start`: the crate-local or +/// group-local WIT tree the build resolves against. +fn find_wit_tree(start: &Path) -> Option { let mut dir = Some(start); while let Some(cur) = dir { let wit = cur.join("wit"); - for candidate in [wit.join("deps").join(package), wit.join(package)] { - if candidate.is_dir() { - return Some(candidate); - } + if wit.is_dir() { + return Some(wit); } dir = cur.parent(); } None } +/// One package directory within a WIT tree: vendored `deps/` +/// before owned ``. +fn resolve_wit_package(wit: &Path, package: &str) -> Option { + [wit.join("deps").join(package), wit.join(package)] + .into_iter() + .find(|candidate| candidate.is_dir()) +} + #[cfg(test)] mod tests { use super::*; @@ -867,11 +881,30 @@ allow = [] #[test] fn missing_package_names_the_paths_tried() { let dir = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(dir.path().join("wit")).unwrap(); let err = resolve_wit_packages(dir.path(), &["pkg"]).unwrap_err(); assert!(err.contains("`pkg` WIT package")); assert!(err.contains("wit/deps/pkg")); } + #[test] + fn absent_wit_tree_is_an_error() { + let dir = tempfile::tempdir().unwrap(); + let err = resolve_wit_packages(dir.path(), &["pkg"]).unwrap_err(); + assert!(err.contains("no `wit/` tree")); + } + + #[test] + fn nearest_tree_never_falls_through_to_an_outer_one() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::create_dir_all(root.join("wit/pkg")).unwrap(); + let leaf = root.join("crates/leaf"); + std::fs::create_dir_all(leaf.join("wit/deps/other")).unwrap(); + let err = resolve_wit_packages(&leaf, &["pkg"]).unwrap_err(); + assert!(err.contains("`pkg` WIT package")); + } + #[test] fn read_surface_methods_parse() { for m in [ diff --git a/nexum/modules/fixtures/clock-reader/src/lib.rs b/nexum/modules/fixtures/clock-reader/src/lib.rs index a79b3e1a..21ee7b01 100644 --- a/nexum/modules/fixtures/clock-reader/src/lib.rs +++ b/nexum/modules/fixtures/clock-reader/src/lib.rs @@ -13,7 +13,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; wit_bindgen::generate!({ path: [ - "../../../../wit/nexum-host", + "../../../wit/nexum-host", ], world: "nexum:host/event-module", generate_all, diff --git a/nexum/modules/fixtures/flaky-bomb/src/lib.rs b/nexum/modules/fixtures/flaky-bomb/src/lib.rs index 91a82bbb..49df3817 100644 --- a/nexum/modules/fixtures/flaky-bomb/src/lib.rs +++ b/nexum/modules/fixtures/flaky-bomb/src/lib.rs @@ -10,7 +10,7 @@ wit_bindgen::generate!({ path: [ - "../../../../wit/nexum-host", + "../../../wit/nexum-host", ], world: "nexum:host/event-module", generate_all, diff --git a/nexum/modules/fixtures/fuel-bomb/src/lib.rs b/nexum/modules/fixtures/fuel-bomb/src/lib.rs index 3fd1bfd8..6ab9e741 100644 --- a/nexum/modules/fixtures/fuel-bomb/src/lib.rs +++ b/nexum/modules/fixtures/fuel-bomb/src/lib.rs @@ -9,7 +9,7 @@ wit_bindgen::generate!({ path: [ - "../../../../wit/nexum-host", + "../../../wit/nexum-host", ], world: "nexum:host/event-module", generate_all, diff --git a/nexum/modules/fixtures/memory-bomb/src/lib.rs b/nexum/modules/fixtures/memory-bomb/src/lib.rs index b09f54f0..02426ab8 100644 --- a/nexum/modules/fixtures/memory-bomb/src/lib.rs +++ b/nexum/modules/fixtures/memory-bomb/src/lib.rs @@ -10,7 +10,7 @@ wit_bindgen::generate!({ path: [ - "../../../../wit/nexum-host", + "../../../wit/nexum-host", ], world: "nexum:host/event-module", generate_all, diff --git a/nexum/modules/fixtures/panic-bomb/src/lib.rs b/nexum/modules/fixtures/panic-bomb/src/lib.rs index e0e711ac..8d9e823c 100644 --- a/nexum/modules/fixtures/panic-bomb/src/lib.rs +++ b/nexum/modules/fixtures/panic-bomb/src/lib.rs @@ -11,7 +11,7 @@ wit_bindgen::generate!({ path: [ - "../../../../wit/nexum-host", + "../../../wit/nexum-host", ], world: "nexum:host/event-module", generate_all, diff --git a/nexum/modules/fixtures/slow-host/src/lib.rs b/nexum/modules/fixtures/slow-host/src/lib.rs index f5ce7b09..7d3f405a 100644 --- a/nexum/modules/fixtures/slow-host/src/lib.rs +++ b/nexum/modules/fixtures/slow-host/src/lib.rs @@ -13,7 +13,7 @@ wit_bindgen::generate!({ path: [ - "../../../../wit/nexum-host", + "../../../wit/nexum-host", ], world: "nexum:host/event-module", generate_all, diff --git a/wit/nexum-host/chain.wit b/nexum/wit/nexum-host/chain.wit similarity index 100% rename from wit/nexum-host/chain.wit rename to nexum/wit/nexum-host/chain.wit diff --git a/wit/nexum-host/event-module.wit b/nexum/wit/nexum-host/event-module.wit similarity index 100% rename from wit/nexum-host/event-module.wit rename to nexum/wit/nexum-host/event-module.wit diff --git a/wit/nexum-host/identity.wit b/nexum/wit/nexum-host/identity.wit similarity index 100% rename from wit/nexum-host/identity.wit rename to nexum/wit/nexum-host/identity.wit diff --git a/wit/nexum-host/local-store.wit b/nexum/wit/nexum-host/local-store.wit similarity index 100% rename from wit/nexum-host/local-store.wit rename to nexum/wit/nexum-host/local-store.wit diff --git a/wit/nexum-host/logging.wit b/nexum/wit/nexum-host/logging.wit similarity index 100% rename from wit/nexum-host/logging.wit rename to nexum/wit/nexum-host/logging.wit diff --git a/wit/nexum-host/messaging.wit b/nexum/wit/nexum-host/messaging.wit similarity index 100% rename from wit/nexum-host/messaging.wit rename to nexum/wit/nexum-host/messaging.wit diff --git a/wit/nexum-host/query-module.wit b/nexum/wit/nexum-host/query-module.wit similarity index 100% rename from wit/nexum-host/query-module.wit rename to nexum/wit/nexum-host/query-module.wit diff --git a/wit/nexum-host/remote-store.wit b/nexum/wit/nexum-host/remote-store.wit similarity index 100% rename from wit/nexum-host/remote-store.wit rename to nexum/wit/nexum-host/remote-store.wit diff --git a/wit/nexum-host/types.wit b/nexum/wit/nexum-host/types.wit similarity index 100% rename from wit/nexum-host/types.wit rename to nexum/wit/nexum-host/types.wit diff --git a/scripts/check-venue-agnostic.sh b/scripts/check-venue-agnostic.sh index 4b317a8d..64abd98e 100755 --- a/scripts/check-venue-agnostic.sh +++ b/scripts/check-venue-agnostic.sh @@ -92,30 +92,30 @@ esac # anywhere in its sources, no cross-package use/import, no venue-domain # vocabulary, and the package resolves standalone. wit_charter='nexum:intent|nexum:adapter|value-flow|videre:|shepherd:cow' -rg -n --no-heading -e "$wit_charter" wit/nexum-host +rg -n --no-heading -e "$wit_charter" nexum/wit/nexum-host case $? in - 0) fail "a foreign WIT namespace leaks into wit/nexum-host" ;; + 0) fail "a foreign WIT namespace leaks into nexum/wit/nexum-host" ;; 1) pass "no foreign WIT namespace named" ;; - *) fail "WIT namespace scan errored (wit/nexum-host missing?)" ;; + *) fail "WIT namespace scan errored (nexum/wit/nexum-host missing?)" ;; esac -rg -n --no-heading -e '^\s*(use|import)\s+[a-z0-9-]+:' wit/nexum-host +rg -n --no-heading -e '^\s*(use|import)\s+[a-z0-9-]+:' nexum/wit/nexum-host case $? in 0) fail "nexum:host references another WIT package" ;; 1) pass "nexum:host has no cross-package reference" ;; - *) fail "WIT scan errored (wit/nexum-host missing?)" ;; + *) fail "WIT scan errored (nexum/wit/nexum-host missing?)" ;; esac # Venue-domain vocabulary must not appear in the core event surface: the # intent-status envelope is a videre-side borsh struct crossing `custom` # as opaque bytes, so a term leaking back into nexum:host is a regression # of the extraction. -rg -n --no-heading -i -e 'venue|receipt|intent-status' wit/nexum-host +rg -n --no-heading -i -e 'venue|receipt|intent-status' nexum/wit/nexum-host case $? in - 0) fail "venue-domain vocabulary leaks into wit/nexum-host" ;; + 0) fail "venue-domain vocabulary leaks into nexum/wit/nexum-host" ;; 1) pass "nexum:host carries no venue-domain vocabulary" ;; - *) fail "WIT vocabulary scan errored (wit/nexum-host missing?)" ;; + *) fail "WIT vocabulary scan errored (nexum/wit/nexum-host missing?)" ;; esac if command -v wasm-tools >/dev/null; then - if wasm-tools component wit wit/nexum-host >/dev/null; then + if wasm-tools component wit nexum/wit/nexum-host >/dev/null; then pass "nexum:host resolves standalone" else fail "nexum:host does not resolve standalone" diff --git a/shepherd/crates/cow-venue/tests/wit_layering.rs b/shepherd/crates/cow-venue/tests/wit_layering.rs index 6e26e1c7..03e72e86 100644 --- a/shepherd/crates/cow-venue/tests/wit_layering.rs +++ b/shepherd/crates/cow-venue/tests/wit_layering.rs @@ -1,17 +1,15 @@ //! Layering gate: no generic WIT package references `shepherd:cow`. //! The bundle-layer package carries only the event ABIs; the generic -//! host and videre packages must never name it. +//! host and videre packages this group vendors into `wit/deps/` must +//! never name it. use std::path::Path; #[test] fn generic_wit_packages_never_reference_shepherd_cow() { - let wit_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../wit"); - for pkg in std::fs::read_dir(&wit_root).expect("wit dir") { - let pkg = pkg.expect("wit dir entry").path(); - if pkg.file_name().is_some_and(|n| n == "shepherd-cow") { - continue; - } + let deps = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../wit/deps"); + for pkg in std::fs::read_dir(&deps).expect("wit/deps dir") { + let pkg = pkg.expect("wit/deps entry").path(); for file in std::fs::read_dir(&pkg).expect("wit package dir") { let path = file.expect("wit package entry").path(); let text = std::fs::read_to_string(&path).expect("read wit file"); diff --git a/shepherd/modules/ethflow-watcher/module.toml b/shepherd/modules/ethflow-watcher/module.toml index 98b88cae..fc6194f4 100644 --- a/shepherd/modules/ethflow-watcher/module.toml +++ b/shepherd/modules/ethflow-watcher/module.toml @@ -24,7 +24,7 @@ allow = [] # CoWSwapEthFlow.OrderPlacement on Sepolia. topic-0 = keccak256( # "OrderPlacement(address,(address,address,address,uint256,uint256,uint32, # bytes32,uint256,bytes32,bool,bytes32,bytes32),(uint8,bytes),bytes)"), -# pinned in wit/shepherd-cow/cow-events.wit and parity-tested in +# pinned in shepherd/wit/shepherd-cow/cow-events.wit and parity-tested in # keeper.rs. # `address` is the Sepolia ETH_FLOW_PRODUCTION deployment from # `cowprotocol/ethflowcontract/networks.prod.json`. Unlike diff --git a/shepherd/modules/ethflow-watcher/src/keeper.rs b/shepherd/modules/ethflow-watcher/src/keeper.rs index deed6689..56bca16f 100644 --- a/shepherd/modules/ethflow-watcher/src/keeper.rs +++ b/shepherd/modules/ethflow-watcher/src/keeper.rs @@ -563,7 +563,7 @@ mod tests { /// EthFlow event. #[test] fn topic0_matches_the_cow_events_package_of_record() { - let wit = include_str!("../../../../wit/shepherd-cow/cow-events.wit"); + let wit = include_str!("../../../wit/shepherd-cow/cow-events.wit"); let expected = format!("{:#x}", OrderPlacement::SIGNATURE_HASH); assert!( wit.contains(&expected), diff --git a/shepherd/wit/deps.lock b/shepherd/wit/deps.lock new file mode 100644 index 00000000..cef76964 --- /dev/null +++ b/shepherd/wit/deps.lock @@ -0,0 +1,19 @@ +[nexum-host] +path = "../../nexum/wit/nexum-host" +sha256 = "9022b7254b45bca9c2915e27921d649f0d890728aa4037aa560e1cea0a71882e" +sha512 = "2534135c91e48399b66bd691e4882fb4ff29c5115e734ebd86da25735a96057cfb3c70f27b43b98b9c4d7ee2d8bddc98ab18fa547f6137410c0de35c0b6ef540" + +[videre-types] +path = "../../videre/wit/videre-types" +sha256 = "e2b05e534cfe59dc3716411fecc9085bc8006531d1771fbd91538edf74ba0832" +sha512 = "ef72be3a32006c8eb347339c838ed464f1a64dfd99ee501de4a3c0574b9e5cd9472e6deb157f13e85e8d156dab3ba35b14c7454ca6a830ed41b642323f2c4647" + +[videre-value-flow] +path = "../../videre/wit/videre-value-flow" +sha256 = "1ef3f4822c4098da9813343f38991a1d678f11fc2bac5d36198c9a1f64846ecf" +sha512 = "bd3188540f9d0d4075b9ee278d0a0b56b4f99ba8dd133254e4a11b4b793e0cc6c2d710f24dcaa39b8986fe4b1815ea785609683f2f6e3a6aa15345191c0547a2" + +[videre-venue] +path = "../../videre/wit/videre-venue" +sha256 = "33ead08014f1fd1f6e0aad0de5726d817cd05f94079cd839b01fa5de23be0eb3" +sha512 = "4f70eb19339c3e3c84983d98043b62c8e7dc5eea03b60b14f16f06009e0bea1de853135f44b5ffa2abd4dfaad58f567ed6da87ea9c8496ec448494726efec7c7" diff --git a/shepherd/wit/deps.toml b/shepherd/wit/deps.toml new file mode 100644 index 00000000..b10ccb77 --- /dev/null +++ b/shepherd/wit/deps.toml @@ -0,0 +1,7 @@ +# Cross-group WIT dependencies, vendored into wit/deps/ by wit-deps. +# Path sources are transitional: they flip to pinned git-tag tarballs at +# the physical carve. +nexum-host = "../../nexum/wit/nexum-host" +videre-types = "../../videre/wit/videre-types" +videre-value-flow = "../../videre/wit/videre-value-flow" +videre-venue = "../../videre/wit/videre-venue" diff --git a/shepherd/wit/deps/nexum-host/chain.wit b/shepherd/wit/deps/nexum-host/chain.wit new file mode 100644 index 00000000..1d812dc6 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/chain.wit @@ -0,0 +1,60 @@ +package nexum:host@0.1.0; + +interface chain { + use types.{chain-id, fault}; + + /// A JSON-RPC error response from the upstream node. `code` is the + /// node-reported numeric (typically `-32000` for an `eth_call` + /// revert). `data` is the decoded `error.data` payload: the host + /// hex-decodes the upstream JSON string once, so a guest receives + /// the raw abi-encoded revert bytes without any string handling. + record rpc-error { + code: s32, + message: string, + data: option>, + } + + /// Failure of a chain call: either a shared host `fault` (transport + /// down, timed out, denied, ...) or a structured JSON-RPC error + /// carrying the node code and any decoded revert payload. + variant chain-error { + fault(fault), + rpc(rpc-error), + } + + /// A single JSON-RPC request to be executed as part of a batch. + record rpc-request { + method: string, + params: string, + } + + /// Result of a single request inside a batch. Each entry is independent; + /// one failing call does not abort the others. + variant rpc-result { + ok(string), + err(chain-error), + } + + /// Execute a JSON-RPC request against the specified chain. + /// + /// The host routes to its configured provider for the given chain, + /// applying whatever middleware is appropriate for the platform + /// (timeout, retry, rate-limit, fallback on server; simple HTTP + /// on mobile; window.ethereum or injected provider in WebView). + /// + /// `method` includes the namespace prefix (e.g. "eth_call"). Each + /// host enforces its own permitted method surface; the reference + /// server host forwards only a read-only set and refuses signing + /// or mutating methods with a `denied` fault before they reach the + /// provider. `params` and the success value are JSON-encoded + /// strings. + request: func(chain-id: chain-id, method: string, params: string) + -> result; + + /// Execute several JSON-RPC requests against the same chain in a single + /// round trip where the host transport supports it. Hosts that cannot + /// batch natively MUST fall back to sequential `request` calls. The + /// returned list is the same length as `requests` and in the same order. + request-batch: func(chain-id: chain-id, requests: list) + -> result, chain-error>; +} diff --git a/shepherd/wit/deps/nexum-host/event-module.wit b/shepherd/wit/deps/nexum-host/event-module.wit new file mode 100644 index 00000000..277134a9 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/event-module.wit @@ -0,0 +1,24 @@ +package nexum:host@0.1.0; + +/// Event-driven module — automation, background processing. +/// No UI capabilities. Runs on any conforming host. +world event-module { + use types.{config, event, fault}; + + // Six core primitives (always provided by a conforming host). + import chain; + import identity; + import local-store; + import remote-store; + import messaging; + import logging; + + // Time, randomness, and outbound HTTP are WASI concerns, not + // nexum:host interfaces: wasi:clocks and wasi:random are linked + // into every module store, and hosts link + // wasi:http/outgoing-handler, gated per-module by the + // `[capabilities.http].allow` allowlist in module.toml. + + export init: func(config: config) -> result<_, fault>; + export on-event: func(event: event) -> result<_, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/identity.wit b/shepherd/wit/deps/nexum-host/identity.wit new file mode 100644 index 00000000..2e82c9fe --- /dev/null +++ b/shepherd/wit/deps/nexum-host/identity.wit @@ -0,0 +1,24 @@ +package nexum:host@0.1.0; + +/// Identity / signing capability. +/// +/// 0.2 ships a single, minimal interface. A future release (0.4+) is +/// expected to split this into `identity-read` and `identity-sign` and to +/// introduce a richer `signing-result` variant; for 0.2 the simple shape is +/// sufficient because user rejection can already be expressed via +/// `fault.denied`. +interface identity { + use types.{fault}; + + /// Return the list of account addresses (20-byte EVM addresses) the host + /// is willing to sign for. Empty list means no signing capability. + accounts: func() -> result>, fault>; + + /// Sign an arbitrary message with personal_sign semantics (prepends the + /// "\x19Ethereum Signed Message:\n" prefix). Returns a 65-byte signature. + sign: func(account: list, message: list) -> result, fault>; + + /// Sign EIP-712 typed data. `typed-data` is a JSON-encoded EIP-712 payload. + /// Returns a 65-byte signature. + sign-typed-data: func(account: list, typed-data: string) -> result, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/local-store.wit b/shepherd/wit/deps/nexum-host/local-store.wit new file mode 100644 index 00000000..814ef724 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/local-store.wit @@ -0,0 +1,47 @@ +package nexum:host@0.1.0; + +interface local-store { + use types.{fault}; + + /// Get a value by key. Returns none if the key does not exist. + get: func(key: string) -> result>, fault>; + + /// Set a key-value pair. Overwrites any existing value. + /// The host may enforce a size quota; if exceeded, returns err. + set: func(key: string, value: list) -> result<_, fault>; + + /// Delete a key. No-op if the key does not exist. + delete: func(key: string) -> result<_, fault>; + + /// List all keys matching a prefix. Empty prefix returns all keys. + list-keys: func(prefix: string) -> result, fault>; + + /// Whether the key exists, without transferring the value. + contains: func(key: string) -> result; + + /// Value byte length, none if the key is absent, without + /// transferring the value. On some backends this may be a scan. + len: func(key: string) -> result, fault>; + + /// Number of keys matching a prefix, without materialising the key + /// list. On some backends this may be a scan. + count: func(prefix: string) -> result; + + /// One pair in an apply batch. + record key-value { + key: string, + value: list, + } + + /// One write in an apply batch. + variant write-op { + set(key-value), + delete(string), + } + + /// Apply a batch of writes atomically: every op lands or none + /// does. Later ops on a key supersede earlier ones. Quota is + /// charged on the net whole-batch footprint; the host caps the op + /// count and total value bytes per batch. + apply: func(ops: list) -> result<_, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/logging.wit b/shepherd/wit/deps/nexum-host/logging.wit new file mode 100644 index 00000000..8cd98140 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/logging.wit @@ -0,0 +1,15 @@ +package nexum:host@0.1.0; + +interface logging { + enum level { + trace, + debug, + info, + warn, + error, + } + + /// Emit a structured log message. + /// The host decides how to handle it (stdout, file, discard). + log: func(level: level, message: string); +} diff --git a/shepherd/wit/deps/nexum-host/messaging.wit b/shepherd/wit/deps/nexum-host/messaging.wit new file mode 100644 index 00000000..30777ca7 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/messaging.wit @@ -0,0 +1,19 @@ +package nexum:host@0.1.0; + +interface messaging { + use types.{fault, message}; + + /// Publish a message to a content topic. + /// + /// Content topics follow the format: //// + /// e.g. "/nexum/1/twap-updates/proto" + publish: func(content-topic: string, payload: list) -> result<_, fault>; + + /// Query historical messages from the Waku store protocol. + query: func( + content-topic: string, + start-time: option, + end-time: option, + limit: option, + ) -> result, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/query-module.wit b/shepherd/wit/deps/nexum-host/query-module.wit new file mode 100644 index 00000000..ffb29b87 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/query-module.wit @@ -0,0 +1,25 @@ +package nexum:host@0.1.0; + +/// Query module — synchronous, side-effect-free evaluation. +/// +/// EXPERIMENTAL (0.2): the shape of this world is provisional and may +/// change in a future minor release without a major bump. Hosts and SDKs +/// should expect breakage here until the world is stabilised. +/// +/// A query module exposes a single pure `evaluate` entry point. It is given +/// read-only access to the local store (for cached/derived state) and to +/// logging; everything else (chain access, network, messaging, signing) is +/// deliberately excluded so the host can run queries inside a tight +/// deterministic sandbox. +world query-module { + use types.{config, fault}; + + import local-store; + import logging; + + export init: func(config: config) -> result<_, fault>; + + /// Evaluate the query. `input` and the returned bytes are opaque to the + /// host; the module and its caller agree on the encoding. + export evaluate: func(input: list) -> result, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/remote-store.wit b/shepherd/wit/deps/nexum-host/remote-store.wit new file mode 100644 index 00000000..39b36bae --- /dev/null +++ b/shepherd/wit/deps/nexum-host/remote-store.wit @@ -0,0 +1,33 @@ +package nexum:host@0.1.0; + +interface remote-store { + use types.{fault}; + + /// Upload raw data to the decentralised store. + /// Returns the 32-byte content reference (Swarm address). + upload: func(data: list) -> result, fault>; + + /// Download raw data by 32-byte content reference. + download: func(reference: list) -> result, fault>; + + /// Read the latest value from a mutable feed. + /// + /// Feeds are mutable pointers: (owner, topic) -> latest chunk. + /// `owner`: 20-byte Ethereum address of the feed owner. + /// `topic`: 32-byte topic hash. + read-feed: func( + owner: list, + topic: list, + ) -> result>, fault>; + + /// Update a mutable feed with new data. + /// + /// The host signs the feed update with its configured identity. + /// `topic`: 32-byte topic hash. + /// `data`: the payload to publish. + /// Returns the 32-byte reference of the new chunk. + write-feed: func( + topic: list, + data: list, + ) -> result, fault>; +} diff --git a/shepherd/wit/deps/nexum-host/types.wit b/shepherd/wit/deps/nexum-host/types.wit new file mode 100644 index 00000000..0142f119 --- /dev/null +++ b/shepherd/wit/deps/nexum-host/types.wit @@ -0,0 +1,103 @@ +package nexum:host@0.1.0; + +/// Common types shared across all runtime interfaces. +/// +/// All `u64` timestamps in this package are milliseconds since the Unix +/// epoch, UTC. +interface types { + type chain-id = u64; + + record block { + chain-id: chain-id, + number: u64, + hash: list, + timestamp: u64, + } + + /// One decoded log, mirroring the RPC `eth_getLogs` shape field for + /// field so the guest can rebuild the native alloy log losslessly. + /// Fixed-width byte fields are carried raw: `address` is 20 bytes, + /// each topic and hash is 32. The block-scoped fields are absent on a + /// pending log (mined logs carry them all). + record chain-log { + address: list, + topics: list>, + data: list, + block-hash: option>, + block-number: option, + block-timestamp: option, + transaction-hash: option>, + transaction-index: option, + log-index: option, + removed: bool, + } + + /// A batch of logs delivered from one subscription. The alloy log type + /// carries no chain id, so it sits here once: every log in a delivery + /// shares the chain of the subscription that produced it. + record chain-logs { + chain-id: chain-id, + logs: list, + } + + /// A message delivered over the messaging interface. Defined here (rather + /// than only in `messaging.wit`) so the `event` variant can reference it + /// without a cross-interface use clause. + record message { + content-topic: string, + payload: list, + timestamp: u64, + /// Optional sender identity (protocol-dependent). + sender: option>, + } + + /// Fired by the host on a configured cadence. `fired-at` is the host's + /// wall-clock time (ms since Unix epoch, UTC) at which the tick was + /// generated. + record tick { + fired-at: u64, + } + + /// The generic extension event: a domain extension's own event kind + /// and its opaque payload. The core routes by `kind` and never reads + /// `payload`; the subscribing module decodes it against the extension + /// that emitted it. + record custom-event { + /// Extension-scoped event kind, matched against a module's + /// `[[subscription]]` kind. + kind: string, + /// Opaque bytes the emitting extension defines and the module + /// decodes. + payload: list, + } + + variant event { + block(block), + chain-logs(chain-logs), + tick(tick), + message(message), + custom(custom-event), + } + + /// Opaque config from module.toml [config] section. + type config = list>; + + /// The cross-domain failure vocabulary richer interfaces embed as a + /// case. Each payload-bearing case carries a human-readable detail; + /// `rate-limited` carries structured backoff guidance instead. + variant fault { + unsupported(string), + unavailable(string), + denied(string), + rate-limited(rate-limit), + timeout, + invalid-input(string), + internal(string), + } + + /// Backoff guidance for a `fault.rate-limited`. `retry-after-ms` is + /// the host's suggested wait before retrying, when known. + record rate-limit { + retry-after-ms: option, + } +} diff --git a/wit/videre-types/types.wit b/shepherd/wit/deps/videre-types/types.wit similarity index 100% rename from wit/videre-types/types.wit rename to shepherd/wit/deps/videre-types/types.wit diff --git a/wit/videre-value-flow/types.wit b/shepherd/wit/deps/videre-value-flow/types.wit similarity index 100% rename from wit/videre-value-flow/types.wit rename to shepherd/wit/deps/videre-value-flow/types.wit diff --git a/wit/videre-venue/venue.wit b/shepherd/wit/deps/videre-venue/venue.wit similarity index 100% rename from wit/videre-venue/venue.wit rename to shepherd/wit/deps/videre-venue/venue.wit diff --git a/wit/shepherd-cow/cow-events.wit b/shepherd/wit/shepherd-cow/cow-events.wit similarity index 100% rename from wit/shepherd-cow/cow-events.wit rename to shepherd/wit/shepherd-cow/cow-events.wit diff --git a/videre/crates/videre-host/src/bindings.rs b/videre/crates/videre-host/src/bindings.rs index df0a3625..9cfbed2a 100644 --- a/videre/crates/videre-host/src/bindings.rs +++ b/videre/crates/videre-host/src/bindings.rs @@ -10,10 +10,10 @@ mod venue_adapter { wasmtime::component::bindgen!({ path: [ - "../../../wit/videre-value-flow", - "../../../wit/videre-types", - "../../../wit/nexum-host", - "../../../wit/videre-venue", + "../../wit/videre-value-flow", + "../../wit/videre-types", + "../../wit/deps/nexum-host", + "../../wit/videre-venue", ], world: "videre:venue/venue-adapter", imports: { default: async }, @@ -42,10 +42,10 @@ mod client_host { } ", path: [ - "../../../wit/videre-value-flow", - "../../../wit/videre-types", - "../../../wit/nexum-host", - "../../../wit/videre-venue", + "../../wit/videre-value-flow", + "../../wit/videre-types", + "../../wit/deps/nexum-host", + "../../wit/videre-venue", ], imports: { default: async }, with: { @@ -117,7 +117,7 @@ mod value_flow_smoke { import videre:value-flow/types@0.1.0; } ", - path: ["../../../wit/videre-value-flow"], + path: ["../../wit/videre-value-flow"], }); #[test] @@ -151,10 +151,10 @@ mod client_smoke { } ", path: [ - "../../../wit/videre-value-flow", - "../../../wit/videre-types", - "../../../wit/nexum-host", - "../../../wit/videre-venue", + "../../wit/videre-value-flow", + "../../wit/videre-types", + "../../wit/deps/nexum-host", + "../../wit/videre-venue", ], }); diff --git a/videre/crates/videre-sdk/src/bindings.rs b/videre/crates/videre-sdk/src/bindings.rs index 71ec301e..29b112ff 100644 --- a/videre/crates/videre-sdk/src/bindings.rs +++ b/videre/crates/videre-sdk/src/bindings.rs @@ -24,10 +24,10 @@ world sdk-imports { } ", path: [ - "../../../wit/videre-value-flow", - "../../../wit/videre-types", - "../../../wit/nexum-host", - "../../../wit/videre-venue", + "../../wit/videre-value-flow", + "../../wit/videre-types", + "../../wit/deps/nexum-host", + "../../wit/videre-venue", ], world: "videre:sdk-shims/sdk-imports", generate_all, diff --git a/videre/wit/deps.lock b/videre/wit/deps.lock new file mode 100644 index 00000000..7d4a2577 --- /dev/null +++ b/videre/wit/deps.lock @@ -0,0 +1,4 @@ +[nexum-host] +path = "../../nexum/wit/nexum-host" +sha256 = "9022b7254b45bca9c2915e27921d649f0d890728aa4037aa560e1cea0a71882e" +sha512 = "2534135c91e48399b66bd691e4882fb4ff29c5115e734ebd86da25735a96057cfb3c70f27b43b98b9c4d7ee2d8bddc98ab18fa547f6137410c0de35c0b6ef540" diff --git a/videre/wit/deps.toml b/videre/wit/deps.toml new file mode 100644 index 00000000..087a8fa5 --- /dev/null +++ b/videre/wit/deps.toml @@ -0,0 +1,4 @@ +# Cross-group WIT dependencies, vendored into wit/deps/ by wit-deps. +# Path sources are transitional: they flip to pinned git-tag tarballs at +# the physical carve. +nexum-host = "../../nexum/wit/nexum-host" diff --git a/videre/wit/deps/nexum-host/chain.wit b/videre/wit/deps/nexum-host/chain.wit new file mode 100644 index 00000000..1d812dc6 --- /dev/null +++ b/videre/wit/deps/nexum-host/chain.wit @@ -0,0 +1,60 @@ +package nexum:host@0.1.0; + +interface chain { + use types.{chain-id, fault}; + + /// A JSON-RPC error response from the upstream node. `code` is the + /// node-reported numeric (typically `-32000` for an `eth_call` + /// revert). `data` is the decoded `error.data` payload: the host + /// hex-decodes the upstream JSON string once, so a guest receives + /// the raw abi-encoded revert bytes without any string handling. + record rpc-error { + code: s32, + message: string, + data: option>, + } + + /// Failure of a chain call: either a shared host `fault` (transport + /// down, timed out, denied, ...) or a structured JSON-RPC error + /// carrying the node code and any decoded revert payload. + variant chain-error { + fault(fault), + rpc(rpc-error), + } + + /// A single JSON-RPC request to be executed as part of a batch. + record rpc-request { + method: string, + params: string, + } + + /// Result of a single request inside a batch. Each entry is independent; + /// one failing call does not abort the others. + variant rpc-result { + ok(string), + err(chain-error), + } + + /// Execute a JSON-RPC request against the specified chain. + /// + /// The host routes to its configured provider for the given chain, + /// applying whatever middleware is appropriate for the platform + /// (timeout, retry, rate-limit, fallback on server; simple HTTP + /// on mobile; window.ethereum or injected provider in WebView). + /// + /// `method` includes the namespace prefix (e.g. "eth_call"). Each + /// host enforces its own permitted method surface; the reference + /// server host forwards only a read-only set and refuses signing + /// or mutating methods with a `denied` fault before they reach the + /// provider. `params` and the success value are JSON-encoded + /// strings. + request: func(chain-id: chain-id, method: string, params: string) + -> result; + + /// Execute several JSON-RPC requests against the same chain in a single + /// round trip where the host transport supports it. Hosts that cannot + /// batch natively MUST fall back to sequential `request` calls. The + /// returned list is the same length as `requests` and in the same order. + request-batch: func(chain-id: chain-id, requests: list) + -> result, chain-error>; +} diff --git a/videre/wit/deps/nexum-host/event-module.wit b/videre/wit/deps/nexum-host/event-module.wit new file mode 100644 index 00000000..277134a9 --- /dev/null +++ b/videre/wit/deps/nexum-host/event-module.wit @@ -0,0 +1,24 @@ +package nexum:host@0.1.0; + +/// Event-driven module — automation, background processing. +/// No UI capabilities. Runs on any conforming host. +world event-module { + use types.{config, event, fault}; + + // Six core primitives (always provided by a conforming host). + import chain; + import identity; + import local-store; + import remote-store; + import messaging; + import logging; + + // Time, randomness, and outbound HTTP are WASI concerns, not + // nexum:host interfaces: wasi:clocks and wasi:random are linked + // into every module store, and hosts link + // wasi:http/outgoing-handler, gated per-module by the + // `[capabilities.http].allow` allowlist in module.toml. + + export init: func(config: config) -> result<_, fault>; + export on-event: func(event: event) -> result<_, fault>; +} diff --git a/videre/wit/deps/nexum-host/identity.wit b/videre/wit/deps/nexum-host/identity.wit new file mode 100644 index 00000000..2e82c9fe --- /dev/null +++ b/videre/wit/deps/nexum-host/identity.wit @@ -0,0 +1,24 @@ +package nexum:host@0.1.0; + +/// Identity / signing capability. +/// +/// 0.2 ships a single, minimal interface. A future release (0.4+) is +/// expected to split this into `identity-read` and `identity-sign` and to +/// introduce a richer `signing-result` variant; for 0.2 the simple shape is +/// sufficient because user rejection can already be expressed via +/// `fault.denied`. +interface identity { + use types.{fault}; + + /// Return the list of account addresses (20-byte EVM addresses) the host + /// is willing to sign for. Empty list means no signing capability. + accounts: func() -> result>, fault>; + + /// Sign an arbitrary message with personal_sign semantics (prepends the + /// "\x19Ethereum Signed Message:\n" prefix). Returns a 65-byte signature. + sign: func(account: list, message: list) -> result, fault>; + + /// Sign EIP-712 typed data. `typed-data` is a JSON-encoded EIP-712 payload. + /// Returns a 65-byte signature. + sign-typed-data: func(account: list, typed-data: string) -> result, fault>; +} diff --git a/videre/wit/deps/nexum-host/local-store.wit b/videre/wit/deps/nexum-host/local-store.wit new file mode 100644 index 00000000..814ef724 --- /dev/null +++ b/videre/wit/deps/nexum-host/local-store.wit @@ -0,0 +1,47 @@ +package nexum:host@0.1.0; + +interface local-store { + use types.{fault}; + + /// Get a value by key. Returns none if the key does not exist. + get: func(key: string) -> result>, fault>; + + /// Set a key-value pair. Overwrites any existing value. + /// The host may enforce a size quota; if exceeded, returns err. + set: func(key: string, value: list) -> result<_, fault>; + + /// Delete a key. No-op if the key does not exist. + delete: func(key: string) -> result<_, fault>; + + /// List all keys matching a prefix. Empty prefix returns all keys. + list-keys: func(prefix: string) -> result, fault>; + + /// Whether the key exists, without transferring the value. + contains: func(key: string) -> result; + + /// Value byte length, none if the key is absent, without + /// transferring the value. On some backends this may be a scan. + len: func(key: string) -> result, fault>; + + /// Number of keys matching a prefix, without materialising the key + /// list. On some backends this may be a scan. + count: func(prefix: string) -> result; + + /// One pair in an apply batch. + record key-value { + key: string, + value: list, + } + + /// One write in an apply batch. + variant write-op { + set(key-value), + delete(string), + } + + /// Apply a batch of writes atomically: every op lands or none + /// does. Later ops on a key supersede earlier ones. Quota is + /// charged on the net whole-batch footprint; the host caps the op + /// count and total value bytes per batch. + apply: func(ops: list) -> result<_, fault>; +} diff --git a/videre/wit/deps/nexum-host/logging.wit b/videre/wit/deps/nexum-host/logging.wit new file mode 100644 index 00000000..8cd98140 --- /dev/null +++ b/videre/wit/deps/nexum-host/logging.wit @@ -0,0 +1,15 @@ +package nexum:host@0.1.0; + +interface logging { + enum level { + trace, + debug, + info, + warn, + error, + } + + /// Emit a structured log message. + /// The host decides how to handle it (stdout, file, discard). + log: func(level: level, message: string); +} diff --git a/videre/wit/deps/nexum-host/messaging.wit b/videre/wit/deps/nexum-host/messaging.wit new file mode 100644 index 00000000..30777ca7 --- /dev/null +++ b/videre/wit/deps/nexum-host/messaging.wit @@ -0,0 +1,19 @@ +package nexum:host@0.1.0; + +interface messaging { + use types.{fault, message}; + + /// Publish a message to a content topic. + /// + /// Content topics follow the format: //// + /// e.g. "/nexum/1/twap-updates/proto" + publish: func(content-topic: string, payload: list) -> result<_, fault>; + + /// Query historical messages from the Waku store protocol. + query: func( + content-topic: string, + start-time: option, + end-time: option, + limit: option, + ) -> result, fault>; +} diff --git a/videre/wit/deps/nexum-host/query-module.wit b/videre/wit/deps/nexum-host/query-module.wit new file mode 100644 index 00000000..ffb29b87 --- /dev/null +++ b/videre/wit/deps/nexum-host/query-module.wit @@ -0,0 +1,25 @@ +package nexum:host@0.1.0; + +/// Query module — synchronous, side-effect-free evaluation. +/// +/// EXPERIMENTAL (0.2): the shape of this world is provisional and may +/// change in a future minor release without a major bump. Hosts and SDKs +/// should expect breakage here until the world is stabilised. +/// +/// A query module exposes a single pure `evaluate` entry point. It is given +/// read-only access to the local store (for cached/derived state) and to +/// logging; everything else (chain access, network, messaging, signing) is +/// deliberately excluded so the host can run queries inside a tight +/// deterministic sandbox. +world query-module { + use types.{config, fault}; + + import local-store; + import logging; + + export init: func(config: config) -> result<_, fault>; + + /// Evaluate the query. `input` and the returned bytes are opaque to the + /// host; the module and its caller agree on the encoding. + export evaluate: func(input: list) -> result, fault>; +} diff --git a/videre/wit/deps/nexum-host/remote-store.wit b/videre/wit/deps/nexum-host/remote-store.wit new file mode 100644 index 00000000..39b36bae --- /dev/null +++ b/videre/wit/deps/nexum-host/remote-store.wit @@ -0,0 +1,33 @@ +package nexum:host@0.1.0; + +interface remote-store { + use types.{fault}; + + /// Upload raw data to the decentralised store. + /// Returns the 32-byte content reference (Swarm address). + upload: func(data: list) -> result, fault>; + + /// Download raw data by 32-byte content reference. + download: func(reference: list) -> result, fault>; + + /// Read the latest value from a mutable feed. + /// + /// Feeds are mutable pointers: (owner, topic) -> latest chunk. + /// `owner`: 20-byte Ethereum address of the feed owner. + /// `topic`: 32-byte topic hash. + read-feed: func( + owner: list, + topic: list, + ) -> result>, fault>; + + /// Update a mutable feed with new data. + /// + /// The host signs the feed update with its configured identity. + /// `topic`: 32-byte topic hash. + /// `data`: the payload to publish. + /// Returns the 32-byte reference of the new chunk. + write-feed: func( + topic: list, + data: list, + ) -> result, fault>; +} diff --git a/videre/wit/deps/nexum-host/types.wit b/videre/wit/deps/nexum-host/types.wit new file mode 100644 index 00000000..0142f119 --- /dev/null +++ b/videre/wit/deps/nexum-host/types.wit @@ -0,0 +1,103 @@ +package nexum:host@0.1.0; + +/// Common types shared across all runtime interfaces. +/// +/// All `u64` timestamps in this package are milliseconds since the Unix +/// epoch, UTC. +interface types { + type chain-id = u64; + + record block { + chain-id: chain-id, + number: u64, + hash: list, + timestamp: u64, + } + + /// One decoded log, mirroring the RPC `eth_getLogs` shape field for + /// field so the guest can rebuild the native alloy log losslessly. + /// Fixed-width byte fields are carried raw: `address` is 20 bytes, + /// each topic and hash is 32. The block-scoped fields are absent on a + /// pending log (mined logs carry them all). + record chain-log { + address: list, + topics: list>, + data: list, + block-hash: option>, + block-number: option, + block-timestamp: option, + transaction-hash: option>, + transaction-index: option, + log-index: option, + removed: bool, + } + + /// A batch of logs delivered from one subscription. The alloy log type + /// carries no chain id, so it sits here once: every log in a delivery + /// shares the chain of the subscription that produced it. + record chain-logs { + chain-id: chain-id, + logs: list, + } + + /// A message delivered over the messaging interface. Defined here (rather + /// than only in `messaging.wit`) so the `event` variant can reference it + /// without a cross-interface use clause. + record message { + content-topic: string, + payload: list, + timestamp: u64, + /// Optional sender identity (protocol-dependent). + sender: option>, + } + + /// Fired by the host on a configured cadence. `fired-at` is the host's + /// wall-clock time (ms since Unix epoch, UTC) at which the tick was + /// generated. + record tick { + fired-at: u64, + } + + /// The generic extension event: a domain extension's own event kind + /// and its opaque payload. The core routes by `kind` and never reads + /// `payload`; the subscribing module decodes it against the extension + /// that emitted it. + record custom-event { + /// Extension-scoped event kind, matched against a module's + /// `[[subscription]]` kind. + kind: string, + /// Opaque bytes the emitting extension defines and the module + /// decodes. + payload: list, + } + + variant event { + block(block), + chain-logs(chain-logs), + tick(tick), + message(message), + custom(custom-event), + } + + /// Opaque config from module.toml [config] section. + type config = list>; + + /// The cross-domain failure vocabulary richer interfaces embed as a + /// case. Each payload-bearing case carries a human-readable detail; + /// `rate-limited` carries structured backoff guidance instead. + variant fault { + unsupported(string), + unavailable(string), + denied(string), + rate-limited(rate-limit), + timeout, + invalid-input(string), + internal(string), + } + + /// Backoff guidance for a `fault.rate-limited`. `retry-after-ms` is + /// the host's suggested wait before retrying, when known. + record rate-limit { + retry-after-ms: option, + } +} diff --git a/videre/wit/videre-types/types.wit b/videre/wit/videre-types/types.wit new file mode 100644 index 00000000..d77d5f61 --- /dev/null +++ b/videre/wit/videre-types/types.wit @@ -0,0 +1,87 @@ +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, + /// An empty or structurally invalid receipt. + invalid-receipt, + /// The venue-returned identifier disagrees with the locally derived one. + receipt-mismatch, + } + + 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/videre/wit/videre-value-flow/types.wit b/videre/wit/videre-value-flow/types.wit new file mode 100644 index 00000000..c3aa9de9 --- /dev/null +++ b/videre/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/videre/wit/videre-venue/venue.wit b/videre/wit/videre-venue/venue.wit new file mode 100644 index 00000000..8a9685f2 --- /dev/null +++ b/videre/wit/videre-venue/venue.wit @@ -0,0 +1,49 @@ +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.{quotation, receipt, intent-status, submit-outcome, venue-error}; + + quote: func(venue: string, body: list) -> result; + submit: func(venue: string, body: list) -> result; + /// Put an externally-obtained receipt (e.g. an on-chain placement) + /// under the host's status watch; an accepted submit is watched + /// implicitly. Idempotent. + observe: func(venue: string, receipt: receipt) -> result<_, venue-error>; + 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, quotation, receipt, intent-status, submit-outcome, venue-error}; + + /// Body-schema versions this adapter decodes. Must equal the + /// manifest `[venue] body_versions` set; install asserts it. + body-versions: func() -> list; + + /// Pure: derive the guard-facing header from a body. No I/O. + derive-header: func(body: list) -> result; + quote: func(body: list) -> result; + submit: func(body: list) -> result; + status: func(receipt: receipt) -> result; + cancel: func(receipt: receipt) -> result<_, venue-error>; +} + +/// A venue adapter component: the provider face over scoped transport only. +/// No local-store, remote-store, identity, or logging import, so an adapter +/// structurally cannot touch host key material or persistent state. Outbound +/// HTTP is wasi:http, linked separately and allowlisted per adapter. +world venue-adapter { + use nexum:host/types@0.1.0.{config, fault}; + + import nexum:host/chain@0.1.0; + import nexum:host/messaging@0.1.0; + + /// Configure the adapter from its `[config]` before any submission. + export init: func(config: config) -> result<_, fault>; + export adapter; +}