v0.2.9-pre.004

This commit is contained in:
2026-08-24 11:55:17 +02:00
parent feb9befb35
commit e4bbc78a41
9 changed files with 1163 additions and 56 deletions

View File

@@ -0,0 +1,562 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
// version: 1
const MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT: usize = 128;
const MAX_GRPC_SUBSCRIBE_DATA_SLICE_LENGTH_BYTES: u64 = 64 * 1024 * 1024;
const MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT: usize = 1_024;
const MAX_GRPC_SUBSCRIBE_FILTER_NAME_LENGTH_BYTES: usize = 128;
/// Validated logical filter name used by the standard Yellowstone `SubscribeRequest` maps.
///
/// Filter names are returned by Yellowstone in update envelopes. KSP therefore keeps them as typed public data, but omits their text from `Debug` so routine
/// diagnostics cannot accidentally disclose caller-defined labels.
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct YellowstoneSubscribeFilterName {
value: std::string::String,
}
impl YellowstoneSubscribeFilterName {
/// Validates one provider-neutral Yellowstone filter-group name.
pub fn new(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
let value = value.into();
if value.is_empty() || value.len() > MAX_GRPC_SUBSCRIBE_FILTER_NAME_LENGTH_BYTES || value.trim() != value || value.chars().any(char::is_control) {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone subscribe filter name is invalid")
.with_context("field", "grpc_subscribe.filter_name"),
);
}
return std::result::Result::Ok(Self { value });
}
/// Returns the validated filter name text.
#[must_use]
pub fn as_str(&self) -> &str {
return self.value.as_str();
}
}
impl std::fmt::Debug for YellowstoneSubscribeFilterName {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("YellowstoneSubscribeFilterName(<redacted>)");
}
}
/// One standard Yellowstone account-data slice applied to subscribed account payloads.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneAccountsDataSlice {
offset: u64,
length: u64,
}
impl YellowstoneAccountsDataSlice {
/// Creates and validates one account-data slice.
pub fn new(offset: u64, length: u64) -> ksp_core_lib::Result<Self> {
if length > MAX_GRPC_SUBSCRIBE_DATA_SLICE_LENGTH_BYTES || offset.checked_add(length).is_none() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone account-data slice exceeds KSP bounds")
.with_context("field", "grpc_subscribe.accounts_data_slice"),
);
}
return std::result::Result::Ok(Self { offset, length });
}
/// Returns the byte offset.
#[must_use]
pub const fn offset(self) -> u64 {
return self.offset;
}
/// Returns the requested byte length.
#[must_use]
pub const fn length(self) -> u64 {
return self.length;
}
fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestAccountsDataSlice {
return yellowstone_grpc_proto::geyser::SubscribeRequestAccountsDataSlice { offset: self.offset, length: self.length };
}
}
/// Optional ping mutation carried inside the standard Yellowstone subscribe stream.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneSubscribePing {
id: i32,
}
impl YellowstoneSubscribePing {
/// Creates one subscribe-stream ping request.
#[must_use]
pub const fn new(id: i32) -> Self {
return Self { id };
}
/// Returns the exact ping identifier carried on the wire.
#[must_use]
pub const fn id(self) -> i32 {
return self.id;
}
fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestPing {
return yellowstone_grpc_proto::geyser::SubscribeRequestPing { id: self.id };
}
}
/// Account-family filter-group shell for the standard Yellowstone subscribe surface.
///
/// `0.2.9-pre.004` intentionally materializes only the top-level map contract. Account selectors and account-specific filter oneofs are added by `pre.005`.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct YellowstoneSubscribeAccountFilter {
_private: (),
}
impl YellowstoneSubscribeAccountFilter {
/// Creates an empty account filter group.
#[must_use]
pub const fn new() -> Self {
return Self { _private: () };
}
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts::default();
}
}
/// Slot-family filter-group shell for the standard Yellowstone subscribe surface.
///
/// Slot optional flags and update decoding are added by `0.2.9-pre.005`.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct YellowstoneSubscribeSlotFilter {
_private: (),
}
impl YellowstoneSubscribeSlotFilter {
/// Creates an empty slot filter group.
#[must_use]
pub const fn new() -> Self {
return Self { _private: () };
}
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots::default();
}
}
/// Transaction-family filter-group shell shared by `transactions` and `transactions_status`.
///
/// Transaction selectors, Cuckoo filters and token-account expansion are added by `0.2.9-pre.006`.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct YellowstoneSubscribeTransactionFilter {
_private: (),
}
impl YellowstoneSubscribeTransactionFilter {
/// Creates an empty transaction filter group.
#[must_use]
pub const fn new() -> Self {
return Self { _private: () };
}
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default();
}
}
/// Block-family filter-group shell for the standard Yellowstone subscribe surface.
///
/// Block selectors, include flags and Cuckoo filters are added by `0.2.9-pre.007`.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct YellowstoneSubscribeBlockFilter {
_private: (),
}
impl YellowstoneSubscribeBlockFilter {
/// Creates an empty block filter group.
#[must_use]
pub const fn new() -> Self {
return Self { _private: () };
}
fn to_wire(&self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks::default();
}
}
/// Named empty filter activating the standard Yellowstone `blocks_meta` family.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct YellowstoneSubscribeBlocksMetaFilter {
_private: (),
}
impl YellowstoneSubscribeBlocksMetaFilter {
/// Creates the empty marker filter.
#[must_use]
pub const fn new() -> Self {
return Self { _private: () };
}
fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocksMeta {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocksMeta::default();
}
}
/// Named empty filter activating the standard Yellowstone `entry` family.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct YellowstoneSubscribeEntryFilter {
_private: (),
}
impl YellowstoneSubscribeEntryFilter {
/// Creates the empty marker filter.
#[must_use]
pub const fn new() -> Self {
return Self { _private: () };
}
fn to_wire(self) -> yellowstone_grpc_proto::geyser::SubscribeRequestFilterEntry {
return yellowstone_grpc_proto::geyser::SubscribeRequestFilterEntry::default();
}
}
/// Provider-neutral standard Yellowstone subscribe request owned by KSP.
///
/// The seven upstream maps are represented independently and retain named empty entries. An entirely empty map is the logical KSP representation of no active
/// filter in that family; protobuf map encoding does not distinguish an omitted map from an empty map. Filter-group names are globally unique across all seven
/// maps so the names echoed by `SubscribeUpdate.filters` remain unambiguous. `Debug` exposes only counts and common scalar options, never filter names or future
/// filter payloads.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneSubscribeRequest {
accounts: std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, crate::YellowstoneSubscribeAccountFilter>,
slots: std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, crate::YellowstoneSubscribeSlotFilter>,
transactions: std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, crate::YellowstoneSubscribeTransactionFilter>,
transactions_status: std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, crate::YellowstoneSubscribeTransactionFilter>,
blocks: std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, crate::YellowstoneSubscribeBlockFilter>,
blocks_meta: std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, crate::YellowstoneSubscribeBlocksMetaFilter>,
entry: std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, crate::YellowstoneSubscribeEntryFilter>,
commitment: std::option::Option<crate::SolanaCommitment>,
accounts_data_slice: std::vec::Vec<crate::YellowstoneAccountsDataSlice>,
ping: std::option::Option<crate::YellowstoneSubscribePing>,
from_slot: std::option::Option<u64>,
}
impl YellowstoneSubscribeRequest {
/// Creates an empty subscribe request. Empty requests are valid because later bidi lifecycle code uses request mutations to clear filters or carry ping state.
#[must_use]
pub fn new() -> Self {
return Self {
accounts: std::collections::BTreeMap::new(),
slots: std::collections::BTreeMap::new(),
transactions: std::collections::BTreeMap::new(),
transactions_status: std::collections::BTreeMap::new(),
blocks: std::collections::BTreeMap::new(),
blocks_meta: std::collections::BTreeMap::new(),
entry: std::collections::BTreeMap::new(),
commitment: std::option::Option::None,
accounts_data_slice: std::vec::Vec::new(),
ping: std::option::Option::None,
from_slot: std::option::Option::None,
};
}
/// Inserts one named account filter group.
pub fn insert_account_filter(
&mut self,
name: crate::YellowstoneSubscribeFilterName,
filter: crate::YellowstoneSubscribeAccountFilter,
) -> ksp_core_lib::Result<()> {
let validation = self.validate_new_filter_name(&name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
self.accounts.insert(name, filter);
return std::result::Result::Ok(());
}
/// Inserts one named slot filter group.
pub fn insert_slot_filter(
&mut self,
name: crate::YellowstoneSubscribeFilterName,
filter: crate::YellowstoneSubscribeSlotFilter,
) -> ksp_core_lib::Result<()> {
let validation = self.validate_new_filter_name(&name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
self.slots.insert(name, filter);
return std::result::Result::Ok(());
}
/// Inserts one named transaction filter group.
pub fn insert_transaction_filter(
&mut self,
name: crate::YellowstoneSubscribeFilterName,
filter: crate::YellowstoneSubscribeTransactionFilter,
) -> ksp_core_lib::Result<()> {
let validation = self.validate_new_filter_name(&name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
self.transactions.insert(name, filter);
return std::result::Result::Ok(());
}
/// Inserts one named transaction-status filter group.
pub fn insert_transaction_status_filter(
&mut self,
name: crate::YellowstoneSubscribeFilterName,
filter: crate::YellowstoneSubscribeTransactionFilter,
) -> ksp_core_lib::Result<()> {
let validation = self.validate_new_filter_name(&name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
self.transactions_status.insert(name, filter);
return std::result::Result::Ok(());
}
/// Inserts one named block filter group.
pub fn insert_block_filter(
&mut self,
name: crate::YellowstoneSubscribeFilterName,
filter: crate::YellowstoneSubscribeBlockFilter,
) -> ksp_core_lib::Result<()> {
let validation = self.validate_new_filter_name(&name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
self.blocks.insert(name, filter);
return std::result::Result::Ok(());
}
/// Inserts one named empty `blocks_meta` filter group.
pub fn insert_blocks_meta_filter(
&mut self,
name: crate::YellowstoneSubscribeFilterName,
filter: crate::YellowstoneSubscribeBlocksMetaFilter,
) -> ksp_core_lib::Result<()> {
let validation = self.validate_new_filter_name(&name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
self.blocks_meta.insert(name, filter);
return std::result::Result::Ok(());
}
/// Inserts one named empty `entry` filter group.
pub fn insert_entry_filter(
&mut self,
name: crate::YellowstoneSubscribeFilterName,
filter: crate::YellowstoneSubscribeEntryFilter,
) -> ksp_core_lib::Result<()> {
let validation = self.validate_new_filter_name(&name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
self.entry.insert(name, filter);
return std::result::Result::Ok(());
}
/// Sets or clears the optional standard Yellowstone commitment.
pub fn set_commitment(&mut self, commitment: std::option::Option<crate::SolanaCommitment>) {
self.commitment = commitment;
}
/// Adds one validated account-data slice while preserving insertion order.
pub fn push_accounts_data_slice(&mut self, slice: crate::YellowstoneAccountsDataSlice) -> ksp_core_lib::Result<()> {
if self.accounts_data_slice.len() >= MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone account-data slice count exceeds KSP bounds")
.with_context("field", "grpc_subscribe.accounts_data_slice"),
);
}
self.accounts_data_slice.push(slice);
return std::result::Result::Ok(());
}
/// Sets or clears the optional subscribe-stream ping mutation.
pub fn set_ping(&mut self, ping: std::option::Option<crate::YellowstoneSubscribePing>) {
self.ping = ping;
}
/// Sets or clears the optional replay starting slot.
pub fn set_from_slot(&mut self, from_slot: std::option::Option<u64>) {
self.from_slot = from_slot;
}
/// Returns the number of active account filter groups.
#[must_use]
pub fn account_filter_count(&self) -> usize {
return self.accounts.len();
}
/// Returns the number of active slot filter groups.
#[must_use]
pub fn slot_filter_count(&self) -> usize {
return self.slots.len();
}
/// Returns the number of active transaction filter groups.
#[must_use]
pub fn transaction_filter_count(&self) -> usize {
return self.transactions.len();
}
/// Returns the number of active transaction-status filter groups.
#[must_use]
pub fn transaction_status_filter_count(&self) -> usize {
return self.transactions_status.len();
}
/// Returns the number of active block filter groups.
#[must_use]
pub fn block_filter_count(&self) -> usize {
return self.blocks.len();
}
/// Returns the number of active block-meta filter groups.
#[must_use]
pub fn blocks_meta_filter_count(&self) -> usize {
return self.blocks_meta.len();
}
/// Returns the number of active entry filter groups.
#[must_use]
pub fn entry_filter_count(&self) -> usize {
return self.entry.len();
}
/// Returns the optional request commitment.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns account-data slices in exact wire order.
#[must_use]
pub fn accounts_data_slices(&self) -> &[crate::YellowstoneAccountsDataSlice] {
return self.accounts_data_slice.as_slice();
}
/// Returns the optional subscribe-stream ping mutation.
#[must_use]
pub const fn ping(&self) -> std::option::Option<crate::YellowstoneSubscribePing> {
return self.ping;
}
/// Returns the optional replay starting slot.
#[must_use]
pub const fn from_slot(&self) -> std::option::Option<u64> {
return self.from_slot;
}
/// Validates all deterministic common subscribe-request bounds before any network I/O.
pub fn validate(&self) -> ksp_core_lib::Result<()> {
if self.total_filter_count() > MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone subscribe filter-group count exceeds KSP bounds")
.with_context("field", "grpc_subscribe.filter_groups"),
);
}
if self.accounts_data_slice.len() > MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone account-data slice count exceeds KSP bounds")
.with_context("field", "grpc_subscribe.accounts_data_slice"),
);
}
return std::result::Result::Ok(());
}
fn to_wire(&self) -> ksp_core_lib::Result<yellowstone_grpc_proto::geyser::SubscribeRequest> {
let validation = self.validate();
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(yellowstone_grpc_proto::geyser::SubscribeRequest {
accounts: self.accounts.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(),
slots: self.slots.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(),
transactions: self.transactions.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(),
transactions_status: self.transactions_status.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(),
blocks: self.blocks.iter().map(|(name, filter)| return (name.as_str().to_owned(), filter.to_wire())).collect(),
blocks_meta: self.blocks_meta.iter().map(|(name, filter)| return (name.as_str().to_owned(), (*filter).to_wire())).collect(),
entry: self.entry.iter().map(|(name, filter)| return (name.as_str().to_owned(), (*filter).to_wire())).collect(),
commitment: commitment_to_wire(self.commitment),
accounts_data_slice: self.accounts_data_slice.iter().copied().map(crate::YellowstoneAccountsDataSlice::to_wire).collect(),
ping: self.ping.map(crate::YellowstoneSubscribePing::to_wire),
from_slot: self.from_slot,
});
}
fn total_filter_count(&self) -> usize {
return self.accounts.len()
+ self.slots.len()
+ self.transactions.len()
+ self.transactions_status.len()
+ self.blocks.len()
+ self.blocks_meta.len()
+ self.entry.len();
}
fn contains_filter_name(&self, name: &crate::YellowstoneSubscribeFilterName) -> bool {
return self.accounts.contains_key(name)
|| self.slots.contains_key(name)
|| self.transactions.contains_key(name)
|| self.transactions_status.contains_key(name)
|| self.blocks.contains_key(name)
|| self.blocks_meta.contains_key(name)
|| self.entry.contains_key(name);
}
fn validate_new_filter_name(&self, name: &crate::YellowstoneSubscribeFilterName) -> ksp_core_lib::Result<()> {
if self.contains_filter_name(name) {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone subscribe filter name must be globally unique")
.with_context("field", "grpc_subscribe.filter_name"),
);
}
if self.total_filter_count() >= MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone subscribe filter-group count exceeds KSP bounds")
.with_context("field", "grpc_subscribe.filter_groups"),
);
}
return std::result::Result::Ok(());
}
}
fn commitment_to_wire(commitment: std::option::Option<crate::SolanaCommitment>) -> std::option::Option<i32> {
return commitment.map(|value| {
return match value {
crate::SolanaCommitment::Processed => yellowstone_grpc_proto::geyser::CommitmentLevel::Processed as i32,
crate::SolanaCommitment::Confirmed => yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed as i32,
crate::SolanaCommitment::Finalized => yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32,
};
});
}
impl std::default::Default for YellowstoneSubscribeRequest {
fn default() -> Self {
return Self::new();
}
}
impl std::fmt::Debug for YellowstoneSubscribeRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneSubscribeRequest")
.field("account_filter_count", &self.accounts.len())
.field("slot_filter_count", &self.slots.len())
.field("transaction_filter_count", &self.transactions.len())
.field("transaction_status_filter_count", &self.transactions_status.len())
.field("block_filter_count", &self.blocks.len())
.field("blocks_meta_filter_count", &self.blocks_meta.len())
.field("entry_filter_count", &self.entry.len())
.field("commitment", &self.commitment)
.field("accounts_data_slice_count", &self.accounts_data_slice.len())
.field("ping", &self.ping)
.field("from_slot", &self.from_slot)
.finish();
}
}
#[cfg(test)]
#[path = "../unit_tests/grpc_subscribe.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 36
// version: 37
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -34,7 +34,9 @@
//! `0.2.9-pre.002` opens the Yellowstone gRPC N1 engine foundation with Transport-owned redacted settings, bounded reconnect/channel/message policies, the
//! published Yellowstone protobuf dependency and a lazy Tonic HTTP/2 channel wrapper that exposes no raw Tonic or upstream protobuf types.
//! `0.2.9-pre.003` adds bounded TLS/WebPKI connection establishment, generic redacted ASCII request metadata and the seven standard Yellowstone unary RPCs
//! through KSP-owned DTOs. Streaming `Subscribe` remains deliberately deferred to `pre.004`.
//! through KSP-owned DTOs.
//! `0.2.9-pre.004` materializes the provider-neutral standard `SubscribeRequest` foundation: all seven named filter maps, global filter-name bounds/uniqueness,
//! commitment, ordered account-data slices, ping and `from_slot`. Family-specific account/slot/transaction/block filter fields remain staged for `pre.005007`.
mod client;
mod constants;
@@ -42,6 +44,7 @@ mod error;
mod executor;
mod grpc_channel;
mod grpc_settings;
mod grpc_subscribe;
mod grpc_unary;
mod json_rpc;
mod pool;
@@ -131,6 +134,26 @@ pub use self::grpc_settings::YellowstoneGrpcReconnectSettings;
pub use self::grpc_settings::YellowstoneGrpcSessionSettings;
/// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine.
pub use self::grpc_settings::YellowstoneGrpcTransportSettings;
/// One standard Yellowstone account-data slice.
pub use self::grpc_subscribe::YellowstoneAccountsDataSlice;
/// Account-family filter-group shell for standard Yellowstone Subscribe.
pub use self::grpc_subscribe::YellowstoneSubscribeAccountFilter;
/// Block-family filter-group shell for standard Yellowstone Subscribe.
pub use self::grpc_subscribe::YellowstoneSubscribeBlockFilter;
/// Empty filter marker activating the standard Yellowstone blocks-meta family.
pub use self::grpc_subscribe::YellowstoneSubscribeBlocksMetaFilter;
/// Empty filter marker activating the standard Yellowstone entry family.
pub use self::grpc_subscribe::YellowstoneSubscribeEntryFilter;
/// Validated globally unique logical filter name for standard Yellowstone Subscribe maps.
pub use self::grpc_subscribe::YellowstoneSubscribeFilterName;
/// Optional ping mutation carried by the standard Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribePing;
/// Provider-neutral standard Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribeRequest;
/// Slot-family filter-group shell for standard Yellowstone Subscribe.
pub use self::grpc_subscribe::YellowstoneSubscribeSlotFilter;
/// Transaction-family filter-group shell shared by transactions and transaction-status maps.
pub use self::grpc_subscribe::YellowstoneSubscribeTransactionFilter;
/// Standard Solana Yellowstone unary facade over one KSP-owned physical gRPC channel.
pub use self::grpc_unary::SolanaYellowstoneGrpcUnaryClient;
/// Block height returned by the standard Yellowstone unary surface.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 41
// version: 42
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -825,3 +825,44 @@ fn public_v0_2_9_pre_003_yellowstone_metadata_and_seven_unary_contracts_are_avai
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.domain(), "onchain_transport");
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.code(), "grpc_status");
}
#[test]
fn public_v0_2_9_pre_004_yellowstone_subscribe_common_contract_is_available_from_crate_root() {
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let account_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("accounts").expect("account filter name must validate");
let slot_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots").expect("slot filter name must validate");
let transaction_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("transactions").expect("transaction filter name must validate");
let transaction_status_name =
ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("transaction-status").expect("transaction-status filter name must validate");
let block_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks").expect("block filter name must validate");
let block_meta_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks-meta").expect("block-meta filter name must validate");
let entry_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("entry").expect("entry filter name must validate");
assert!(request.insert_account_filter(account_name, ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter::new()).is_ok());
assert!(request.insert_slot_filter(slot_name, ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new()).is_ok());
assert!(request.insert_transaction_filter(transaction_name, ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new()).is_ok());
assert!(
request
.insert_transaction_status_filter(transaction_status_name, ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new())
.is_ok()
);
assert!(request.insert_block_filter(block_name, ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new()).is_ok());
assert!(request.insert_blocks_meta_filter(block_meta_name, ksp_onchain_transport_lib::YellowstoneSubscribeBlocksMetaFilter::new()).is_ok());
assert!(request.insert_entry_filter(entry_name, ksp_onchain_transport_lib::YellowstoneSubscribeEntryFilter::new()).is_ok());
request.set_commitment(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
let slice = ksp_onchain_transport_lib::YellowstoneAccountsDataSlice::new(4, 32).expect("account-data slice must validate");
assert!(request.push_accounts_data_slice(slice).is_ok());
request.set_ping(std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribePing::new(7)));
request.set_from_slot(std::option::Option::Some(99));
assert!(request.validate().is_ok());
assert_eq!(request.account_filter_count(), 1);
assert_eq!(request.slot_filter_count(), 1);
assert_eq!(request.transaction_filter_count(), 1);
assert_eq!(request.transaction_status_filter_count(), 1);
assert_eq!(request.block_filter_count(), 1);
assert_eq!(request.blocks_meta_filter_count(), 1);
assert_eq!(request.entry_filter_count(), 1);
assert_eq!(request.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
assert_eq!(request.accounts_data_slices(), &[slice]);
assert_eq!(request.ping(), std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribePing::new(7)));
assert_eq!(request.from_slot(), std::option::Option::Some(99));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 34
// version: 35
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
@@ -1066,3 +1066,48 @@ fn release_v0_2_9_pre_003_adds_tls_metadata_and_exactly_seven_standard_unary_met
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _client = std::any::type_name::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient>();
}
#[test]
fn release_v0_2_9_pre_004_materializes_only_standard_subscribe_common_contract() {
let source = include_str!("../src/grpc_subscribe.rs");
let crate_root = include_str!("../src/lib.rs");
for wire_field in [
"accounts:",
"slots:",
"transactions:",
"transactions_status:",
"blocks:",
"blocks_meta:",
"entry:",
"commitment:",
"accounts_data_slice:",
"ping:",
"from_slot:",
] {
assert!(source.contains(wire_field), "missing standard Yellowstone SubscribeRequest field: {wire_field}");
}
assert!(source.contains("MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_FILTER_NAME_LENGTH_BYTES"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT"));
assert!(source.contains("offset.checked_add(length)"));
assert!(source.contains("globally unique"));
assert!(source.contains("SubscribeRequestFilterBlocksMeta"));
assert!(source.contains("SubscribeRequestFilterEntry"));
assert!(source.contains("YellowstoneSubscribeAccountFilter"));
assert!(source.contains("YellowstoneSubscribeSlotFilter"));
assert!(source.contains("YellowstoneSubscribeTransactionFilter"));
assert!(source.contains("YellowstoneSubscribeBlockFilter"));
assert!(!source.contains("SubscribeDeshred"));
assert!(!source.contains("PublicNode"));
assert!(!source.contains("OrbitFlare"));
assert!(!source.contains("Helius"));
assert!(!source.contains("pub fn to_wire"));
assert!(!source.contains("pub(crate) fn to_wire"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _request = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeRequest>();
let _account = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter>();
let _slot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter>();
let _transaction = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter>();
let _block = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter>();
}

View File

@@ -0,0 +1,125 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
// version: 1
fn filter_name(value: &str) -> crate::YellowstoneSubscribeFilterName {
return crate::YellowstoneSubscribeFilterName::new(value).expect("fixture filter name must validate");
}
#[test]
fn yellowstone_subscribe_filter_name_is_bounded_globally_unique_and_debug_redacted() {
assert!(crate::YellowstoneSubscribeFilterName::new("").is_err());
assert!(crate::YellowstoneSubscribeFilterName::new(" leading").is_err());
assert!(crate::YellowstoneSubscribeFilterName::new("trailing ").is_err());
assert!(crate::YellowstoneSubscribeFilterName::new("line\nbreak").is_err());
assert!(crate::YellowstoneSubscribeFilterName::new("x".repeat(129)).is_err());
let name = filter_name("account-primary");
assert_eq!(name.as_str(), "account-primary");
assert!(!format!("{name:?}").contains("account-primary"));
let mut request = crate::YellowstoneSubscribeRequest::new();
assert!(request.insert_account_filter(name.clone(), crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
let duplicate = request.insert_slot_filter(name, crate::YellowstoneSubscribeSlotFilter::new());
assert!(duplicate.is_err());
}
#[test]
fn yellowstone_subscribe_empty_and_named_empty_maps_encode_exactly() {
let empty = crate::YellowstoneSubscribeRequest::new().to_wire().expect("empty request must encode");
assert!(empty.accounts.is_empty());
assert!(empty.slots.is_empty());
assert!(empty.transactions.is_empty());
assert!(empty.transactions_status.is_empty());
assert!(empty.blocks.is_empty());
assert!(empty.blocks_meta.is_empty());
assert!(empty.entry.is_empty());
assert_eq!(empty.commitment, std::option::Option::None);
assert!(empty.accounts_data_slice.is_empty());
assert_eq!(empty.ping, std::option::Option::None);
assert_eq!(empty.from_slot, std::option::Option::None);
let mut request = crate::YellowstoneSubscribeRequest::new();
assert!(request.insert_account_filter(filter_name("accounts"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
assert!(request.insert_slot_filter(filter_name("slots"), crate::YellowstoneSubscribeSlotFilter::new()).is_ok());
assert!(request.insert_transaction_filter(filter_name("transactions"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok());
assert!(request.insert_transaction_status_filter(filter_name("transaction-status"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok());
assert!(request.insert_block_filter(filter_name("blocks"), crate::YellowstoneSubscribeBlockFilter::new()).is_ok());
assert!(request.insert_blocks_meta_filter(filter_name("blocks-meta"), crate::YellowstoneSubscribeBlocksMetaFilter::new()).is_ok());
assert!(request.insert_entry_filter(filter_name("entry"), crate::YellowstoneSubscribeEntryFilter::new()).is_ok());
let wire = request.to_wire().expect("named empty request must encode");
assert_eq!(wire.accounts.len(), 1);
assert_eq!(wire.slots.len(), 1);
assert_eq!(wire.transactions.len(), 1);
assert_eq!(wire.transactions_status.len(), 1);
assert_eq!(wire.blocks.len(), 1);
assert_eq!(wire.blocks_meta.len(), 1);
assert_eq!(wire.entry.len(), 1);
assert_eq!(wire.accounts.get("accounts"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts::default()));
assert_eq!(wire.slots.get("slots"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots::default()));
assert_eq!(
wire.transactions.get("transactions"),
std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default())
);
assert_eq!(
wire.transactions_status.get("transaction-status"),
std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default())
);
assert_eq!(wire.blocks.get("blocks"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks::default()));
assert!(wire.blocks_meta.contains_key("blocks-meta"));
assert!(wire.entry.contains_key("entry"));
}
#[test]
fn yellowstone_subscribe_common_fields_preserve_optional_and_ordered_wire_semantics() {
let mut request = crate::YellowstoneSubscribeRequest::new();
request.set_commitment(std::option::Option::Some(crate::SolanaCommitment::Finalized));
let first = crate::YellowstoneAccountsDataSlice::new(8, 16).expect("first slice must validate");
let second = crate::YellowstoneAccountsDataSlice::new(64, 0).expect("zero-length slice remains representable");
assert!(request.push_accounts_data_slice(first).is_ok());
assert!(request.push_accounts_data_slice(second).is_ok());
request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7)));
request.set_from_slot(std::option::Option::Some(42));
assert_eq!(request.commitment(), std::option::Option::Some(crate::SolanaCommitment::Finalized));
assert_eq!(request.accounts_data_slices(), &[first, second]);
assert_eq!(request.ping(), std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7)));
assert_eq!(request.from_slot(), std::option::Option::Some(42));
let wire = request.to_wire().expect("common fields must encode");
assert_eq!(wire.commitment, std::option::Option::Some(yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32));
assert_eq!(wire.accounts_data_slice.len(), 2);
assert_eq!(wire.accounts_data_slice[0].offset, 8);
assert_eq!(wire.accounts_data_slice[0].length, 16);
assert_eq!(wire.accounts_data_slice[1].offset, 64);
assert_eq!(wire.accounts_data_slice[1].length, 0);
assert_eq!(wire.ping.map(|ping| return ping.id), std::option::Option::Some(-7));
assert_eq!(wire.from_slot, std::option::Option::Some(42));
}
#[test]
fn yellowstone_subscribe_common_bounds_reject_before_wire_conversion() {
assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024).is_ok());
assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024 + 1).is_err());
assert!(crate::YellowstoneAccountsDataSlice::new(u64::MAX, 1).is_err());
let mut slices = crate::YellowstoneSubscribeRequest::new();
for index in 0..128_u64 {
let slice = crate::YellowstoneAccountsDataSlice::new(index, 1).expect("bounded fixture slice must validate");
assert!(slices.push_accounts_data_slice(slice).is_ok());
}
let excess = crate::YellowstoneAccountsDataSlice::new(129, 1).expect("excess fixture slice itself must validate");
assert!(slices.push_accounts_data_slice(excess).is_err());
let mut filters = crate::YellowstoneSubscribeRequest::new();
for index in 0..1_024_u32 {
let name = crate::YellowstoneSubscribeFilterName::new(format!("f{index}")).expect("bounded filter name must validate");
assert!(filters.insert_account_filter(name, crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
}
let excess_name = crate::YellowstoneSubscribeFilterName::new("excess").expect("excess filter name must validate independently");
assert!(filters.insert_account_filter(excess_name, crate::YellowstoneSubscribeAccountFilter::new()).is_err());
}
#[test]
fn yellowstone_subscribe_debug_omits_filter_names_and_future_payloads() {
let mut request = crate::YellowstoneSubscribeRequest::new();
assert!(request.insert_account_filter(filter_name("sensitive-label"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(9)));
request.set_from_slot(std::option::Option::Some(77));
let debug = format!("{request:?}");
assert!(!debug.contains("sensitive-label"));
assert!(debug.contains("account_filter_count"));
assert!(debug.contains("from_slot"));
}