// file: kb-onchain-transport/src/standard_http_blocks.rs // version: 2 //! Configurable standard block and ledger Solana HTTP JSON-RPC requests. /// Options accepted by `getBlock`. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcBlockConfig { /// Optional transaction encoding. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub encoding: std::option::Option, /// Optional transaction detail level. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub transaction_details: std::option::Option, /// Whether rewards must be included. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub rewards: std::option::Option, /// Optional confirmed or finalized commitment. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub commitment: std::option::Option, /// Highest transaction version the caller can decode. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub max_supported_transaction_version: std::option::Option, } impl crate::RpcBlockConfig { /// Rejects the processed commitment unsupported by block-history methods. pub fn validate(&self) -> kb_core::Result<()> { if self.commitment == std::option::Option::Some(crate::RpcCommitmentLevel::Processed) { return std::result::Result::Err(kb_core::Error::config( "getBlock does not support processed commitment", )); } return std::result::Result::Ok(()); } } /// Inclusive slot range accepted by `getBlockProduction`. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcBlockProductionConfigRange { /// First slot included in the range. pub first_slot: u64, /// Optional final slot included in the range. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub last_slot: std::option::Option, } /// Options accepted by `getBlockProduction`. #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcBlockProductionConfig { /// Optional validator identity filter. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub identity: std::option::Option, /// Optional inclusive slot range. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub range: std::option::Option, /// Optional commitment level. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub commitment: std::option::Option, } impl crate::RpcBlockProductionConfig { /// Validates identity and range ordering. pub fn validate(&self) -> kb_core::Result<()> { if let std::option::Option::Some(identity) = &self.identity { let identity_result = crate::validate_solana_pubkey_text(identity, "getBlockProduction identity"); if let std::result::Result::Err(error) = identity_result { return std::result::Result::Err(error); } } if let std::option::Option::Some(range) = self.range { if let std::option::Option::Some(last_slot) = range.last_slot { if last_slot < range.first_slot { return std::result::Result::Err(kb_core::Error::config( "getBlockProduction last slot must not precede first slot", )); } } } return std::result::Result::Ok(()); } } /// Reward category attached to a block reward entry. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub enum RpcRewardType { /// Transaction fee reward. Fee, /// Rent reward. Rent, /// Staking reward. Staking, /// Vote reward. Voting, } /// One reward entry returned with a block. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcReward { /// Recipient public key. pub pubkey: std::string::String, /// Signed lamport balance change. pub lamports: i64, /// Recipient balance after the reward. pub post_balance: u64, /// Optional reward category. pub reward_type: std::option::Option, /// Optional validator commission percentage. pub commission: std::option::Option, /// Optional validator commission in basis points. #[serde(default, skip_serializing_if = "std::option::Option::is_none")] pub commission_bps: std::option::Option, } /// Encoding-dependent confirmed block returned by `getBlock`. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcConfirmedBlock { /// Block hash. pub blockhash: std::string::String, /// Previous block hash. pub previous_blockhash: std::string::String, /// Parent slot. pub parent_slot: u64, /// Encoding-dependent transaction entries when requested. #[serde(default)] pub transactions: std::option::Option>, /// Signature list when signature-only details are requested. #[serde(default)] pub signatures: std::option::Option>, /// Rewards when requested. #[serde(default)] pub rewards: std::option::Option>, /// Unix block time when available. #[serde(default)] pub block_time: std::option::Option, /// Block height when available. #[serde(default)] pub block_height: std::option::Option, /// Number of reward partitions when available. #[serde(default)] pub num_reward_partitions: std::option::Option, } /// Stake commitment information returned for one block. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcBlockCommitment { /// Commitment stake by lockout depth, or `None` when unavailable. pub commitment: std::option::Option>, /// Total active stake used for the commitment calculation. pub total_stake: u64, } /// Actual slot range represented by a block-production response. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcBlockProductionRange { /// First represented slot. pub first_slot: u64, /// Last represented slot. pub last_slot: u64, } /// Block-production counts grouped by validator identity. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcBlockProduction { /// Validator identity to `(leader slots, blocks produced)` map. pub by_identity: std::collections::BTreeMap, /// Actual represented slot range. pub range: crate::RpcBlockProductionRange, } /// Recent cluster performance sample. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcPerformanceSample { /// Slot at the end of the sample window. pub slot: u64, /// Total transactions processed during the sample. pub num_transactions: u64, /// Optional count excluding vote transactions. #[serde(default)] pub num_non_vote_transactions: std::option::Option, /// Slots processed during the sample. pub num_slots: u64, /// Sample period in seconds. pub sample_period_secs: u16, } /// Typed `getBlock` request. #[derive(Clone, Debug, Eq, PartialEq)] pub struct GetBlockRequest { /// Block slot. pub slot: u64, /// Optional encoding, details, rewards, commitment and version options. pub config: std::option::Option, } impl crate::StandardHttpRequest for crate::GetBlockRequest { type Response = std::option::Option; const METHOD: &'static str = "getBlock"; fn params(&self) -> kb_core::Result> { let mut params = std::vec![serde_json::Value::from(self.slot)]; if let std::option::Option::Some(config) = &self.config { let config_result = config.validate(); if let std::result::Result::Err(error) = config_result { return std::result::Result::Err(error); } let value = match crate::serialize_parameter(Self::METHOD, config) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; params.push(value); } return std::result::Result::Ok(params); } } /// Typed `getBlockCommitment` request. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct GetBlockCommitmentRequest { /// Block slot. pub slot: u64, } impl crate::StandardHttpRequest for crate::GetBlockCommitmentRequest { type Response = crate::RpcBlockCommitment; const METHOD: &'static str = "getBlockCommitment"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec![serde_json::Value::from(self.slot)]); } } /// Typed `getBlockProduction` request. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GetBlockProductionRequest { /// Optional identity, range and commitment options. pub config: std::option::Option, } impl crate::StandardHttpRequest for crate::GetBlockProductionRequest { type Response = crate::RpcResponse; const METHOD: &'static str = "getBlockProduction"; fn params(&self) -> kb_core::Result> { if let std::option::Option::Some(config) = &self.config { let validation_result = config.validate(); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error); } let value = match crate::serialize_parameter(Self::METHOD, config) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(std::vec![value]); } return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getBlocks` request. #[derive(Clone, Debug, Eq, PartialEq)] pub struct GetBlocksRequest { /// First slot included in the scan. pub start_slot: u64, /// Optional final slot included in the scan. pub end_slot: std::option::Option, /// Optional confirmed or finalized context options. pub config: std::option::Option, } impl crate::StandardHttpRequest for crate::GetBlocksRequest { type Response = std::vec::Vec; const METHOD: &'static str = "getBlocks"; fn params(&self) -> kb_core::Result> { if let std::option::Option::Some(end_slot) = self.end_slot { if end_slot >= self.start_slot && end_slot.saturating_sub(self.start_slot) > crate::MAX_BLOCK_RANGE { return std::result::Result::Err(kb_core::Error::config(format!( "getBlocks range must not exceed {} slots", crate::MAX_BLOCK_RANGE ))); } } if let std::option::Option::Some(config) = &self.config { if config.commitment == std::option::Option::Some(crate::RpcCommitmentLevel::Processed) { return std::result::Result::Err(kb_core::Error::config( "getBlocks does not support processed commitment", )); } } let mut params = std::vec![serde_json::Value::from(self.start_slot)]; if let std::option::Option::Some(end_slot) = self.end_slot { params.push(serde_json::Value::from(end_slot)); } if let std::option::Option::Some(config) = &self.config { let value = match crate::serialize_parameter(Self::METHOD, config) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; params.push(value); } return std::result::Result::Ok(params); } } /// Typed `getBlocksWithLimit` request. #[derive(Clone, Debug, Eq, PartialEq)] pub struct GetBlocksWithLimitRequest { /// First slot considered by the scan. pub start_slot: u64, /// Maximum number of block slots returned. pub limit: u64, /// Optional confirmed or finalized context options. pub config: std::option::Option, } impl crate::StandardHttpRequest for crate::GetBlocksWithLimitRequest { type Response = std::vec::Vec; const METHOD: &'static str = "getBlocksWithLimit"; fn params(&self) -> kb_core::Result> { if self.limit > crate::MAX_BLOCK_RANGE { return std::result::Result::Err(kb_core::Error::config(format!( "getBlocksWithLimit limit must not exceed {}", crate::MAX_BLOCK_RANGE ))); } if let std::option::Option::Some(config) = &self.config { if config.commitment == std::option::Option::Some(crate::RpcCommitmentLevel::Processed) { return std::result::Result::Err(kb_core::Error::config( "getBlocksWithLimit does not support processed commitment", )); } } let mut params = std::vec![ serde_json::Value::from(self.start_slot), serde_json::Value::from(self.limit), ]; if let std::option::Option::Some(config) = &self.config { let value = match crate::serialize_parameter(Self::METHOD, config) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; params.push(value); } return std::result::Result::Ok(params); } } /// Typed `getBlockTime` request. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct GetBlockTimeRequest { /// Block slot. pub slot: u64, } impl crate::StandardHttpRequest for crate::GetBlockTimeRequest { type Response = std::option::Option; const METHOD: &'static str = "getBlockTime"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec![serde_json::Value::from(self.slot)]); } } /// Typed `getFirstAvailableBlock` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetFirstAvailableBlockRequest; impl crate::StandardHttpRequest for crate::GetFirstAvailableBlockRequest { type Response = u64; const METHOD: &'static str = "getFirstAvailableBlock"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getRecentPerformanceSamples` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetRecentPerformanceSamplesRequest { /// Optional sample count. Absence delegates the default to the endpoint. pub limit: std::option::Option, } impl crate::StandardHttpRequest for crate::GetRecentPerformanceSamplesRequest { type Response = std::vec::Vec; const METHOD: &'static str = "getRecentPerformanceSamples"; fn params(&self) -> kb_core::Result> { if let std::option::Option::Some(limit) = self.limit { if limit > crate::MAX_PERFORMANCE_SAMPLE_COUNT { return std::result::Result::Err(kb_core::Error::config(format!( "getRecentPerformanceSamples limit must not exceed {}", crate::MAX_PERFORMANCE_SAMPLE_COUNT ))); } return std::result::Result::Ok(std::vec![serde_json::Value::from(limit)]); } return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `minimumLedgerSlot` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct MinimumLedgerSlotRequest; impl crate::StandardHttpRequest for crate::MinimumLedgerSlotRequest { type Response = u64; const METHOD: &'static str = "minimumLedgerSlot"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } #[cfg(test)] mod tests { #[test] fn block_options_remain_independently_selectable() { let request = crate::GetBlockRequest { slot: 55, config: std::option::Option::Some(crate::RpcBlockConfig { encoding: std::option::Option::Some(crate::RpcTransactionEncoding::JsonParsed), transaction_details: std::option::Option::Some( crate::RpcTransactionDetails::Accounts, ), rewards: std::option::Option::Some(false), commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Confirmed), max_supported_transaction_version: std::option::Option::Some(0), }), }; let params = match crate::StandardHttpRequest::params(&request) { std::result::Result::Ok(params) => params, std::result::Result::Err(error) => panic!("params failed: {error}"), }; assert_eq!(params[1]["encoding"], serde_json::Value::String("jsonParsed".to_string())); assert_eq!( params[1]["transactionDetails"], serde_json::Value::String("accounts".to_string()) ); assert_eq!(params[1]["rewards"], serde_json::Value::Bool(false)); assert_eq!(params[1]["maxSupportedTransactionVersion"], serde_json::Value::from(0_u64)); let reward = match serde_json::from_value::(serde_json::json!({ "pubkey": "validator", "lamports": 42, "postBalance": 84, "rewardType": "voting", "commission": 5, "commissionBps": 550 })) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("reward parsing failed: {error}"), }; assert_eq!(reward.commission, std::option::Option::Some(5)); assert_eq!(reward.commission_bps, std::option::Option::Some(550)); } #[test] fn blocks_without_end_slot_places_config_in_second_position() { let request = crate::GetBlocksRequest { start_slot: 10, end_slot: std::option::Option::None, config: std::option::Option::Some(crate::RpcContextConfig { commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized), min_context_slot: std::option::Option::Some(9), }), }; let params = match crate::StandardHttpRequest::params(&request) { std::result::Result::Ok(params) => params, std::result::Result::Err(error) => panic!("params failed: {error}"), }; assert_eq!(params.len(), 2); assert_eq!(params[1]["commitment"], serde_json::Value::String("finalized".to_string())); } #[test] fn block_ranges_and_performance_samples_are_bounded() { let range = crate::GetBlocksRequest { start_slot: 0, end_slot: std::option::Option::Some(500_001), config: std::option::Option::None, }; let samples = crate::GetRecentPerformanceSamplesRequest { limit: std::option::Option::Some(721) }; assert!(crate::StandardHttpRequest::params(&range).is_err()); assert!(crate::StandardHttpRequest::params(&samples).is_err()); let reversed_range = crate::GetBlocksRequest { start_slot: 10, end_slot: std::option::Option::Some(9), config: std::option::Option::None, }; let zero_blocks = crate::GetBlocksWithLimitRequest { start_slot: 10, limit: 0, config: std::option::Option::None, }; let zero_samples = crate::GetRecentPerformanceSamplesRequest { limit: std::option::Option::Some(0) }; let reversed_params = match crate::StandardHttpRequest::params(&reversed_range) { std::result::Result::Ok(params) => params, std::result::Result::Err(error) => panic!("reversed range failed: {error}"), }; let zero_block_params = match crate::StandardHttpRequest::params(&zero_blocks) { std::result::Result::Ok(params) => params, std::result::Result::Err(error) => panic!("zero block limit failed: {error}"), }; let zero_sample_params = match crate::StandardHttpRequest::params(&zero_samples) { std::result::Result::Ok(params) => params, std::result::Result::Err(error) => panic!("zero sample limit failed: {error}"), }; assert_eq!( reversed_params, std::vec![serde_json::Value::from(10_u64), serde_json::Value::from(9_u64),] ); assert_eq!( zero_block_params, std::vec![serde_json::Value::from(10_u64), serde_json::Value::from(0_u64),] ); assert_eq!(zero_sample_params, std::vec![serde_json::Value::from(0_u64)]); } }