v0.2.8-pre.006
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
235
deltas/0.2.8/pre.006.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.**
|
||||
|
||||
Reference in New Issue
Block a user