// file: crates/ksp-onchain-transport-lib/src/ws_blocks.rs // version: 2 /// Filter accepted by unstable Solana `blockSubscribe`. #[derive(Clone, Debug, Eq, PartialEq)] pub enum SolanaBlockSubscribeFilter { /// Subscribe to every block that reaches the configured commitment. All, /// Subscribe only to blocks containing a transaction that mentions the account or program. MentionsAccountOrProgram(ksp_core_lib::Pubkey), } impl SolanaBlockSubscribeFilter { fn to_json_value(&self) -> serde_json::Value { return match self { Self::All => serde_json::Value::String("all".to_owned()), Self::MentionsAccountOrProgram(pubkey) => serde_json::json!({"mentionsAccountOrProgram": pubkey.to_string()}), }; } } /// Optional configuration accepted by unstable Solana `blockSubscribe`. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct SolanaBlockSubscribeConfig { commitment: std::option::Option, encoding: std::option::Option, transaction_details: std::option::Option, max_supported_transaction_version: std::option::Option, show_rewards: std::option::Option, } impl SolanaBlockSubscribeConfig { /// Creates an explicit unstable block-subscription configuration. #[must_use] pub const fn new( commitment: std::option::Option, encoding: std::option::Option, transaction_details: std::option::Option, max_supported_transaction_version: std::option::Option, show_rewards: std::option::Option, ) -> Self { return Self { commitment, encoding, transaction_details, max_supported_transaction_version, show_rewards }; } /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } /// Returns the optional transaction encoding. #[must_use] pub const fn encoding(&self) -> std::option::Option { return self.encoding; } /// Returns the optional transaction detail level. #[must_use] pub const fn transaction_details(&self) -> std::option::Option { return self.transaction_details; } /// Returns the highest transaction version the caller declares it can consume. #[must_use] pub const fn max_supported_transaction_version(&self) -> std::option::Option { return self.max_supported_transaction_version; } /// Returns whether rewards were explicitly requested for block notifications. #[must_use] pub const fn show_rewards(&self) -> std::option::Option { return self.show_rewards; } fn is_empty(&self) -> bool { return self.commitment.is_none() && self.encoding.is_none() && self.transaction_details.is_none() && self.max_supported_transaction_version.is_none() && self.show_rewards.is_none(); } fn validate(&self) -> ksp_core_lib::Result<()> { if self.commitment == std::option::Option::Some(crate::SolanaCommitment::Processed) { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "blockSubscribe commitment must be confirmed or finalized when explicitly provided", ) .with_context("rpc_method", "blockSubscribe") .with_context("commitment", "processed"), ); } 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(version) = self.max_supported_transaction_version { object.insert("maxSupportedTransactionVersion".to_owned(), serde_json::Value::Number(version.into())); } if let std::option::Option::Some(show_rewards) = self.show_rewards { object.insert("showRewards".to_owned(), serde_json::Value::Bool(show_rewards)); } return serde_json::Value::Object(object); } } /// Typed value carried inside an unstable Solana `blockNotification` response. #[derive(Clone, Debug, PartialEq)] pub struct SolanaBlockNotification { slot: u64, block: std::option::Option, err: std::option::Option, } impl SolanaBlockNotification { /// Returns the slot associated with this block update. #[must_use] pub const fn slot(&self) -> u64 { return self.slot; } /// Returns the decoded block when the unstable notification contains one. #[must_use] pub const fn block(&self) -> std::option::Option<&crate::SolanaConfirmedBlock> { return self.block.as_ref(); } /// Returns the nullable publication error without interpreting its unstable wire shape. #[must_use] pub const fn err(&self) -> std::option::Option<&serde_json::Value> { return self.err.as_ref(); } } impl crate::WsSession { /// Subscribes to unstable standard Solana block notifications through `blockSubscribe`. /// /// Solana documents this method as unstable and requires validator-side block-subscription support. KSP emits a warning through its logging facade when /// this family is requested. An explicitly supplied commitment must be `confirmed` or `finalized`. pub async fn block_subscribe( &self, filter: &crate::SolanaBlockSubscribeFilter, config: std::option::Option<&crate::SolanaBlockSubscribeConfig>, ) -> ksp_core_lib::Result>> { if let std::option::Option::Some(config) = config { let validated = config.validate(); if let std::result::Result::Err(error) = validated { return std::result::Result::Err(error); } } let mut params = std::vec![filter.to_json_value()]; if let std::option::Option::Some(config) = config && !config.is_empty() { params.push((*config).to_json_value()); } return self.subscribe_typed(crate::WsSubscriptionKind::Block, params, |value| return decode_block_notification("blockSubscribe", value)).await; } } #[derive(serde::Deserialize)] struct WireBlockNotification { slot: u64, block: std::option::Option, err: serde_json::Value, } fn decode_block_notification(method: &str, value: serde_json::Value) -> ksp_core_lib::Result> { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let context = crate::SolanaRpcContext::decode_wire(method, wire.context); let context = match context { std::result::Result::Ok(context) => context, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let block = match wire.value.block { std::option::Option::Some(value) => { let decoded = crate::SolanaConfirmedBlock::decode_wire(method, value); match decoded { std::result::Result::Ok(block) => std::option::Option::Some(block), std::result::Result::Err(error) => return std::result::Result::Err(error), } }, std::option::Option::None => std::option::Option::None, }; let err = match wire.value.err { serde_json::Value::Null => std::option::Option::None, value => std::option::Option::Some(value), }; return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, crate::SolanaBlockNotification { slot: wire.value.slot, block, err })); } #[derive(serde::Deserialize)] struct WireRpcResponse { context: serde_json::Value, value: WireBlockNotification, } impl crate::SolanaStandardWsSession { /// Subscribes to unstable standard Solana block notifications through `blockSubscribe`. pub async fn block_subscribe( &self, filter: &crate::SolanaBlockSubscribeFilter, config: std::option::Option<&crate::SolanaBlockSubscribeConfig>, ) -> ksp_core_lib::Result>> { return self.physical_session().block_subscribe(filter, config).await; } } #[cfg(test)] #[path = "../unit_tests/ws_blocks.rs"] mod tests;