// file: kb-onchain-transport/src/standard_http_cluster.rs // version: 2 //! Configurable standard cluster-oriented Solana HTTP JSON-RPC requests. /// Contact information returned for one cluster node. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcContactInfo { /// Node identity public key. pub pubkey: std::string::String, /// Gossip socket address. pub gossip: std::option::Option, /// TVU UDP socket address. pub tvu: std::option::Option, /// TPU UDP socket address. pub tpu: std::option::Option, /// TPU QUIC socket address. pub tpu_quic: std::option::Option, /// TPU forwarding UDP socket address. pub tpu_forwards: std::option::Option, /// TPU forwarding QUIC socket address. pub tpu_forwards_quic: std::option::Option, /// TPU vote socket address. pub tpu_vote: std::option::Option, /// Repair service socket address. pub serve_repair: std::option::Option, /// JSON-RPC socket address. pub rpc: std::option::Option, /// PubSub socket address. pub pubsub: std::option::Option, /// Validator software version. pub version: std::option::Option, /// Validator client identifier. pub client_id: std::option::Option, /// Feature-set identifier prefix. pub feature_set: std::option::Option, /// Shred version. pub shred_version: std::option::Option, } /// Epoch schedule derived from the cluster genesis configuration. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcEpochSchedule { /// Slots in a normal epoch. pub slots_per_epoch: u64, /// Leader-schedule offset in slots. pub leader_schedule_slot_offset: u64, /// Whether warmup epochs are enabled. pub warmup: bool, /// First epoch using the normal slot count. pub first_normal_epoch: u64, /// First slot of the first normal epoch. pub first_normal_slot: u64, } /// Highest complete and incremental snapshot slots available from a node. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct RpcSnapshotSlotInfo { /// Highest full snapshot slot. pub full: u64, /// Highest incremental snapshot slot when available. pub incremental: std::option::Option, } /// Node identity response. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct RpcIdentity { /// Node identity public key. pub identity: std::string::String, } /// Options accepted by `getLeaderSchedule`. #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcLeaderScheduleConfig { /// Optional validator identity whose schedule is requested. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub identity: std::option::Option, /// Optional commitment level. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub commitment: std::option::Option, } impl crate::RpcLeaderScheduleConfig { /// Validates the optional identity public key. pub fn validate(&self) -> kb_core::Result<()> { if let std::option::Option::Some(identity) = &self.identity { return crate::validate_solana_pubkey_text(identity, "getLeaderSchedule identity"); } return std::result::Result::Ok(()); } } /// Leader schedule keyed by validator identity. pub type RpcLeaderSchedule = std::collections::BTreeMap>; /// Validator software version information. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] pub struct RpcVersionInfo { /// Validator software version. pub solana_core: std::string::String, /// Feature-set identifier prefix. pub feature_set: std::option::Option, } /// Options accepted by `getVoteAccounts`. #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcGetVoteAccountsConfig { /// Optional vote account public key filter. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub vote_pubkey: std::option::Option, /// Optional commitment level. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub commitment: std::option::Option, /// Whether unstaked delinquent validators must be retained. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub keep_unstaked_delinquents: std::option::Option, /// Optional delinquency threshold in slots. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub delinquent_slot_distance: std::option::Option, } impl crate::RpcGetVoteAccountsConfig { /// Validates the optional vote account public key. pub fn validate(&self) -> kb_core::Result<()> { if let std::option::Option::Some(vote_pubkey) = &self.vote_pubkey { return crate::validate_solana_pubkey_text(vote_pubkey, "getVoteAccounts vote account"); } return std::result::Result::Ok(()); } } /// Vote account information returned by `getVoteAccounts`. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RpcVoteAccountInfo { /// Vote account public key. pub vote_pubkey: std::string::String, /// Validator identity public key. pub node_pubkey: std::string::String, /// Activated stake in lamports. pub activated_stake: u64, /// Vote commission percentage. pub commission: u8, /// Vote inflation-reward commission in basis points when exposed by the node. #[serde(default, skip_serializing_if = "std::option::Option::is_none")] pub inflation_rewards_commission_bps: std::option::Option, /// Whether the vote account is staked in the current epoch. pub epoch_vote_account: bool, /// `(epoch, credits, previous credits)` history. pub epoch_credits: std::vec::Vec<(u64, u64, u64)>, /// Most recent voted slot. pub last_vote: u64, /// Current root slot. pub root_slot: u64, } /// Current and delinquent validator vote accounts. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct RpcVoteAccountStatus { /// Current validator vote accounts. pub current: std::vec::Vec, /// Delinquent validator vote accounts. pub delinquent: std::vec::Vec, } /// Typed `getClusterNodes` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetClusterNodesRequest; impl crate::StandardHttpRequest for crate::GetClusterNodesRequest { type Response = std::vec::Vec; const METHOD: &'static str = "getClusterNodes"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getEpochSchedule` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetEpochScheduleRequest; impl crate::StandardHttpRequest for crate::GetEpochScheduleRequest { type Response = crate::RpcEpochSchedule; const METHOD: &'static str = "getEpochSchedule"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getHealth` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetHealthRequest; impl crate::StandardHttpRequest for crate::GetHealthRequest { type Response = std::string::String; const METHOD: &'static str = "getHealth"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getHighestSnapshotSlot` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetHighestSnapshotSlotRequest; impl crate::StandardHttpRequest for crate::GetHighestSnapshotSlotRequest { type Response = crate::RpcSnapshotSlotInfo; const METHOD: &'static str = "getHighestSnapshotSlot"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getIdentity` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetIdentityRequest; impl crate::StandardHttpRequest for crate::GetIdentityRequest { type Response = crate::RpcIdentity; const METHOD: &'static str = "getIdentity"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getLeaderSchedule` request. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GetLeaderScheduleRequest { /// Optional slot selecting the epoch whose schedule is requested. pub slot: std::option::Option, /// Optional identity and commitment options. pub config: std::option::Option, } impl crate::StandardHttpRequest for crate::GetLeaderScheduleRequest { type Response = std::option::Option; const METHOD: &'static str = "getLeaderSchedule"; 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 mut params = std::vec::Vec::new(); if let std::option::Option::Some(slot) = self.slot { params.push(serde_json::Value::from(slot)); } else if self.config.is_some() { params.push(serde_json::Value::Null); } 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 `getMaxRetransmitSlot` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetMaxRetransmitSlotRequest; impl crate::StandardHttpRequest for crate::GetMaxRetransmitSlotRequest { type Response = u64; const METHOD: &'static str = "getMaxRetransmitSlot"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getMaxShredInsertSlot` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetMaxShredInsertSlotRequest; impl crate::StandardHttpRequest for crate::GetMaxShredInsertSlotRequest { type Response = u64; const METHOD: &'static str = "getMaxShredInsertSlot"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getSlot` request. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GetSlotRequest { /// Optional commitment and minimum-context options. pub config: std::option::Option, } impl crate::StandardHttpRequest for crate::GetSlotRequest { type Response = u64; const METHOD: &'static str = "getSlot"; fn params(&self) -> kb_core::Result> { 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), }; return std::result::Result::Ok(std::vec![value]); } return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getSlotLeader` request. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GetSlotLeaderRequest { /// Optional commitment and minimum-context options. pub config: std::option::Option, } impl crate::StandardHttpRequest for crate::GetSlotLeaderRequest { type Response = std::string::String; const METHOD: &'static str = "getSlotLeader"; fn params(&self) -> kb_core::Result> { 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), }; return std::result::Result::Ok(std::vec![value]); } return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getSlotLeaders` request. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct GetSlotLeadersRequest { /// First slot whose leader is requested. pub start_slot: u64, /// Number of consecutive leaders requested. pub limit: u64, } impl crate::StandardHttpRequest for crate::GetSlotLeadersRequest { type Response = std::vec::Vec; const METHOD: &'static str = "getSlotLeaders"; fn params(&self) -> kb_core::Result> { if self.limit == 0 || self.limit > crate::MAX_SLOT_LEADER_COUNT { return std::result::Result::Err(kb_core::Error::config(format!( "getSlotLeaders limit must be between 1 and {}", crate::MAX_SLOT_LEADER_COUNT ))); } return std::result::Result::Ok(std::vec![ serde_json::Value::from(self.start_slot), serde_json::Value::from(self.limit), ]); } } /// Typed `getVersion` request. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GetVersionRequest; impl crate::StandardHttpRequest for crate::GetVersionRequest { type Response = crate::RpcVersionInfo; const METHOD: &'static str = "getVersion"; fn params(&self) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } /// Typed `getVoteAccounts` request. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GetVoteAccountsRequest { /// Optional vote-account, commitment and delinquency options. pub config: std::option::Option, } impl crate::StandardHttpRequest for crate::GetVoteAccountsRequest { type Response = crate::RpcVoteAccountStatus; const METHOD: &'static str = "getVoteAccounts"; 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()); } } #[cfg(test)] mod tests { fn pubkey(seed: u8) -> std::string::String { return bs58::encode([seed; 32]).into_string(); } #[test] fn leader_schedule_uses_null_slot_placeholder_when_only_config_is_selected() { let request = crate::GetLeaderScheduleRequest { slot: std::option::Option::None, config: std::option::Option::Some(crate::RpcLeaderScheduleConfig { identity: std::option::Option::Some(pubkey(1)), commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Processed), }), }; 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[0], serde_json::Value::Null); assert_eq!(params[1]["identity"], serde_json::Value::String(pubkey(1))); let maximum = crate::GetSlotLeadersRequest { start_slot: 1, limit: crate::MAX_SLOT_LEADER_COUNT, }; let too_many = crate::GetSlotLeadersRequest { start_slot: 1, limit: crate::MAX_SLOT_LEADER_COUNT + 1, }; assert!(crate::StandardHttpRequest::params(&maximum).is_ok()); assert!(crate::StandardHttpRequest::params(&too_many).is_err()); } #[test] fn vote_account_options_are_independently_selectable() { let request = crate::GetVoteAccountsRequest { config: std::option::Option::Some(crate::RpcGetVoteAccountsConfig { vote_pubkey: std::option::Option::Some(pubkey(2)), commitment: std::option::Option::None, keep_unstaked_delinquents: std::option::Option::Some(true), delinquent_slot_distance: std::option::Option::Some(512), }), }; let params = match crate::StandardHttpRequest::params(&request) { std::result::Result::Ok(params) => params, std::result::Result::Err(error) => panic!("params failed: {error}"), }; assert!(params[0].get("commitment").is_none()); assert_eq!(params[0]["keepUnstakedDelinquents"], serde_json::Value::Bool(true)); assert_eq!(params[0]["delinquentSlotDistance"], serde_json::Value::from(512_u64)); let vote_account = match serde_json::from_value::(serde_json::json!({ "votePubkey": pubkey(3), "nodePubkey": pubkey(4), "activatedStake": 1, "commission": 5, "inflationRewardsCommissionBps": 525, "epochVoteAccount": true, "epochCredits": [[1, 2, 1]], "lastVote": 8, "rootSlot": 7 })) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("vote account parsing failed: {error}"), }; assert_eq!(vote_account.inflation_rewards_commission_bps, std::option::Option::Some(525)); } #[test] fn version_response_accepts_kebab_case_wire_fields() { let value = serde_json::json!({ "solana-core": "4.0.0", "feature-set": 123 }); let parsed = match serde_json::from_value::(value) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("version parsing failed: {error}"), }; assert_eq!(parsed.solana_core, "4.0.0"); assert_eq!(parsed.feature_set, std::option::Option::Some(123)); } }