v0.2.4-pre.005

This commit is contained in:
2026-08-18 21:03:00 +02:00
parent 561b6678ed
commit d3cc7c93f0
8 changed files with 544 additions and 12 deletions

View File

@@ -0,0 +1,11 @@
{
"jsonrpc":"2.0",
"result":{
"context":{"slot":430000123,"apiVersion":"4.2.1"},
"value":{
"byIdentity":{"not-a-pubkey":[8,7]},
"range":{"firstSlot":430000000,"lastSlot":430000099}
}
},
"id":1
}

View File

@@ -0,0 +1,11 @@
{
"jsonrpc":"2.0",
"result":{
"context":{"slot":430000123,"apiVersion":"4.2.1"},
"value":{
"byIdentity":{"11111111111111111111111111111111":[8,7]},
"range":{"firstSlot":430000000,"lastSlot":430000099}
}
},
"id":1
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
// version: 4
// version: 5
/// Transaction detail level accepted by modern `getBlock` requests.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
@@ -143,7 +143,6 @@ impl SolanaBlockProductionRange {
/// Serializes this range to the exact Solana JSON-RPC object.
#[must_use]
#[cfg(test)]
pub(crate) fn to_json_value(self) -> serde_json::Value {
let mut object = serde_json::Map::new();
object.insert("firstSlot".to_owned(), serde_json::Value::Number(self.first_slot.into()));
@@ -192,14 +191,12 @@ impl SolanaBlockProductionConfig {
}
/// Returns whether this config would serialize to an empty object.
#[cfg(test)]
pub(crate) const fn is_empty(&self) -> bool {
return self.commitment.is_none() && self.identity.is_none() && self.range.is_none();
}
/// Serializes this config to the exact Solana JSON-RPC object.
#[must_use]
#[cfg(test)]
pub(crate) fn to_json_value(&self) -> serde_json::Value {
let mut object = serde_json::Map::new();
if let std::option::Option::Some(commitment) = self.commitment {
@@ -287,7 +284,6 @@ impl SolanaBlockProduction {
}
/// Decodes a block-production result from its Solana JSON wire shape.
#[cfg(test)]
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
let decoded = crate::decode_wire_json::<WireBlockProduction>(method, value);
let wire = match decoded {
@@ -672,6 +668,38 @@ impl crate::HttpTransportPool {
return self.get_blocks_u64_list("getBlocksWithLimit", role, params).await;
}
/// Executes typed `getBlockProduction` with optional identity, range, and commitment filters.
pub async fn get_block_production(
&self,
role: &crate::HttpRoleName,
config: std::option::Option<&crate::SolanaBlockProductionConfig>,
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaBlockProduction>> {
if let std::option::Option::Some(config) = config
&& let std::option::Option::Some(range) = config.range()
&& let std::option::Option::Some(last_slot) = range.last_slot()
&& last_slot < range.first_slot()
{
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getBlockProduction lastSlot must not be less than firstSlot")
.with_context("rpc_method", "getBlockProduction")
.with_context("first_slot", range.first_slot().to_string())
.with_context("last_slot", last_slot.to_string()),
);
}
let mut params = std::vec::Vec::new();
if let std::option::Option::Some(config) = config
&& !config.is_empty()
{
params.push(config.to_json_value());
}
let value = self.execute_blocks_rpc("getBlockProduction", role, params).await;
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return decode_block_production_response("getBlockProduction", value);
}
/// Executes typed `getRecentPerformanceSamples`, preserving runtime order and older sample shapes.
pub async fn get_recent_performance_samples(
&self,
@@ -770,6 +798,24 @@ fn invalid_blocks_limit<T>(method: &'static str, message: &'static str, limit: u
);
}
fn decode_block_production_response(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaBlockProduction>> {
let decoded = crate::decode_wire_json::<WireBlockProductionRpcResponse>(method, value);
let wire = match decoded {
std::result::Result::Ok(wire) => wire,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
let context = match context {
std::result::Result::Ok(context) => context,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let value = crate::SolanaBlockProduction::decode_wire(method, wire.value);
return match value {
std::result::Result::Ok(value) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn decode_performance_samples(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaPerformanceSample>> {
let values = crate::decode_wire_json::<std::vec::Vec<serde_json::Value>>(method, value);
let values = match values {
@@ -878,7 +924,6 @@ struct WireBlockCommitment {
total_stake: u64,
}
#[cfg(test)]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProductionRange {
@@ -886,7 +931,6 @@ struct WireBlockProductionRange {
last_slot: u64,
}
#[cfg(test)]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProduction {
@@ -894,6 +938,12 @@ struct WireBlockProduction {
range: WireBlockProductionRange,
}
#[derive(serde::Deserialize)]
struct WireBlockProductionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
#[cfg(test)]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 18
// version: 19
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -402,3 +402,17 @@ fn public_v0_2_4_pre_004_range_and_performance_wrappers_are_available_from_crate
let _get_blocks_with_limit = ksp_onchain_transport_lib::HttpTransportPool::get_blocks_with_limit;
let _get_recent_performance_samples = ksp_onchain_transport_lib::HttpTransportPool::get_recent_performance_samples;
}
#[test]
fn public_v0_2_4_pre_005_block_production_wrapper_is_available_from_crate_root() {
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("public fixture identity must parse");
let range = ksp_onchain_transport_lib::SolanaBlockProductionRange::new(10, std::option::Option::Some(20));
let config = ksp_onchain_transport_lib::SolanaBlockProductionConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
std::option::Option::Some(identity),
std::option::Option::Some(range),
);
assert_eq!(config.identity(), std::option::Option::Some(&identity));
assert_eq!(config.range().expect("public range must exist").last_slot(), std::option::Option::Some(20));
let _method = ksp_onchain_transport_lib::HttpTransportPool::get_block_production;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 16
// version: 17
//! Release-level completeness canaries for the staged HTTP wrapper sequence.
@@ -521,3 +521,12 @@ fn release_v0_2_4_pre_004_range_performance_subset_is_exact_and_retry_safe() {
assert_eq!(actual, expected);
assert_eq!(actual.len(), 3);
}
#[test]
fn release_v0_2_4_pre_005_block_production_subset_is_exact_and_retry_safe() {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method("getBlockProduction").expect("getBlockProduction descriptor must exist");
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Blocks);
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_4);
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
// version: 3
// version: 4
#[test]
fn transaction_details_and_get_block_config_preserve_all_modern_options() {
@@ -453,3 +453,85 @@ async fn typed_get_recent_performance_samples_rejects_above_720_before_io() {
assert_eq!(error.context()[1].key(), "limit");
assert_eq!(error.context()[1].value(), "721");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_block_production_serializes_full_config_and_decodes_contextual_result() {
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture identity must parse");
let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::Some(430_000_099));
let config = crate::SolanaBlockProductionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Finalized),
std::option::Option::Some(identity),
std::option::Option::Some(range),
);
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.success.json"));
let pool = pool_for_url(url.as_str());
let response = pool
.get_block_production(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
.await
.expect("block production fixture must succeed");
assert_eq!(response.context().slot(), 430_000_123);
assert_eq!(response.context().api_version(), std::option::Option::Some("4.2.1"));
assert_eq!(response.value().by_identity().get(&identity), std::option::Option::Some(&(8, 7)));
assert_eq!(response.value().range().first_slot(), 430_000_000);
assert_eq!(response.value().range().last_slot(), 430_000_099);
let request = handle.join().expect("fixture server must join");
let body = request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getBlockProduction"));
assert_eq!(
body["params"],
serde_json::json!([{
"commitment":"finalized",
"identity":"11111111111111111111111111111111",
"range":{"firstSlot":430000000,"lastSlot":430000099}
}])
);
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_block_production_omits_empty_config_and_supports_open_range_with_processed_commitment() {
let role = crate::HttpRoleName::new("default");
let empty = crate::SolanaBlockProductionConfig::default();
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.success.json"));
let pool = pool_for_url(url.as_str());
let response = pool.get_block_production(&role, std::option::Option::Some(&empty)).await.expect("empty block production config must succeed");
assert_eq!(response.value().range().last_slot(), 430_000_099);
let request = handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::None);
let config = crate::SolanaBlockProductionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Processed),
std::option::Option::None,
std::option::Option::Some(range),
);
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.success.json"));
let pool = pool_for_url(url.as_str());
pool.get_block_production(&role, std::option::Option::Some(&config)).await.expect("open block production range must succeed");
let request = handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"processed","range":{"firstSlot":430000000}}]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_block_production_rejects_reversed_range_before_io() {
let range = crate::SolanaBlockProductionRange::new(430_000_100, std::option::Option::Some(430_000_099));
let config = crate::SolanaBlockProductionConfig::new(std::option::Option::None, std::option::Option::None, std::option::Option::Some(range));
let pool = pool_for_url("http://127.0.0.1:9");
let result = pool.get_block_production(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)).await;
let error = result.expect_err("reversed block production range must reject before I/O");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
assert_eq!(error.context()[1].key(), "first_slot");
assert_eq!(error.context()[1].value(), "430000100");
assert_eq!(error.context()[2].key(), "last_slot");
assert_eq!(error.context()[2].value(), "430000099");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_block_production_rejects_invalid_wire_identity_without_echoing_value() {
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.invalid_identity.json"));
let pool = pool_for_url(url.as_str());
let result = pool.get_block_production(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
let error = result.expect_err("invalid block production identity must fail closed");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
assert!(!error.to_string().contains("not-a-pubkey"));
handle.join().expect("fixture server must join");
}