From 0871b85df91e6c914592d3b8a2aa756dfa2f9a54 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sun, 23 Aug 2026 13:15:42 +0200 Subject: [PATCH] v0.2.8-pre.002 --- Cargo.toml | 4 +- crates/ksp-onchain-transport-lib/src/lib.rs | 12 +- .../src/ws_protocol_session.rs | 182 ++++++++++++ .../src/ws_session.rs | 25 +- .../src/ws_settings.rs | 9 +- .../tests/public_api.rs | 24 +- .../tests/release_completeness.rs | 28 +- .../unit_tests/ws_protocol_session.rs | 94 ++++++ .../unit_tests/ws_settings.rs | 6 +- deltas/0.2.8/pre.002.md | 278 ++++++++++++++++++ ...0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md | 73 ++++- ...011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md | 82 ++++-- 12 files changed, 767 insertions(+), 50 deletions(-) create mode 100644 crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs create mode 100644 crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs create mode 100644 deltas/0.2.8/pre.002.md diff --git a/Cargo.toml b/Cargo.toml index e92712b..a46ee0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 217 +# version: 218 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.2.8-pre.1" +version = "0.2.8-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs index 8af0367..7df406c 100644 --- a/crates/ksp-onchain-transport-lib/src/lib.rs +++ b/crates/ksp-onchain-transport-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/lib.rs -// version: 29 +// version: 30 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -24,7 +24,8 @@ //! routing. `0.2.7-pre.007` adds finite reconnect, deterministic resubscribe and continuity-gap tracking. `0.2.7-pre.008` makes per-subscription notification //! backpressure terminal and observable, preserves safe terminal error codes, performs best-effort remote cleanup and proves bounded capacity reuse. //! `0.2.7-pre.009` opens the first stable typed WebSocket wrappers for account, program-account and transaction-log subscriptions without exposing a raw -//! provider-extension subscription API. +//! provider-extension subscription API. `0.2.8-pre.002` adds a Helius LaserStream WebSocket protocol discriminator and two typed protocol facades while +//! keeping the `WsSession` actor/socket implementation unique and the historical generic constructor standard-only. mod client; mod constants; @@ -47,6 +48,7 @@ mod ws_accounts; mod ws_blocks; mod ws_cluster; mod ws_lifecycle; +mod ws_protocol_session; mod ws_session; mod ws_settings; mod ws_subscription; @@ -350,7 +352,11 @@ pub use self::ws_lifecycle::WsSubscriptionKind; pub use self::ws_lifecycle::WsSubscriptionSnapshot; /// Observable lifecycle state of one logical WebSocket subscription. pub use self::ws_lifecycle::WsSubscriptionState; -/// Shareable handle for one explicitly created physical WebSocket session. +/// Typed facade for one Helius LaserStream WebSocket physical session. +pub use self::ws_protocol_session::HeliusLaserStreamWsSession; +/// Typed facade for one standard Solana WebSocket physical session. +pub use self::ws_protocol_session::SolanaStandardWsSession; +/// Shareable compatibility handle for one explicitly created standard Solana physical WebSocket session. pub use self::ws_session::WsSession; /// Open cluster or network descriptor used by WebSocket endpoint settings. pub use self::ws_settings::WsClusterName; diff --git a/crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs b/crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs new file mode 100644 index 0000000..e4b3b53 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs @@ -0,0 +1,182 @@ +// file: crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs +// version: 1 + +/// Typed facade for one standard Solana WebSocket physical session. +/// +/// The facade delegates to the same [`crate::WsSession`] actor used by the compatibility API. It owns no socket, registry, reconnect loop or queue of its +/// own and therefore does not duplicate the physical WebSocket runtime. +#[derive(Clone)] +pub struct SolanaStandardWsSession { + inner: crate::WsSession, +} + +impl SolanaStandardWsSession { + /// Opens one standard Solana WebSocket session through the shared physical actor. + pub async fn connect(endpoint: crate::WsEndpointSettings) -> ksp_core_lib::Result { + let connected = crate::WsSession::connect_for_protocol(endpoint, crate::WsProtocolKind::SolanaStandard).await; + return match connected { + std::result::Result::Ok(inner) => std::result::Result::Ok(Self { inner }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + + /// Returns the stable local session identity. + #[must_use] + pub const fn id(&self) -> crate::WsSessionId { + return self.inner.id(); + } + + /// Returns the latest safe runtime snapshot published by the shared actor. + #[must_use] + pub fn snapshot(&self) -> crate::WsSessionSnapshot { + return self.inner.snapshot(); + } + + /// Returns the latest observable physical-session state. + #[must_use] + pub fn state(&self) -> crate::WsSessionState { + return self.inner.state(); + } + + /// Explicitly closes the shared physical session under its configured close timeout. + pub async fn close(&self) -> ksp_core_lib::Result<()> { + return self.inner.close().await; + } + + /// Subscribes to changes for one Solana account through standard `accountSubscribe`. + pub async fn account_subscribe( + &self, + account: &ksp_core_lib::Pubkey, + config: std::option::Option<&crate::SolanaAccountSubscribeConfig>, + ) -> ksp_core_lib::Result>> { + return self.inner.account_subscribe(account, config).await; + } + + /// Subscribes to unstable standard Solana block notifications through `blockSubscribe`. + pub async fn block_subscribe( + &self, + filter: &crate::SolanaBlockSubscribeFilter, + config: std::option::Option<&crate::SolanaBlockSubscribeConfig>, + ) -> ksp_core_lib::Result>> { + return self.inner.block_subscribe(filter, config).await; + } + + /// Subscribes to Solana transaction logs through standard `logsSubscribe`. + pub async fn logs_subscribe( + &self, + filter: &crate::SolanaLogsSubscribeFilter, + config: std::option::Option<&crate::SolanaCommitmentConfig>, + ) -> ksp_core_lib::Result>> { + return self.inner.logs_subscribe(filter, config).await; + } + + /// Subscribes to account changes owned by one Solana program through standard `programSubscribe`. + pub async fn program_subscribe( + &self, + program_id: &ksp_core_lib::Pubkey, + config: std::option::Option<&crate::SolanaProgramSubscribeConfig>, + ) -> ksp_core_lib::Result> { + return self.inner.program_subscribe(program_id, config).await; + } + + /// Subscribes to standard Solana root-slot notifications through `rootSubscribe`. + pub async fn root_subscribe(&self) -> ksp_core_lib::Result> { + return self.inner.root_subscribe().await; + } + + /// Subscribes to one Solana transaction signature through standard `signatureSubscribe`. + pub async fn signature_subscribe( + &self, + signature: &str, + config: std::option::Option<&crate::SolanaSignatureSubscribeConfig>, + ) -> ksp_core_lib::Result>> { + return self.inner.signature_subscribe(signature, config).await; + } + + /// Subscribes to standard Solana slot-processing notifications through `slotSubscribe`. + pub async fn slot_subscribe(&self) -> ksp_core_lib::Result> { + return self.inner.slot_subscribe().await; + } + + /// Subscribes to unstable standard Solana slot-lifecycle notifications through `slotsUpdatesSubscribe`. + pub async fn slots_updates_subscribe(&self) -> ksp_core_lib::Result> { + return self.inner.slots_updates_subscribe().await; + } + + /// Subscribes to unstable pre-consensus gossip vote notifications through `voteSubscribe`. + pub async fn vote_subscribe(&self) -> ksp_core_lib::Result> { + return self.inner.vote_subscribe().await; + } +} + +impl std::fmt::Debug for SolanaStandardWsSession { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.debug_struct("SolanaStandardWsSession").field("id", &self.id()).field("snapshot", &self.snapshot()).finish(); + } +} + +/// Typed facade for one Helius LaserStream WebSocket physical session. +/// +/// `0.2.8-pre.002` intentionally exposes only physical-session lifecycle through this facade. The six Helius-supported standard subscription wrappers are +/// added in the next tranche after exact delegation and API-absence canaries are established. No public inner handle is exposed, so callers cannot bypass +/// the provider-specific surface by recovering a generic [`crate::WsSession`]. +/// +/// ```compile_fail +/// async fn unsupported(session: &ksp_onchain_transport_lib::HeliusLaserStreamWsSession) { +/// let _ = session.block_subscribe().await; +/// } +/// ``` +/// +/// ```compile_fail +/// fn no_escape_hatch(session: ksp_onchain_transport_lib::HeliusLaserStreamWsSession) { +/// let _ = session.into_inner(); +/// } +/// ``` +#[derive(Clone)] +pub struct HeliusLaserStreamWsSession { + inner: crate::WsSession, +} + +impl HeliusLaserStreamWsSession { + /// Opens one Helius LaserStream WebSocket session through the shared physical actor. + pub async fn connect(endpoint: crate::WsEndpointSettings) -> ksp_core_lib::Result { + let connected = crate::WsSession::connect_for_protocol(endpoint, crate::WsProtocolKind::HeliusLaserStream).await; + return match connected { + std::result::Result::Ok(inner) => std::result::Result::Ok(Self { inner }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + + /// Returns the stable local session identity. + #[must_use] + pub const fn id(&self) -> crate::WsSessionId { + return self.inner.id(); + } + + /// Returns the latest safe runtime snapshot published by the shared actor. + #[must_use] + pub fn snapshot(&self) -> crate::WsSessionSnapshot { + return self.inner.snapshot(); + } + + /// Returns the latest observable physical-session state. + #[must_use] + pub fn state(&self) -> crate::WsSessionState { + return self.inner.state(); + } + + /// Explicitly closes the shared physical session under its configured close timeout. + pub async fn close(&self) -> ksp_core_lib::Result<()> { + return self.inner.close().await; + } +} + +impl std::fmt::Debug for HeliusLaserStreamWsSession { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.debug_struct("HeliusLaserStreamWsSession").field("id", &self.id()).field("snapshot", &self.snapshot()).finish(); + } +} + +#[cfg(test)] +#[path = "../unit_tests/ws_protocol_session.rs"] +mod tests; diff --git a/crates/ksp-onchain-transport-lib/src/ws_session.rs b/crates/ksp-onchain-transport-lib/src/ws_session.rs index 1442e15..3e6c06f 100644 --- a/crates/ksp-onchain-transport-lib/src/ws_session.rs +++ b/crates/ksp-onchain-transport-lib/src/ws_session.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/ws_session.rs -// version: 12 +// version: 13 use futures_util::SinkExt; // rust-rules: trait-import use futures_util::StreamExt; // rust-rules: trait-import @@ -24,15 +24,32 @@ pub struct WsSession { } impl WsSession { - /// Opens one physical WebSocket connection for the supplied endpoint settings. + /// Opens one physical standard Solana WebSocket connection for the supplied endpoint settings. /// - /// Calling this function twice with the same endpoint creates two independent physical sessions. The function returns only after the WebSocket - /// handshake succeeds or the configured command timeout expires. + /// This historical constructor remains standard-only after provider-specific protocol kinds are added. Calling this function twice with the same endpoint + /// creates two independent physical sessions. Provider-specific callers must use their typed protocol facade instead of obtaining a generic handle. pub async fn connect(endpoint: crate::WsEndpointSettings) -> ksp_core_lib::Result { + return Self::connect_for_protocol(endpoint, crate::WsProtocolKind::SolanaStandard).await; + } + + /// Opens one physical WebSocket connection after validating the typed facade protocol. + pub(crate) async fn connect_for_protocol(endpoint: crate::WsEndpointSettings, expected_protocol: crate::WsProtocolKind) -> ksp_core_lib::Result { let validation = crate::WsTransportSettings::new(std::vec![endpoint.clone()]).validate(); if let std::result::Result::Err(error) = validation { return std::result::Result::Err(error); } + if endpoint.protocol() != expected_protocol { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket session constructor does not accept the endpoint protocol") + .with_context("field", "ws_endpoints.protocol") + .with_context("expected_protocol", expected_protocol.as_str()) + .with_context("actual_protocol", endpoint.protocol().as_str()), + ); + } + return Self::connect_physical(endpoint).await; + } + + async fn connect_physical(endpoint: crate::WsEndpointSettings) -> ksp_core_lib::Result { let id_result = next_session_id(); let id = match id_result { std::result::Result::Ok(id) => id, diff --git a/crates/ksp-onchain-transport-lib/src/ws_settings.rs b/crates/ksp-onchain-transport-lib/src/ws_settings.rs index df897ff..dcbaa42 100644 --- a/crates/ksp-onchain-transport-lib/src/ws_settings.rs +++ b/crates/ksp-onchain-transport-lib/src/ws_settings.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/ws_settings.rs -// version: 3 +// version: 4 const DEFAULT_WS_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); const DEFAULT_WS_COMMAND_QUEUE_CAPACITY: usize = 128; @@ -119,13 +119,15 @@ impl WsClusterName { /// WebSocket protocol family understood by KSP Transport. /// -/// `0.2.7` exposes only standard Solana WebSocket. The non-exhaustive contract allows later provider-specific families without changing the common endpoint -/// container or injecting provider-only options into [`WsSessionSettings`]. +/// The protocol discriminator belongs specifically to the WebSocket runtime. Provider products using another transport, including a future Helius +/// LaserStream gRPC backend, require a distinct transport-owned descriptor instead of reusing this enum. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum WsProtocolKind { /// Standard Solana JSON-RPC WebSocket PubSub. SolanaStandard, + /// Helius LaserStream WebSocket protocol surface. + HeliusLaserStream, } impl WsProtocolKind { @@ -134,6 +136,7 @@ impl WsProtocolKind { pub const fn as_str(self) -> &'static str { return match self { Self::SolanaStandard => "solana_standard", + Self::HeliusLaserStream => "helius_laserstream", }; } } diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index 46f0d92..5b1128a 100644 --- a/crates/ksp-onchain-transport-lib/tests/public_api.rs +++ b/crates/ksp-onchain-transport-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/public_api.rs -// version: 33 +// version: 34 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -676,3 +676,25 @@ fn public_v0_2_7_pre_012_complete_standard_websocket_surface_is_available_from_c ]; assert_eq!(kinds.len(), 9); } + +#[test] +fn public_v0_2_8_pre_002_protocol_facades_are_available_without_replacing_the_standard_session_contract() { + assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard.as_str(), "solana_standard"); + assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream"); + let _historical_connect = ksp_onchain_transport_lib::WsSession::connect; + let _standard_connect = ksp_onchain_transport_lib::SolanaStandardWsSession::connect; + let _standard_close = ksp_onchain_transport_lib::SolanaStandardWsSession::close; + let _standard_snapshot = ksp_onchain_transport_lib::SolanaStandardWsSession::snapshot; + let _standard_account = ksp_onchain_transport_lib::SolanaStandardWsSession::account_subscribe; + let _standard_block = ksp_onchain_transport_lib::SolanaStandardWsSession::block_subscribe; + let _standard_logs = ksp_onchain_transport_lib::SolanaStandardWsSession::logs_subscribe; + let _standard_program = ksp_onchain_transport_lib::SolanaStandardWsSession::program_subscribe; + let _standard_root = ksp_onchain_transport_lib::SolanaStandardWsSession::root_subscribe; + let _standard_signature = ksp_onchain_transport_lib::SolanaStandardWsSession::signature_subscribe; + let _standard_slot = ksp_onchain_transport_lib::SolanaStandardWsSession::slot_subscribe; + let _standard_slots_updates = ksp_onchain_transport_lib::SolanaStandardWsSession::slots_updates_subscribe; + let _standard_vote = ksp_onchain_transport_lib::SolanaStandardWsSession::vote_subscribe; + let _helius_connect = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect; + let _helius_close = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::close; + let _helius_snapshot = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::snapshot; +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index d502a0b..6911e3e 100644 --- a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs +++ b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs -// version: 24 +// version: 25 //! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage. @@ -769,3 +769,29 @@ fn release_v0_2_7_pre_012_http_inventory_remains_52_current_plus_14_historical() assert!(current.iter().all(|descriptor| return descriptor.runtime_status() == ksp_onchain_transport_lib::RpcRuntimeStatus::Supported)); assert!(historical.iter().all(|descriptor| return descriptor.runtime_status() == ksp_onchain_transport_lib::RpcRuntimeStatus::Removed)); } + +#[test] +fn release_v0_2_8_pre_002_protocol_facades_preserve_the_standard_partition() { + assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard.as_str(), "solana_standard"); + assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream"); + let standard_kinds = [ + ksp_onchain_transport_lib::WsSubscriptionKind::Account, + ksp_onchain_transport_lib::WsSubscriptionKind::Block, + ksp_onchain_transport_lib::WsSubscriptionKind::Logs, + ksp_onchain_transport_lib::WsSubscriptionKind::Program, + ksp_onchain_transport_lib::WsSubscriptionKind::Root, + ksp_onchain_transport_lib::WsSubscriptionKind::Signature, + ksp_onchain_transport_lib::WsSubscriptionKind::Slot, + ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates, + ksp_onchain_transport_lib::WsSubscriptionKind::Vote, + ]; + assert_eq!(standard_kinds.len(), 9); + assert_eq!( + std::any::type_name::().rsplit("::").next(), + std::option::Option::Some("SolanaStandardWsSession") + ); + assert_eq!( + std::any::type_name::().rsplit("::").next(), + std::option::Option::Some("HeliusLaserStreamWsSession") + ); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs b/crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs new file mode 100644 index 0000000..834d208 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs @@ -0,0 +1,94 @@ +// file: crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs +// version: 1 + +use futures_util::StreamExt; // rust-rules: trait-import + +fn endpoint(url: &str, protocol: crate::WsProtocolKind) -> crate::WsEndpointSettings { + return crate::WsEndpointSettings::new( + "local_protocol_fixture", + true, + crate::WsProviderName::new("local-fixture"), + crate::WsClusterName::new("local"), + protocol, + crate::WsEndpointUrl::parse(url).expect("local WebSocket URL must parse"), + crate::WsSessionSettings::default(), + ); +} + +async fn bind_local_listener() -> (tokio::net::TcpListener, std::string::String) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("local listener must bind"); + let address = listener.local_addr().expect("local listener must expose address"); + return (listener, format!("ws://{address}")); +} + +async fn accept_until_close(listener: tokio::net::TcpListener) { + let (stream, _) = listener.accept().await.expect("local peer must accept connection"); + let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed"); + while let std::option::Option::Some(message) = websocket.next().await { + let message = message.expect("local peer message must decode"); + if message.is_close() { + return; + } + } +} + +#[tokio::test] +async fn protocol_facades_share_the_existing_physical_session_path() { + let (standard_listener, standard_url) = bind_local_listener().await; + let standard_server = tokio::spawn(accept_until_close(standard_listener)); + let standard = crate::SolanaStandardWsSession::connect(endpoint(standard_url.as_str(), crate::WsProtocolKind::SolanaStandard)) + .await + .expect("standard facade must connect"); + assert_eq!(standard.snapshot().protocol(), crate::WsProtocolKind::SolanaStandard); + standard.close().await.expect("standard facade must close"); + standard_server.await.expect("standard peer task must finish"); + let (helius_listener, helius_url) = bind_local_listener().await; + let helius_server = tokio::spawn(accept_until_close(helius_listener)); + let helius_url = format!("{helius_url}/?api-key=SECRET-CANARY"); + let helius = crate::HeliusLaserStreamWsSession::connect(endpoint(helius_url.as_str(), crate::WsProtocolKind::HeliusLaserStream)) + .await + .expect("Helius facade must connect"); + assert_eq!(helius.snapshot().protocol(), crate::WsProtocolKind::HeliusLaserStream); + let rendered = format!("{helius:?}"); + assert!(!rendered.contains("SECRET-CANARY")); + assert!(!rendered.contains(helius_url.as_str())); + helius.close().await.expect("Helius facade must close"); + helius_server.await.expect("Helius peer task must finish"); +} + +#[tokio::test] +async fn historical_generic_constructor_remains_standard_only_before_network_io() { + let endpoint = endpoint("ws://127.0.0.1:9", crate::WsProtocolKind::HeliusLaserStream); + let error = crate::WsSession::connect(endpoint).await.expect_err("generic historical constructor must reject Helius protocol"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS); + assert_eq!( + error.context().iter().find(|entry| entry.key() == "expected_protocol").map(|entry| entry.value()), + std::option::Option::Some("solana_standard") + ); + assert_eq!( + error.context().iter().find(|entry| entry.key() == "actual_protocol").map(|entry| entry.value()), + std::option::Option::Some("helius_laserstream") + ); +} + +#[tokio::test] +async fn typed_facades_reject_protocol_mismatch_before_network_io() { + let helius_error = crate::HeliusLaserStreamWsSession::connect(endpoint("ws://127.0.0.1:9", crate::WsProtocolKind::SolanaStandard)) + .await + .expect_err("Helius facade must reject standard endpoint"); + assert_eq!(helius_error.code(), crate::ERROR_CODE_INVALID_SETTINGS); + let standard_error = crate::SolanaStandardWsSession::connect(endpoint("ws://127.0.0.1:9", crate::WsProtocolKind::HeliusLaserStream)) + .await + .expect_err("standard facade must reject Helius endpoint"); + assert_eq!(standard_error.code(), crate::ERROR_CODE_INVALID_SETTINGS); +} + +#[test] +fn protocol_facades_define_no_second_actor_socket_or_public_inner_escape_hatch() { + let source = include_str!("../src/ws_protocol_session.rs"); + assert!(!source.contains("tokio::spawn")); + assert!(!source.contains("tokio_tungstenite")); + assert!(!source.contains("WsSessionCommand")); + assert!(!source.contains("pub fn inner(")); + assert!(!source.contains("pub fn into_inner(")); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs b/crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs index 6197db4..505b1c4 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs -// version: 1 +// version: 2 fn valid_endpoint(name: &str, url_text: &str) -> crate::WsEndpointSettings { return crate::WsEndpointSettings::new( @@ -47,8 +47,10 @@ fn websocket_endpoint_url_errors_do_not_echo_sensitive_url() { } #[test] -fn websocket_protocol_kind_is_extensible_but_only_standard_is_available_now() { +fn websocket_protocol_kind_distinguishes_standard_and_helius_laserstream_websocket() { assert_eq!(crate::WsProtocolKind::SolanaStandard.as_str(), "solana_standard"); + assert_eq!(crate::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream"); + assert_ne!(crate::WsProtocolKind::SolanaStandard, crate::WsProtocolKind::HeliusLaserStream); } #[test] diff --git a/deltas/0.2.8/pre.002.md b/deltas/0.2.8/pre.002.md new file mode 100644 index 0000000..f4e7a34 --- /dev/null +++ b/deltas/0.2.8/pre.002.md @@ -0,0 +1,278 @@ + + + +# Delta `0.2.8-pre.002` — Socle protocolaire et façades WebSocket + +## 1. Objet + +Cette tranche matérialise le socle protocolaire décidé par `pre.001-fix.001`/`fix.002` sans commencer Config Helius, `transactionSubscribe` ni le heartbeat provider. + +Version workspace : + +```text +0.2.8-pre.2 +``` + +Livraison : + +```text +0.2.8-pre.002 +``` + +Commit attendu : + +```text +v0.2.8-pre.002 +``` + +Aucun tag prerelease. + +## 2. Protocol kind WebSocket + +`WsProtocolKind` contient désormais : + +```text +SolanaStandard -> solana_standard +HeliusLaserStream -> helius_laserstream +``` + +Le nom `HeliusLaserStream` appartient explicitement au namespace WebSocket. Cette tranche n'ajoute aucun type, discriminateur ou backend LaserStream gRPC. + +## 3. Deux façades, un seul moteur physique + +Nouvelles façades publiques : + +```text +SolanaStandardWsSession +HeliusLaserStreamWsSession +``` + +Elles contiennent un `WsSession` privé et délèguent toutes les opérations physiques au moteur acquis en `0.2.7`. + +Le chemin partagé est : + +```text +facade::connect + -> WsSession::connect_for_protocol # crate-internal guard + -> WsSession::connect_physical # unique physical constructor + -> tokio::spawn(run_ws_session_actor) +``` + +Aucun second : + +```text +socket type +actor +WsSessionCommand +pending registry +subscription registry +reconnect loop +backpressure path +shutdown path +snapshot model +``` + +n'est créé. + +## 4. Compatibilité `WsSession` historique + +`WsSession::connect` reste public pour les consommateurs `0.2.7`, mais devient explicitement : + +```text +standard-only +``` + +Un endpoint `WsProtocolKind::HeliusLaserStream` présenté à ce constructeur est rejeté avant toute I/O avec : + +```text +ERROR_CODE_INVALID_SETTINGS +field = ws_endpoints.protocol +expected_protocol = solana_standard +actual_protocol = helius_laserstream +``` + +Ces contextes sont des descriptors sûrs ; aucune URL ou credential n'est copiée. + +La façade Helius n'expose ni `inner()` ni `into_inner()` et ne permet donc pas de récupérer un `WsSession` générique afin de contourner sa surface provider-specific. + +## 5. Surface standard dans la nouvelle façade + +`SolanaStandardWsSession` délègue immédiatement les neuf wrappers standard acquis, sans dupliquer leur wire ou leurs decoders : + +```text +account_subscribe +block_subscribe +logs_subscribe +program_subscribe +root_subscribe +signature_subscribe +slot_subscribe +slots_updates_subscribe +vote_subscribe +``` + +Les méthodes historiques correspondantes restent aussi disponibles sur `WsSession` pour compatibilité. + +## 6. Surface Helius volontairement minimale dans `pre.002` + +`HeliusLaserStreamWsSession` expose uniquement : + +```text +connect +id +snapshot +state +close +``` + +Elle n'expose encore aucune subscription. Cela garde la tranche sur le socle et réserve à `pre.003` l'ajout contrôlé des six familles Helius documentées comme compatibles avec le wire standard : + +```text +account +logs +program +root +signature +slot +``` + +`block`, `slotsUpdates` et `vote` restent absents. Des rustdocs `compile_fail` verrouillent dès cette tranche l'absence de `block_subscribe` et de `into_inner`. + +## 7. Tests et canaries ajoutés + +Unitaires Transport : + +```text +WsProtocolKind expose deux descriptors distincts +les deux façades ouvrent et ferment un WebSocket contre un peer local +snapshot de chaque façade conserve le protocol kind attendu +WsSession::connect rejette Helius avant I/O +chaque façade rejette le mauvais protocol kind avant I/O +Debug Helius ne projette pas une api-key canary présente dans l'URL +module de façade sans second tokio::spawn / tokio_tungstenite / WsSessionCommand +module de façade sans getter inner/into_inner public +``` + +Public API : + +```text +WsProtocolKind::HeliusLaserStream visible au crate-root +SolanaStandardWsSession visible au crate-root +HeliusLaserStreamWsSession visible au crate-root +9 wrappers standard accessibles via SolanaStandardWsSession +release-completeness conserve les 9 kinds standard et les deux descripteurs protocolaires +WsSession historique toujours visible +``` + +Les tests `compile_fail` de la rustdoc couvrent : + +```text +HeliusLaserStreamWsSession::block_subscribe absent +HeliusLaserStreamWsSession::into_inner absent +``` + +## 8. Hors périmètre préservé + +Cette tranche ne modifie pas : + +```text +ksp-config-lib +config/std.transport.json +config/schemas/std.transport.schema.json +.env.example +WsSubscriptionKind +transactionSubscribe / transactionUnsubscribe +notification transaction Helius +heartbeat/idle timer +HTTP 52/14 +Store / Program / Wallet +``` + +Aucune dépendance Rust n'est ajoutée. + +## 9. Fichiers de la livraison + +Nouveaux : + +```text +crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs +crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs +deltas/0.2.8/pre.002.md +``` + +Modifiés : + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/src/ws_session.rs +crates/ksp-onchain-transport-lib/src/ws_settings.rs +crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs +crates/ksp-onchain-transport-lib/tests/public_api.rs +crates/ksp-onchain-transport-lib/tests/release_completeness.rs +docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md +docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md +``` + +## 10. Validation exécutée dans le sandbox de préparation + +Exécuté après modification : + +```text +python3 scripts/audit_rust_workspace_rules.py +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +KSP workspace Rust rule audit: clean +``` + +Le sandbox de préparation ne fournit pas `cargo`/`rustfmt`. Les commandes compilées ne sont donc pas déclarées réussies ici. + +## 11. Gates opérateur avant commit + +Après application de l'overlay : + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +``` + +Puis, si le ciblé est vert : + +```bash +cargo test --workspace +``` + +Points à surveiller spécifiquement dans la sortie : + +```text +les deux doctests compile_fail doivent réussir +aucun warning missing_docs/unreachable_pub +aucune régression des 309+ tests Transport hérités +aucune régression Config causée par l'ajout de la variante non_exhaustive +``` + +## 12. Suite + +`0.2.8-pre.003` doit ajouter uniquement sur `HeliusLaserStreamWsSession` : + +```text +account_subscribe +logs_subscribe +program_subscribe +root_subscribe +signature_subscribe +slot_subscribe +``` + +avec réutilisation exacte des DTOs/wire standard, puis prouver durablement que : + +```text +block_subscribe absent +slots_updates_subscribe absent +vote_subscribe absent +``` + +La tranche `pre.003` ne doit toujours pas commencer Config Helius ni `transactionSubscribe`. diff --git a/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md b/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md index b6cdd50..31cf93a 100644 --- a/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md +++ b/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md @@ -1,9 +1,9 @@ - + # Plan `0.2.8` — Helius LaserStream WebSocket -> **Statut : gate `0.2.8-pre.001` positif, architecture rectifiée par `pre.001-fix.001` puis planification/nomenclature normalisées par `pre.001-fix.002`.** Le moteur physique `WsSession` de `0.2.7` reste unique et partagé ; les surfaces publiques sont séparées entre `SolanaStandardWsSession` et `HeliusLaserStreamWsSession`. Le forecast reprend le format compact des plans KSP précédents et les corrections `fix.*` sont regroupées sous leur prerelease d'origine. Les deux fixes sont documentaires et conservent `workspace.package.version = 0.2.8-pre.1`. +> **Statut : `0.2.8-pre.002` matérialise le socle protocolaire décidé par le gate `pre.001`.** Le moteur physique `WsSession` de `0.2.7` reste unique et partagé ; `SolanaStandardWsSession` devient la façade standard explicite et `HeliusLaserStreamWsSession` ouvre le lifecycle Helius LaserStream WebSocket sans encore exposer ses subscriptions. Le constructeur historique `WsSession::connect` reste standard-only et les six wrappers Helius communs restent réservés à `pre.003`. ## 1. Objet, base et état du gate @@ -32,9 +32,10 @@ Correctifs documentaires appliqués à `pre.001` : 0.2.8-pre.001-fix.001 séparation des façades protocolaires + fermeture du gate Cargo 0.2.8-pre.001-fix.002 normalisation du forecast + clarification LaserStream WebSocket/gRPC -livraison courante = 0.2.8-pre.001-fix.002 -workspace.package.version = 0.2.8-pre.1 # inchangé : documentation uniquement -commit attendu = v0.2.8-pre.001-fix.002 +livraison précédente = 0.2.8-pre.001-fix.002 +livraison courante = 0.2.8-pre.002 +workspace.package.version = 0.2.8-pre.2 +commit attendu = v0.2.8-pre.002 aucun tag prerelease ``` @@ -47,7 +48,7 @@ cargo tree -p ksp-onchain-transport-lib --duplicates exécuté Constat : aucune dépendance Helius/SDK ajoutée ; les doublons visibles sont transitifs (`syn` 2/3 et `webpki-roots` 0.26/1.0) et ne constituent pas une anomalie bloquante de `pre.001`. -**Verdict courant : gate `pre.001` positif.** `pre.002` peut commencer après commit de ce fix. +**Verdict courant : gate `pre.001` positif ; socle `pre.002` implémenté dans la présente livraison.** Les validations compilées restent à exécuter par l'opérateur avant commit. ## 2. Décisions structurantes de `pre.001-fix.001` / `fix.002` — séparer les surfaces, qualifier le protocole @@ -117,9 +118,9 @@ vote Les 18 opérations standard restent donc présentes et testées : 9 subscribe + 9 unsubscribe. -### 2.2 Surface `HeliusLaserStreamWsSession` +### 2.2 Surface cible `HeliusLaserStreamWsSession` -La façade Helius expose uniquement : +À partir de `pre.003`, la façade Helius doit exposer uniquement les familles de subscription suivantes : ```text account @@ -131,7 +132,7 @@ slot transaction ``` -Elle **n'expose pas** : +Même après `pre.003`, elle **n'expose pas** : ```text block @@ -214,6 +215,48 @@ le registry/actor interne reste unique et provider-neutral Si `pre.002` montre qu'un descriptor interne distinct rend le snapshot plus propre sans rupture publique, cette forme peut être retenue. Aucun refactor ne doit toutefois remplacer une séparation de façade simple par une hiérarchie générique lourde. +### 2.6 Réalisation de `pre.002` + +Le socle concret retient une séparation minimale sans dupliquer les wrappers wire ni l'actor : + +```text +WsSession::connect(endpoint) + -> accepte uniquement WsProtocolKind::SolanaStandard + -> compatibilité publique 0.2.7 conservée + +SolanaStandardWsSession::connect(endpoint) + -> guard SolanaStandard + -> WsSession::connect_for_protocol(...) interne + -> même connect_physical / même run_ws_session_actor + -> délègue les 9 wrappers standard existants + +HeliusLaserStreamWsSession::connect(endpoint) + -> guard HeliusLaserStream + -> WsSession::connect_for_protocol(...) interne + -> même connect_physical / même run_ws_session_actor + -> lifecycle seulement en pre.002 + -> aucune subscription Helius exposée avant pre.003 +``` + +`WsProtocolKind` possède désormais : + +```text +SolanaStandard -> solana_standard +HeliusLaserStream -> helius_laserstream +``` + +Les mismatches de constructeur sont rejetés avec `ERROR_CODE_INVALID_SETTINGS` et des contextes sûrs `expected_protocol` / `actual_protocol`, avant toute tentative réseau. Aucune URL ni credential n'entre dans cette erreur. + +La façade Helius conserve son `WsSession` interne privé et ne fournit ni `inner()` ni `into_inner()`. Des canaries `compile_fail` verrouillent l'absence de `block_subscribe` et de l'escape hatch à ce stade. Une canary source confirme aussi que le module de façade ne contient ni second `tokio::spawn`, ni `tokio_tungstenite`, ni `WsSessionCommand`. + +La façade standard délègue déjà les neuf familles existantes sans recopier leurs encoders/decoders : + +```text +account block logs program root signature slot slotsUpdates vote +``` + +La façade Helius ne recevra les six délégations communes qu'en `pre.003`, afin de garder la tranche actuelle limitée au socle protocolaire et aux guards. + ## 3. Forecast souple recalibré Le forecast suit le format historique des plans KSP `0.2.5` et `0.2.7` : un bloc compact, directement révisable, qui porte aussi l'état courant des tranches. Les `fix.*` sont rattachés à leur prerelease d'origine au lieu d'être présentés comme des tranches planifiées autonomes. @@ -223,7 +266,7 @@ pre.001 DONE — audit Helius actuel + matrice + architecture + threat model + fix.001 DONE — séparation des façades standard/Helius + moteur unique + gate Cargo fermé fix.002 DONE — forecast normalisé + namespace LaserStream WebSocket/gRPC clarifié -pre.002 socle protocolaire : WsProtocolKind::HeliusLaserStream + façades standard/Helius +pre.002 PREPARED — socle protocolaire : WsProtocolKind::HeliusLaserStream + façades standard/Helius + connexion physique partagée + guards, sans duplication de l'actor pre.003 surface Helius standard supportée : account/logs/program/root/signature/slot + absence typée de block/slotsUpdates/vote sur Helius + non-régression standard 9/9 @@ -831,7 +874,7 @@ validation finale fermée prompt 0.2.9 prêt ``` -## 16. Verdict après `pre.001-fix.002` +## 16. Verdict après `pre.002` Décisions fermées : @@ -856,6 +899,12 @@ credential URL Secret résolue par Config new dependency aucune historical/lossless replay aucune promesse forecast format compact KSP, fixes groupés, pre.001 -> pre.011 +pre.002 protocol kind matérialisé : SolanaStandard + HeliusLaserStream +pre.002 standard facade 9 wrappers existants délégués, aucun wire dupliqué +pre.002 Helius facade lifecycle seulement, inner privé +pre.002 generic WsSession constructeur public standard-only +pre.002 physical connection chemin unique partagé, aucun second actor +pre.002 Config/transaction/heartbeat non commencés conformément au forecast ``` -**Gate `pre.001` positif. Prochaine tranche : `0.2.8-pre.002` — socle protocolaire et façades séparées.** +**`pre.002` est préparé ; après validations opérateur vertes, la prochaine tranche est `0.2.8-pre.003` — six familles standard supportées sur la façade Helius et absence typée durable des trois familles non supportées.** diff --git a/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md b/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md index d78a555..ade3687 100644 --- a/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md +++ b/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md @@ -1,9 +1,9 @@ - + # Validation `0.2.8` — Helius LaserStream WebSocket -> **Statut : gate `0.2.8-pre.001` positif ; architecture corrigée par `pre.001-fix.001`, forecast et nomenclature clarifiés par `pre.001-fix.002`.** Les graphes Cargo opérateur sont reçus et non bloquants. La validation runtime doit prouver deux façades protocolaires distinctes partageant un seul moteur `WsSession` et préserver une distinction explicite entre Helius LaserStream **WebSocket** et le futur LaserStream gRPC. +> **Statut : `0.2.8-pre.002` implémente le socle des deux façades protocolaires sur le moteur `WsSession` unique.** Le protocol kind Helius LaserStream WebSocket, les guards de constructeur, la compatibilité standard historique et l'absence d'escape hatch Helius sont matérialisés. Les six subscriptions standard supportées par Helius restent volontairement ouvertes pour `pre.003`. ## 1. Références @@ -101,13 +101,13 @@ Verdict : doublons transitifs, aucune nouvelle dépendance Helius, aucune anomal | Critère | Décision | Preuve cible | État | |-------------------------------------|------------------------------------------------------------------|---------------------------------|--------| -| actor physique | un seul `WsSession` actor partagé | source/runtime canary | décidé | -| façade standard | `SolanaStandardWsSession` | public API canary | décidé | -| façade Helius | `HeliusLaserStreamWsSession` | public API canary | décidé | +| actor physique | un seul `WsSession` actor partagé | source/runtime canary | pre.002 implémenté | +| façade standard | `SolanaStandardWsSession` | public API canary | pre.002 implémenté | +| façade Helius | `HeliusLaserStreamWsSession` | public API canary | pre.002 implémenté | | namespace LaserStream WS | `WsProtocolKind` + `ws_endpoints` possèdent `helius_laserstream` | API/Config/docs canary | décidé | | LaserStream gRPC | backend/type/Config distincts, hors `0.2.8` | absence de réutilisation WS | décidé | -| escape hatch Helius | aucun `inner()`/`into_inner()` public | API/source canary | décidé | -| generic Helius `WsSession::connect` | ne doit pas permettre de contourner la façade | invalid protocol pre-I/O canary | décidé | +| escape hatch Helius | aucun `inner()`/`into_inner()` public | compile-fail/source canary | pre.002 implémenté | +| generic Helius `WsSession::connect` | ne doit pas permettre de contourner la façade | invalid protocol pre-I/O canary | pre.002 implémenté | | Helius unsupported | absent de la façade | compile-fail/API absence canary | décidé | | standard Helius commun | délégation vers le même wire/actor | exact fixture | décidé | | DTO duplication | seulement si wire/sémantique divergent | public/source audit | décidé | @@ -159,19 +159,19 @@ aucun payload brut dans logs/snapshots ## 6. Validation lifecycle/provider à construire ```text -[ ] un seul actor/socket implementation path -[ ] SolanaStandardWsSession public et constructible -[ ] HeliusLaserStreamWsSession public et constructible -[ ] `helius_laserstream` reste possédé par le namespace WebSocket -[ ] aucune API/Config gRPC n'est anticipée ou aliasée sur le contrat WS -[ ] WsSession historique standard reste compatible -[ ] Helius facade n'expose pas blockSubscribe -[ ] Helius facade n'expose pas slotsUpdatesSubscribe -[ ] Helius facade n'expose pas voteSubscribe -[ ] SolanaStandard facade n'expose pas transactionSubscribe -[ ] Helius facade n'expose aucun escape hatch vers le handle générique -[ ] protocol mismatch rejeté avant I/O -[ ] 6 familles standard Helius utilisent le wire standard exact +[x] un seul actor/socket implementation path dans le code `pre.002` +[ ] SolanaStandardWsSession public et constructible — canary ajoutée, exécution opérateur requise +[ ] HeliusLaserStreamWsSession public et constructible — canary ajoutée, exécution opérateur requise +[x] `helius_laserstream` reste possédé par le namespace WebSocket +[x] aucune API/Config gRPC n'est anticipée ou aliasée sur le contrat WS +[x] WsSession historique reste public et son constructeur devient explicitement standard-only +[x] Helius facade n'expose pas blockSubscribe en `pre.002` +[x] Helius facade n'expose pas slotsUpdatesSubscribe en `pre.002` +[x] Helius facade n'expose pas voteSubscribe en `pre.002` +[x] SolanaStandard facade n'expose pas transactionSubscribe +[x] Helius facade n'expose aucun escape hatch vers le handle générique +[ ] protocol mismatch rejeté avant I/O — fixtures ajoutées, exécution opérateur requise +[ ] 6 familles standard Helius utilisent le wire standard exact — `pre.003` [ ] transaction subscribe/ack exact [ ] transaction unsubscribe/result exact [ ] transaction notification dispatch exact @@ -232,7 +232,7 @@ Le plan est la source normative du forecast. La validation reprend la même form pre.001 DONE — audit/sizing/matrice/dependencies fix.001 DONE — façades séparées + gate Cargo fermé fix.002 DONE — forecast compact + namespace LaserStream WS/gRPC -pre.002 socle protocolaire/façades/connect partagé +pre.002 PREPARED — socle protocolaire/façades/connect partagé pre.003 6 familles Helius communes + absence 3 unsupported pre.004 Config V2 + secrets pre.005 transaction request/unsubscribe @@ -274,4 +274,42 @@ Le forecast reste souple ; les fixes ne sont pas comptés comme des prereleases [x] doublons ciblés analysés comme transitifs/non bloquants ``` -Verdict : **gate `0.2.8-pre.001` positif. `pre.002` peut ouvrir le socle protocolaire/façades sans commencer prématurément `transactionSubscribe`.** +Verdict : **gate `0.2.8-pre.001` positif ; socle `pre.002` matérialisé, validations compilées opérateur encore requises avant commit.** + +## 12. Preuve spécifique `pre.002` + +Surface introduite : + +```text +WsProtocolKind::HeliusLaserStream -> "helius_laserstream" +SolanaStandardWsSession +HeliusLaserStreamWsSession +WsSession::connect_for_protocol -> crate-internal +WsSession::connect_physical -> unique physical spawn path +``` + +Canaries ajoutées/prévues dans la livraison : + +```text +[x] protocol descriptors distincts +[x] façade standard publique + 9 délégations vers les wrappers 0.2.7 +[x] façade Helius publique + lifecycle partagé +[x] generic WsSession::connect rejette Helius avant I/O +[x] chaque façade rejette le mauvais protocol kind avant I/O +[ ] les deux façades ouvrent/ferment une connexion contre un peer local via le même actor — test ajouté +[ ] Debug Helius ne projette pas une query api-key canary — test ajouté +[x] source de façade sans second spawn/socket/command actor +[ ] compile-fail Helius blockSubscribe — doctest ajouté +[ ] compile-fail Helius into_inner — doctest ajouté +[ ] six wrappers standard Helius — `pre.003` +``` + +Validation du sandbox de préparation : + +```text +python3 scripts/audit_rust_workspace_rules.py = clean +cargo fmt/check/clippy/test = non exécutables dans le sandbox de préparation +``` + +Les gates compilés opérateur de `pre.002` restent requis avant commit. +