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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 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) {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 35
// version: 36
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -713,3 +713,34 @@ fn public_v0_2_8_pre_003_helius_standard_surface_reuses_shared_typed_contracts()
let _shared_signature_config = std::any::type_name::<ksp_onchain_transport_lib::SolanaSignatureSubscribeConfig>();
let _shared_slot_notification = std::any::type_name::<ksp_onchain_transport_lib::SolanaSlotNotification>();
}
#[test]
fn public_v0_2_8_pre_005_helius_transaction_request_contract_is_available_without_live_handle() {
let account = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
std::option::Option::Some(false),
std::option::Option::Some(false),
std::option::Option::Some("fixture-signature".to_owned()),
std::option::Option::Some(std::vec![account]),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged),
);
let options = ksp_onchain_transport_lib::HeliusTransactionSubscribeOptions::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeEncoding::JsonParsed),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
std::option::Option::Some(false),
std::option::Option::Some(0),
);
let request = ksp_onchain_transport_lib::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options));
assert!(request.validate().is_ok());
assert_eq!(request.filter().token_accounts(), std::option::Option::Some(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged));
assert_eq!(
request.options().and_then(ksp_onchain_transport_lib::HeliusTransactionSubscribeOptions::encoding),
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeEncoding::JsonParsed)
);
assert_eq!(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::None.as_str(), "none");
assert_eq!(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged.as_str(), "balanceChanged");
assert_eq!(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::All.as_str(), "all");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 26
// version: 27
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
@@ -811,3 +811,28 @@ fn release_v0_2_8_pre_003_helius_surface_is_exactly_six_standard_families_before
assert!(!source.contains("transaction_subscribe"));
assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream");
}
#[test]
fn release_v0_2_8_pre_005_helius_transaction_request_surface_is_typed_before_actor_integration() {
let filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::All),
);
let options = ksp_onchain_transport_lib::HeliusTransactionSubscribeOptions::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeEncoding::Base64),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Signatures),
std::option::Option::Some(true),
std::option::Option::None,
);
let request = ksp_onchain_transport_lib::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options));
assert!(request.validate().is_ok());
let facade_source = include_str!("../src/ws_protocol_session.rs");
assert!(!facade_source.contains("pub async fn transaction_subscribe"));
assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream");
}

View File

@@ -0,0 +1,326 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs
// version: 2
use futures_util::SinkExt; // rust-rules: trait-import
use futures_util::StreamExt; // rust-rules: trait-import
fn pubkey(value: &str) -> ksp_core_lib::Pubkey {
return value.parse::<ksp_core_lib::Pubkey>().expect("fixture public key must parse");
}
fn base_filter() -> crate::HeliusTransactionSubscribeFilter {
return crate::HeliusTransactionSubscribeFilter::new(
std::option::Option::Some(false),
std::option::Option::Some(false),
std::option::Option::Some("fixture-signature-secret-canary".to_owned()),
std::option::Option::Some(std::vec![pubkey("11111111111111111111111111111111")]),
std::option::Option::Some(std::vec![pubkey("SysvarC1ock11111111111111111111111111111111")]),
std::option::Option::Some(std::vec![pubkey("Vote111111111111111111111111111111111111111")]),
std::option::Option::Some(crate::HeliusTokenAccountsFilter::BalanceChanged),
);
}
fn assert_oversized_filter_rejected(filter: crate::HeliusTransactionSubscribeFilter) {
let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::None);
let error = request.validate().expect_err("50,001 Helius account filters must fail before I/O");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
assert!(error.to_string().contains("invalid_rpc_parameters"));
assert!(!error.to_string().contains("11111111111111111111111111111111"));
}
fn helius_endpoint(url: &str) -> crate::WsEndpointSettings {
return crate::WsEndpointSettings::new(
"local_helius_transaction_fixture",
true,
crate::WsProviderName::new("helius"),
crate::WsClusterName::new("local"),
crate::WsProtocolKind::HeliusLaserStream,
crate::WsEndpointUrl::parse(url).expect("local Helius WebSocket URL must parse"),
crate::WsSessionSettings::default(),
);
}
async fn bind_local_listener() -> (tokio::net::TcpListener, std::string::String) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("local listener must bind");
let address = listener.local_addr().expect("local listener must expose address");
return (listener, format!("ws://{address}"));
}
async fn read_request(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> serde_json::Value {
let message = websocket.next().await.expect("request message must exist").expect("request message must decode");
let text = message.to_text().expect("request must be text");
return serde_json::from_str(text).expect("request must contain JSON");
}
async fn send_result(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, request: &serde_json::Value, result: serde_json::Value) {
let id = request.get("id").and_then(serde_json::Value::as_u64).expect("request id must be numeric");
let response = serde_json::json!({"jsonrpc":"2.0","id":id,"result":result});
websocket.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await.expect("local response must send");
return;
}
async fn wait_for_close_frame(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) {
loop {
let message = websocket.next().await;
match message {
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
std::option::Option::Some(std::result::Result::Ok(_)) => {},
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
}
}
}
#[test]
fn helius_transaction_filter_and_option_enums_match_documented_wire_labels() {
assert_eq!(crate::HeliusTokenAccountsFilter::None.as_str(), "none");
assert_eq!(crate::HeliusTokenAccountsFilter::BalanceChanged.as_str(), "balanceChanged");
assert_eq!(crate::HeliusTokenAccountsFilter::All.as_str(), "all");
assert_eq!(crate::HeliusTransactionSubscribeEncoding::Base58.as_str(), "base58");
assert_eq!(crate::HeliusTransactionSubscribeEncoding::Base64.as_str(), "base64");
assert_eq!(crate::HeliusTransactionSubscribeEncoding::JsonParsed.as_str(), "jsonParsed");
}
#[test]
fn helius_transaction_subscribe_request_serializes_complete_documented_filter_and_options() {
let filter = base_filter();
let options = crate::HeliusTransactionSubscribeOptions::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::HeliusTransactionSubscribeEncoding::JsonParsed),
std::option::Option::Some(crate::SolanaTransactionDetails::Accounts),
std::option::Option::Some(true),
std::option::Option::Some(0),
);
let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options));
let params = request.to_params().expect("complete documented Helius request must validate");
assert_eq!(
params,
serde_json::json!([
{
"vote": false,
"failed": false,
"signature": "fixture-signature-secret-canary",
"accountInclude": ["11111111111111111111111111111111"],
"accountExclude": ["SysvarC1ock11111111111111111111111111111111"],
"accountRequired": ["Vote111111111111111111111111111111111111111"],
"tokenAccounts": "balanceChanged"
},
{
"commitment": "confirmed",
"encoding": "jsonParsed",
"transactionDetails": "accounts",
"showRewards": true,
"maxSupportedTransactionVersion": 0
}
])
);
assert_eq!(request.filter().vote(), std::option::Option::Some(false));
assert_eq!(request.filter().failed(), std::option::Option::Some(false));
assert_eq!(request.filter().signature(), std::option::Option::Some("fixture-signature-secret-canary"));
assert_eq!(request.filter().account_include().map(<[ksp_core_lib::Pubkey]>::len), std::option::Option::Some(1));
assert_eq!(request.filter().account_exclude().map(<[ksp_core_lib::Pubkey]>::len), std::option::Option::Some(1));
assert_eq!(request.filter().account_required().map(<[ksp_core_lib::Pubkey]>::len), std::option::Option::Some(1));
assert_eq!(request.filter().token_accounts(), std::option::Option::Some(crate::HeliusTokenAccountsFilter::BalanceChanged));
let options = request.options().expect("options must remain available");
assert_eq!(options.commitment(), std::option::Option::Some(crate::SolanaCommitment::Confirmed));
assert_eq!(options.encoding(), std::option::Option::Some(crate::HeliusTransactionSubscribeEncoding::JsonParsed));
assert_eq!(options.transaction_details(), std::option::Option::Some(crate::SolanaTransactionDetails::Accounts));
assert_eq!(options.show_rewards(), std::option::Option::Some(true));
assert_eq!(options.max_supported_transaction_version(), std::option::Option::Some(0));
}
#[test]
fn helius_transaction_request_preserves_omitted_explicit_empty_and_explicit_none_states() {
let omitted = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::None);
assert_eq!(omitted.to_params().expect("fully omitted optional request must validate"), serde_json::json!([{}]));
let explicit = crate::HeliusTransactionSubscribeRequest::new(
crate::HeliusTransactionSubscribeFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(std::vec::Vec::new()),
std::option::Option::Some(std::vec::Vec::new()),
std::option::Option::Some(std::vec::Vec::new()),
std::option::Option::Some(crate::HeliusTokenAccountsFilter::None),
),
std::option::Option::Some(crate::HeliusTransactionSubscribeOptions::default()),
);
assert_eq!(
explicit.to_params().expect("explicit empty Helius request states must validate"),
serde_json::json!([{"accountInclude":[],"accountExclude":[],"accountRequired":[],"tokenAccounts":"none"}, {}])
);
}
#[test]
fn helius_transaction_filter_enforces_each_documented_fifty_thousand_account_bound() {
let key = pubkey("11111111111111111111111111111111");
let maximum = std::vec![key; 50_000];
let accepted = crate::HeliusTransactionSubscribeRequest::new(
crate::HeliusTransactionSubscribeFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(maximum),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
),
std::option::Option::None,
);
assert!(accepted.validate().is_ok());
assert_oversized_filter_rejected(crate::HeliusTransactionSubscribeFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(std::vec![key; 50_001]),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
));
assert_oversized_filter_rejected(crate::HeliusTransactionSubscribeFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(std::vec![key; 50_001]),
std::option::Option::None,
std::option::Option::None,
));
assert_oversized_filter_rejected(crate::HeliusTransactionSubscribeFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(std::vec![key; 50_001]),
std::option::Option::None,
));
}
#[test]
fn helius_transaction_details_require_max_supported_version_only_for_accounts_and_full() {
for details in [crate::SolanaTransactionDetails::Full, crate::SolanaTransactionDetails::Accounts] {
let options = crate::HeliusTransactionSubscribeOptions::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(details),
std::option::Option::None,
std::option::Option::None,
);
let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::Some(options));
let error = request.validate().expect_err("full/accounts details must require maxSupportedTransactionVersion");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
}
for details in [crate::SolanaTransactionDetails::Signatures, crate::SolanaTransactionDetails::None] {
let options = crate::HeliusTransactionSubscribeOptions::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(details),
std::option::Option::None,
std::option::Option::None,
);
let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::Some(options));
assert!(request.validate().is_ok());
}
let full_with_version = crate::HeliusTransactionSubscribeOptions::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
std::option::Option::None,
std::option::Option::Some(0),
);
let request =
crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::Some(full_with_version));
assert!(request.validate().is_ok());
}
#[test]
fn helius_transaction_subscribe_and_unsubscribe_control_wire_is_exact() {
assert_eq!(crate::helius_transaction_subscribe_method(), "transactionSubscribe");
assert_eq!(crate::helius_transaction_unsubscribe_method(), "transactionUnsubscribe");
assert_eq!(
crate::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!(crate::helius_transaction_unsubscribe_params(4_743_323_479_349_712), serde_json::json!([4_743_323_479_349_712_u64]));
assert!(crate::decode_helius_transaction_unsubscribe_result(serde_json::json!(true)).expect("boolean true must decode"));
assert!(!crate::decode_helius_transaction_unsubscribe_result(serde_json::json!(false)).expect("boolean false must decode"));
assert_eq!(
crate::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!(
crate::decode_helius_transaction_unsubscribe_result(serde_json::json!(1)).expect_err("non-boolean unsubscribe ack must fail").code(),
crate::ERROR_CODE_INVALID_RESPONSE
);
}
#[test]
fn helius_transaction_filter_debug_omits_signature_and_account_values() {
let filter = base_filter();
let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::None);
let debug = format!("{request:?}");
assert!(debug.contains("signature_present"));
assert!(debug.contains("account_include_count"));
assert!(!debug.contains("fixture-signature-secret-canary"));
assert!(!debug.contains("11111111111111111111111111111111"));
assert!(!debug.contains("SysvarC1ock11111111111111111111111111111111"));
assert!(!debug.contains("Vote111111111111111111111111111111111111111"));
}
#[tokio::test(flavor = "current_thread")]
async fn helius_transaction_control_wire_round_trips_through_shared_physical_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");
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
let subscribe = read_request(&mut websocket).await;
assert_eq!(subscribe["method"], serde_json::json!("transactionSubscribe"));
assert_eq!(
subscribe["params"],
serde_json::json!([
{"failed":false,"accountInclude":["11111111111111111111111111111111"],"tokenAccounts":"balanceChanged"},
{"commitment":"confirmed","encoding":"jsonParsed","transactionDetails":"full","showRewards":false,"maxSupportedTransactionVersion":0}
])
);
send_result(&mut websocket, &subscribe, serde_json::json!(4242)).await;
let unsubscribe = read_request(&mut websocket).await;
assert_eq!(unsubscribe["method"], serde_json::json!("transactionUnsubscribe"));
assert_eq!(unsubscribe["params"], serde_json::json!([4242]));
send_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await;
wait_for_close_frame(&mut websocket).await;
});
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint(url.as_str())).await.expect("Helius facade must connect");
let filter = crate::HeliusTransactionSubscribeFilter::new(
std::option::Option::None,
std::option::Option::Some(false),
std::option::Option::None,
std::option::Option::Some(std::vec![pubkey("11111111111111111111111111111111")]),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::HeliusTokenAccountsFilter::BalanceChanged),
);
let options = crate::HeliusTransactionSubscribeOptions::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::HeliusTransactionSubscribeEncoding::JsonParsed),
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
std::option::Option::Some(false),
std::option::Option::Some(0),
);
let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options));
let params = request.to_params().expect("typed Helius request must validate before I/O");
let subscribe_result = session
.physical_session()
.execute_json_rpc(crate::helius_transaction_subscribe_method(), params)
.await
.expect("transactionSubscribe acknowledgement must arrive");
let remote_id = crate::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(crate::helius_transaction_unsubscribe_method(), crate::helius_transaction_unsubscribe_params(remote_id))
.await
.expect("transactionUnsubscribe acknowledgement must arrive");
assert!(crate::decode_helius_transaction_unsubscribe_result(unsubscribe_result).expect("transactionUnsubscribe boolean must decode"));
session.close().await.expect("Helius fixture session must close");
server.await.expect("local Helius transaction server must finish");
}

277
deltas/0.2.8/pre.005.md Normal file
View File

@@ -0,0 +1,277 @@
<!-- file: deltas/0.2.8/pre.005.md -->
<!-- version: 1 -->
# Delta `0.2.8-pre.005` — contrat typed Helius `transactionSubscribe`
## 1. Objet
Cette tranche matérialise le contrat de requête Helius LaserStream WebSocket `transactionSubscribe` : filtres, options, `tokenAccounts`, validations déterministes, acknowledgement numérique et wire `transactionUnsubscribe`.
Elle ne publie volontairement **pas encore** de handle live transaction : `transactionNotification`, le registry actor, les remaps de remote IDs et les races reconnect/unsubscribe doivent arriver atomiquement en `pre.006` afin de ne jamais exposer un abonnement public incapable de livrer correctement ses notifications.
Le checkpoint opérateur de `pre.004-fix.002` est intégralement vert.
Version workspace :
```text
0.2.8-pre.5
```
Livraison / commit attendu :
```text
0.2.8-pre.005
v0.2.8-pre.005
```
Aucun tag prerelease.
## 2. Audit Helius courant verrouillé
La documentation Helius relue le 2026-08-23 confirme pour `transactionSubscribe` :
```text
filter.vote bool optionnel
filter.failed bool optionnel
filter.signature signature exacte optionnelle
filter.accountInclude liste OR, <= 50_000 adresses
filter.accountExclude liste d'exclusion, <= 50_000 adresses
filter.accountRequired liste AND, <= 50_000 adresses
filter.tokenAccounts none | balanceChanged | all
options.commitment processed | confirmed | finalized
options.encoding base58 | base64 | jsonParsed
options.transactionDetails full | signatures | accounts | none
options.showRewards bool optionnel
options.maxSupportedTransactionVersion
requis pour transactionDetails = accounts | full
subscribe result integer subscription id
unsubscribe params [subscriptionId]
unsubscribe result bool
late notifications possibles brièvement après unsubscribe
```
`tokenAccounts = none` est équivalent à l'omission du champ. `balanceChanged` et `all` étendent le matching d'un `accountInclude` wallet aux token accounts qu'il possède selon les règles Helius documentées.
## 3. Contrat public typed
Nouveau module ciblé :
```text
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
```
Il publie depuis le crate-root :
```text
HeliusTokenAccountsFilter
HeliusTransactionSubscribeEncoding
HeliusTransactionSubscribeFilter
HeliusTransactionSubscribeOptions
HeliusTransactionSubscribeRequest
```
Le contrat réutilise les types KSP existants lorsqu'ils sont wire-identiques :
```text
commitment -> SolanaCommitment
transactionDetails -> SolanaTransactionDetails
account filters -> ksp_core_lib::Pubkey
```
Aucun DTO Solana commun n'est recopié sous un nom Helius sans nécessité wire.
## 4. Sémantique des filtres et options
`HeliusTransactionSubscribeFilter` conserve explicitement la différence entre :
```text
champ omis
liste présente mais vide []
liste présente avec valeurs
```
pour `accountInclude`, `accountExclude` et `accountRequired`.
Chaque liste est validée indépendamment avec la limite Helius :
```text
0 ..= 50_000 accepté
50_001 rejeté avant I/O
```
Les erreurs déterministes n'incluent aucune signature ni adresse du filtre ; elles transportent uniquement le nom du champ et les cardinalités sûres.
`HeliusTransactionSubscribeOptions` impose avant I/O :
```text
transactionDetails = full -> maxSupportedTransactionVersion requis
transactionDetails = accounts -> maxSupportedTransactionVersion requis
transactionDetails = signatures -> version optionnelle
transactionDetails = none -> version optionnelle
```
La distinction suivante est préservée sur le wire :
```text
options = None -> params = [filter]
options = Some(default) -> params = [filter, {}]
```
## 5. Wire subscribe/unsubscribe préparé
Les helpers crate-private préparés pour l'intégration actor de `pre.006` verrouillent :
```text
transactionSubscribe
transactionUnsubscribe
subscribe result integer -> u64
unsubscribe params -> [remote_subscription_id]
unsubscribe result -> bool
```
Les décodeurs refusent les formes de réponse d'un type différent au lieu de les coercer.
Ces helpers restent crate-private : aucun raw provider-extension API public n'est introduit.
## 6. Réutilisation du moteur physique
Une fixture locale passe réellement :
```text
HeliusLaserStreamWsSession
-> physical_session() crate-private
-> WsSession::execute_json_rpc
-> actor/socket unique existant
-> transactionSubscribe
-> transactionUnsubscribe
```
Elle vérifie les méthodes, params, acknowledgements et résultat d'unsubscribe exacts contre un peer WebSocket local.
Aucun second :
```text
actor
socket
pending map
reconnect loop
subscription engine
```
n'est ajouté.
## 7. Sécurité et surface différée
`Debug` pour le filtre/requête expose seulement des indicateurs, modes et cardinalités ; il ne rend ni la signature exacte ni les valeurs des comptes filtrés.
`HeliusLaserStreamWsSession` n'expose toujours pas :
```text
pub async fn transaction_subscribe(...)
```
Cette absence est verrouillée par release-completeness. Le handle live arrive en `pre.006` avec :
```text
transactionNotification
registry local/remote
reconnect + resubscribe
unsubscribe races
late notifications
backpressure ciblée
```
Le heartbeat Helius reste réservé à `pre.007`.
## 8. Canaries et non-régressions
Les nouveaux tests couvrent :
```text
strings wire exactes tokenAccounts/encoding
serialization complète filtre/options
omission vs [] explicite
borne 50_000 / rejet 50_001 pour les trois listes
règle conditionnelle maxSupportedTransactionVersion
ack subscribe numérique strict
wire/result unsubscribe strict
Debug sans signature/adresses
round-trip local via actor physique partagé
public API des nouveaux types
absence du live handle avant pre.006
```
Les surfaces acquises restent inchangées :
```text
SolanaStandardWsSession 9 familles standard
HeliusLaserStreamWsSession 6 familles standard supportées
HTTP 52 current + 14 historiques
Config Helius mainnet + devnet, secret/redaction/provenance validés
```
Aucune nouvelle dépendance Rust n'est ajoutée.
## 9. Preuve opérateur héritée
Le checkpoint `pre.004-fix.002` fourni le 2026-08-23 est intégralement vert :
```text
cargo fmt --all OK
python3 scripts/audit_rust_workspace_rules.py clean
cargo check --workspace OK
cargo clippy --workspace --all-targets OK
cargo test -p ksp-config-lib OK 110/110 + ownership/public API
cargo test -p ksp-onchain-transport-lib OK 314 unit + 38 public + 26 completeness + 4 doctests
cargo test --workspace OK
```
`pre.004`, `pre.004-fix.001` et `pre.004-fix.002` sont donc `DONE` avant cette tranche.
## 10. Fichiers de la livraison
Nouveaux :
```text
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs
deltas/0.2.8/pre.005.md
```
Modifiés :
```text
Cargo.toml
crates/ksp-onchain-transport-lib/src/lib.rs
crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
```
Aucun fichier Config, schema, `.env`, README/USAGE, ROADMAP ou CHANGELOG n'est modifié.
## 11. Validation de préparation et gate opérateur
Validation statique disponible dans le sandbox de préparation :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
```
Le sandbox ne fournit pas Cargo/rustfmt ; la tranche reste donc `PREPARED` jusqu'au checkpoint opérateur suivant :
```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
```

View File

@@ -1,9 +1,9 @@
<!-- file: docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md -->
<!-- version: 12 -->
<!-- version: 13 -->
# Plan `0.2.8` — Helius LaserStream WebSocket
> **Statut : `0.2.8-pre.003` est validé ; `pre.004` est fonctionnellement implémenté mais son gate Config a nécessité deux corrections de canari. `pre.004-fix.001` a corrigé la redaction segmentaire et ajouté Devnet ; son checkpoint a ensuite révélé une seconde hypothèse de test erronée sur la provenance d'une chaîne composée. `pre.004-fix.002` corrige uniquement cette cardinalité/provenance et reste préparé.** Le runtime Config/Transport, les profils mainnet/devnet, le schema et le mapping Helius restent inchangés.
> **Statut : `pre.004` et ses deux fixes sont validés par checkpoint opérateur complet. `0.2.8-pre.005` est préparé : contrat typed `transactionSubscribe`, filtres/options Helius, `tokenAccounts`, limites 50k et wire `transactionUnsubscribe`, sans exposer encore de handle transaction avant l'intégration actor de `pre.006`.**
## 1. Objet, base et état courant
@@ -27,12 +27,13 @@ pre.001-fix.002 forecast compact + namespace LaserStream WS/gRPC
pre.002 socle protocolaire validé
pre.002-fix.001 correction Clippy + normalisation structure documentaire validées
pre.003 six familles standard Helius validées
pre.004 Config V2 Helius implémenté ; checkpoint bloqué par un faux négatif de test
pre.004-fix.001 correction safe_value + couverture Devnet Helius ; checkpoint bloqué par provenance
pre.004-fix.002 correction du canari de provenance composée préparée
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 préparé
workspace.package.version courant = 0.2.8-pre.4.fix.2
commit attendu = v0.2.8-pre.004-fix.002
workspace.package.version courant = 0.2.8-pre.5
commit attendu = v0.2.8-pre.005
aucun tag prerelease
```
@@ -52,14 +53,12 @@ pre.002 DONE — socle protocolaire : WsProtocolKind::HeliusLaserStream + faça
fix.001 DONE — correction Clippy `implicit_return` + audit/normalisation structure docs
pre.003 DONE — surface Helius standard supportée : account/logs/program/root/signature/slot
+ absence typée de block/slotsUpdates/vote sur Helius + non-régression standard 9/9
pre.004 CHECKPOINT — Config V2 helius_laserstream + schema/fixtures + mapping Config -> Transport
+ stratégie de secret Helius et redaction URL ; runtime/check/clippy/Transport verts
fix.001 CHECKPOINT — redaction segmentaire corrigée + représentation Devnet Helius ajoutée
+ gate Config bloqué uniquement par une attente de provenance `1` au lieu de `2`
fix.002 PREPARED — provenance composée attend `DocumentLiteral` + `EnvironmentProcess`
+ aucun changement runtime/schema/fixture/profil
pre.005 transactionSubscribe request typed + filters/options/tokenAccounts + transactionUnsubscribe
+ bounds 50k + maxSupportedTransactionVersion conditionnel
pre.004 DONE — Config V2 helius_laserstream + schema/fixtures + mapping Config -> Transport
+ 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 PREPARED — transactionSubscribe request typed + filters/options/tokenAccounts + transactionUnsubscribe
+ bounds 50k + maxSupportedTransactionVersion conditionnel ; live handle différé à pre.006
pre.006 transactionNotification + actor integration + reconnect/resubscribe/unsubscribe races
+ late notifications + backpressure ciblée
pre.007 heartbeat Helius WebSocket/idle + timers + interaction reconnect/control frames/shutdown
@@ -897,7 +896,7 @@ validation finale fermée
prompt 0.2.9 prêt
```
## 13. Checkpoints `pre.004`, `fix.001` et `fix.002`
## 13. Checkpoints `pre.004`, fixes et préparation `pre.005`
Le checkpoint `pre.003` est fermé par preuve opérateur :
@@ -962,16 +961,37 @@ provenance attendue = 1 segment
Pour une URL composée `littéral + ${KSP_SECRET_HELIUS_API_KEY}`, le contrat Config enregistre correctement deux provenances ordonnées : `DocumentLiteral`, puis `EnvironmentProcess(KSP_SECRET_HELIUS_API_KEY)`. `pre.004-fix.002` corrige seulement cette attente pour mainnet et devnet ; aucune donnée Config ni logique runtime n'est modifiée.
Le checkpoint à rejouer avant de déclarer `pre.004` + `fix.001` + `fix.002` `DONE` est :
Le checkpoint opérateur reçu après `pre.004-fix.002` ferme cette tranche :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-onchain-transport-lib
cargo test --workspace
```text
cargo fmt --all OK
python3 scripts/audit_rust_workspace_rules.py clean
cargo check --workspace OK
cargo clippy --workspace --all-targets OK
cargo test -p ksp-config-lib OK 110/110 + ownership/public API
cargo test -p ksp-onchain-transport-lib OK 314 unit + 38 public + 26 completeness + 4 doctests
cargo test --workspace OK
```
Si ce checkpoint est vert, `pre.004` et ses deux fixes peuvent être fermés, puis `pre.005` peut ouvrir la requête typed `transactionSubscribe` / `transactionUnsubscribe`, ses filtres/options et validations déterministes.
Les profils Helius mainnet/devnet, la redaction segmentaire, la provenance composée, le mapping Config -> Transport et l'absence de nouvelle dépendance sont donc acquis avant `pre.005`.
`pre.005` prépare maintenant :
```text
HeliusTransactionSubscribeFilter vote/failed/signature/accountInclude/accountExclude/accountRequired/tokenAccounts
HeliusTokenAccountsFilter none / balanceChanged / all
HeliusTransactionSubscribeOptions commitment/encoding/transactionDetails/showRewards/maxSupportedTransactionVersion
HeliusTransactionSubscribeRequest validation déterministe + params exacts
accountInclude/Exclude/Required <= 50_000 chacun
transactionDetails accounts/full maxSupportedTransactionVersion obligatoire
transactionSubscribe ack remote id numérique décodé en interne
transactionUnsubscribe méthode/params/résultat bool exacts
Debug filtre/request signature et valeurs d'adresses non rendues
actor/socket chemin physique existant réutilisé dans fixture locale
live transaction handle volontairement absent jusqu'à pre.006
transactionNotification toujours hors scope pre.005
heartbeat toujours hors scope pre.005
new dependency aucune
```
La séparation `pre.005` / `pre.006` est normative : publier dès maintenant un `transaction_subscribe()` public sans registry de notification/reconnect produirait un handle transitoire qui perdrait les notifications. Le contrat public de requête est donc stable dès `pre.005`, tandis que l'abonnement live est ajouté atomiquement avec l'actor integration en `pre.006`.

View File

@@ -1,9 +1,9 @@
<!-- file: docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md -->
<!-- version: 12 -->
<!-- version: 13 -->
# Validation `0.2.8` — Helius LaserStream WebSocket
> **Statut : `pre.003` validé ; `pre.004` est implémenté mais son gate Config a exposé deux hypothèses de test incorrectes. `pre.004-fix.001` a corrigé `safe_value` et ajouté Devnet ; son checkpoint est vert sur `fmt`, audit, `check`, `clippy` et Transport mais échoue encore sur la cardinalité de provenance. `pre.004-fix.002` corrige uniquement ce canari : une URL composée porte `DocumentLiteral` puis `EnvironmentProcess`.**
> **Statut : `pre.004` + `fix.001` + `fix.002` validés par checkpoint opérateur complet. `pre.005` est préparé : contrat public typed de requête Helius transaction, validations 50k/version, `tokenAccounts`, wire subscribe/unsubscribe et canari actor local ; le handle live reste volontairement différé à `pre.006`.**
## 1. Références
@@ -20,6 +20,7 @@ pre.003 deltas/0.2.8/pre.003.md
pre.004 deltas/0.2.8/pre.004.md
pre.004 redaction/devnet fix deltas/0.2.8/pre.004-fix.001.md
pre.004 provenance fix deltas/0.2.8/pre.004-fix.002.md
pre.005 deltas/0.2.8/pre.005.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
@@ -133,22 +134,22 @@ Verdict : **gate `0.2.8-pre.001` positif ; `pre.002` + `fix.001` et `pre.003` so
### 4.2 Invariants architecture
| Critère | Décision | Preuve cible | État |
|-------------------------------------|------------------------------------------------------------------|---------------------------------|--------------------|
| Critère | Décision | Preuve cible | État |
|-------------------------------------|------------------------------------------------------------------|---------------------------------|--------|
| actor physique | un seul `WsSession` actor partagé | source/runtime canary | pre.002 implémenté |
| façade standard | `SolanaStandardWsSession` | public API canary | pre.002 implémenté |
| façade Helius | `HeliusLaserStreamWsSession` | public API canary | pre.002 implémenté |
| namespace LaserStream WS | `WsProtocolKind` + `ws_endpoints` possèdent `helius_laserstream` | API/Config/docs canary | pre.004 préparé |
| LaserStream gRPC | backend/type/Config distincts, hors `0.2.8` | absence de réutilisation WS | décidé |
| namespace LaserStream WS | `WsProtocolKind` + `ws_endpoints` possèdent `helius_laserstream` | API/Config/docs canary | pre.004 validé |
| LaserStream gRPC | backend/type/Config distincts, hors `0.2.8` | absence de réutilisation WS | décidé |
| escape hatch Helius | aucun `inner()`/`into_inner()` public | compile-fail/source canary | pre.002 implémenté |
| generic Helius `WsSession::connect` | ne doit pas permettre de contourner la façade | invalid protocol pre-I/O canary | pre.002 implémenté |
| Helius unsupported | absent de la façade | compile-fail/API absence canary | pre.003 validé |
| standard Helius commun | délégation vers le même wire/actor | exact fixture | pre.003 validé |
| DTO duplication | seulement si wire/sémantique divergent | public/source audit | décidé |
| Config direction | Config -> Transport uniquement | ownership tests | pre.004 préparé |
| heartbeat | Helius-only, actor commun | deterministic timers | décidé |
| secret | query URL derrière `WsEndpointUrl` | redaction canaries | pre.004 préparé |
| new Rust dependency | aucune | manifest/tree audit | décidé |
| Helius unsupported | absent de la façade | compile-fail/API absence canary | pre.003 validé |
| standard Helius commun | délégation vers le même wire/actor | exact fixture | pre.003 validé |
| DTO duplication | seulement si wire/sémantique divergent | public/source audit | décidé |
| Config direction | Config -> Transport uniquement | ownership tests | pre.004 validé |
| heartbeat | Helius-only, actor commun | deterministic timers | décidé |
| secret | query URL derrière `WsEndpointUrl` | redaction canaries | pre.004 validé |
| new Rust dependency | aucune | manifest/tree audit | décidé |
## 5. Contrat `transactionSubscribe` à valider
@@ -206,8 +207,8 @@ 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
[ ] transaction unsubscribe/result exact
[ ] 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
[ ] notification decode failure isole la logical subscription
[ ] provider RPC application error ne tue pas la session
@@ -262,7 +263,7 @@ Statut initial : **non exécuté**.
Contrat cible : opt-in, Config résout l'api-key, Transport ne lit pas l'environnement. Aucun secret n'est versionné ou affiché. Un échec lié au plan/credential provider doit rester distinguable d'une panne du moteur.
## 10. Preuves `pre.003` et préparation `pre.004`
## 10. Preuves `pre.003` et `pre.004`
Checkpoint `pre.003` reçu :
@@ -299,18 +300,18 @@ Surface `pre.004` préparée :
[x] schema `ws_endpoints[].kind` accepte `helius_laserstream`
[x] adapter Config mappe `helius_laserstream` vers `WsProtocolKind::HeliusLaserStream`
[x] fixture Config matérialise un endpoint Helius mainnet typé
[ ] fixture Config matérialise un profil Helius devnet distinct — ajouté par fix.001, checkpoint à rejouer
[x] fixture Config matérialise un profil Helius devnet distinct — fix.001/fix.002 validés
[x] exemple Transport matérialise Helius mainnet sans credential réel
[ ] exemple Transport matérialise Helius devnet dans un profil distinct — ajouté par fix.001, checkpoint à rejouer
[x] exemple Transport matérialise Helius devnet dans un profil distinct — fix.001/fix.002 validés
[x] `.env.example` inventorie `KSP_SECRET_HELIUS_API_KEY`
[x] interpolation du secret produit l'URL runtime Helius attendue
[ ] safe_value conserve les littéraux Helius mainnet/devnet et redige uniquement l'api-key (`...api-key=********`) — fix.001 à rejouer
[ ] Debug de `ResolvedTransportConfig` n'expose pas la clé — assertion à rejouer après fix.001
[x] safe_value conserve les littéraux Helius mainnet/devnet et redige uniquement l'api-key (`...api-key=********`)
[x] Debug de `ResolvedTransportConfig` n'expose pas la clé
[x] Config produit un endpoint composable avec `HeliusLaserStreamWsSession`
[x] `config/std.transport.json` canonique reste standard-only
```
Aucun `transactionSubscribe`, heartbeat, changement du moteur WebSocket ou nouvelle dépendance n'est introduit dans cette tranche.
`pre.004` n'introduisait aucun `transactionSubscribe`, heartbeat, changement du moteur WebSocket ou nouvelle dépendance ; son checkpoint final est désormais vert.
## 11. Audit structurel des documents actifs
@@ -338,7 +339,6 @@ Constats `pre.002-fix.001` :
Aucun split en fichiers supplémentaires n'est retenu : les plans historiques `0.2.5``0.2.7` sont de taille comparable ou supérieure, et le contenu du plan `015` reste entièrement centré sur une seule release. Le problème identifié était **l'ordre interne et la duplication de responsabilités**, pas la nécessité d'un nouveau type de document.
## 12. Gates opérateur `pre.004` / `fix.001` / `fix.002`
Premier passage `pre.004` reçu le 2026-08-23 :
@@ -389,16 +389,69 @@ Le comportement runtime est correct. Une chaîne `wss://...api-key=${KSP_SECRET_
`pre.004-fix.002` modifie uniquement le canari mainnet/devnet pour vérifier ces deux segments. Les profils, URLs, redaction, schema et mapping restent inchangés.
À rejouer après `pre.004-fix.002` :
Checkpoint reçu après `pre.004-fix.002` :
```text
[x] cargo fmt --all
[x] python3 scripts/audit_rust_workspace_rules.py = clean
[x] cargo check --workspace
[x] cargo clippy --workspace --all-targets
[x] cargo test -p ksp-config-lib — 110/110 + ownership/public API
[x] cargo test -p ksp-onchain-transport-lib — 314 unit + 38 public + 26 completeness + 4 doctests
[x] cargo test --workspace
```
Verdict : **`pre.004` DONE ; `pre.004-fix.001` DONE ; `pre.004-fix.002` DONE.**
## 13. Préparation `pre.005` — contrat Helius transaction
Sources matérialisées :
```text
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
```
Contrats préparés :
```text
[x] HeliusTransactionSubscribeFilter couvre vote/failed/signature/accountInclude/accountExclude/accountRequired/tokenAccounts
[x] accountInclude/accountExclude/accountRequired conservent omission vs [] explicite
[x] chacune des trois listes est bornée localement à 50_000
[x] HeliusTokenAccountsFilter = none/balanceChanged/all
[x] HeliusTransactionSubscribeEncoding = base58/base64/jsonParsed
[x] transactionDetails réutilise SolanaTransactionDetails = full/signatures/accounts/none
[x] maxSupportedTransactionVersion est exigé pour accounts/full
[x] options absentes vs objet {} explicite restent distinctes
[x] transactionSubscribe ack numérique et transactionUnsubscribe booléen ont des décodeurs stricts
[x] transactionUnsubscribe encode exactement [remoteSubscriptionId]
[x] Debug filtre/request expose des comptes et indicateurs, jamais signature/adresses
[x] fixture locale route subscribe/unsubscribe via le WsSession actor existant
[x] aucun second actor/socket
[x] aucun transaction_subscribe public avant pre.006
[x] aucune nouvelle dépendance
```
Validation statique disponible dans l'environnement de préparation :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
```
Gates opérateur `pre.005` à exécuter :
```text
[ ] cargo fmt --all
[ ] python3 scripts/audit_rust_workspace_rules.py = clean
[ ] cargo check --workspace
[ ] cargo clippy --workspace --all-targets
[ ] cargo test -p ksp-config-lib
[ ] cargo test -p ksp-onchain-transport-lib
[ ] cargo test --workspace
```
Verdict courant : **`pre.003` DONE ; `pre.004` fonctionnellement implémenté mais non fermé ; `pre.004-fix.001` CHECKPOINT ; `pre.004-fix.002` PREPARED.**
Verdict courant : **`pre.004` et ses fixes DONE ; `pre.005` PREPARED.**