v0.2.7-pre.011
This commit is contained in:
215
crates/ksp-onchain-transport-lib/src/ws_blocks.rs
Normal file
215
crates/ksp-onchain-transport-lib/src/ws_blocks.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_blocks.rs
|
||||
// version: 1
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/ws_blocks.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user