Files
khadhroony-bot3/kb-onchain-transport/src/standard_http_blocks.rs
2026-07-25 00:48:29 +02:00

549 lines
22 KiB
Rust

// 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<crate::RpcTransactionEncoding>,
/// Optional transaction detail level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub transaction_details: std::option::Option<crate::RpcTransactionDetails>,
/// Whether rewards must be included.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub rewards: std::option::Option<bool>,
/// Optional confirmed or finalized commitment.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Highest transaction version the caller can decode.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub max_supported_transaction_version: std::option::Option<u8>,
}
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<u64>,
}
/// 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<std::string::String>,
/// Optional inclusive slot range.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub range: std::option::Option<crate::RpcBlockProductionConfigRange>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
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<crate::RpcRewardType>,
/// Optional validator commission percentage.
pub commission: std::option::Option<u8>,
/// Optional validator commission in basis points.
#[serde(default, skip_serializing_if = "std::option::Option::is_none")]
pub commission_bps: std::option::Option<u16>,
}
/// 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<std::vec::Vec<serde_json::Value>>,
/// Signature list when signature-only details are requested.
#[serde(default)]
pub signatures: std::option::Option<std::vec::Vec<std::string::String>>,
/// Rewards when requested.
#[serde(default)]
pub rewards: std::option::Option<std::vec::Vec<crate::RpcReward>>,
/// Unix block time when available.
#[serde(default)]
pub block_time: std::option::Option<i64>,
/// Block height when available.
#[serde(default)]
pub block_height: std::option::Option<u64>,
/// Number of reward partitions when available.
#[serde(default)]
pub num_reward_partitions: std::option::Option<u64>,
}
/// 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<std::vec::Vec<u64>>,
/// 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<std::string::String, (usize, usize)>,
/// 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<u64>,
/// 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<crate::RpcBlockConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlockRequest {
type Response = std::option::Option<crate::RpcConfirmedBlock>;
const METHOD: &'static str = "getBlock";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
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<std::vec::Vec<serde_json::Value>> {
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<crate::RpcBlockProductionConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlockProductionRequest {
type Response = crate::RpcResponse<crate::RpcBlockProduction>;
const METHOD: &'static str = "getBlockProduction";
fn params(&self) -> kb_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());
}
}
/// 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<u64>,
/// Optional confirmed or finalized context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlocksRequest {
type Response = std::vec::Vec<u64>;
const METHOD: &'static str = "getBlocks";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
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<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlocksWithLimitRequest {
type Response = std::vec::Vec<u64>;
const METHOD: &'static str = "getBlocksWithLimit";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
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<i64>;
const METHOD: &'static str = "getBlockTime";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
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<std::vec::Vec<serde_json::Value>> {
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<usize>,
}
impl crate::StandardHttpRequest for crate::GetRecentPerformanceSamplesRequest {
type Response = std::vec::Vec<crate::RpcPerformanceSample>;
const METHOD: &'static str = "getRecentPerformanceSamples";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
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<std::vec::Vec<serde_json::Value>> {
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::<crate::RpcReward>(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)]);
}
}