v0.2.7-pre.011
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 28
|
||||
// version: 29
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -44,6 +44,7 @@ mod rpc_tokens;
|
||||
mod rpc_transactions;
|
||||
mod settings;
|
||||
mod ws_accounts;
|
||||
mod ws_blocks;
|
||||
mod ws_cluster;
|
||||
mod ws_lifecycle;
|
||||
mod ws_session;
|
||||
@@ -321,8 +322,20 @@ pub use self::ws_accounts::SolanaAccountSubscribeConfig;
|
||||
pub use self::ws_accounts::SolanaProgramNotification;
|
||||
/// Configuration accepted by the standard Solana `programSubscribe` WebSocket method.
|
||||
pub use self::ws_accounts::SolanaProgramSubscribeConfig;
|
||||
/// Typed value carried inside an unstable Solana `blockNotification` response.
|
||||
pub use self::ws_blocks::SolanaBlockNotification;
|
||||
/// Optional configuration accepted by unstable Solana `blockSubscribe`.
|
||||
pub use self::ws_blocks::SolanaBlockSubscribeConfig;
|
||||
/// Filter accepted by unstable Solana `blockSubscribe`.
|
||||
pub use self::ws_blocks::SolanaBlockSubscribeFilter;
|
||||
/// Slot relationship reported by the standard Solana `slotNotification` WebSocket method.
|
||||
pub use self::ws_cluster::SolanaSlotNotification;
|
||||
/// Typed unstable Solana slot-lifecycle update with an unknown-variant fallback.
|
||||
pub use self::ws_cluster::SolanaSlotUpdate;
|
||||
/// Execution statistics attached to unstable Solana `slotsUpdatesNotification` frozen updates.
|
||||
pub use self::ws_cluster::SolanaSlotUpdateStats;
|
||||
/// Typed unstable gossip-vote notification delivered by standard Solana `voteSubscribe`.
|
||||
pub use self::ws_cluster::SolanaVoteNotification;
|
||||
/// Stable local identity assigned to one physical WebSocket session.
|
||||
pub use self::ws_lifecycle::WsSessionId;
|
||||
/// Safe runtime snapshot for one physical WebSocket session.
|
||||
|
||||
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;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_cluster.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Slot relationship reported by the standard Solana `slotNotification` WebSocket method.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -29,6 +29,162 @@ impl SolanaSlotNotification {
|
||||
}
|
||||
}
|
||||
|
||||
/// Execution statistics attached to unstable Solana `slotsUpdatesNotification` frozen updates.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaSlotUpdateStats {
|
||||
max_transactions_per_entry: u64,
|
||||
num_failed_transactions: u64,
|
||||
num_successful_transactions: u64,
|
||||
num_transaction_entries: u64,
|
||||
}
|
||||
|
||||
impl SolanaSlotUpdateStats {
|
||||
/// Returns the maximum transactions per entry observed for the frozen bank.
|
||||
#[must_use]
|
||||
pub const fn max_transactions_per_entry(&self) -> u64 {
|
||||
return self.max_transactions_per_entry;
|
||||
}
|
||||
|
||||
/// Returns the failed transaction count.
|
||||
#[must_use]
|
||||
pub const fn num_failed_transactions(&self) -> u64 {
|
||||
return self.num_failed_transactions;
|
||||
}
|
||||
|
||||
/// Returns the successful transaction count.
|
||||
#[must_use]
|
||||
pub const fn num_successful_transactions(&self) -> u64 {
|
||||
return self.num_successful_transactions;
|
||||
}
|
||||
|
||||
/// Returns the transaction-entry count.
|
||||
#[must_use]
|
||||
pub const fn num_transaction_entries(&self) -> u64 {
|
||||
return self.num_transaction_entries;
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed unstable Solana slot-lifecycle update with an unknown-variant fallback.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SolanaSlotUpdate {
|
||||
/// The first shred for a slot was received.
|
||||
FirstShredReceived { slot: u64, timestamp: i64 },
|
||||
/// All shreds for a slot were received.
|
||||
Completed { slot: u64, timestamp: i64 },
|
||||
/// A bank was created for the slot.
|
||||
CreatedBank { slot: u64, timestamp: i64, parent: u64 },
|
||||
/// A bank was frozen and execution statistics are available.
|
||||
Frozen { slot: u64, timestamp: i64, stats: crate::SolanaSlotUpdateStats },
|
||||
/// The slot was marked dead.
|
||||
Dead { slot: u64, timestamp: i64, error: std::string::String },
|
||||
/// The slot reached the current unstable optimistic-confirmation marker.
|
||||
OptimisticConfirmation { slot: u64, timestamp: i64 },
|
||||
/// The slot became root.
|
||||
Root { slot: u64, timestamp: i64 },
|
||||
/// A future upstream variant that KSP does not yet interpret.
|
||||
///
|
||||
/// `raw` is bounded by the physical session's configured inbound WebSocket message limit before JSON decoding.
|
||||
Unknown { update_type: std::string::String, raw: serde_json::Value },
|
||||
}
|
||||
|
||||
impl SolanaSlotUpdate {
|
||||
/// Returns the slot for known variants, or the optional slot found in an unknown raw variant.
|
||||
#[must_use]
|
||||
pub fn slot(&self) -> std::option::Option<u64> {
|
||||
return match self {
|
||||
Self::FirstShredReceived { slot, .. }
|
||||
| Self::Completed { slot, .. }
|
||||
| Self::CreatedBank { slot, .. }
|
||||
| Self::Frozen { slot, .. }
|
||||
| Self::Dead { slot, .. }
|
||||
| Self::OptimisticConfirmation { slot, .. }
|
||||
| Self::Root { slot, .. } => std::option::Option::Some(*slot),
|
||||
Self::Unknown { raw, .. } => raw.get("slot").and_then(serde_json::Value::as_u64),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the millisecond Unix timestamp for known variants, or an optional timestamp from an unknown raw variant.
|
||||
#[must_use]
|
||||
pub fn timestamp(&self) -> std::option::Option<i64> {
|
||||
return match self {
|
||||
Self::FirstShredReceived { timestamp, .. }
|
||||
| Self::Completed { timestamp, .. }
|
||||
| Self::CreatedBank { timestamp, .. }
|
||||
| Self::Frozen { timestamp, .. }
|
||||
| Self::Dead { timestamp, .. }
|
||||
| Self::OptimisticConfirmation { timestamp, .. }
|
||||
| Self::Root { timestamp, .. } => std::option::Option::Some(*timestamp),
|
||||
Self::Unknown { raw, .. } => raw.get("timestamp").and_then(serde_json::Value::as_i64),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the upstream `type` string, including unknown future values.
|
||||
#[must_use]
|
||||
pub fn update_type(&self) -> &str {
|
||||
return match self {
|
||||
Self::FirstShredReceived { .. } => "firstShredReceived",
|
||||
Self::Completed { .. } => "completed",
|
||||
Self::CreatedBank { .. } => "createdBank",
|
||||
Self::Frozen { .. } => "frozen",
|
||||
Self::Dead { .. } => "dead",
|
||||
Self::OptimisticConfirmation { .. } => "optimisticConfirmation",
|
||||
Self::Root { .. } => "root",
|
||||
Self::Unknown { update_type, .. } => update_type.as_str(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the bounded raw object only for an unknown future upstream variant.
|
||||
#[must_use]
|
||||
pub const fn unknown_raw(&self) -> std::option::Option<&serde_json::Value> {
|
||||
return match self {
|
||||
Self::Unknown { raw, .. } => std::option::Option::Some(raw),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed unstable gossip-vote notification delivered by standard Solana `voteSubscribe`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaVoteNotification {
|
||||
vote_pubkey: ksp_core_lib::Pubkey,
|
||||
slots: std::vec::Vec<u64>,
|
||||
hash: std::string::String,
|
||||
timestamp: std::option::Option<i64>,
|
||||
signature: std::string::String,
|
||||
}
|
||||
|
||||
impl SolanaVoteNotification {
|
||||
/// Returns the vote-account public key.
|
||||
#[must_use]
|
||||
pub const fn vote_pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.vote_pubkey;
|
||||
}
|
||||
|
||||
/// Returns the ordered slots covered by the observed vote.
|
||||
#[must_use]
|
||||
pub fn slots(&self) -> &[u64] {
|
||||
return self.slots.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the vote hash exactly as reported by the unstable upstream wire.
|
||||
#[must_use]
|
||||
pub fn hash(&self) -> &str {
|
||||
return self.hash.as_str();
|
||||
}
|
||||
|
||||
/// Returns the optional vote timestamp, preserving omitted and explicit-null wire forms as `None`.
|
||||
#[must_use]
|
||||
pub const fn timestamp(&self) -> std::option::Option<i64> {
|
||||
return self.timestamp;
|
||||
}
|
||||
|
||||
/// Returns the vote transaction signature exactly as reported by the unstable upstream wire.
|
||||
#[must_use]
|
||||
pub fn signature(&self) -> &str {
|
||||
return self.signature.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::WsSession {
|
||||
/// Subscribes to standard Solana slot-processing notifications through `slotSubscribe`.
|
||||
pub async fn slot_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaSlotNotification>> {
|
||||
@@ -47,6 +203,24 @@ impl crate::WsSession {
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Subscribes to unstable standard Solana slot-lifecycle notifications through `slotsUpdatesSubscribe`.
|
||||
pub async fn slots_updates_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaSlotUpdate>> {
|
||||
return self
|
||||
.subscribe_typed(crate::WsSubscriptionKind::SlotsUpdates, std::vec::Vec::new(), |value| {
|
||||
return decode_slots_update_notification("slotsUpdatesSubscribe", value);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Subscribes to unstable pre-consensus gossip vote notifications through `voteSubscribe`.
|
||||
pub async fn vote_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaVoteNotification>> {
|
||||
return self
|
||||
.subscribe_typed(crate::WsSubscriptionKind::Vote, std::vec::Vec::new(), |value| {
|
||||
return decode_vote_notification("voteSubscribe", value);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -65,6 +239,122 @@ fn decode_slot_notification(method: &str, value: serde_json::Value) -> ksp_core_
|
||||
return std::result::Result::Ok(crate::SolanaSlotNotification { slot: wire.slot, parent: wire.parent, root: wire.root });
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSlotUpdateStats {
|
||||
max_transactions_per_entry: u64,
|
||||
num_failed_transactions: u64,
|
||||
num_successful_transactions: u64,
|
||||
num_transaction_entries: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireVoteNotification {
|
||||
#[serde(rename = "votePubkey")]
|
||||
vote_pubkey: std::string::String,
|
||||
slots: std::vec::Vec<u64>,
|
||||
hash: std::string::String,
|
||||
#[serde(default)]
|
||||
timestamp: std::option::Option<i64>,
|
||||
signature: std::string::String,
|
||||
}
|
||||
|
||||
fn decode_slots_update_notification(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaSlotUpdate> {
|
||||
let object = match value.as_object() {
|
||||
std::option::Option::Some(object) => object,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "slotsUpdatesSubscribe notification must be an object")
|
||||
.with_context("rpc_method", method),
|
||||
);
|
||||
},
|
||||
};
|
||||
let update_type = match object.get("type").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(update_type) => update_type,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "slotsUpdatesSubscribe notification is missing a string type")
|
||||
.with_context("rpc_method", method),
|
||||
);
|
||||
},
|
||||
};
|
||||
if !matches!(update_type, "firstShredReceived" | "completed" | "createdBank" | "frozen" | "dead" | "optimisticConfirmation" | "root") {
|
||||
return std::result::Result::Ok(crate::SolanaSlotUpdate::Unknown { update_type: update_type.to_owned(), raw: value.clone() });
|
||||
}
|
||||
let slot = match object.get("slot").and_then(serde_json::Value::as_u64) {
|
||||
std::option::Option::Some(slot) => slot,
|
||||
std::option::Option::None => return invalid_slots_update(method, "known slots update is missing numeric slot"),
|
||||
};
|
||||
let timestamp = match object.get("timestamp").and_then(serde_json::Value::as_i64) {
|
||||
std::option::Option::Some(timestamp) => timestamp,
|
||||
std::option::Option::None => return invalid_slots_update(method, "known slots update is missing numeric timestamp"),
|
||||
};
|
||||
return match update_type {
|
||||
"firstShredReceived" => std::result::Result::Ok(crate::SolanaSlotUpdate::FirstShredReceived { slot, timestamp }),
|
||||
"completed" => std::result::Result::Ok(crate::SolanaSlotUpdate::Completed { slot, timestamp }),
|
||||
"createdBank" => match object.get("parent").and_then(serde_json::Value::as_u64) {
|
||||
std::option::Option::Some(parent) => std::result::Result::Ok(crate::SolanaSlotUpdate::CreatedBank { slot, timestamp, parent }),
|
||||
std::option::Option::None => invalid_slots_update(method, "createdBank update is missing numeric parent"),
|
||||
},
|
||||
"frozen" => {
|
||||
let stats = match object.get("stats") {
|
||||
std::option::Option::Some(stats) => crate::decode_wire_json::<WireSlotUpdateStats>(method, stats.clone()),
|
||||
std::option::Option::None => return invalid_slots_update(method, "frozen update is missing stats"),
|
||||
};
|
||||
let stats = match stats {
|
||||
std::result::Result::Ok(stats) => stats,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
std::result::Result::Ok(crate::SolanaSlotUpdate::Frozen {
|
||||
slot,
|
||||
timestamp,
|
||||
stats: crate::SolanaSlotUpdateStats {
|
||||
max_transactions_per_entry: stats.max_transactions_per_entry,
|
||||
num_failed_transactions: stats.num_failed_transactions,
|
||||
num_successful_transactions: stats.num_successful_transactions,
|
||||
num_transaction_entries: stats.num_transaction_entries,
|
||||
},
|
||||
})
|
||||
},
|
||||
"dead" => match object.get("err").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(error) => std::result::Result::Ok(crate::SolanaSlotUpdate::Dead { slot, timestamp, error: error.to_owned() }),
|
||||
std::option::Option::None => invalid_slots_update(method, "dead update is missing string err"),
|
||||
},
|
||||
"optimisticConfirmation" => std::result::Result::Ok(crate::SolanaSlotUpdate::OptimisticConfirmation { slot, timestamp }),
|
||||
"root" => std::result::Result::Ok(crate::SolanaSlotUpdate::Root { slot, timestamp }),
|
||||
_ => invalid_slots_update(method, "known slots update type dispatch failed"),
|
||||
};
|
||||
}
|
||||
|
||||
fn invalid_slots_update(method: &str, message: &'static str) -> ksp_core_lib::Result<crate::SolanaSlotUpdate> {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method));
|
||||
}
|
||||
|
||||
fn decode_vote_notification(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaVoteNotification> {
|
||||
let decoded = crate::decode_wire_json::<WireVoteNotification>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let vote_pubkey = wire.vote_pubkey.parse::<ksp_core_lib::Pubkey>();
|
||||
let vote_pubkey = match vote_pubkey {
|
||||
std::result::Result::Ok(vote_pubkey) => vote_pubkey,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "voteSubscribe notification contains an invalid votePubkey")
|
||||
.with_context("rpc_method", method),
|
||||
);
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(crate::SolanaVoteNotification {
|
||||
vote_pubkey,
|
||||
slots: wire.slots,
|
||||
hash: wire.hash,
|
||||
timestamp: wire.timestamp,
|
||||
signature: wire.signature,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/ws_cluster.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
/// Stable local identity assigned to one physical WebSocket session.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
@@ -161,6 +161,25 @@ impl WsSubscriptionKind {
|
||||
Self::Vote => "voteNotification",
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns whether Solana documents this standard subscription family as unstable.
|
||||
pub(crate) const fn is_unstable(self) -> bool {
|
||||
return matches!(self, Self::Block | Self::SlotsUpdates | Self::Vote);
|
||||
}
|
||||
|
||||
/// Emits the centralized KSP warning required before opening an unstable standard subscription.
|
||||
pub(crate) fn warn_if_unstable(self) {
|
||||
if self.is_unstable() {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
rpc_method = self.subscribe_method(),
|
||||
subscription_kind = self.as_str(),
|
||||
documentation_status = "unstable",
|
||||
"unstable Solana WebSocket subscription requested"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe lifecycle projection for one logical WebSocket subscription.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
use futures_util::SinkExt; // rust-rules: trait-import
|
||||
use futures_util::StreamExt; // rust-rules: trait-import
|
||||
@@ -187,6 +187,7 @@ impl WsSession {
|
||||
if self.state() != crate::WsSessionState::Active {
|
||||
return std::result::Result::Err(ws_session_closed_error(self.id, "WebSocket session is not active"));
|
||||
}
|
||||
kind.warn_if_unstable();
|
||||
let (dispatcher, notification_rx) = crate::typed_notification_channel_with_completion(self.notification_queue_capacity, decoder, is_terminal);
|
||||
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
|
||||
let command = WsSessionCommand::Subscribe { kind, params, dispatcher, response_tx };
|
||||
|
||||
Reference in New Issue
Block a user