v0.2.8-pre.006
This commit is contained in:
@@ -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>,
|
||||
}
|
||||
|
||||
#[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())];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
#[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"),
|
||||
),
|
||||
};
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireHeliusFullTransactionNotification {
|
||||
transaction: serde_json::Value,
|
||||
signature: std::string::String,
|
||||
slot: u64,
|
||||
transaction_index: u64,
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user