v0.2.8-pre.005

This commit is contained in:
2026-08-23 14:49:56 +02:00
parent cbb4e7b0de
commit 92224e5ac6
10 changed files with 1238 additions and 60 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 30
// version: 31
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -26,6 +26,9 @@
//! `0.2.7-pre.009` opens the first stable typed WebSocket wrappers for account, program-account and transaction-log subscriptions without exposing a raw
//! provider-extension subscription API. `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.
mod client;
mod constants;
@@ -47,6 +50,7 @@ mod settings;
mod ws_accounts;
mod ws_blocks;
mod ws_cluster;
mod ws_helius_transactions;
mod ws_lifecycle;
mod ws_protocol_session;
mod ws_session;
@@ -338,6 +342,16 @@ 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;
/// Helius `tokenAccounts` expansion mode accepted by `transactionSubscribe`.
pub use self::ws_helius_transactions::HeliusTokenAccountsFilter;
/// Transaction encoding accepted by Helius `transactionSubscribe`.
pub use self::ws_helius_transactions::HeliusTransactionSubscribeEncoding;
/// Helius-specific filter object accepted as the first `transactionSubscribe` parameter.
pub use self::ws_helius_transactions::HeliusTransactionSubscribeFilter;
/// Optional Helius `transactionSubscribe` result-shaping configuration.
pub use self::ws_helius_transactions::HeliusTransactionSubscribeOptions;
/// Complete typed request contract for Helius `transactionSubscribe` before actor registration.
pub use self::ws_helius_transactions::HeliusTransactionSubscribeRequest;
/// Stable local identity assigned to one physical WebSocket session.
pub use self::ws_lifecycle::WsSessionId;
/// Safe runtime snapshot for one physical WebSocket session.
@@ -401,6 +415,16 @@ pub(crate) use self::rpc_common::decode_wire_json;
pub(crate) use self::rpc_common::parse_wire_pubkey;
/// Validates endpoint settings.
pub(crate) use self::settings::validate_endpoint_settings;
/// Crate-internal decoder for Helius transaction-subscribe acknowledgement IDs.
pub(crate) use self::ws_helius_transactions::decode_helius_transaction_subscribe_result;
/// Crate-internal decoder for Helius transaction-unsubscribe boolean results.
pub(crate) use self::ws_helius_transactions::decode_helius_transaction_unsubscribe_result;
/// Crate-internal exact Helius transaction-subscribe method descriptor.
pub(crate) use self::ws_helius_transactions::helius_transaction_subscribe_method;
/// Crate-internal exact Helius transaction-unsubscribe method descriptor.
pub(crate) use self::ws_helius_transactions::helius_transaction_unsubscribe_method;
/// Crate-internal Helius transaction-unsubscribe parameter encoder.
pub(crate) use self::ws_helius_transactions::helius_transaction_unsubscribe_params;
/// Crate-internal command surface shared by the physical session and typed subscription handle.
pub(crate) use self::ws_session::WsSessionCommand;
/// Crate-internal notification dispatch result.

View File

@@ -0,0 +1,421 @@
// file: crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
// version: 1
const MAX_HELIUS_TRANSACTION_FILTER_ACCOUNTS: usize = 50_000;
/// Helius `tokenAccounts` expansion mode accepted by `transactionSubscribe`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum HeliusTokenAccountsFilter {
/// Disable token-account owner expansion explicitly; equivalent to omitting `tokenAccounts`.
None,
/// Match transactions where a token balance owned by an included account changes or its token account closes.
BalanceChanged,
/// Match transactions referencing any token account owned by an included account, even if the balance does not change.
All,
}
impl HeliusTokenAccountsFilter {
/// Returns the exact Helius WebSocket wire string.
#[must_use]
pub const fn as_str(self) -> &'static str {
return match self {
Self::None => "none",
Self::BalanceChanged => "balanceChanged",
Self::All => "all",
};
}
}
/// Transaction encoding accepted by Helius `transactionSubscribe`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum HeliusTransactionSubscribeEncoding {
/// Base58 encoded transaction bytes.
Base58,
/// Base64 encoded transaction bytes.
Base64,
/// Parsed JSON transaction representation.
JsonParsed,
}
impl HeliusTransactionSubscribeEncoding {
/// Returns the exact Helius WebSocket wire string.
#[must_use]
pub const fn as_str(self) -> &'static str {
return match self {
Self::Base58 => "base58",
Self::Base64 => "base64",
Self::JsonParsed => "jsonParsed",
};
}
}
/// Helius-specific filter object accepted as the first `transactionSubscribe` parameter.
///
/// Debug output intentionally exposes only filter presence, modes and account counts. Transaction signatures and account values are omitted so routine
/// diagnostics cannot accidentally disclose the caller's complete provider filter payload.
#[derive(Clone, Default, Eq, PartialEq)]
pub struct HeliusTransactionSubscribeFilter {
vote: std::option::Option<bool>,
failed: std::option::Option<bool>,
signature: std::option::Option<std::string::String>,
account_include: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
account_exclude: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
account_required: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
token_accounts: std::option::Option<crate::HeliusTokenAccountsFilter>,
}
impl HeliusTransactionSubscribeFilter {
/// Creates a complete Helius transaction filter while preserving omitted versus explicitly empty account arrays.
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
vote: std::option::Option<bool>,
failed: std::option::Option<bool>,
signature: std::option::Option<std::string::String>,
account_include: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
account_exclude: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
account_required: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
token_accounts: std::option::Option<crate::HeliusTokenAccountsFilter>,
) -> Self {
return Self { vote, failed, signature, account_include, account_exclude, account_required, token_accounts };
}
/// Returns the optional vote-transaction filter flag.
#[must_use]
pub const fn vote(&self) -> std::option::Option<bool> {
return self.vote;
}
/// Returns the optional failed-transaction filter flag.
#[must_use]
pub const fn failed(&self) -> std::option::Option<bool> {
return self.failed;
}
/// Returns the optional exact transaction signature filter.
#[must_use]
pub fn signature(&self) -> std::option::Option<&str> {
return match self.signature.as_ref() {
std::option::Option::Some(signature) => std::option::Option::Some(signature.as_str()),
std::option::Option::None => std::option::Option::None,
};
}
/// Returns the optional OR-style account inclusion list.
#[must_use]
pub fn account_include(&self) -> std::option::Option<&[ksp_core_lib::Pubkey]> {
return match self.account_include.as_ref() {
std::option::Option::Some(accounts) => std::option::Option::Some(accounts.as_slice()),
std::option::Option::None => std::option::Option::None,
};
}
/// Returns the optional account exclusion list.
#[must_use]
pub fn account_exclude(&self) -> std::option::Option<&[ksp_core_lib::Pubkey]> {
return match self.account_exclude.as_ref() {
std::option::Option::Some(accounts) => std::option::Option::Some(accounts.as_slice()),
std::option::Option::None => std::option::Option::None,
};
}
/// Returns the optional AND-style required-account list.
#[must_use]
pub fn account_required(&self) -> std::option::Option<&[ksp_core_lib::Pubkey]> {
return match self.account_required.as_ref() {
std::option::Option::Some(accounts) => std::option::Option::Some(accounts.as_slice()),
std::option::Option::None => std::option::Option::None,
};
}
/// Returns the optional Helius token-account owner-expansion mode.
#[must_use]
pub const fn token_accounts(&self) -> std::option::Option<crate::HeliusTokenAccountsFilter> {
return self.token_accounts;
}
fn validate(&self) -> ksp_core_lib::Result<()> {
let include = validate_account_list("accountInclude", self.account_include.as_deref());
if let std::result::Result::Err(error) = include {
return std::result::Result::Err(error);
}
let exclude = validate_account_list("accountExclude", self.account_exclude.as_deref());
if let std::result::Result::Err(error) = exclude {
return std::result::Result::Err(error);
}
let required = validate_account_list("accountRequired", self.account_required.as_deref());
if let std::result::Result::Err(error) = required {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
fn to_json_value(&self) -> serde_json::Value {
let mut object = serde_json::Map::new();
if let std::option::Option::Some(vote) = self.vote {
object.insert("vote".to_owned(), serde_json::Value::Bool(vote));
}
if let std::option::Option::Some(failed) = self.failed {
object.insert("failed".to_owned(), serde_json::Value::Bool(failed));
}
if let std::option::Option::Some(signature) = self.signature.as_ref() {
object.insert("signature".to_owned(), serde_json::Value::String(signature.clone()));
}
insert_account_list(&mut object, "accountInclude", self.account_include.as_deref());
insert_account_list(&mut object, "accountExclude", self.account_exclude.as_deref());
insert_account_list(&mut object, "accountRequired", self.account_required.as_deref());
if let std::option::Option::Some(token_accounts) = self.token_accounts {
object.insert("tokenAccounts".to_owned(), serde_json::Value::String(token_accounts.as_str().to_owned()));
}
return serde_json::Value::Object(object);
}
}
impl std::fmt::Debug for HeliusTransactionSubscribeFilter {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("HeliusTransactionSubscribeFilter")
.field("vote", &self.vote)
.field("failed", &self.failed)
.field("signature_present", &self.signature.is_some())
.field("account_include_count", &self.account_include.as_ref().map(std::vec::Vec::len))
.field("account_exclude_count", &self.account_exclude.as_ref().map(std::vec::Vec::len))
.field("account_required_count", &self.account_required.as_ref().map(std::vec::Vec::len))
.field("token_accounts", &self.token_accounts)
.finish();
}
}
/// Optional Helius `transactionSubscribe` result-shaping configuration.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct HeliusTransactionSubscribeOptions {
commitment: std::option::Option<crate::SolanaCommitment>,
encoding: std::option::Option<crate::HeliusTransactionSubscribeEncoding>,
transaction_details: std::option::Option<crate::SolanaTransactionDetails>,
show_rewards: std::option::Option<bool>,
max_supported_transaction_version: std::option::Option<u8>,
}
impl HeliusTransactionSubscribeOptions {
/// Creates a complete optional Helius transaction-subscription configuration.
#[must_use]
pub const fn new(
commitment: std::option::Option<crate::SolanaCommitment>,
encoding: std::option::Option<crate::HeliusTransactionSubscribeEncoding>,
transaction_details: std::option::Option<crate::SolanaTransactionDetails>,
show_rewards: std::option::Option<bool>,
max_supported_transaction_version: std::option::Option<u8>,
) -> Self {
return Self { commitment, encoding, transaction_details, show_rewards, max_supported_transaction_version };
}
/// Returns the optional commitment level.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns the optional Helius transaction encoding.
#[must_use]
pub const fn encoding(&self) -> std::option::Option<crate::HeliusTransactionSubscribeEncoding> {
return self.encoding;
}
/// Returns the optional transaction detail level.
#[must_use]
pub const fn transaction_details(&self) -> std::option::Option<crate::SolanaTransactionDetails> {
return self.transaction_details;
}
/// Returns whether rewards were explicitly requested.
#[must_use]
pub const fn show_rewards(&self) -> std::option::Option<bool> {
return self.show_rewards;
}
/// Returns the highest transaction version the caller declares it can consume.
#[must_use]
pub const fn max_supported_transaction_version(&self) -> std::option::Option<u8> {
return self.max_supported_transaction_version;
}
fn validate(&self) -> ksp_core_lib::Result<()> {
let requires_version =
matches!(self.transaction_details, std::option::Option::Some(crate::SolanaTransactionDetails::Full | crate::SolanaTransactionDetails::Accounts));
if requires_version && self.max_supported_transaction_version.is_none() {
let detail = match self.transaction_details {
std::option::Option::Some(detail) => detail.as_str(),
std::option::Option::None => "omitted",
};
return std::result::Result::Err(
ksp_core_lib::Error::new(
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
"Helius transactionSubscribe requires maxSupportedTransactionVersion for full or accounts transaction details",
)
.with_context("rpc_method", "transactionSubscribe")
.with_context("field", "maxSupportedTransactionVersion")
.with_context("transaction_details", detail),
);
}
return std::result::Result::Ok(());
}
fn to_json_value(self) -> serde_json::Value {
let mut object = serde_json::Map::new();
if let std::option::Option::Some(commitment) = self.commitment {
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
}
if let std::option::Option::Some(encoding) = self.encoding {
object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned()));
}
if let std::option::Option::Some(transaction_details) = self.transaction_details {
object.insert("transactionDetails".to_owned(), serde_json::Value::String(transaction_details.as_str().to_owned()));
}
if let std::option::Option::Some(show_rewards) = self.show_rewards {
object.insert("showRewards".to_owned(), serde_json::Value::Bool(show_rewards));
}
if let std::option::Option::Some(version) = self.max_supported_transaction_version {
object.insert("maxSupportedTransactionVersion".to_owned(), serde_json::Value::Number(version.into()));
}
return serde_json::Value::Object(object);
}
}
/// Complete typed request contract for Helius `transactionSubscribe` before actor registration.
///
/// 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.
#[derive(Clone, Eq, PartialEq)]
pub struct HeliusTransactionSubscribeRequest {
filter: crate::HeliusTransactionSubscribeFilter,
options: std::option::Option<crate::HeliusTransactionSubscribeOptions>,
}
impl HeliusTransactionSubscribeRequest {
/// Creates one typed Helius transaction-subscription request.
#[must_use]
pub fn new(filter: crate::HeliusTransactionSubscribeFilter, options: std::option::Option<crate::HeliusTransactionSubscribeOptions>) -> Self {
return Self { filter, options };
}
/// Returns the provider transaction filter.
#[must_use]
pub const fn filter(&self) -> &crate::HeliusTransactionSubscribeFilter {
return &self.filter;
}
/// Returns the optional provider result-shaping configuration.
#[must_use]
pub const fn options(&self) -> std::option::Option<&crate::HeliusTransactionSubscribeOptions> {
return self.options.as_ref();
}
/// Validates deterministic Helius request constraints before any WebSocket I/O.
pub fn validate(&self) -> ksp_core_lib::Result<()> {
let filter = self.filter.validate();
if let std::result::Result::Err(error) = filter {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(options) = self.options {
let options = options.validate();
if let std::result::Result::Err(error) = options {
return std::result::Result::Err(error);
}
}
return std::result::Result::Ok(());
}
/// Builds the exact JSON-RPC params array after deterministic validation.
#[allow(dead_code)] // Consumed by actor-owned transaction subscription registration in pre.006.
pub(crate) fn to_params(&self) -> ksp_core_lib::Result<std::vec::Vec<serde_json::Value>> {
let validation = self.validate();
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let mut params = std::vec![self.filter.to_json_value()];
if let std::option::Option::Some(options) = self.options {
params.push(options.to_json_value());
}
return std::result::Result::Ok(params);
}
}
impl std::fmt::Debug for HeliusTransactionSubscribeRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("HeliusTransactionSubscribeRequest").field("filter", &self.filter).field("options", &self.options).finish();
}
}
/// Returns the exact Helius transaction-subscribe JSON-RPC method name.
#[allow(dead_code)] // Consumed by actor-owned transaction subscription registration in pre.006.
pub(crate) const fn helius_transaction_subscribe_method() -> &'static str {
return "transactionSubscribe";
}
/// Returns the exact Helius transaction-unsubscribe JSON-RPC method name.
#[allow(dead_code)] // Consumed by actor-owned transaction subscription cleanup in pre.006.
pub(crate) const fn helius_transaction_unsubscribe_method() -> &'static str {
return "transactionUnsubscribe";
}
/// Decodes a successful Helius transaction-subscribe acknowledgement without exposing the remote ID publicly.
#[allow(dead_code)] // Consumed by actor-owned transaction subscription registration in pre.006.
pub(crate) 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"),
),
};
}
/// Builds the exact Helius transaction-unsubscribe params array for one actor-owned remote subscription ID.
#[allow(dead_code)] // Consumed by actor-owned transaction subscription cleanup in pre.006.
pub(crate) fn helius_transaction_unsubscribe_params(remote_id: u64) -> std::vec::Vec<serde_json::Value> {
return std::vec![serde_json::Value::Number(remote_id.into())];
}
/// Decodes the boolean Helius transaction-unsubscribe result.
#[allow(dead_code)] // Consumed by actor-owned transaction subscription cleanup in pre.006.
pub(crate) 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"),
),
};
}
fn validate_account_list(field: &'static str, accounts: std::option::Option<&[ksp_core_lib::Pubkey]>) -> ksp_core_lib::Result<()> {
if let std::option::Option::Some(accounts) = accounts
&& accounts.len() > MAX_HELIUS_TRANSACTION_FILTER_ACCOUNTS
{
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Helius transactionSubscribe account filter exceeds the provider limit")
.with_context("rpc_method", "transactionSubscribe")
.with_context("field", field)
.with_context("actual_count", accounts.len().to_string())
.with_context("max_count", MAX_HELIUS_TRANSACTION_FILTER_ACCOUNTS.to_string()),
);
}
return std::result::Result::Ok(());
}
fn insert_account_list(
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
field: &'static str,
accounts: std::option::Option<&[ksp_core_lib::Pubkey]>,
) {
if let std::option::Option::Some(accounts) = accounts {
let values = accounts.iter().map(|account| return serde_json::Value::String(account.to_string())).collect::<std::vec::Vec<_>>();
object.insert(field.to_owned(), serde_json::Value::Array(values));
}
return;
}
#[cfg(test)]
#[path = "../unit_tests/ws_helius_transactions.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
// version: 2
// version: 3
/// Typed facade for one standard Solana WebSocket physical session.
///
@@ -58,9 +58,10 @@ impl std::fmt::Debug for SolanaStandardWsSession {
/// Typed facade for one Helius LaserStream WebSocket physical session.
///
/// The facade exposes only the six standard Solana subscription families that Helius documents as supported. Provider-specific transaction subscription
/// support is intentionally deferred to a later tranche. No public inner handle is exposed, so callers cannot bypass the provider-specific surface by
/// recovering a generic [`crate::WsSession`].
/// 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
/// provider-specific surface by recovering a generic [`crate::WsSession`].
///
/// ```compile_fail
/// async fn unsupported_block(session: &ksp_onchain_transport_lib::HeliusLaserStreamWsSession) {