227 lines
9.3 KiB
Rust
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;
|