v0.2.8-pre.005
This commit is contained in:
@@ -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.
|
||||
|
||||
421
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
Normal file
421
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user