Files
khadhroony-solana-project/crates/ksp-onchain-transport-lib/src/ws_blocks.rs
2026-08-23 13:52:57 +02:00

227 lines
9.3 KiB
Rust

// 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<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionEncoding>,
transaction_details: std::option::Option<crate::SolanaTransactionDetails>,
max_supported_transaction_version: std::option::Option<u8>,
show_rewards: std::option::Option<bool>,
}
impl SolanaBlockSubscribeConfig {
/// Creates an explicit unstable block-subscription configuration.
#[must_use]
pub const fn new(
commitment: std::option::Option<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionEncoding>,
transaction_details: std::option::Option<crate::SolanaTransactionDetails>,
max_supported_transaction_version: std::option::Option<u8>,
show_rewards: std::option::Option<bool>,
) -> 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<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns the optional transaction encoding.
#[must_use]
pub const fn encoding(&self) -> std::option::Option<crate::SolanaTransactionEncoding> {
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 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;
}
/// Returns whether rewards were explicitly requested for block notifications.
#[must_use]
pub const fn show_rewards(&self) -> std::option::Option<bool> {
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<crate::SolanaConfirmedBlock>,
err: std::option::Option<serde_json::Value>,
}
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<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaBlockNotification>>> {
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<serde_json::Value>,
err: serde_json::Value,
}
fn decode_block_notification(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaBlockNotification>> {
let decoded = crate::decode_wire_json::<WireRpcResponse>(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<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaBlockNotification>>> {
return self.physical_session().block_subscribe(filter, config).await;
}
}
#[cfg(test)]
#[path = "../unit_tests/ws_blocks.rs"]
mod tests;