v0.2.8-pre.006

This commit is contained in:
2026-08-23 15:41:54 +02:00
parent 8e739b9e55
commit 1c8d69778b
13 changed files with 862 additions and 121 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 225
# version: 226
[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.5.fix.2"
version = "0.2.8-pre.6"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 32
// version: 33
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -27,8 +27,8 @@
//! 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.
//! `0.2.8-pre.003` exposes the six standard families Helius supports through the provider facade, while `0.2.8-pre.005` adds the typed Helius
//! `transactionSubscribe` request contract, provider filter/options validation and exact subscribe/unsubscribe control-wire helpers without exposing a live
//! transaction subscription handle before actor integration.
//! `transactionSubscribe` request contract and provider filter/options validation. `0.2.8-pre.006` integrates the live transaction handle and typed
//! `transactionNotification` union into the same actor-owned registry, remote-ID remap, unsubscribe-race handling and per-subscription backpressure path.
mod client;
mod constants;
@@ -342,8 +342,14 @@ pub use self::ws_cluster::SolanaSlotUpdate;
pub use self::ws_cluster::SolanaSlotUpdateStats;
/// Typed unstable gossip-vote notification delivered by standard Solana `voteSubscribe`.
pub use self::ws_cluster::SolanaVoteNotification;
/// Full/accounts-mode notification delivered by Helius `transactionSubscribe`.
pub use self::ws_helius_transactions::HeliusFullTransactionNotification;
/// Helius `tokenAccounts` expansion mode accepted by `transactionSubscribe`.
pub use self::ws_helius_transactions::HeliusTokenAccountsFilter;
/// Typed Helius `transactionNotification` payload union.
pub use self::ws_helius_transactions::HeliusTransactionNotification;
/// Signatures-mode notification delivered by Helius `transactionSubscribe`.
pub use self::ws_helius_transactions::HeliusTransactionSignatureNotification;
/// Transaction encoding accepted by Helius `transactionSubscribe`.
pub use self::ws_helius_transactions::HeliusTransactionSubscribeEncoding;
/// Helius-specific filter object accepted as the first `transactionSubscribe` parameter.
@@ -360,7 +366,7 @@ pub use self::ws_lifecycle::WsSessionSnapshot;
pub use self::ws_lifecycle::WsSessionState;
/// Stable local identity assigned to one logical WebSocket subscription.
pub use self::ws_lifecycle::WsSubscriptionId;
/// Standard Solana subscription family represented by one logical WebSocket subscription.
/// WebSocket subscription family represented by one logical subscription.
pub use self::ws_lifecycle::WsSubscriptionKind;
/// Safe lifecycle projection for one logical WebSocket subscription.
pub use self::ws_lifecycle::WsSubscriptionSnapshot;
@@ -390,7 +396,7 @@ pub use self::ws_settings::WsResubscribePolicy;
pub use self::ws_settings::WsSessionSettings;
/// Complete runtime settings consumed by the KSP WebSocket transport foundation.
pub use self::ws_settings::WsTransportSettings;
/// Typed handle for one logical Solana WebSocket subscription.
/// Typed handle for one logical WebSocket subscription.
pub use self::ws_subscription::WsSubscription;
/// Typed value carried by a contextual Solana `logsNotification`.
pub use self::ws_transactions::SolanaLogsNotification;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
// version: 3
// version: 4
const MAX_HELIUS_TRANSACTION_FILTER_ACCOUNTS: usize = 50_000;
@@ -150,7 +150,6 @@ impl HeliusTransactionSubscribeFilter {
return std::result::Result::Ok(());
}
#[cfg(test)]
fn to_json_value(&self) -> serde_json::Value {
let mut object = serde_json::Map::new();
if let std::option::Option::Some(vote) = self.vote {
@@ -261,7 +260,6 @@ impl HeliusTransactionSubscribeOptions {
return std::result::Result::Ok(());
}
#[cfg(test)]
fn to_json_value(self) -> serde_json::Value {
let mut object = serde_json::Map::new();
if let std::option::Option::Some(commitment) = self.commitment {
@@ -283,11 +281,10 @@ impl HeliusTransactionSubscribeOptions {
}
}
/// Complete typed request contract for Helius `transactionSubscribe` before actor registration.
/// Complete typed request contract for Helius `transactionSubscribe`.
///
/// The request owns the exact provider filter and optional result-shaping object. `pre.005` deliberately does not expose a public live subscription method:
/// actor-owned registration, notification delivery, reconnect and unsubscribe races are added atomically in `pre.006` so callers never receive an incomplete
/// provider subscription handle.
/// The request owns the exact provider filter and optional result-shaping object. Validation and serialization occur before actor registration so deterministic
/// provider constraints fail without WebSocket I/O.
#[derive(Clone, Eq, PartialEq)]
pub struct HeliusTransactionSubscribeRequest {
filter: crate::HeliusTransactionSubscribeFilter,
@@ -335,7 +332,6 @@ impl std::fmt::Debug for HeliusTransactionSubscribeRequest {
}
}
#[cfg(test)]
fn helius_transaction_subscribe_params(request: &crate::HeliusTransactionSubscribeRequest) -> ksp_core_lib::Result<std::vec::Vec<serde_json::Value>> {
let validation = request.validate();
if let std::result::Result::Err(error) = validation {
@@ -348,41 +344,189 @@ fn helius_transaction_subscribe_params(request: &crate::HeliusTransactionSubscri
return std::result::Result::Ok(params);
}
#[cfg(test)]
const fn helius_transaction_subscribe_method() -> &'static str {
return "transactionSubscribe";
/// Full/accounts-mode notification delivered by Helius `transactionSubscribe`.
///
/// The nested transaction payload is deliberately retained as JSON because its exact Solana wire representation depends on the requested encoding and detail
/// mode. KSP types the stable provider envelope while preserving the full nested payload without Program-specific decoding.
#[derive(Clone, Debug, PartialEq)]
pub struct HeliusFullTransactionNotification {
transaction: serde_json::Value,
signature: std::string::String,
slot: u64,
transaction_index: u64,
}
#[cfg(test)]
const fn helius_transaction_unsubscribe_method() -> &'static str {
return "transactionUnsubscribe";
impl HeliusFullTransactionNotification {
/// Returns the provider transaction/status payload without interpreting Program-specific contents.
#[must_use]
pub const fn transaction(&self) -> &serde_json::Value {
return &self.transaction;
}
/// Returns the base58 transaction signature reported by Helius.
#[must_use]
pub fn signature(&self) -> &str {
return self.signature.as_str();
}
/// Returns the slot in which the transaction was processed.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns the zero-based transaction position within the block.
#[must_use]
pub const fn transaction_index(&self) -> u64 {
return self.transaction_index;
}
}
#[cfg(test)]
fn decode_helius_transaction_subscribe_result(value: serde_json::Value) -> ksp_core_lib::Result<u64> {
return match value.as_u64() {
std::option::Option::Some(remote_id) => std::result::Result::Ok(remote_id),
std::option::Option::None => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "Helius transactionSubscribe acknowledgement must contain a numeric subscription id")
.with_context("rpc_method", "transactionSubscribe"),
),
/// Signatures-mode notification delivered by Helius `transactionSubscribe`.
#[derive(Clone, Debug, PartialEq)]
pub struct HeliusTransactionSignatureNotification {
signature: std::string::String,
slot: u64,
transaction_index: u64,
err: crate::SolanaWireField<serde_json::Value>,
memo: crate::SolanaWireField<std::string::String>,
block_time: crate::SolanaWireField<i64>,
confirmation_status: crate::SolanaWireField<std::string::String>,
}
impl HeliusTransactionSignatureNotification {
/// Returns the base58 transaction signature reported by Helius.
#[must_use]
pub fn signature(&self) -> &str {
return self.signature.as_str();
}
/// Returns the slot in which the transaction was processed.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns the zero-based transaction position within the block.
#[must_use]
pub const fn transaction_index(&self) -> u64 {
return self.transaction_index;
}
/// Returns the optional transaction error while preserving omitted/null/value wire states.
#[must_use]
pub const fn err(&self) -> &crate::SolanaWireField<serde_json::Value> {
return &self.err;
}
/// Returns the optional memo while preserving omitted/null/value wire states.
#[must_use]
pub const fn memo(&self) -> &crate::SolanaWireField<std::string::String> {
return &self.memo;
}
/// Returns the optional block time while preserving omitted/null/value wire states.
#[must_use]
pub const fn block_time(&self) -> &crate::SolanaWireField<i64> {
return &self.block_time;
}
/// Returns the optional confirmation-status label while preserving omitted/null/value wire states.
#[must_use]
pub const fn confirmation_status(&self) -> &crate::SolanaWireField<std::string::String> {
return &self.confirmation_status;
}
}
/// Typed Helius `transactionNotification` payload union.
///
/// `Full` also covers the provider `accounts` detail mode because both contain the nested `transaction` member. `Signature` covers the lightweight
/// signatures mode. `Unknown` preserves `none` mode and forward-compatible provider shapes instead of failing the logical subscription.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum HeliusTransactionNotification {
/// Full/accounts notification carrying the nested transaction payload.
Full(crate::HeliusFullTransactionNotification),
/// Lightweight signatures notification.
Signature(crate::HeliusTransactionSignatureNotification),
/// Provider shape not currently typed by KSP, preserved losslessly.
Unknown(serde_json::Value),
}
impl crate::HeliusLaserStreamWsSession {
/// Opens one Helius `transactionSubscribe` logical subscription through the shared physical actor.
///
/// The returned handle keeps a stable local identity across physical reconnects. Helius remote subscription IDs stay actor-private and are remapped after
/// resubscribe. Calling [`crate::WsSubscription::unsubscribe`] removes the remote mapping before sending `transactionUnsubscribe`, so provider messages
/// already in flight after cancellation are ignored without reactivating the logical subscription.
pub async fn transaction_subscribe(
&self,
request: &crate::HeliusTransactionSubscribeRequest,
) -> ksp_core_lib::Result<crate::WsSubscription<crate::HeliusTransactionNotification>> {
let params = helius_transaction_subscribe_params(request);
let params = match params {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return self
.physical_session()
.subscribe_typed(crate::WsSubscriptionKind::HeliusTransaction, params, |value| return decode_helius_transaction_notification(value))
.await;
}
}
#[cfg(test)]
fn helius_transaction_unsubscribe_params(remote_id: u64) -> std::vec::Vec<serde_json::Value> {
return std::vec![serde_json::Value::Number(remote_id.into())];
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireHeliusFullTransactionNotification {
transaction: serde_json::Value,
signature: std::string::String,
slot: u64,
transaction_index: u64,
}
#[cfg(test)]
fn decode_helius_transaction_unsubscribe_result(value: serde_json::Value) -> ksp_core_lib::Result<bool> {
return match value.as_bool() {
std::option::Option::Some(unsubscribed) => std::result::Result::Ok(unsubscribed),
std::option::Option::None => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "Helius transactionUnsubscribe acknowledgement must contain a boolean result")
.with_context("rpc_method", "transactionUnsubscribe"),
),
};
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireHeliusTransactionSignatureNotification {
signature: std::string::String,
slot: u64,
transaction_index: u64,
#[serde(default)]
err: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
memo: crate::SolanaWireField<std::string::String>,
#[serde(default)]
block_time: crate::SolanaWireField<i64>,
#[serde(default)]
confirmation_status: crate::SolanaWireField<std::string::String>,
}
fn decode_helius_transaction_notification(value: serde_json::Value) -> ksp_core_lib::Result<crate::HeliusTransactionNotification> {
if value.get("transaction").is_some() {
let decoded = crate::decode_wire_json::<WireHeliusFullTransactionNotification>("transactionNotification", value.clone());
if let std::result::Result::Ok(decoded) = decoded {
return std::result::Result::Ok(crate::HeliusTransactionNotification::Full(crate::HeliusFullTransactionNotification {
transaction: decoded.transaction,
signature: decoded.signature,
slot: decoded.slot,
transaction_index: decoded.transaction_index,
}));
}
}
if value.get("signature").is_some() && value.get("slot").is_some() && value.get("transactionIndex").is_some() {
let decoded = crate::decode_wire_json::<WireHeliusTransactionSignatureNotification>("transactionNotification", value.clone());
if let std::result::Result::Ok(decoded) = decoded {
return std::result::Result::Ok(crate::HeliusTransactionNotification::Signature(crate::HeliusTransactionSignatureNotification {
signature: decoded.signature,
slot: decoded.slot,
transaction_index: decoded.transaction_index,
err: decoded.err,
memo: decoded.memo,
block_time: decoded.block_time,
confirmation_status: decoded.confirmation_status,
}));
}
}
return std::result::Result::Ok(crate::HeliusTransactionNotification::Unknown(value));
}
fn validate_account_list(field: &'static str, accounts: std::option::Option<&[ksp_core_lib::Pubkey]>) -> ksp_core_lib::Result<()> {
@@ -400,7 +544,6 @@ fn validate_account_list(field: &'static str, accounts: std::option::Option<&[ks
return std::result::Result::Ok(());
}
#[cfg(test)]
fn insert_account_list(
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
field: &'static str,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
// version: 6
// version: 7
/// Stable local identity assigned to one physical WebSocket session.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
@@ -76,7 +76,7 @@ pub enum WsSubscriptionState {
Failed,
}
/// Standard Solana subscription family represented by one logical WebSocket subscription.
/// WebSocket subscription family represented by one logical subscription.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum WsSubscriptionKind {
@@ -98,10 +98,12 @@ pub enum WsSubscriptionKind {
SlotsUpdates,
/// `voteSubscribe` family.
Vote,
/// Helius LaserStream WebSocket `transactionSubscribe` extension family.
HeliusTransaction,
}
impl WsSubscriptionKind {
/// Returns the stable KSP descriptor for this standard subscription family.
/// Returns the stable KSP descriptor for this WebSocket subscription family.
#[must_use]
pub const fn as_str(self) -> &'static str {
return match self {
@@ -114,10 +116,11 @@ impl WsSubscriptionKind {
Self::Slot => "slot",
Self::SlotsUpdates => "slots_updates",
Self::Vote => "vote",
Self::HeliusTransaction => "helius_transaction",
};
}
/// Returns the exact standard Solana subscribe JSON-RPC method for this family.
/// Returns the exact subscribe JSON-RPC method for this family.
pub(crate) const fn subscribe_method(self) -> &'static str {
return match self {
Self::Account => "accountSubscribe",
@@ -129,10 +132,11 @@ impl WsSubscriptionKind {
Self::Slot => "slotSubscribe",
Self::SlotsUpdates => "slotsUpdatesSubscribe",
Self::Vote => "voteSubscribe",
Self::HeliusTransaction => "transactionSubscribe",
};
}
/// Returns the exact standard Solana unsubscribe JSON-RPC method for this family.
/// Returns the exact unsubscribe JSON-RPC method for this family.
pub(crate) const fn unsubscribe_method(self) -> &'static str {
return match self {
Self::Account => "accountUnsubscribe",
@@ -144,10 +148,11 @@ impl WsSubscriptionKind {
Self::Slot => "slotUnsubscribe",
Self::SlotsUpdates => "slotsUpdatesUnsubscribe",
Self::Vote => "voteUnsubscribe",
Self::HeliusTransaction => "transactionUnsubscribe",
};
}
/// Returns the exact standard Solana notification method emitted for this family.
/// Returns the exact notification method emitted for this family.
pub(crate) const fn notification_method(self) -> &'static str {
return match self {
Self::Account => "accountNotification",
@@ -159,10 +164,13 @@ impl WsSubscriptionKind {
Self::Slot => "slotNotification",
Self::SlotsUpdates => "slotsUpdatesNotification",
Self::Vote => "voteNotification",
Self::HeliusTransaction => "transactionNotification",
};
}
/// Returns whether Solana documents this standard subscription family as unstable.
///
/// Provider extensions are stable here unless explicitly classified otherwise.
pub(crate) const fn is_unstable(self) -> bool {
return matches!(self, Self::Block | Self::SlotsUpdates | Self::Vote);
}
@@ -211,7 +219,7 @@ impl WsSubscriptionSnapshot {
return self.id;
}
/// Returns the standard subscription family.
/// Returns the logical WebSocket subscription family.
#[must_use]
pub const fn kind(&self) -> crate::WsSubscriptionKind {
return self.kind;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
// version: 3
// version: 4
/// Typed facade for one standard Solana WebSocket physical session.
///
@@ -58,9 +58,9 @@ impl std::fmt::Debug for SolanaStandardWsSession {
/// Typed facade for one Helius LaserStream WebSocket physical session.
///
/// The facade exposes the six standard Solana subscription families that Helius documents as supported. `0.2.8-pre.005` also publishes the typed Helius
/// transaction request/filter/options contract, but the live transaction-subscription method remains intentionally absent until `pre.006` integrates
/// `transactionNotification`, reconnect and unsubscribe races into the shared actor. No public inner handle is exposed, so callers cannot bypass the
/// The facade exposes the six standard Solana subscription families that Helius documents as supported plus the Helius-specific typed
/// `transactionSubscribe` lifecycle. Transaction notifications, reconnect/resubscribe, unsubscribe races and bounded backpressure all delegate to the same
/// shared [`crate::WsSession`] actor; the facade owns no second socket, registry or queue. No public inner handle is exposed, so callers cannot bypass the
/// provider-specific surface by recovering a generic [`crate::WsSession`].
///
/// ```compile_fail

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-onchain-transport-lib/src/ws_subscription.rs
// version: 5
// version: 6
/// Typed handle for one logical Solana WebSocket subscription.
/// Typed handle for one logical WebSocket subscription.
///
/// The handle owns the bounded typed notification receiver while the physical session actor owns the remote subscription identity and socket. The remote
/// numeric subscription identifier is intentionally never exposed because it is transient and is remapped by the session actor after reconnect.
@@ -43,7 +43,7 @@ impl<T> WsSubscription<T> {
return self.id;
}
/// Returns the standard Solana subscription family.
/// Returns the logical WebSocket subscription family.
#[must_use]
pub const fn kind(&self) -> crate::WsSubscriptionKind {
return self.kind;
@@ -71,7 +71,7 @@ impl<T> WsSubscription<T> {
return self.notification_rx.recv().await;
}
/// Cancels this logical subscription and sends the matching Solana unsubscribe request when a remote binding still exists.
/// Cancels this logical subscription and sends the matching protocol unsubscribe request when a remote binding still exists.
///
/// The returned boolean preserves the standard Solana unsubscribe result when the current remote binding is reachable. Local cancellation is terminal
/// for this handle; during reconnect it wins before resubscribe selection, and a late remote acknowledgement is cleaned up best-effort without
@@ -198,11 +198,11 @@ impl WsSubscriptionRegistration {
pub(crate) struct WsSubscriptionRuntime {
/// Stable local identity.
pub(crate) id: crate::WsSubscriptionId,
/// Standard Solana subscription family.
/// WebSocket subscription family.
pub(crate) kind: crate::WsSubscriptionKind,
/// Current logical lifecycle state.
pub(crate) state: crate::WsSubscriptionState,
/// Original standard subscribe parameters retained internally for deterministic resubscribe.
/// Original subscribe parameters retained internally for deterministic resubscribe.
pub(crate) params: std::vec::Vec<serde_json::Value>,
/// Current transient remote subscription identity when bound.
pub(crate) remote_id: std::option::Option<u64>,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 36
// version: 37
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -744,3 +744,12 @@ fn public_v0_2_8_pre_005_helius_transaction_request_contract_is_available_withou
assert_eq!(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged.as_str(), "balanceChanged");
assert_eq!(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::All.as_str(), "all");
}
#[test]
fn public_v0_2_8_pre_006_helius_transaction_live_handle_and_notification_types_are_available() {
let _subscribe = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::transaction_subscribe;
let _notification = std::any::type_name::<ksp_onchain_transport_lib::HeliusTransactionNotification>();
let _full = std::any::type_name::<ksp_onchain_transport_lib::HeliusFullTransactionNotification>();
let _signature = std::any::type_name::<ksp_onchain_transport_lib::HeliusTransactionSignatureNotification>();
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction.as_str(), "helius_transaction");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 27
// version: 28
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
@@ -836,3 +836,17 @@ fn release_v0_2_8_pre_005_helius_transaction_request_surface_is_typed_before_act
assert!(!facade_source.contains("pub async fn transaction_subscribe"));
assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream");
}
#[test]
fn release_v0_2_8_pre_006_helius_transaction_lifecycle_is_actor_integrated_without_advancing_heartbeat() {
let _transaction = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::transaction_subscribe;
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction.as_str(), "helius_transaction");
let source = include_str!("../src/ws_helius_transactions.rs");
assert!(source.contains("transactionNotification"));
assert!(source.contains("WsSubscriptionKind::HeliusTransaction"));
assert!(!source.contains("tokio_tungstenite::connect_async"));
let protocol_source = include_str!("../src/ws_protocol_session.rs");
assert!(protocol_source.contains("unsupported_block"));
assert!(protocol_source.contains("unsupported_slots_updates"));
assert!(protocol_source.contains("unsupported_vote"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs
// version: 3
// version: 4
use futures_util::SinkExt; // rust-rules: trait-import
use futures_util::StreamExt; // rust-rules: trait-import
@@ -29,6 +29,10 @@ fn assert_oversized_filter_rejected(filter: crate::HeliusTransactionSubscribeFil
}
fn helius_endpoint(url: &str) -> crate::WsEndpointSettings {
return helius_endpoint_with_session(url, crate::WsSessionSettings::default());
}
fn helius_endpoint_with_session(url: &str, session: crate::WsSessionSettings) -> crate::WsEndpointSettings {
return crate::WsEndpointSettings::new(
"local_helius_transaction_fixture",
true,
@@ -36,7 +40,41 @@ fn helius_endpoint(url: &str) -> crate::WsEndpointSettings {
crate::WsClusterName::new("local"),
crate::WsProtocolKind::HeliusLaserStream,
crate::WsEndpointUrl::parse(url).expect("local Helius WebSocket URL must parse"),
crate::WsSessionSettings::default(),
session,
);
}
fn reconnect_session_settings(backoff: std::time::Duration) -> crate::WsSessionSettings {
let defaults = crate::WsSessionSettings::default();
return crate::WsSessionSettings::new(
std::time::Duration::from_millis(250),
std::time::Duration::from_millis(200),
crate::WsReconnectSettings::new(2, backoff, backoff),
crate::WsResubscribePolicy::ActiveSubscriptions,
defaults.command_queue_capacity(),
defaults.notification_queue_capacity(),
defaults.max_active_subscriptions(),
defaults.max_pending_requests(),
defaults.max_message_size_bytes(),
defaults.max_frame_size_bytes(),
defaults.max_write_buffer_size_bytes(),
);
}
fn backpressure_session_settings() -> crate::WsSessionSettings {
let defaults = crate::WsSessionSettings::default();
return crate::WsSessionSettings::new(
std::time::Duration::from_millis(250),
std::time::Duration::from_millis(200),
crate::WsReconnectSettings::new(0, std::time::Duration::from_millis(10), std::time::Duration::from_millis(10)),
crate::WsResubscribePolicy::ActiveSubscriptions,
defaults.command_queue_capacity(),
1,
2,
defaults.max_pending_requests(),
defaults.max_message_size_bytes(),
defaults.max_frame_size_bytes(),
defaults.max_write_buffer_size_bytes(),
);
}
@@ -59,6 +97,54 @@ async fn send_result(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::n
return;
}
async fn send_notification(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, subscription: u64, result: serde_json::Value) {
let notification = serde_json::json!({"jsonrpc":"2.0","method":"transactionNotification","params":{"subscription":subscription,"result":result}});
websocket.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await.expect("local notification must send");
return;
}
async fn send_root_notification(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, subscription: u64, root: u64) {
let notification = serde_json::json!({"jsonrpc":"2.0","method":"rootNotification","params":{"subscription":subscription,"result":root}});
websocket
.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into()))
.await
.expect("local root notification must send");
return;
}
async fn wait_for_gap_count(session: &crate::HeliusLaserStreamWsSession, expected: u64) {
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
if session.snapshot().continuity_gap_count() >= expected {
return;
}
assert!(tokio::time::Instant::now() < deadline, "Helius session continuity gap count must advance before timeout");
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
}
async fn wait_for_overflow_count(session: &crate::HeliusLaserStreamWsSession, expected: u64) {
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
if session.snapshot().overflow_count() >= expected {
return;
}
assert!(tokio::time::Instant::now() < deadline, "Helius session overflow count must advance before timeout");
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
}
async fn wait_for_subscription_state<T>(subscription: &crate::WsSubscription<T>, expected: crate::WsSubscriptionState) {
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
if subscription.state() == expected {
return;
}
assert!(tokio::time::Instant::now() < deadline, "Helius logical subscription state must advance before timeout");
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
}
async fn wait_for_close_frame(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) {
loop {
let message = websocket.next().await;
@@ -237,24 +323,48 @@ fn helius_transaction_details_require_max_supported_version_only_for_accounts_an
}
#[test]
fn helius_transaction_subscribe_and_unsubscribe_control_wire_is_exact() {
assert_eq!(super::helius_transaction_subscribe_method(), "transactionSubscribe");
assert_eq!(super::helius_transaction_unsubscribe_method(), "transactionUnsubscribe");
assert_eq!(
super::decode_helius_transaction_subscribe_result(serde_json::json!(4_743_323_479_349_712_u64)).expect("numeric ack must decode"),
4_743_323_479_349_712
);
assert_eq!(super::helius_transaction_unsubscribe_params(4_743_323_479_349_712), std::vec![serde_json::json!(4_743_323_479_349_712_u64)]);
assert!(super::decode_helius_transaction_unsubscribe_result(serde_json::json!(true)).expect("boolean true must decode"));
assert!(!super::decode_helius_transaction_unsubscribe_result(serde_json::json!(false)).expect("boolean false must decode"));
assert_eq!(
super::decode_helius_transaction_subscribe_result(serde_json::json!("not-an-id")).expect_err("non-numeric subscribe ack must fail").code(),
crate::ERROR_CODE_INVALID_RESPONSE
);
assert_eq!(
super::decode_helius_transaction_unsubscribe_result(serde_json::json!(1)).expect_err("non-boolean unsubscribe ack must fail").code(),
crate::ERROR_CODE_INVALID_RESPONSE
);
fn helius_transaction_notification_decoder_preserves_full_signature_and_unknown_shapes() {
let full_value = serde_json::json!({
"transaction":{"transaction":["AAAA","base64"],"meta":{"err":null}},
"signature":"full-signature",
"slot":224341380,
"transactionIndex":42
});
let full = super::decode_helius_transaction_notification(full_value.clone()).expect("full Helius notification must decode");
match full {
crate::HeliusTransactionNotification::Full(notification) => {
assert_eq!(notification.transaction(), &full_value["transaction"]);
assert_eq!(notification.signature(), "full-signature");
assert_eq!(notification.slot(), 224341380);
assert_eq!(notification.transaction_index(), 42);
},
_ => panic!("transaction member must select the full Helius notification variant"),
}
let signature_value = serde_json::json!({
"signature":"signature-only",
"slot":224341381,
"transactionIndex":43,
"err":null,
"memo":"memo-canary",
"blockTime":1720000000,
"confirmationStatus":"confirmed"
});
let signature = super::decode_helius_transaction_notification(signature_value).expect("signature Helius notification must decode");
match signature {
crate::HeliusTransactionNotification::Signature(notification) => {
assert_eq!(notification.signature(), "signature-only");
assert_eq!(notification.slot(), 224341381);
assert_eq!(notification.transaction_index(), 43);
assert!(matches!(notification.err(), crate::SolanaWireField::Null));
assert!(matches!(notification.memo(), crate::SolanaWireField::Value(value) if value == "memo-canary"));
assert!(matches!(notification.block_time(), crate::SolanaWireField::Value(1720000000)));
assert!(matches!(notification.confirmation_status(), crate::SolanaWireField::Value(value) if value == "confirmed"));
},
_ => panic!("signature envelope must select the lightweight Helius notification variant"),
}
let unknown_value = serde_json::json!({"futureProviderShape":{"value":7}});
let unknown = super::decode_helius_transaction_notification(unknown_value.clone()).expect("unknown Helius notification must remain forward-compatible");
assert!(matches!(unknown, crate::HeliusTransactionNotification::Unknown(value) if value == unknown_value));
}
#[test]
@@ -271,7 +381,7 @@ fn helius_transaction_filter_debug_omits_signature_and_account_values() {
}
#[tokio::test(flavor = "current_thread")]
async fn helius_transaction_control_wire_round_trips_through_shared_physical_actor() {
async fn helius_transaction_live_handle_decodes_notification_and_unsubscribes_through_shared_actor() {
let (listener, url) = bind_local_listener().await;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("local server must accept client");
@@ -286,6 +396,17 @@ async fn helius_transaction_control_wire_round_trips_through_shared_physical_act
])
);
send_result(&mut websocket, &subscribe, serde_json::json!(4242)).await;
send_notification(
&mut websocket,
4242,
serde_json::json!({
"transaction":{"transaction":["AAAA","base64"],"meta":{"err":null}},
"signature":"live-signature",
"slot":99,
"transactionIndex":7
}),
)
.await;
let unsubscribe = read_request(&mut websocket).await;
assert_eq!(unsubscribe["method"], serde_json::json!("transactionUnsubscribe"));
assert_eq!(unsubscribe["params"], serde_json::json!([4242]));
@@ -310,20 +431,110 @@ async fn helius_transaction_control_wire_round_trips_through_shared_physical_act
std::option::Option::Some(0),
);
let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options));
let params = super::helius_transaction_subscribe_params(&request).expect("typed Helius request must validate before I/O");
let subscribe_result = session
.physical_session()
.execute_json_rpc(super::helius_transaction_subscribe_method(), params)
.await
.expect("transactionSubscribe acknowledgement must arrive");
let remote_id = super::decode_helius_transaction_subscribe_result(subscribe_result).expect("transactionSubscribe id must decode");
assert_eq!(remote_id, 4242);
let unsubscribe_result = session
.physical_session()
.execute_json_rpc(super::helius_transaction_unsubscribe_method(), super::helius_transaction_unsubscribe_params(remote_id))
.await
.expect("transactionUnsubscribe acknowledgement must arrive");
assert!(super::decode_helius_transaction_unsubscribe_result(unsubscribe_result).expect("transactionUnsubscribe boolean must decode"));
let mut subscription = session.transaction_subscribe(&request).await.expect("public Helius transaction subscription must register");
assert_eq!(subscription.kind(), crate::WsSubscriptionKind::HeliusTransaction);
let notification = subscription.recv().await.expect("Helius transaction notification must arrive").expect("Helius notification must decode");
match notification {
crate::HeliusTransactionNotification::Full(notification) => {
assert_eq!(notification.signature(), "live-signature");
assert_eq!(notification.slot(), 99);
assert_eq!(notification.transaction_index(), 7);
},
_ => panic!("full live payload must decode as HeliusTransactionNotification::Full"),
}
assert!(subscription.unsubscribe().await.expect("transactionUnsubscribe must complete"));
assert_eq!(subscription.state(), crate::WsSubscriptionState::Closed);
session.close().await.expect("Helius fixture session must close");
server.await.expect("local Helius transaction server must finish");
}
#[tokio::test(flavor = "current_thread")]
async fn helius_transaction_reconnect_remaps_remote_id_and_ignores_late_notification_after_unsubscribe() {
let (listener, url) = bind_local_listener().await;
let server = tokio::spawn(async move {
let (first_stream, _) = listener.accept().await.expect("initial Helius client must connect");
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial Helius handshake must succeed");
let first_subscribe = read_request(&mut first).await;
assert_eq!(first_subscribe["method"], serde_json::json!("transactionSubscribe"));
send_result(&mut first, &first_subscribe, serde_json::json!(41)).await;
send_notification(&mut first, 41, serde_json::json!({"signature":"generation-one","slot":1,"transactionIndex":0})).await;
drop(first);
let (second_stream, _) = listener.accept().await.expect("replacement Helius client must connect");
let mut second = tokio_tungstenite::accept_async(second_stream).await.expect("replacement Helius handshake must succeed");
let second_subscribe = read_request(&mut second).await;
assert_eq!(second_subscribe["method"], serde_json::json!("transactionSubscribe"));
assert_eq!(second_subscribe["params"], first_subscribe["params"]);
send_result(&mut second, &second_subscribe, serde_json::json!(99)).await;
send_notification(&mut second, 99, serde_json::json!({"signature":"generation-two","slot":2,"transactionIndex":1})).await;
let unsubscribe = read_request(&mut second).await;
assert_eq!(unsubscribe["method"], serde_json::json!("transactionUnsubscribe"));
assert_eq!(unsubscribe["params"], serde_json::json!([99]));
send_notification(&mut second, 99, serde_json::json!({"signature":"late-after-cancel","slot":3,"transactionIndex":2})).await;
send_result(&mut second, &unsubscribe, serde_json::json!(true)).await;
wait_for_close_frame(&mut second).await;
});
let settings = reconnect_session_settings(std::time::Duration::from_millis(20));
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint_with_session(url.as_str(), settings)).await.expect("Helius facade must connect");
let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::None);
let mut subscription = session.transaction_subscribe(&request).await.expect("initial Helius transaction subscription must register");
let stable_id = subscription.id();
let first = subscription.recv().await.expect("first generation notification must arrive").expect("first generation notification must decode");
assert!(matches!(first, crate::HeliusTransactionNotification::Signature(ref value) if value.signature() == "generation-one"));
let second = tokio::time::timeout(std::time::Duration::from_secs(2), subscription.recv())
.await
.expect("resubscribed Helius notification must remain bounded")
.expect("resubscribed Helius channel must remain open")
.expect("resubscribed Helius notification must decode");
assert!(matches!(second, crate::HeliusTransactionNotification::Signature(ref value) if value.signature() == "generation-two"));
assert_eq!(subscription.id(), stable_id);
assert_eq!(subscription.state(), crate::WsSubscriptionState::Active);
wait_for_gap_count(&session, 1).await;
assert_eq!(session.snapshot().continuity_gap_count(), 1);
assert!(subscription.unsubscribe().await.expect("Helius transaction cancellation must complete"));
assert_eq!(subscription.state(), crate::WsSubscriptionState::Closed);
assert!(tokio::time::timeout(std::time::Duration::from_millis(100), subscription.recv()).await.expect("closed Helius channel must settle").is_none());
session.close().await.expect("Helius fixture session must close");
server.await.expect("local reconnect Helius server must finish");
}
#[tokio::test(flavor = "current_thread")]
async fn helius_transaction_backpressure_fails_only_slow_subscription_and_uses_transaction_unsubscribe_cleanup() {
let (listener, url) = bind_local_listener().await;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("local server must accept Helius client");
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local Helius handshake must succeed");
let transaction_subscribe = read_request(&mut websocket).await;
assert_eq!(transaction_subscribe["method"], serde_json::json!("transactionSubscribe"));
send_result(&mut websocket, &transaction_subscribe, serde_json::json!(41)).await;
let root_subscribe = read_request(&mut websocket).await;
assert_eq!(root_subscribe["method"], serde_json::json!("rootSubscribe"));
send_result(&mut websocket, &root_subscribe, serde_json::json!(42)).await;
send_notification(&mut websocket, 41, serde_json::json!({"signature":"queued","slot":1,"transactionIndex":0})).await;
send_notification(&mut websocket, 41, serde_json::json!({"signature":"overflow","slot":2,"transactionIndex":1})).await;
let cleanup = read_request(&mut websocket).await;
assert_eq!(cleanup["method"], serde_json::json!("transactionUnsubscribe"));
assert_eq!(cleanup["params"], serde_json::json!([41]));
send_result(&mut websocket, &cleanup, serde_json::json!(true)).await;
send_root_notification(&mut websocket, 42, 99).await;
wait_for_close_frame(&mut websocket).await;
});
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint_with_session(url.as_str(), backpressure_session_settings()))
.await
.expect("Helius facade must connect");
let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::None);
let mut slow = session.transaction_subscribe(&request).await.expect("slow Helius transaction subscription must register");
let mut healthy = session.root_subscribe().await.expect("healthy Helius root subscription must register");
wait_for_subscription_state(&slow, crate::WsSubscriptionState::Failed).await;
wait_for_overflow_count(&session, 1).await;
assert_eq!(slow.terminal_error_code(), std::option::Option::Some(crate::ERROR_CODE_WS_BACKPRESSURE_OVERFLOW));
assert_eq!(session.state(), crate::WsSessionState::Active);
assert_eq!(session.snapshot().subscription_count(), 1);
let queued = slow.recv().await.expect("first Helius notification must remain queued").expect("queued Helius notification must decode");
assert!(matches!(queued, crate::HeliusTransactionNotification::Signature(ref value) if value.signature() == "queued"));
assert!(slow.recv().await.is_none());
assert_eq!(healthy.recv().await.expect("healthy root notification must arrive").expect("healthy root notification must decode"), 99);
assert_eq!(healthy.state(), crate::WsSubscriptionState::Active);
assert_eq!(healthy.terminal_error_code(), std::option::Option::None);
session.close().await.expect("Helius fixture session must close");
server.await.expect("local Helius backpressure server must finish");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
// version: 4
// version: 5
fn non_zero(value: u64) -> std::num::NonZeroU64 {
return std::num::NonZeroU64::new(value).expect("test ID must be non-zero");
@@ -123,3 +123,13 @@ fn websocket_unstable_subscription_partition_is_exact() {
assert_eq!(kind.is_unstable(), unstable);
}
}
#[test]
fn helius_transaction_subscription_kind_maps_exact_provider_method_triplet_without_expanding_standard_partition() {
let kind = crate::WsSubscriptionKind::HeliusTransaction;
assert_eq!(kind.as_str(), "helius_transaction");
assert_eq!(kind.subscribe_method(), "transactionSubscribe");
assert_eq!(kind.unsubscribe_method(), "transactionUnsubscribe");
assert_eq!(kind.notification_method(), "transactionNotification");
assert!(!kind.is_unstable());
}

235
deltas/0.2.8/pre.006.md Normal file
View File

@@ -0,0 +1,235 @@
<!-- file: deltas/0.2.8/pre.006.md -->
<!-- version: 1 -->
# Delta `0.2.8-pre.006` — Helius transactionNotification + lifecycle actor
## 1. Base et objet
Base appliquée :
```text
0.2.8-pre.5.fix.2
```
Le checkpoint opérateur de cette base est intégralement vert et sans warning : `cargo fmt`, audit Rust, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, tests Transport et `cargo test --workspace` passent. Transport compte alors `322` tests unitaires, `39` tests public API, `27` tests release-completeness et `4` doctests compile-fail.
Cette tranche transforme le contrat de requête Helius préparé en `pre.005` en une souscription live complète, sans créer de second moteur WebSocket :
```text
transactionSubscribe
-> registry actor existant
-> WsSubscription<HeliusTransactionNotification>
-> transactionNotification
-> reconnect/resubscribe/remap remote ID
-> transactionUnsubscribe
```
## 2. Version technique
```text
workspace.package.version = 0.2.8-pre.6
commit attendu = v0.2.8-pre.006
Git tag = aucun tag prerelease
```
Le header root `Cargo.toml` passe en version `226`.
## 3. Subscription kind provider
`WsSubscriptionKind` gagne :
```text
HeliusTransaction
```
avec le triplet exact :
```text
as_str helius_transaction
subscribe_method transactionSubscribe
unsubscribe_method transactionUnsubscribe
notification_method transactionNotification
```
Cette extension ne modifie pas la partition standard Solana de neuf familles et reste hors des trois familles standard classées unstable (`Block`, `SlotsUpdates`, `Vote`).
Le generic actor existant reste l'unique propriétaire :
- du socket physique ;
- du pending map JSON-RPC ;
- des local IDs ;
- des remote IDs ;
- du registry de subscriptions ;
- du reconnect/resubscribe ;
- des queues de notifications ;
- du cleanup unsubscribe ;
- du shutdown.
## 4. Handle live Helius
`HeliusLaserStreamWsSession` expose maintenant :
```rust
transaction_subscribe(
&self,
request: &HeliusTransactionSubscribeRequest,
) -> Result<WsSubscription<HeliusTransactionNotification>>
```
La validation déterministe et la sérialisation de `pre.005` restent exécutées avant l'enregistrement actor. Le helper de sérialisation et ses sous-helpers redeviennent du code de production uniquement parce qu'ils ont désormais un consommateur réel ; ils restent strictement privés au module.
Aucune visibilité n'est élargie pour les tests. Les canaris du sous-module accèdent aux helpers privés avec `super::Item`; les contrats publics sont consommés via `crate::Item`.
## 5. Notification typed
Trois formes publiques sont exposées au crate-root.
### 5.1 Full/accounts
`HeliusFullTransactionNotification` conserve :
```text
transaction serde_json::Value
signature String
slot u64
transactionIndex u64
```
Le nested `transaction` reste lossless en JSON, car sa forme dépend de `encoding` et `transactionDetails`; Transport ne décode pas les Programs.
### 5.2 Signatures
`HeliusTransactionSignatureNotification` conserve :
```text
signature String
slot u64
transactionIndex u64
err Omitted | Null | Value(JSON)
memo Omitted | Null | Value(String)
blockTime Omitted | Null | Value(i64)
confirmationStatus Omitted | Null | Value(String)
```
Les champs optionnels réutilisent `SolanaWireField` afin de ne pas confondre omission et `null`.
### 5.3 Union publique
```text
HeliusTransactionNotification::Full(...)
HeliusTransactionNotification::Signature(...)
HeliusTransactionNotification::Unknown(JSON)
```
`Unknown` conserve uniquement le `params.result` provider. L'enveloppe JSON-RPC complète et `params.subscription` ne franchissent pas le boundary public. Cette forme couvre notamment un `transactionDetails=none` ou une évolution provider non encore typée sans tuer arbitrairement la logical subscription.
## 6. Reconnect, unsubscribe tardif et backpressure
Le support Helius s'appuie directement sur les garanties du moteur `0.2.7` :
- les params `transactionSubscribe` originaux sont conservés par le registry ;
- après reconnect, un nouvel ID remote remplace l'ancien ;
- le `WsSubscriptionId` local reste stable ;
- le remote ID n'est jamais public ;
- au début d'un unsubscribe, le mapping remote -> local est retiré avant l'émission de `transactionUnsubscribe` ;
- une notification provider déjà en vol après cancellation est donc ignorée ;
- un overflow de queue échoue seulement la logical subscription lente ;
- le cleanup best-effort utilise automatiquement `transactionUnsubscribe` grâce au nouveau `WsSubscriptionKind`.
Cette sémantique correspond au contrat Helius actuel qui précise que quelques messages en vol peuvent encore arriver brièvement après `transactionUnsubscribe`.
## 7. Canaris ajoutés/actualisés
Les tests Helius transaction couvrent maintenant :
```text
notification Full / Signature / Unknown
live transactionSubscribe exact via façade publique
transactionNotification routée vers WsSubscription
transactionUnsubscribe exact via handle public
reconnect : remote ID 41 -> 99
resubscribe : params identiques
stable local WsSubscriptionId
late transactionNotification après demande unsubscribe ignorée
overflow transaction : handle lent Failed + ERROR_CODE_WS_BACKPRESSURE_OVERFLOW
cleanup overflow : transactionUnsubscribe [remote_id]
Helius root sain reste Active et reçoit encore sa notification
```
Un canari lifecycle verrouille aussi le triplet exact du nouveau `WsSubscriptionKind::HeliusTransaction`.
Les public/release canaries gagnent :
- le symbole public `HeliusLaserStreamWsSession::transaction_subscribe` ;
- les trois types publics de notification ;
- la présence du kind provider ;
- l'absence de second `connect_async`/actor dans le module Helius ;
- la conservation des compile-fail Helius block/slotsUpdates/vote/escape-hatch.
Comptages attendus :
```text
Transport unit 325
Transport public API 40
release completeness 28
doctests compile-fail 4
```
## 8. Documentation
Le plan `015` :
- ferme `pre.005`, `fix.001` et `fix.002` après preuve opérateur sans warning ;
- marque `pre.006` PREPARED ;
- documente l'union notification, le remap remote/local et les nouveaux canaris lifecycle.
La validation `011` :
- enregistre le checkpoint final `pre.005` ;
- ouvre la gate `pre.006` ;
- conserve heartbeat, adversarial élargi et smoke live dans leurs tranches prévues.
## 9. Hors scope
Restent explicitement hors de `pre.006` :
```text
heartbeat / idle timer pre.007
provider adversarial/security élargi pre.008
compliance finale pre.009
smoke Helius live opt-in pre.010
LaserStream gRPC future transport séparé
```
Aucune nouvelle dépendance n'est ajoutée.
## 10. Fichiers modifiés
```text
Cargo.toml
crates/ksp-onchain-transport-lib/src/lib.rs
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
crates/ksp-onchain-transport-lib/src/ws_subscription.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs
crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
deltas/0.2.8/pre.006.md
```
## 11. Gate opérateur
```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
cargo test --workspace
```
Critère de fermeture : aucune erreur, aucun warning nouveau, audit Rust clean et tous les nouveaux canaris lifecycle Helius verts.

View File

@@ -1,9 +1,9 @@
<!-- file: docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md -->
<!-- version: 15 -->
<!-- version: 16 -->
# Plan `0.2.8` — Helius LaserStream WebSocket
> **Statut : `pre.004` et ses deux fixes sont validés. `pre.005-fix.001` corrige les erreurs de type et la visibilité test/private, mais son checkpoint reste non fermable à cause de neuf warnings `dead_code` sur des helpers wire strictement privés qui n'ont pas encore de consommateur de production. `0.2.8-pre.005-fix.002` les borne à `#[cfg(test)]` jusqu'à leur consommation réelle par l'actor en `pre.006`, sans `allow(dead_code)` ni élargissement de visibilité.**
> **Statut : `pre.005` + `fix.001` + `fix.002` sont validés sans warning. `0.2.8-pre.006` prépare maintenant le handle live Helius `transactionSubscribe`, le décodage `transactionNotification` et son intégration au registry/reconnect/backpressure de l'actor WebSocket unique.**
## 1. Objet, base et état courant
@@ -30,12 +30,13 @@ pre.003 six familles standard Helius validées
pre.004 Config V2 Helius validé
pre.004-fix.001 redaction segmentaire + couverture Devnet Helius validées
pre.004-fix.002 provenance composée validée
pre.005 contrat typed transactionSubscribe/unsubscribe ; fix requis après checkpoint
pre.005-fix.001 correction types de canaris + visibilité/tests/règles appliquée
pre.005-fix.002 suppression warnings dead_code via cfg(test) des helpers wire préparée
pre.005 contrat typed transactionSubscribe/unsubscribe validé
pre.005-fix.001 correction types de canaris + visibilité/tests/règles validée
pre.005-fix.002 suppression warnings dead_code via cfg(test) validée
pre.006 transactionNotification + lifecycle actor préparé
workspace.package.version courant = 0.2.8-pre.5.fix.2
commit attendu = v0.2.8-pre.005-fix.002
workspace.package.version courant = 0.2.8-pre.6
commit attendu = v0.2.8-pre.006
aucun tag prerelease
```
@@ -59,12 +60,12 @@ pre.004 DONE — Config V2 helius_laserstream + schema/fixtures + mapping Confi
+ secret/redaction Helius mainnet/devnet validés
fix.001 DONE — redaction segmentaire corrigée + représentation Devnet Helius ajoutée
fix.002 DONE — provenance composée `DocumentLiteral` + `EnvironmentProcess` corrigée
pre.005 FIX REQUIRED — transactionSubscribe request typed + filters/options/tokenAccounts + transactionUnsubscribe
pre.005 DONE — transactionSubscribe request typed + filters/options/tokenAccounts + transactionUnsubscribe
+ bounds 50k + maxSupportedTransactionVersion conditionnel ; live handle différé à pre.006
fix.001 APPLIED — assertions Vec<Value>/Value corrigées + helpers test-only privés + audit super/crate durci
fix.002 PREPARED — helpers wire non consommés en production bornés à #[cfg(test)] ; zéro allow/dead-code compensatoire
pre.006 transactionNotification + actor integration + reconnect/resubscribe/unsubscribe races
+ late notifications + backpressure ciblée
fix.001 DONE — assertions Vec<Value>/Value corrigées + helpers test-only privés + audit super/crate durci
fix.002 DONE — helpers wire non consommés en production bornés à #[cfg(test)] ; zéro warning dead_code
pre.006 PREPARED — transactionNotification + handle live + actor registry/reconnect/resubscribe/unsubscribe races
+ late notifications + backpressure ciblée, sans second actor/socket
pre.007 heartbeat Helius WebSocket/idle + timers + interaction reconnect/control frames/shutdown
pre.008 provider adversarial lifecycle + capability guards + payload/backpressure + security/redaction
pre.009 compliance Helius WebSocket + non-régressions Solana standard 18/18 + HTTP 52/14
@@ -1024,3 +1025,57 @@ Ces éléments ne sont pas encore consommés par un chemin de production en `pre
Leur promotion éventuelle en code de production est différée à `pre.006`, exactement au moment où l'actor WebSocket les consommera réellement ; si cette promotion exige `pub(crate)`, elle suivra alors la façade crate-root et les appels `crate::Item`.
## 14. Fermeture `pre.005` et préparation `pre.006`
Le checkpoint opérateur reçu après `pre.005-fix.002` ferme entièrement la tranche transaction-request :
```text
cargo fmt --all OK
python3 scripts/audit_rust_workspace_rules.py clean
cargo check --workspace OK, sans warning
cargo clippy --workspace --all-targets OK, sans warning
cargo test -p ksp-onchain-transport-lib OK 322 unit + 39 public API + 27 completeness + 4 doctests
cargo test --workspace OK
```
`pre.006` promeut uniquement les helpers désormais réellement consommés par le chemin live. Ils restent strictement privés dans leur module lorsque leur consommation est locale ; aucun élargissement de visibilité n'est effectué pour les tests. Les tests continuent d'accéder aux privés du parent via `super::Item`, tandis que les types publics passent par la façade crate-root `crate::Item`.
La tranche ajoute :
```text
WsSubscriptionKind::HeliusTransaction
subscribe method transactionSubscribe
unsubscribe method transactionUnsubscribe
notification method transactionNotification
HeliusLaserStreamWsSession::transaction_subscribe(request)
-> WsSubscription<HeliusTransactionNotification>
HeliusTransactionNotification
Full transaction + signature + slot + transactionIndex
Signature signature + slot + transactionIndex + champs optionnels conservés
Unknown payload provider futur/none conservé sans faire tomber la souscription
```
Le variant `Unknown` est volontairement borné à la valeur `result` de `transactionNotification` : KSP ne publie ni l'enveloppe JSON-RPC complète ni l'identifiant remote de subscription. Le remote ID reste propriété exclusive de l'actor et peut donc être remappé après reconnexion sans changer l'identité locale du handle.
Les canaris locaux `pre.006` doivent prouver ensemble :
```text
[ ] requête live exacte via HeliusLaserStreamWsSession::transaction_subscribe
[ ] transactionNotification Full décodée par le handle public
[ ] formes Signature et Unknown préservées
[ ] transactionUnsubscribe utilise le remote ID courant
[ ] reconnexion renvoie le même params transactionSubscribe
[ ] remote ID 41 -> 99 remappé sans changer WsSubscriptionId
[ ] notification tardive après demande unsubscribe ignorée
[ ] overflow transaction échoue seulement la souscription lente
[ ] cleanup overflow appelle transactionUnsubscribe
[ ] une souscription Helius root saine continue de recevoir ses notifications
[ ] standard 9 familles / 18 opérations non régressées
[ ] aucun heartbeat ajouté avant pre.007
[ ] aucun second actor/socket/registry/queue
[ ] aucune nouvelle dépendance
```
Hors scope inchangé : heartbeat/idle (`pre.007`), adversarial provider/security élargi (`pre.008`), smoke Helius live (`pre.010`) et LaserStream gRPC.

View File

@@ -1,9 +1,9 @@
<!-- file: docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md -->
<!-- version: 15 -->
<!-- version: 16 -->
# Validation `0.2.8` — Helius LaserStream WebSocket
> **Statut : `pre.004` + ses fixes sont validés. `pre.005-fix.001` corrige les erreurs de type et les chemins de visibilité, mais son checkpoint émet encore neuf warnings `dead_code` sur des helpers wire privés non consommés en production. `pre.005-fix.002` les compile uniquement sous `#[cfg(test)]` jusqu'à `pre.006`, sans modifier le contrat public Helius.**
> **Statut : `pre.005` + ses deux fixes sont validés sans warning. `pre.006` est préparé avec `transactionNotification`, handle live, remap reconnect, late-notification handling et backpressure ciblée sur l'actor WebSocket unique.**
## 1. Références
@@ -23,6 +23,7 @@ pre.004 provenance fix deltas/0.2.8/pre.004-fix.002.md
pre.005 deltas/0.2.8/pre.005.md
pre.005 visibility/test fix deltas/0.2.8/pre.005-fix.001.md
pre.005 dead-code fix deltas/0.2.8/pre.005-fix.002.md
pre.006 actor transaction deltas/0.2.8/pre.006.md
validation standard WS docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md
HTTP compliance docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md
KSP-TRANSPORT-007 docs/validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md
@@ -209,21 +210,21 @@ aucun payload brut dans logs/snapshots
[x] Helius facade n'expose aucun escape hatch vers le handle générique
[x] protocol mismatch rejeté avant I/O — fixture opérateur verte
[x] 6 familles standard Helius utilisent le wire standard exact — fixture `pre.003` opérateur verte
[ ] transaction subscribe/ack exact — fixture `pre.005` préparée, gate opérateur requis
[ ] transaction unsubscribe/result exact — fixture `pre.005` préparée, gate opérateur requis
[ ] transaction notification dispatch exact
[x] transaction subscribe/ack exact — fixture `pre.005` validée puis consommée par le handle live `pre.006`
[x] transaction unsubscribe/result exact — fixture `pre.005` validée puis registry actor `pre.006`
[ ] transaction notification dispatch exact — canari `pre.006` préparé
[ ] notification decode failure isole la logical subscription
[ ] provider RPC application error ne tue pas la session
[ ] late notification après unsubscribe ne réactive rien
[ ] reconnect invalide/remappe remote ids
[ ] resubscribe garde local id
[ ] late notification après unsubscribe ne réactive rien — canari `pre.006` préparé
[ ] reconnect invalide/remappe remote ids — canari `pre.006` 41 -> 99 préparé
[ ] resubscribe garde local id — canari `pre.006` préparé
[ ] unsubscribe pendant reconnect gagne
[ ] heartbeat n'écrit qu'en Active
[ ] heartbeat est annulé au close
[ ] heartbeat write failure suit reconnect borné
[ ] shutdown interrompt heartbeat/backoff
[ ] oversized inbound frame reste borné
[ ] queue overflow d'une transaction subscription n'affecte pas les autres
[ ] queue overflow d'une transaction subscription n'affecte pas les autres — canari transaction + root `pre.006` préparé
[ ] payload/filters/api-key absents de Debug/snapshots sûrs
```
@@ -519,5 +520,54 @@ Critères `pre.005-fix.002` :
[ ] tests Transport/workspace verts
```
Verdict courant : **`pre.005` FIX REQUIRED ; `pre.005-fix.001` APPLIED ; `pre.005-fix.002` PREPARED.**
Verdict après checkpoint final : **`pre.005` DONE ; `pre.005-fix.001` DONE ; `pre.005-fix.002` DONE.**
## 16. Gate `pre.005-fix.002` fermé et préparation `pre.006`
Checkpoint opérateur final `pre.005` reçu le 2026-08-23 :
```text
[x] cargo fmt --all
[x] python3 scripts/audit_rust_workspace_rules.py = clean
[x] cargo check --workspace = sans warning
[x] cargo clippy --workspace --all-targets = sans warning
[x] cargo test -p ksp-onchain-transport-lib = 322 unit + 39 public API + 27 completeness + 4 doctests
[x] cargo test --workspace = vert
```
Verdict : **`pre.005` et ses deux fixes sont fermés.**
Critères préparés pour `pre.006` :
```text
[ ] WsSubscriptionKind::HeliusTransaction mappe exactement subscribe/unsubscribe/notification
[ ] HeliusLaserStreamWsSession::transaction_subscribe retourne WsSubscription<HeliusTransactionNotification>
[ ] notification Full conserve transaction/signature/slot/transactionIndex
[ ] notification Signature conserve signature/slot/transactionIndex et états optionnels
[ ] forme inconnue reste HeliusTransactionNotification::Unknown sans casser le handle
[ ] l'identité locale du handle reste stable après reconnect/resubscribe
[ ] le remote ID est remappé et jamais exposé publiquement
[ ] notification Helius en vol après transactionUnsubscribe est ignorée après retrait du mapping
[ ] overflow du handle transaction est terminal seulement pour ce handle
[ ] cleanup overflow envoie transactionUnsubscribe avec le remote ID courant
[ ] une souscription Helius root saine reste active pendant l'overflow transaction
[ ] quatre compile-fail Helius block/slotsUpdates/vote/escape-hatch restent verts
[ ] aucun heartbeat avant pre.007
[ ] aucun second actor/socket/registry/queue
[ ] aucun nouvel accès non canonique private/pub/pub(crate) dans unit_tests
```
Gate opérateur `pre.006` :
```text
[ ] cargo fmt --all
[ ] python3 scripts/audit_rust_workspace_rules.py = clean
[ ] cargo check --workspace = sans warning
[ ] cargo clippy --workspace --all-targets = sans warning
[ ] cargo test -p ksp-onchain-transport-lib
[ ] cargo test --workspace
```
Comptages attendus si les nouveaux canaris passent : **325 tests unitaires Transport**, **40 public API**, **28 release-completeness**, **4 doctests compile-fail**.
Verdict courant : **`pre.006` PREPARED.**