Files
khadhroony-bot3/ks-onchain-transport/src/standard_http_cluster.rs
2026-08-09 19:34:08 +02:00

506 lines
19 KiB
Rust

// file: ks-onchain-transport/src/standard_http_cluster.rs
// version: 3
//! 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<std::string::String>,
/// TVU UDP socket address.
pub tvu: std::option::Option<std::string::String>,
/// TPU UDP socket address.
pub tpu: std::option::Option<std::string::String>,
/// TPU QUIC socket address.
pub tpu_quic: std::option::Option<std::string::String>,
/// TPU forwarding UDP socket address.
pub tpu_forwards: std::option::Option<std::string::String>,
/// TPU forwarding QUIC socket address.
pub tpu_forwards_quic: std::option::Option<std::string::String>,
/// TPU vote socket address.
pub tpu_vote: std::option::Option<std::string::String>,
/// Repair service socket address.
pub serve_repair: std::option::Option<std::string::String>,
/// JSON-RPC socket address.
pub rpc: std::option::Option<std::string::String>,
/// PubSub socket address.
pub pubsub: std::option::Option<std::string::String>,
/// Validator software version.
pub version: std::option::Option<std::string::String>,
/// Validator client identifier.
pub client_id: std::option::Option<std::string::String>,
/// Feature-set identifier prefix.
pub feature_set: std::option::Option<u32>,
/// Shred version.
pub shred_version: std::option::Option<u16>,
}
/// 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<u64>,
}
/// 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<std::string::String>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
impl crate::RpcLeaderScheduleConfig {
/// Validates the optional identity public key.
pub fn validate(&self) -> ks_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<std::string::String, std::vec::Vec<usize>>;
/// 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<u32>,
}
/// 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<std::string::String>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Whether unstaked delinquent validators must be retained.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub keep_unstaked_delinquents: std::option::Option<bool>,
/// Optional delinquency threshold in slots.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub delinquent_slot_distance: std::option::Option<u64>,
}
impl crate::RpcGetVoteAccountsConfig {
/// Validates the optional vote account public key.
pub fn validate(&self) -> ks_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<u16>,
/// 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<crate::RpcVoteAccountInfo>,
/// Delinquent validator vote accounts.
pub delinquent: std::vec::Vec<crate::RpcVoteAccountInfo>,
}
/// Typed `getClusterNodes` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetClusterNodesRequest;
impl crate::StandardHttpRequest for crate::GetClusterNodesRequest {
type Response = std::vec::Vec<crate::RpcContactInfo>;
const METHOD: &'static str = "getClusterNodes";
fn params(&self) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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<u64>,
/// Optional identity and commitment options.
pub config: std::option::Option<crate::RpcLeaderScheduleConfig>,
}
impl crate::StandardHttpRequest for crate::GetLeaderScheduleRequest {
type Response = std::option::Option<crate::RpcLeaderSchedule>;
const METHOD: &'static str = "getLeaderSchedule";
fn params(&self) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetSlotRequest {
type Response = u64;
const METHOD: &'static str = "getSlot";
fn params(&self) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetSlotLeaderRequest {
type Response = std::string::String;
const METHOD: &'static str = "getSlotLeader";
fn params(&self) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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<std::string::String>;
const METHOD: &'static str = "getSlotLeaders";
fn params(&self) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
if self.limit == 0 || self.limit > crate::MAX_SLOT_LEADER_COUNT {
return std::result::Result::Err(ks_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) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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<crate::RpcGetVoteAccountsConfig>,
}
impl crate::StandardHttpRequest for crate::GetVoteAccountsRequest {
type Response = crate::RpcVoteAccountStatus;
const METHOD: &'static str = "getVoteAccounts";
fn params(&self) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
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::<crate::RpcVoteAccountInfo>(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::<crate::RpcVersionInfo>(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));
}
}