diff --git a/crates/nexum-runtime/src/host/component/chain.rs b/crates/nexum-runtime/src/host/component/chain.rs index 687abf3a..294eba71 100644 --- a/crates/nexum-runtime/src/host/component/chain.rs +++ b/crates/nexum-runtime/src/host/component/chain.rs @@ -5,67 +5,13 @@ use std::future::Future; use alloy_chains::Chain; use alloy_rpc_types_eth::Filter; -use strum::{EnumString, IntoStaticStr}; use crate::host::provider_pool::{BlockStream, CanonicalLogStream, ProviderError, ProviderPool}; -/// The permitted JSON-RPC read surface as a closed type. Methods that -/// sign or mutate node state have no variant, so a guest-supplied -/// signing method (for example `eth_sign` or `eth_sendTransaction`) -/// cannot be represented and never reaches the provider. This is the -/// structural ceiling; an operator allowlist narrows within it and -/// never widens it. -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, IntoStaticStr)] -#[non_exhaustive] -pub enum ChainMethod { - #[strum(serialize = "eth_blockNumber")] - EthBlockNumber, - #[strum(serialize = "eth_call")] - EthCall, - #[strum(serialize = "eth_chainId")] - EthChainId, - #[strum(serialize = "eth_estimateGas")] - EthEstimateGas, - #[strum(serialize = "eth_feeHistory")] - EthFeeHistory, - #[strum(serialize = "eth_gasPrice")] - EthGasPrice, - #[strum(serialize = "eth_maxPriorityFeePerGas")] - EthMaxPriorityFeePerGas, - #[strum(serialize = "eth_getBalance")] - EthGetBalance, - #[strum(serialize = "eth_getBlockByHash")] - EthGetBlockByHash, - #[strum(serialize = "eth_getBlockByNumber")] - EthGetBlockByNumber, - #[strum(serialize = "eth_getBlockReceipts")] - EthGetBlockReceipts, - #[strum(serialize = "eth_getCode")] - EthGetCode, - #[strum(serialize = "eth_getLogs")] - EthGetLogs, - #[strum(serialize = "eth_getProof")] - EthGetProof, - #[strum(serialize = "eth_getStorageAt")] - EthGetStorageAt, - #[strum(serialize = "eth_getTransactionByHash")] - EthGetTransactionByHash, - #[strum(serialize = "eth_getTransactionCount")] - EthGetTransactionCount, - #[strum(serialize = "eth_getTransactionReceipt")] - EthGetTransactionReceipt, - #[strum(serialize = "net_version")] - NetVersion, -} - -impl ChainMethod { - /// The wire method name forwarded to the provider. `&'static` - /// because the permitted set is closed, so the name drops straight - /// into alloy's `Cow<'static, str>` method slot without allocating. - pub fn as_str(self) -> &'static str { - self.into() - } -} +/// The read surface is defined once in `nexum-world`; host and guest +/// re-export the same type, so the dispatch table and the guest +/// allowlist cannot drift. +pub use nexum_world::ChainMethod; /// Async chain backend. Methods mirror [`ProviderPool`] one-to-one; /// the `impl Future + Send` form bakes in the Send bound generic @@ -134,51 +80,3 @@ impl ChainProvider for ProviderPool { ProviderPool::request(self, chain, method, params_json) } } - -#[cfg(test)] -mod tests { - use super::ChainMethod; - - #[test] - fn read_surface_methods_parse() { - for m in [ - "eth_call", - "eth_blockNumber", - "eth_getBalance", - "eth_getLogs", - "eth_getTransactionReceipt", - "net_version", - ] { - assert!(ChainMethod::try_from(m).is_ok(), "{m} should parse"); - } - } - - #[test] - fn signing_and_mutating_methods_have_no_variant() { - for m in [ - "eth_sign", - "eth_signTransaction", - "eth_sendTransaction", - "eth_sendRawTransaction", - "eth_accounts", - "personal_sign", - "personal_unlockAccount", - "admin_peers", - "debug_traceCall", - "miner_start", - "eth_notAMethod", - "", - ] { - assert!(ChainMethod::try_from(m).is_err(), "{m} must be rejected"); - } - } - - #[test] - fn as_str_round_trips_the_wire_name() { - assert_eq!(ChainMethod::EthCall.as_str(), "eth_call"); - assert_eq!( - ChainMethod::try_from(ChainMethod::EthGetBalance.as_str()).unwrap(), - ChainMethod::EthGetBalance, - ); - } -} diff --git a/crates/nexum-sdk/Cargo.toml b/crates/nexum-sdk/Cargo.toml index 07260cfd..7d28b7a4 100644 --- a/crates/nexum-sdk/Cargo.toml +++ b/crates/nexum-sdk/Cargo.toml @@ -23,6 +23,10 @@ stderr-echo = [] # calls back into this crate (`bind_host_via_wit_bindgen!`, the host # trait seam, the tracing facade). nexum-module-macros = { path = "../nexum-module-macros" } +# The single-source vocabularies: `ChainMethod` (re-exported as +# `chain::ChainMethod`) and the fault labels. Its world-synthesis half +# is unused here, so the guest links `toml` and never calls it. +nexum-world = { path = "../nexum-world" } alloy-primitives.workspace = true # Typed EIP-155 chain id; already in the guest graph via alloy-provider. alloy-chains.workspace = true @@ -62,8 +66,6 @@ proptest.workspace = true # The keeper never touches the orderbook, so a CoW-layer mock would only drag # the domain crates into this crate's dev graph. nexum-sdk-test = { path = "../nexum-sdk-test" } -# Pins the strum-derived fault labels to the single-source vocabulary. -nexum-world = { path = "../nexum-world" } # The wasi:http client only links on the wasm guest target; host-side # consumers (tests, backtest tooling) compile the `http` module's types diff --git a/crates/nexum-sdk/src/chain/method.rs b/crates/nexum-sdk/src/chain/method.rs deleted file mode 100644 index 58ecfae6..00000000 --- a/crates/nexum-sdk/src/chain/method.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! The typed JSON-RPC method surface, guest side. - -use strum::{EnumString, IntoStaticStr}; - -/// The permitted JSON-RPC read surface as a closed type, mirroring the -/// runtime's `ChainMethod` case for case. Signing and mutating methods -/// have no variant, so they cannot be represented and never cross the -/// WIT edge; [`HostTransport`](super::HostTransport) rejects anything -/// outside this set before calling the host. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumString, IntoStaticStr)] -#[non_exhaustive] -pub enum ChainMethod { - /// `eth_blockNumber`. - #[strum(serialize = "eth_blockNumber")] - EthBlockNumber, - /// `eth_call`. - #[strum(serialize = "eth_call")] - EthCall, - /// `eth_chainId`. - #[strum(serialize = "eth_chainId")] - EthChainId, - /// `eth_estimateGas`. - #[strum(serialize = "eth_estimateGas")] - EthEstimateGas, - /// `eth_feeHistory`. - #[strum(serialize = "eth_feeHistory")] - EthFeeHistory, - /// `eth_gasPrice`. - #[strum(serialize = "eth_gasPrice")] - EthGasPrice, - /// `eth_maxPriorityFeePerGas`. - #[strum(serialize = "eth_maxPriorityFeePerGas")] - EthMaxPriorityFeePerGas, - /// `eth_getBalance`. - #[strum(serialize = "eth_getBalance")] - EthGetBalance, - /// `eth_getBlockByHash`. - #[strum(serialize = "eth_getBlockByHash")] - EthGetBlockByHash, - /// `eth_getBlockByNumber`. - #[strum(serialize = "eth_getBlockByNumber")] - EthGetBlockByNumber, - /// `eth_getBlockReceipts`. - #[strum(serialize = "eth_getBlockReceipts")] - EthGetBlockReceipts, - /// `eth_getCode`. - #[strum(serialize = "eth_getCode")] - EthGetCode, - /// `eth_getLogs`. - #[strum(serialize = "eth_getLogs")] - EthGetLogs, - /// `eth_getProof`. - #[strum(serialize = "eth_getProof")] - EthGetProof, - /// `eth_getStorageAt`. - #[strum(serialize = "eth_getStorageAt")] - EthGetStorageAt, - /// `eth_getTransactionByHash`. - #[strum(serialize = "eth_getTransactionByHash")] - EthGetTransactionByHash, - /// `eth_getTransactionCount`. - #[strum(serialize = "eth_getTransactionCount")] - EthGetTransactionCount, - /// `eth_getTransactionReceipt`. - #[strum(serialize = "eth_getTransactionReceipt")] - EthGetTransactionReceipt, - /// `net_version`. - #[strum(serialize = "net_version")] - NetVersion, -} - -impl ChainMethod { - /// The wire method name. `&'static` because the set is closed. - pub fn as_str(self) -> &'static str { - self.into() - } -} - -#[cfg(test)] -mod tests { - use super::ChainMethod; - - #[test] - fn read_surface_methods_parse() { - for m in [ - "eth_call", - "eth_blockNumber", - "eth_getBalance", - "eth_getLogs", - "eth_getTransactionReceipt", - "net_version", - ] { - assert!(ChainMethod::try_from(m).is_ok(), "{m} should parse"); - } - } - - #[test] - fn signing_and_mutating_methods_have_no_variant() { - for m in [ - "eth_sign", - "eth_signTransaction", - "eth_sendTransaction", - "eth_sendRawTransaction", - "eth_accounts", - "personal_sign", - "admin_peers", - "debug_traceCall", - "", - ] { - assert!(ChainMethod::try_from(m).is_err(), "{m} must be rejected"); - } - } - - #[test] - fn as_str_round_trips_the_wire_name() { - assert_eq!(ChainMethod::EthCall.as_str(), "eth_call"); - assert_eq!( - ChainMethod::try_from(ChainMethod::EthGetBalance.as_str()), - Ok(ChainMethod::EthGetBalance), - ); - } -} diff --git a/crates/nexum-sdk/src/chain/mod.rs b/crates/nexum-sdk/src/chain/mod.rs index c547f921..dc8ff363 100644 --- a/crates/nexum-sdk/src/chain/mod.rs +++ b/crates/nexum-sdk/src/chain/mod.rs @@ -9,12 +9,13 @@ pub mod chainlink; pub mod eth_call; -pub mod method; pub mod provider; pub mod transport; pub use alloy_chains::Chain; pub use eth_call::{eth_call_params, parse_eth_call_result}; -pub use method::ChainMethod; +/// The read surface is defined once in `nexum-world`; guest and host +/// re-export the same type, so the allowlist cannot drift. +pub use nexum_world::ChainMethod; pub use provider::{ProviderHost, block_on}; pub use transport::HostTransport; diff --git a/crates/nexum-world/src/lib.rs b/crates/nexum-world/src/lib.rs index 9a451c70..4a6aae1e 100644 --- a/crates/nexum-world/src/lib.rs +++ b/crates/nexum-world/src/lib.rs @@ -80,6 +80,85 @@ pub enum FaultLabel { Internal, } +/// The permitted JSON-RPC read surface as a closed type: the single +/// source both the guest allowlist (`nexum_sdk::chain::ChainMethod`) +/// and the host dispatch table (`nexum_runtime::host::component:: +/// ChainMethod`) emit from. Methods that sign or mutate node state have +/// no variant, so a guest-supplied signing method (for example +/// `eth_sign` or `eth_sendTransaction`) cannot be represented and never +/// crosses the WIT edge. This is the structural ceiling; an operator +/// allowlist narrows within it and never widens it. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, EnumString, IntoStaticStr)] +#[non_exhaustive] +pub enum ChainMethod { + /// `eth_blockNumber`. + #[strum(serialize = "eth_blockNumber")] + EthBlockNumber, + /// `eth_call`. + #[strum(serialize = "eth_call")] + EthCall, + /// `eth_chainId`. + #[strum(serialize = "eth_chainId")] + EthChainId, + /// `eth_estimateGas`. + #[strum(serialize = "eth_estimateGas")] + EthEstimateGas, + /// `eth_feeHistory`. + #[strum(serialize = "eth_feeHistory")] + EthFeeHistory, + /// `eth_gasPrice`. + #[strum(serialize = "eth_gasPrice")] + EthGasPrice, + /// `eth_maxPriorityFeePerGas`. + #[strum(serialize = "eth_maxPriorityFeePerGas")] + EthMaxPriorityFeePerGas, + /// `eth_getBalance`. + #[strum(serialize = "eth_getBalance")] + EthGetBalance, + /// `eth_getBlockByHash`. + #[strum(serialize = "eth_getBlockByHash")] + EthGetBlockByHash, + /// `eth_getBlockByNumber`. + #[strum(serialize = "eth_getBlockByNumber")] + EthGetBlockByNumber, + /// `eth_getBlockReceipts`. + #[strum(serialize = "eth_getBlockReceipts")] + EthGetBlockReceipts, + /// `eth_getCode`. + #[strum(serialize = "eth_getCode")] + EthGetCode, + /// `eth_getLogs`. + #[strum(serialize = "eth_getLogs")] + EthGetLogs, + /// `eth_getProof`. + #[strum(serialize = "eth_getProof")] + EthGetProof, + /// `eth_getStorageAt`. + #[strum(serialize = "eth_getStorageAt")] + EthGetStorageAt, + /// `eth_getTransactionByHash`. + #[strum(serialize = "eth_getTransactionByHash")] + EthGetTransactionByHash, + /// `eth_getTransactionCount`. + #[strum(serialize = "eth_getTransactionCount")] + EthGetTransactionCount, + /// `eth_getTransactionReceipt`. + #[strum(serialize = "eth_getTransactionReceipt")] + EthGetTransactionReceipt, + /// `net_version`. + #[strum(serialize = "net_version")] + NetVersion, +} + +impl ChainMethod { + /// The wire method name. `&'static` because the permitted set is + /// closed, so the name drops straight into alloy's + /// `Cow<'static, str>` method slot without allocating. + pub fn as_str(self) -> &'static str { + self.into() + } +} + /// One manifest capability and its world wiring. pub struct Capability { /// The name declared under `[capabilities].required` / `optional`. @@ -804,4 +883,47 @@ allow = [] assert!(err.contains("`pkg` WIT package")); assert!(err.contains("wit/deps/pkg")); } + + #[test] + fn read_surface_methods_parse() { + for m in [ + "eth_call", + "eth_blockNumber", + "eth_getBalance", + "eth_getLogs", + "eth_getTransactionReceipt", + "net_version", + ] { + assert!(ChainMethod::try_from(m).is_ok(), "{m} should parse"); + } + } + + #[test] + fn signing_and_mutating_methods_have_no_variant() { + for m in [ + "eth_sign", + "eth_signTransaction", + "eth_sendTransaction", + "eth_sendRawTransaction", + "eth_accounts", + "personal_sign", + "personal_unlockAccount", + "admin_peers", + "debug_traceCall", + "miner_start", + "eth_notAMethod", + "", + ] { + assert!(ChainMethod::try_from(m).is_err(), "{m} must be rejected"); + } + } + + #[test] + fn as_str_round_trips_the_wire_name() { + assert_eq!(ChainMethod::EthCall.as_str(), "eth_call"); + assert_eq!( + ChainMethod::try_from(ChainMethod::EthGetBalance.as_str()), + Ok(ChainMethod::EthGetBalance), + ); + } }