// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs // version: 8 const MAX_GET_BLOCKS_RANGE: u64 = 500_000; const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720; /// Transaction detail level accepted by modern `getBlock` requests. #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub enum SolanaTransactionDetails { /// Return complete transaction payloads and metadata. #[default] Full, /// Return transaction signatures only. Signatures, /// Return no transaction payloads. None, /// Return the account-list transaction representation. Accounts, } impl SolanaTransactionDetails { /// Returns the Solana JSON-RPC wire string. #[must_use] pub const fn as_str(self) -> &'static str { return match self { Self::Full => "full", Self::Signatures => "signatures", Self::None => "none", Self::Accounts => "accounts", }; } } /// Modern configuration object accepted by `getBlock`. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct SolanaGetBlockConfig { commitment: std::option::Option, encoding: std::option::Option, transaction_details: std::option::Option, max_supported_transaction_version: std::option::Option, rewards: std::option::Option, } impl SolanaGetBlockConfig { /// Creates a modern `getBlock` 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, rewards: std::option::Option, ) -> Self { return Self { commitment, encoding, transaction_details, max_supported_transaction_version, 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 or suppressed. #[must_use] pub const fn rewards(&self) -> std::option::Option { return self.rewards; } /// Returns whether this config would serialize to an empty object. #[cfg(test)] pub(crate) const 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.rewards.is_none(); } /// Serializes this config to the exact Solana JSON-RPC object. #[must_use] pub(crate) 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(details) = self.transaction_details { object.insert("transactionDetails".to_owned(), serde_json::Value::String(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(rewards) = self.rewards { object.insert("rewards".to_owned(), serde_json::Value::Bool(rewards)); } return serde_json::Value::Object(object); } } /// Slot range accepted inside `getBlockProduction` configuration. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct SolanaBlockProductionRange { first_slot: u64, last_slot: std::option::Option, } impl SolanaBlockProductionRange { /// Creates a block-production range. #[must_use] pub const fn new(first_slot: u64, last_slot: std::option::Option) -> Self { return Self { first_slot, last_slot }; } /// Returns the first slot in the requested range. #[must_use] pub const fn first_slot(&self) -> u64 { return self.first_slot; } /// Returns the optional last slot in the requested range. #[must_use] pub const fn last_slot(&self) -> std::option::Option { return self.last_slot; } /// Serializes this range to the exact Solana JSON-RPC object. #[must_use] pub(crate) fn to_json_value(self) -> serde_json::Value { let mut object = serde_json::Map::new(); object.insert("firstSlot".to_owned(), serde_json::Value::Number(self.first_slot.into())); if let std::option::Option::Some(last_slot) = self.last_slot { object.insert("lastSlot".to_owned(), serde_json::Value::Number(last_slot.into())); } return serde_json::Value::Object(object); } } /// Optional configuration accepted by `getBlockProduction`. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SolanaBlockProductionConfig { commitment: std::option::Option, identity: std::option::Option, range: std::option::Option, } impl SolanaBlockProductionConfig { /// Creates a block-production configuration. #[must_use] pub const fn new( commitment: std::option::Option, identity: std::option::Option, range: std::option::Option, ) -> Self { return Self { commitment, identity, range }; } /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } /// Returns the optional validator identity filter. #[must_use] pub const fn identity(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { return self.identity.as_ref(); } /// Returns the optional slot range. #[must_use] pub const fn range(&self) -> std::option::Option<&crate::SolanaBlockProductionRange> { return self.range.as_ref(); } /// Returns whether this config would serialize to an empty object. pub(crate) const fn is_empty(&self) -> bool { return self.commitment.is_none() && self.identity.is_none() && self.range.is_none(); } /// Serializes this config to the exact Solana JSON-RPC object. #[must_use] pub(crate) 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(identity) = self.identity.as_ref() { object.insert("identity".to_owned(), serde_json::Value::String(identity.to_string())); } if let std::option::Option::Some(range) = self.range { object.insert("range".to_owned(), range.to_json_value()); } return serde_json::Value::Object(object); } } /// Commitment distribution returned by `getBlockCommitment`. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SolanaBlockCommitment { commitment: std::option::Option>, total_stake: u64, } impl SolanaBlockCommitment { /// Returns the nullable commitment stake distribution. #[must_use] pub fn commitment(&self) -> std::option::Option<&[u64]> { return self.commitment.as_deref(); } /// Returns the total active stake considered by the response. #[must_use] pub const fn total_stake(&self) -> u64 { return self.total_stake; } /// Decodes a block-commitment result from its Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { std::result::Result::Ok(wire) => std::result::Result::Ok(Self { commitment: wire.commitment, total_stake: wire.total_stake }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// Effective range reported inside `getBlockProduction` results. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct SolanaBlockProductionResultRange { first_slot: u64, last_slot: u64, } impl SolanaBlockProductionResultRange { /// Returns the first slot in the effective range. #[must_use] pub const fn first_slot(&self) -> u64 { return self.first_slot; } /// Returns the last slot in the effective range. #[must_use] pub const fn last_slot(&self) -> u64 { return self.last_slot; } } /// Block-production counts returned by `getBlockProduction` before the shared RPC context wrapper is applied. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SolanaBlockProduction { by_identity: std::collections::BTreeMap, range: crate::SolanaBlockProductionResultRange, } impl SolanaBlockProduction { /// Returns validator identities mapped to `(leader slots, blocks produced)` counts. #[must_use] pub const fn by_identity(&self) -> &std::collections::BTreeMap { return &self.by_identity; } /// Returns the effective slot range reported by the runtime. #[must_use] pub const fn range(&self) -> &crate::SolanaBlockProductionResultRange { return &self.range; } /// Decodes a block-production result from its Solana JSON wire shape. pub(crate) fn decode_wire(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 mut by_identity = std::collections::BTreeMap::new(); for (identity, counts) in wire.by_identity { let parsed = crate::parse_wire_pubkey(method, "byIdentity", identity.as_str()); let identity = match parsed { std::result::Result::Ok(identity) => identity, std::result::Result::Err(error) => return std::result::Result::Err(error), }; by_identity.insert(identity, counts); } return std::result::Result::Ok(Self { by_identity, range: crate::SolanaBlockProductionResultRange { first_slot: wire.range.first_slot, last_slot: wire.range.last_slot }, }); } } /// One reward entry returned in a confirmed block. #[derive(Clone, Debug, PartialEq)] pub struct SolanaBlockReward { pubkey: ksp_core_lib::Pubkey, lamports: i64, post_balance: u64, reward_type: std::option::Option, commission: std::option::Option, commission_bps: crate::SolanaWireField, } impl SolanaBlockReward { /// Returns the rewarded account public key. #[must_use] pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.pubkey; } /// Returns the signed lamport delta applied by the reward. #[must_use] pub const fn lamports(&self) -> i64 { return self.lamports; } /// Returns the post-reward account balance in lamports. #[must_use] pub const fn post_balance(&self) -> u64 { return self.post_balance; } /// Returns the runtime reward type text without imposing a local economic taxonomy. #[must_use] pub fn reward_type(&self) -> std::option::Option<&str> { return self.reward_type.as_deref(); } /// Returns the nullable legacy/effective commission percentage. #[must_use] pub const fn commission(&self) -> std::option::Option { return self.commission; } /// Returns the basis-point commission while preserving omitted/null/present wire states. #[must_use] pub const fn commission_bps(&self) -> &crate::SolanaWireField { return &self.commission_bps; } fn decode_wire(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 pubkey = crate::parse_wire_pubkey(method, "rewards.pubkey", wire.pubkey.as_str()); return match pubkey { std::result::Result::Ok(pubkey) => std::result::Result::Ok(Self { pubkey, lamports: wire.lamports, post_balance: wire.post_balance, reward_type: wire.reward_type, commission: wire.commission, commission_bps: wire.commission_bps, }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// One transaction entry returned inside a confirmed block. #[derive(Clone, Debug, PartialEq)] pub struct SolanaBlockTransaction { transaction: crate::SolanaEncodedTransaction, meta: crate::SolanaWireField, version: crate::SolanaWireField, } impl SolanaBlockTransaction { /// Returns the encoded or JSON transaction payload without Program-specific decoding. #[must_use] pub const fn transaction(&self) -> &crate::SolanaEncodedTransaction { return &self.transaction; } /// Returns transaction metadata while preserving omission versus explicit `null`. #[must_use] pub const fn meta(&self) -> &crate::SolanaWireField { return &self.meta; } /// Returns the transaction version while preserving omission versus explicit `null`. #[must_use] pub const fn version(&self) -> &crate::SolanaWireField { return &self.version; } fn decode_wire(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 transaction = crate::SolanaEncodedTransaction::decode_wire(method, wire.transaction); let transaction = match transaction { std::result::Result::Ok(transaction) => transaction, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let version = decode_block_transaction_version(method, wire.version); return match version { std::result::Result::Ok(version) => std::result::Result::Ok(Self { transaction, meta: wire.meta, version }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// Confirmed block wire result returned by `getBlock` when the RPC result is non-null. #[derive(Clone, Debug, PartialEq)] pub struct SolanaConfirmedBlock { previous_blockhash: std::string::String, blockhash: std::string::String, parent_slot: u64, transactions: crate::SolanaWireField>, signatures: crate::SolanaWireField>, rewards: crate::SolanaWireField>, num_reward_partitions: crate::SolanaWireField, block_time: std::option::Option, block_height: std::option::Option, } impl SolanaConfirmedBlock { /// Returns the previous blockhash exactly as reported by the RPC wire. #[must_use] pub fn previous_blockhash(&self) -> &str { return self.previous_blockhash.as_str(); } /// Returns this block's blockhash exactly as reported by the RPC wire. #[must_use] pub fn blockhash(&self) -> &str { return self.blockhash.as_str(); } /// Returns the parent slot. #[must_use] pub const fn parent_slot(&self) -> u64 { return self.parent_slot; } /// Returns block transactions while preserving omitted/null/present states. #[must_use] pub const fn transactions(&self) -> &crate::SolanaWireField> { return &self.transactions; } /// Returns block signatures while preserving omitted/null/present states. #[must_use] pub const fn signatures(&self) -> &crate::SolanaWireField> { return &self.signatures; } /// Returns block rewards while preserving omitted/null/present states. #[must_use] pub const fn rewards(&self) -> &crate::SolanaWireField> { return &self.rewards; } /// Returns the SIMD-0118 reward-partition count while preserving omission versus explicit `null`. #[must_use] pub const fn num_reward_partitions(&self) -> &crate::SolanaWireField { return &self.num_reward_partitions; } /// Returns the nullable block time as a Unix timestamp. #[must_use] pub const fn block_time(&self) -> std::option::Option { return self.block_time; } /// Returns the nullable block height. #[must_use] pub const fn block_height(&self) -> std::option::Option { return self.block_height; } /// Decodes a confirmed block from its Solana JSON wire shape. pub(crate) fn decode_wire(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 transactions = decode_block_transactions(method, wire.transactions); let transactions = match transactions { std::result::Result::Ok(transactions) => transactions, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let rewards = decode_block_rewards(method, wire.rewards); let rewards = match rewards { std::result::Result::Ok(rewards) => rewards, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(Self { previous_blockhash: wire.previous_blockhash, blockhash: wire.blockhash, parent_slot: wire.parent_slot, transactions, signatures: wire.signatures, rewards, num_reward_partitions: wire.num_reward_partitions, block_time: wire.block_time, block_height: wire.block_height, }); } } /// One entry returned by `getRecentPerformanceSamples`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct SolanaPerformanceSample { slot: u64, num_transactions: u64, num_non_vote_transactions: std::option::Option, num_slots: u64, sample_period_secs: u16, } impl SolanaPerformanceSample { /// Returns the sample's highest slot. #[must_use] pub const fn slot(&self) -> u64 { return self.slot; } /// Returns the total transaction count in the sample. #[must_use] pub const fn num_transactions(&self) -> u64 { return self.num_transactions; } /// Returns the optional non-vote transaction count exposed by newer runtimes. #[must_use] pub const fn num_non_vote_transactions(&self) -> std::option::Option { return self.num_non_vote_transactions; } /// Returns the number of slots covered by the sample. #[must_use] pub const fn num_slots(&self) -> u64 { return self.num_slots; } /// Returns the sample duration in seconds. #[must_use] pub const fn sample_period_secs(&self) -> u16 { return self.sample_period_secs; } /// Decodes one performance sample from its Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { std::result::Result::Ok(wire) => std::result::Result::Ok(Self { slot: wire.slot, num_transactions: wire.num_transactions, num_non_vote_transactions: wire.num_non_vote_transactions, num_slots: wire.num_slots, sample_period_secs: wire.sample_period_secs, }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } impl crate::HttpTransportPool { /// Executes the current object-form `getBlock` request through the common KSP HTTP transport path. /// /// `None` omits the optional second parameter. `Some(config)` sends the modern object form exactly, including an empty `{}` when the caller /// explicitly supplies an empty modern configuration. The runtime requires an explicitly supplied commitment to be at least `confirmed`. pub async fn get_block( &self, role: &crate::HttpRoleName, slot: u64, config: std::option::Option<&crate::SolanaGetBlockConfig>, ) -> ksp_core_lib::Result> { let params = get_block_params(slot, config); let params = match params { std::result::Result::Ok(params) => params, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return self.execute_get_block(role, params).await; } /// Executes the current object-form `getBlock` request and reports the safe identity of the endpoint that produced the successful response. /// /// Routing, admission, timeout and retry behavior are identical to [`Self::get_block`]. The returned observation never contains an endpoint URL, /// HTTP headers or a raw HTTP body. pub async fn get_block_observed( &self, role: &crate::HttpRoleName, slot: u64, config: std::option::Option<&crate::SolanaGetBlockConfig>, ) -> ksp_core_lib::Result>> { let params = get_block_params(slot, config); let params = match params { std::result::Result::Ok(params) => params, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let observed = self.execute_blocks_rpc_observed("getBlock", role, params).await; let observed = match observed { std::result::Result::Ok(observed) => observed, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let (value, endpoint_name, provider) = observed.into_parts(); let block = decode_get_block(value); return match block { std::result::Result::Ok(block) => std::result::Result::Ok(crate::HttpObservedValue::new(block, endpoint_name, provider)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes the deprecated bare-encoding `getBlock` request form retained by Solana RPC for backwards compatibility. #[deprecated(note = "use HttpTransportPool::get_block with SolanaGetBlockConfig; the bare encoding request form is deprecated")] pub async fn get_block_legacy( &self, role: &crate::HttpRoleName, slot: u64, encoding: crate::SolanaTransactionEncoding, ) -> ksp_core_lib::Result> { ksp_logging_lib::warn!( target: crate::TRACING_TARGET, rpc_method = "getBlock", request_form = "bare_encoding", encoding = encoding.as_str(), "deprecated Solana HTTP RPC request form used" ); let params = std::vec![serde_json::json!(slot), serde_json::Value::String(encoding.as_str().to_owned())]; return self.execute_get_block(role, params).await; } async fn execute_get_block( &self, role: &crate::HttpRoleName, params: std::vec::Vec, ) -> ksp_core_lib::Result> { let value = self.execute_blocks_rpc("getBlock", role, params).await; return match value { std::result::Result::Ok(value) => decode_get_block(value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `getBlockCommitment` through the common KSP HTTP transport path. pub async fn get_block_commitment(&self, role: &crate::HttpRoleName, slot: u64) -> ksp_core_lib::Result { let value = self.execute_blocks_rpc("getBlockCommitment", role, std::vec![serde_json::json!(slot)]).await; return match value { std::result::Result::Ok(value) => crate::SolanaBlockCommitment::decode_wire("getBlockCommitment", value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `getBlockHeight` through the common KSP HTTP transport path. pub async fn get_block_height(&self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result { let mut params = std::vec::Vec::new(); push_blocks_context_config(&mut params, config); return self.get_blocks_u64("getBlockHeight", role, params).await; } /// Executes typed `getBlockTime` and preserves a runtime `null` as `None`. pub async fn get_block_time(&self, role: &crate::HttpRoleName, slot: u64) -> ksp_core_lib::Result> { let value = self.execute_blocks_rpc("getBlockTime", role, std::vec![serde_json::json!(slot)]).await; return match value { std::result::Result::Ok(value) => crate::decode_wire_json::>("getBlockTime", value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `getFirstAvailableBlock` through the common KSP HTTP transport path. pub async fn get_first_available_block(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result { return self.get_blocks_u64("getFirstAvailableBlock", role, std::vec::Vec::new()).await; } /// Executes typed `minimumLedgerSlot` through the common KSP HTTP transport path. pub async fn minimum_ledger_slot(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result { return self.get_blocks_u64("minimumLedgerSlot", role, std::vec::Vec::new()).await; } /// Executes typed `getBlocks` while preserving all four supported parameter overloads. pub async fn get_blocks( &self, role: &crate::HttpRoleName, start_slot: u64, end_slot: std::option::Option, config: std::option::Option<&crate::SolanaContextConfig>, ) -> ksp_core_lib::Result> { let validated = validate_blocks_context_commitment("getBlocks", config); if let std::result::Result::Err(error) = validated { return std::result::Result::Err(error); } if let std::option::Option::Some(end_slot) = end_slot && end_slot >= start_slot && end_slot - start_slot > MAX_GET_BLOCKS_RANGE { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getBlocks range must not exceed 500000 slots") .with_context("rpc_method", "getBlocks") .with_context("start_slot", start_slot.to_string()) .with_context("end_slot", end_slot.to_string()), ); } let mut params = std::vec![serde_json::json!(start_slot)]; if let std::option::Option::Some(end_slot) = end_slot { params.push(serde_json::json!(end_slot)); } push_blocks_explicit_context_config(&mut params, config); return self.get_blocks_u64_list("getBlocks", role, params).await; } /// Executes typed `getBlocksWithLimit` with the runtime-supported inclusive upper bound. pub async fn get_blocks_with_limit( &self, role: &crate::HttpRoleName, start_slot: u64, limit: u64, config: std::option::Option<&crate::SolanaContextConfig>, ) -> ksp_core_lib::Result> { let validated = validate_blocks_context_commitment("getBlocksWithLimit", config); if let std::result::Result::Err(error) = validated { return std::result::Result::Err(error); } if limit > MAX_GET_BLOCKS_RANGE { return invalid_blocks_limit("getBlocksWithLimit", "getBlocksWithLimit limit must not exceed 500000", limit); } let mut params = std::vec![serde_json::json!(start_slot), serde_json::json!(limit)]; push_blocks_explicit_context_config(&mut params, config); return self.get_blocks_u64_list("getBlocksWithLimit", role, params).await; } /// Executes typed `getBlockProduction` with optional identity, range, and commitment filters. pub async fn get_block_production( &self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaBlockProductionConfig>, ) -> ksp_core_lib::Result> { if let std::option::Option::Some(config) = config && let std::option::Option::Some(range) = config.range() && let std::option::Option::Some(last_slot) = range.last_slot() && last_slot < range.first_slot() { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getBlockProduction lastSlot must not be less than firstSlot") .with_context("rpc_method", "getBlockProduction") .with_context("first_slot", range.first_slot().to_string()) .with_context("last_slot", last_slot.to_string()), ); } let mut params = std::vec::Vec::new(); if let std::option::Option::Some(config) = config && !config.is_empty() { params.push(config.to_json_value()); } let value = self.execute_blocks_rpc("getBlockProduction", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return decode_block_production_response("getBlockProduction", value); } /// Executes typed `getRecentPerformanceSamples`, preserving runtime order and older sample shapes. pub async fn get_recent_performance_samples( &self, role: &crate::HttpRoleName, limit: std::option::Option, ) -> ksp_core_lib::Result> { if let std::option::Option::Some(limit) = limit && limit > MAX_GET_RECENT_PERFORMANCE_SAMPLES { return invalid_blocks_limit("getRecentPerformanceSamples", "getRecentPerformanceSamples limit must not exceed 720", limit); } let mut params = std::vec::Vec::new(); if let std::option::Option::Some(limit) = limit { params.push(serde_json::json!(limit)); } let value = self.execute_blocks_rpc("getRecentPerformanceSamples", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return decode_performance_samples("getRecentPerformanceSamples", value); } async fn get_blocks_u64_list( &self, method_name: &'static str, role: &crate::HttpRoleName, params: std::vec::Vec, ) -> ksp_core_lib::Result> { let value = self.execute_blocks_rpc(method_name, role, params).await; return match value { std::result::Result::Ok(value) => crate::decode_wire_json::>(method_name, value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } async fn get_blocks_u64( &self, method_name: &'static str, role: &crate::HttpRoleName, params: std::vec::Vec, ) -> ksp_core_lib::Result { let value = self.execute_blocks_rpc(method_name, role, params).await; return match value { std::result::Result::Ok(value) => crate::decode_wire_json::(method_name, value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } async fn execute_blocks_rpc( &self, method_name: &'static str, role: &crate::HttpRoleName, params: std::vec::Vec, ) -> ksp_core_lib::Result { let method = blocks_descriptor(method_name); let method = match method { std::result::Result::Ok(method) => method, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return self.execute_standard_rpc(role, method, params).await; } async fn execute_blocks_rpc_observed( &self, method_name: &'static str, role: &crate::HttpRoleName, params: std::vec::Vec, ) -> ksp_core_lib::Result> { let method = blocks_descriptor(method_name); let method = match method { std::result::Result::Ok(method) => method, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return self.execute_standard_rpc_observed(role, method, params).await; } } fn decode_get_block(value: serde_json::Value) -> ksp_core_lib::Result> { if value.is_null() { return std::result::Result::Ok(std::option::Option::None); } let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value); return match block { std::result::Result::Ok(block) => std::result::Result::Ok(std::option::Option::Some(block)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn get_block_params(slot: u64, config: std::option::Option<&crate::SolanaGetBlockConfig>) -> ksp_core_lib::Result> { if let std::option::Option::Some(config) = config && config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed) { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getBlock commitment must be confirmed or finalized when explicitly provided") .with_context("rpc_method", "getBlock") .with_context("commitment", "processed"), ); } let mut params = std::vec![serde_json::json!(slot)]; if let std::option::Option::Some(config) = config { params.push((*config).to_json_value()); } return std::result::Result::Ok(params); } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockCommitment { commitment: std::option::Option>, total_stake: u64, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockProductionRange { first_slot: u64, last_slot: u64, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockProduction { by_identity: std::collections::BTreeMap, range: WireBlockProductionRange, } #[derive(serde::Deserialize)] struct WireBlockProductionRpcResponse { context: serde_json::Value, value: serde_json::Value, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockReward { pubkey: std::string::String, lamports: i64, post_balance: u64, #[serde(default)] reward_type: std::option::Option, #[serde(default)] commission: std::option::Option, #[serde(default)] commission_bps: crate::SolanaWireField, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockTransaction { transaction: serde_json::Value, #[serde(default)] meta: crate::SolanaWireField, #[serde(default)] version: crate::SolanaWireField, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireConfirmedBlock { previous_blockhash: std::string::String, blockhash: std::string::String, parent_slot: u64, #[serde(default)] transactions: crate::SolanaWireField>, #[serde(default)] signatures: crate::SolanaWireField>, #[serde(default)] rewards: crate::SolanaWireField>, #[serde(default)] num_reward_partitions: crate::SolanaWireField, block_time: std::option::Option, block_height: std::option::Option, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WirePerformanceSample { slot: u64, num_transactions: u64, #[serde(default)] num_non_vote_transactions: std::option::Option, num_slots: u64, sample_period_secs: u16, } fn validate_blocks_context_commitment(method: &'static str, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result<()> { if let std::option::Option::Some(config) = config && config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed) { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "block-range commitment must be confirmed or finalized when explicitly provided", ) .with_context("rpc_method", method) .with_context("commitment", "processed"), ); } return std::result::Result::Ok(()); } fn push_blocks_explicit_context_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaContextConfig>) { if let std::option::Option::Some(config) = config { params.push((*config).to_json_value()); } return; } fn invalid_blocks_limit(method: &'static str, message: &'static str, limit: u64) -> ksp_core_lib::Result { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message) .with_context("rpc_method", method) .with_context("limit", limit.to_string()), ); } fn decode_block_production_response(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 value = crate::SolanaBlockProduction::decode_wire(method, wire.value); return match value { std::result::Result::Ok(value) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, value)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn decode_performance_samples(method: &str, value: serde_json::Value) -> ksp_core_lib::Result> { let values = crate::decode_wire_json::>(method, value); let values = match values { std::result::Result::Ok(values) => values, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut samples = std::vec::Vec::with_capacity(values.len()); for value in values { let sample = crate::SolanaPerformanceSample::decode_wire(method, value); match sample { std::result::Result::Ok(sample) => samples.push(sample), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(samples); } fn push_blocks_context_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaContextConfig>) { if let std::option::Option::Some(config) = config && (config.commitment().is_some() || config.min_context_slot().is_some()) { params.push((*config).to_json_value()); } return; } fn blocks_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> { let descriptor = crate::find_http_rpc_method(method); return match descriptor { std::option::Option::Some(descriptor) if descriptor.category() == crate::HttpRpcCategory::Blocks && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_4 => { std::result::Result::Ok(descriptor) }, _ => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Blocks descriptor is missing from the audited 0.2.4 registry") .with_context("rpc_method", method), ), }; } fn decode_block_transaction_version( method: &str, field: crate::SolanaWireField, ) -> ksp_core_lib::Result> { return match field { crate::SolanaWireField::Omitted => std::result::Result::Ok(crate::SolanaWireField::Omitted), crate::SolanaWireField::Null => std::result::Result::Ok(crate::SolanaWireField::Null), crate::SolanaWireField::Value(value) => { let decoded = crate::SolanaTransactionVersion::decode_wire(method, value); match decoded { std::result::Result::Ok(version) => std::result::Result::Ok(crate::SolanaWireField::Value(version)), std::result::Result::Err(error) => std::result::Result::Err(error), } }, }; } fn decode_block_transactions( method: &str, field: crate::SolanaWireField>, ) -> ksp_core_lib::Result>> { let values = match field { crate::SolanaWireField::Omitted => return std::result::Result::Ok(crate::SolanaWireField::Omitted), crate::SolanaWireField::Null => return std::result::Result::Ok(crate::SolanaWireField::Null), crate::SolanaWireField::Value(values) => values, }; let mut transactions = std::vec::Vec::with_capacity(values.len()); for value in values { let decoded = crate::SolanaBlockTransaction::decode_wire(method, value); match decoded { std::result::Result::Ok(transaction) => transactions.push(transaction), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(crate::SolanaWireField::Value(transactions)); } fn decode_block_rewards( method: &str, field: crate::SolanaWireField>, ) -> ksp_core_lib::Result>> { let values = match field { crate::SolanaWireField::Omitted => return std::result::Result::Ok(crate::SolanaWireField::Omitted), crate::SolanaWireField::Null => return std::result::Result::Ok(crate::SolanaWireField::Null), crate::SolanaWireField::Value(values) => values, }; let mut rewards = std::vec::Vec::with_capacity(values.len()); for value in values { let decoded = crate::SolanaBlockReward::decode_wire(method, value); match decoded { std::result::Result::Ok(reward) => rewards.push(reward), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(crate::SolanaWireField::Value(rewards)); } #[cfg(test)] #[path = "../unit_tests/rpc_blocks.rs"] mod tests;