506 lines
19 KiB
Rust
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));
|
|
}
|
|
}
|