From e500024c3a945085e1700dcbf566a864d5e67aac Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Tue, 18 Aug 2026 20:51:52 +0200 Subject: [PATCH] v0.2.4-pre.003 --- Cargo.toml | 4 +- .../http/get_block_commitment.success.json | 1 + .../http/get_block_height.success.json | 1 + .../fixtures/http/get_block_time.null.json | 1 + .../fixtures/http/get_block_time.success.json | 1 + .../get_first_available_block.success.json | 1 + .../http/minimum_ledger_slot.success.json | 1 + crates/ksp-onchain-transport-lib/src/lib.rs | 6 +- .../src/rpc_blocks.rs | 92 ++++- .../tests/public_api.rs | 11 +- .../tests/release_completeness.rs | 29 +- .../unit_tests/rpc_blocks.rs | 165 +++++++- deltas/0.2.4/pre.003.md | 378 ++++++++++++++++++ 13 files changed, 679 insertions(+), 12 deletions(-) create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_block_commitment.success.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_block_height.success.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.null.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.success.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_first_available_block.success.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/minimum_ledger_slot.success.json create mode 100644 deltas/0.2.4/pre.003.md diff --git a/Cargo.toml b/Cargo.toml index ae7d527..88e3d76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 133 +# version: 134 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"] [workspace.package] -version = "0.2.4-pre.2.fix.1" +version = "0.2.4-pre.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block_commitment.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_commitment.success.json new file mode 100644 index 0000000..204f94b --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_commitment.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"commitment":[1,2,3,4],"totalStake":1000000000},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block_height.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_height.success.json new file mode 100644 index 0000000..7c23e19 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_height.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":410000000,"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.null.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.null.json new file mode 100644 index 0000000..e00e008 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.null.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":null,"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.success.json new file mode 100644 index 0000000..d782a83 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":1787072400,"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_first_available_block.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_first_available_block.success.json new file mode 100644 index 0000000..4ba9bca --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_first_available_block.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":250000,"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/minimum_ledger_slot.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/minimum_ledger_slot.success.json new file mode 100644 index 0000000..c499cb3 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/minimum_ledger_slot.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":123456,"id":1} diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs index d14bc70..afc36bc 100644 --- a/crates/ksp-onchain-transport-lib/src/lib.rs +++ b/crates/ksp-onchain-transport-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/lib.rs -// version: 17 +// version: 18 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -12,8 +12,8 @@ //! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC //! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and all eleven Transaction wrappers through //! `pre.007`: eight reads, two write submissions with centralized no-resend protection, and retry-safe `simulateTransaction`, including complete -//! modern/legacy `getTransaction` coverage. `0.2.4-pre.002` adds the shared Blocks/Economics wire, config and result primitives without yet advancing -//! any `V0_2_4` method to a typed wrapper. +//! modern/legacy `getTransaction` coverage. `0.2.4-pre.002` adds the shared Blocks/Economics wire, config and result primitives. `0.2.4-pre.003` +//! activates the first five typed Blocks reads: `getBlockCommitment`, `getBlockHeight`, `getBlockTime`, `getFirstAvailableBlock` and `minimumLedgerSlot`. mod client; mod constants; diff --git a/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs b/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs index 45c5140..355b67d 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs -// version: 2 +// version: 3 /// Transaction detail level accepted by modern `getBlock` requests. #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -236,7 +236,6 @@ impl SolanaBlockCommitment { } /// Decodes a block-commitment result from its Solana JSON wire shape. - #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { @@ -587,6 +586,94 @@ impl SolanaPerformanceSample { } } +impl crate::HttpTransportPool { + /// Executes typed `getBlockCommitment` through the common KSP HTTP transport path. + pub async fn get_block_commitment(&self, role: &crate::HttpRoleName, slot: u64) -> ksp_core_lib::Result { + let value = self.execute_blocks_rpc("getBlockCommitment", role, std::vec![serde_json::json!(slot)]).await; + return match value { + std::result::Result::Ok(value) => crate::SolanaBlockCommitment::decode_wire("getBlockCommitment", value), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + + /// Executes typed `getBlockHeight` through the common KSP HTTP transport path. + pub async fn get_block_height(&self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result { + let mut params = std::vec::Vec::new(); + push_blocks_context_config(&mut params, config); + return self.get_blocks_u64("getBlockHeight", role, params).await; + } + + /// Executes typed `getBlockTime` and preserves a runtime `null` as `None`. + pub async fn get_block_time(&self, role: &crate::HttpRoleName, slot: u64) -> ksp_core_lib::Result> { + let value = self.execute_blocks_rpc("getBlockTime", role, std::vec![serde_json::json!(slot)]).await; + return match value { + std::result::Result::Ok(value) => crate::decode_wire_json::>("getBlockTime", value), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + + /// Executes typed `getFirstAvailableBlock` through the common KSP HTTP transport path. + pub async fn get_first_available_block(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result { + return self.get_blocks_u64("getFirstAvailableBlock", role, std::vec::Vec::new()).await; + } + + /// Executes typed `minimumLedgerSlot` through the common KSP HTTP transport path. + pub async fn minimum_ledger_slot(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result { + return self.get_blocks_u64("minimumLedgerSlot", role, std::vec::Vec::new()).await; + } + + async fn get_blocks_u64( + &self, + method_name: &'static str, + role: &crate::HttpRoleName, + params: std::vec::Vec, + ) -> ksp_core_lib::Result { + let value = self.execute_blocks_rpc(method_name, role, params).await; + return match value { + std::result::Result::Ok(value) => crate::decode_wire_json::(method_name, value), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + + async fn execute_blocks_rpc( + &self, + method_name: &'static str, + role: &crate::HttpRoleName, + params: std::vec::Vec, + ) -> ksp_core_lib::Result { + let method = blocks_descriptor(method_name); + let method = match method { + std::result::Result::Ok(method) => method, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return self.execute_standard_rpc(role, method, params).await; + } +} + +fn push_blocks_context_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaContextConfig>) { + if let std::option::Option::Some(config) = config + && (config.commitment().is_some() || config.min_context_slot().is_some()) + { + params.push((*config).to_json_value()); + } + return; +} + +fn blocks_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> { + let descriptor = crate::find_http_rpc_method(method); + return match descriptor { + std::option::Option::Some(descriptor) + if descriptor.category() == crate::HttpRpcCategory::Blocks && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_4 => + { + std::result::Result::Ok(descriptor) + }, + _ => std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Blocks descriptor is missing from the audited 0.2.4 registry") + .with_context("rpc_method", method), + ), + }; +} + #[cfg(test)] fn decode_block_transaction_version( method: &str, @@ -647,7 +734,6 @@ fn decode_block_rewards( return std::result::Result::Ok(crate::SolanaWireField::Value(rewards)); } -#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireBlockCommitment { diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index 506db7c..aa087f0 100644 --- a/crates/ksp-onchain-transport-lib/tests/public_api.rs +++ b/crates/ksp-onchain-transport-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/public_api.rs -// version: 16 +// version: 17 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -386,3 +386,12 @@ fn public_v0_2_4_pre_002_blocks_economics_types_are_available_from_crate_root() let _reward: std::option::Option = std::option::Option::None; let _supply: std::option::Option = std::option::Option::None; } + +#[test] +fn public_v0_2_4_pre_003_simple_block_wrappers_are_available_from_crate_root() { + let _get_block_commitment = ksp_onchain_transport_lib::HttpTransportPool::get_block_commitment; + let _get_block_height = ksp_onchain_transport_lib::HttpTransportPool::get_block_height; + let _get_block_time = ksp_onchain_transport_lib::HttpTransportPool::get_block_time; + let _get_first_available_block = ksp_onchain_transport_lib::HttpTransportPool::get_first_available_block; + let _minimum_ledger_slot = ksp_onchain_transport_lib::HttpTransportPool::minimum_ledger_slot; +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index 1d60fbf..b30b4cd 100644 --- a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs +++ b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs -// version: 14 +// version: 15 //! Release-level completeness canaries for the staged HTTP wrapper sequence. @@ -432,7 +432,7 @@ fn release_pre_008_ksp_transport_007_retro_audit_covers_all_typed_current_method } #[test] -fn release_v0_2_4_descriptor_set_is_exact_and_remains_read_retry_safe_before_wrappers() { +fn release_v0_2_4_descriptor_set_is_exact_and_remains_read_retry_safe_during_staged_wrappers() { let mut expected_blocks = std::vec![ "getBlock", "getBlockCommitment", @@ -471,3 +471,28 @@ fn release_v0_2_4_descriptor_set_is_exact_and_remains_read_retry_safe_before_wra let get_block = ksp_onchain_transport_lib::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist"); assert!(get_block.request_form_status().has_deprecated_legacy()); } + +#[test] +fn release_v0_2_4_pre_003_simple_blocks_subset_is_exact_and_retry_safe() { + let mut expected = std::vec!["getBlockCommitment", "getBlockHeight", "getBlockTime", "getFirstAvailableBlock", "minimumLedgerSlot"]; + let mut actual = std::vec::Vec::new(); + for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() { + if descriptor.coverage_release() != ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_4 + || descriptor.category() != ksp_onchain_transport_lib::HttpRpcCategory::Blocks + { + continue; + } + match descriptor.method() { + "getBlockCommitment" | "getBlockHeight" | "getBlockTime" | "getFirstAvailableBlock" | "minimumLedgerSlot" => { + assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read); + assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe); + actual.push(descriptor.method()); + }, + _ => {}, + } + } + actual.sort_unstable(); + expected.sort_unstable(); + assert_eq!(actual, expected); + assert_eq!(actual.len(), 5); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs index 1753ea5..ca719da 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs -// version: 1 +// version: 2 #[test] fn transaction_details_and_get_block_config_preserve_all_modern_options() { @@ -148,3 +148,166 @@ fn block_reward_rejects_invalid_pubkey_without_echoing_value() { assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); assert!(!error.to_string().contains("not-a-pubkey")); } + +fn pool_for_url(url: &str) -> crate::HttpTransportPool { + let role = crate::HttpEndpointRoleSettings::new( + crate::HttpRoleName::new("default"), + true, + std::vec![crate::HttpRequestKind::wildcard()], + 10, + crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), + ); + let endpoint = crate::HttpEndpointSettings::new( + "fixture", + true, + crate::HttpProviderName::new("fixture"), + crate::HttpClusterName::new("local"), + crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"), + std::time::Duration::from_secs(1), + std::time::Duration::from_secs(1), + std::option::Option::Some(1), + std::vec![role], + ); + let settings = crate::HttpTransportSettings::new( + std::vec![endpoint], + crate::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)), + ); + return crate::HttpTransportPool::new(settings).expect("fixture pool must build"); +} + +fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHandle) { + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind"); + let address = listener.local_addr().expect("fixture listener address must resolve"); + let handle = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("fixture server must accept one request"); + let request = read_request(&mut stream); + let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body); + std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write"); + return request; + }); + return (format!("http://{address}"), handle); +} + +fn read_request(stream: &mut std::net::TcpStream) -> std::string::String { + let mut bytes = std::vec::Vec::new(); + let mut buffer = [0_u8; 1024]; + loop { + let count = std::io::Read::read(stream, &mut buffer).expect("fixture request must read"); + if count == 0 { + break; + } + bytes.extend_from_slice(&buffer[..count]); + if request_complete(bytes.as_slice()) { + break; + } + } + return std::string::String::from_utf8(bytes).expect("fixture request must be UTF-8"); +} + +fn request_complete(bytes: &[u8]) -> bool { + let text = match std::str::from_utf8(bytes) { + std::result::Result::Ok(text) => text, + std::result::Result::Err(_) => return false, + }; + let header_end = match text.find("\r\n\r\n") { + std::option::Option::Some(value) => value, + std::option::Option::None => return false, + }; + let mut content_length = 0_usize; + for line in text[..header_end].lines() { + let (name, value) = match line.split_once(':') { + std::option::Option::Some(parts) => parts, + std::option::Option::None => continue, + }; + if name.eq_ignore_ascii_case("content-length") { + content_length = value.trim().parse::().expect("content length must parse"); + } + } + return bytes.len() >= header_end.saturating_add(4).saturating_add(content_length); +} + +fn request_body(request: &str) -> serde_json::Value { + let body = request.split("\r\n\r\n").nth(1).expect("fixture request body must exist"); + return serde_json::from_str(body).expect("fixture request body must be JSON"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_commitment_serializes_slot_and_preserves_distribution() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_commitment.success.json")); + let pool = pool_for_url(url.as_str()); + let result = pool.get_block_commitment(&crate::HttpRoleName::new("default"), 430_000_123).await.expect("block commitment fixture must succeed"); + assert_eq!(result.commitment(), std::option::Option::Some(&[1, 2, 3, 4][..])); + assert_eq!(result.total_stake(), 1_000_000_000); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getBlockCommitment")); + assert_eq!(body["params"], serde_json::json!([430000123])); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_height_serializes_context_config() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_height.success.json")); + let pool = pool_for_url(url.as_str()); + let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(429_000_000)); + let height = pool + .get_block_height(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)) + .await + .expect("block height fixture must succeed"); + assert_eq!(height, 410_000_000); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"finalized","minContextSlot":429000000}])); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_height_omits_explicitly_empty_config() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_height.success.json")); + let pool = pool_for_url(url.as_str()); + let config = crate::SolanaContextConfig::default(); + let height = pool + .get_block_height(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)) + .await + .expect("block height fixture must succeed"); + assert_eq!(height, 410_000_000); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([])); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_block_time_preserves_timestamp_and_null() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_time.success.json")); + let pool = pool_for_url(url.as_str()); + let timestamp = pool.get_block_time(&crate::HttpRoleName::new("default"), 430_000_123).await.expect("block time fixture must succeed"); + assert_eq!(timestamp, std::option::Option::Some(1_787_072_400)); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000123])); + + let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_time.null.json")); + let pool = pool_for_url(url.as_str()); + let timestamp = pool.get_block_time(&crate::HttpRoleName::new("default"), 430_000_124).await.expect("null block time fixture must succeed"); + assert_eq!(timestamp, std::option::Option::None); + handle.join().expect("fixture server must join"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_first_available_block_has_no_params() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_first_available_block.success.json")); + let pool = pool_for_url(url.as_str()); + let slot = pool.get_first_available_block(&crate::HttpRoleName::new("default")).await.expect("first available block fixture must succeed"); + assert_eq!(slot, 250_000); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getFirstAvailableBlock")); + assert_eq!(body["params"], serde_json::json!([])); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_minimum_ledger_slot_has_no_params() { + let (url, handle) = serve_once(include_str!("../fixtures/http/minimum_ledger_slot.success.json")); + let pool = pool_for_url(url.as_str()); + let slot = pool.minimum_ledger_slot(&crate::HttpRoleName::new("default")).await.expect("minimum ledger slot fixture must succeed"); + assert_eq!(slot, 123_456); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("minimumLedgerSlot")); + assert_eq!(body["params"], serde_json::json!([])); +} diff --git a/deltas/0.2.4/pre.003.md b/deltas/0.2.4/pre.003.md new file mode 100644 index 0000000..89f4ced --- /dev/null +++ b/deltas/0.2.4/pre.003.md @@ -0,0 +1,378 @@ + + + +# Delta `0.2.4-pre.003` — cinq wrappers Blocks simples + +## Base requise + +Livraison précédente : + +```text +0.2.4-pre.002-fix.001 +workspace.package.version = "0.2.4-pre.2.fix.1" +``` + +Les validations locales fournies pour cette base sont propres : + +```text +cargo fmt --all -> terminé +cargo check --workspace -> terminé sans warning +cargo clippy --workspace --all-targets -> terminé sans warning +cargo test -p ksp-onchain-transport-lib -> 196 unit tests OK + 20 public API tests OK + 15 release-completeness tests OK + 1 smoke Devnet ignoré comme prévu + 0 échec +``` + +Le plan canonique reste `docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md` version 2. + +## Objectif + +Implémenter exactement la première tranche de wrappers Blocks prévue par le plan : + +```text +getBlockCommitment +getBlockHeight +getBlockTime +getFirstAvailableBlock +minimumLedgerSlot +``` + +Les cinq méthodes sont déjà présentes dans le registre audité `V0_2_4`, toutes classées `Read / RetrySafe`. Cette prerelease ne modifie donc pas le registre ; elle matérialise leurs wrappers typed au-dessus du chemin Transport commun existant. + +Les méthodes Blocks restantes restent hors de cette tranche : + +```text +getBlock +getBlockProduction +getBlocks +getBlocksWithLimit +getRecentPerformanceSamples +``` + +Les cinq méthodes Economics restent également sans wrapper runtime à ce stade. + +## Réaudit RPC de la tranche + +Le contrat HTTP courant a été revérifié avant implémentation : + +- `getBlockCommitment` prend un `slot: u64` et retourne un objet avec `commitment: array ou null` et `totalStake: u64` ; +- `getBlockHeight` accepte un objet optionnel `commitment` / `minContextSlot` et retourne `u64` ; +- `getBlockTime` prend un `slot: u64` et retourne un timestamp Unix `i64` lorsqu'il est disponible ; KSP conserve également le résultat JSON `null` sous `Option` conformément au wire déjà audité ; +- `getFirstAvailableBlock` ne prend aucun paramètre et retourne un `u64` ; +- `minimumLedgerSlot` ne prend aucun paramètre et retourne un `u64`. + +Aucun nouveau champ, overload ou invariant déterministe n'a été identifié pour ces cinq méthodes par rapport au plan `pre.001`. + +## Version Cargo + +Nouvelle prerelease technique : + +```text +0.2.4-pre.2.fix.1 -> 0.2.4-pre.3 +``` + +Aucune dépendance ni feature Cargo n'est ajoutée ou modifiée. + +## Implémentation + +### Chemin Blocks commun + +`rpc_blocks.rs` ajoute un `impl HttpTransportPool` dédié aux wrappers Blocks. Tous les appels passent par : + +```text +wrapper typed Blocks + -> blocks_descriptor(...) + -> execute_standard_rpc(...) + -> pool/admission + -> executor HTTP + -> validation JSON-RPC + -> decode typed +``` + +`blocks_descriptor` exige simultanément : + +```text +category == HttpRpcCategory::Blocks +coverage_release == HttpRpcCoverageRelease::V0_2_4 +``` + +Un descripteur absent ou incohérent échoue donc avant l'I/O normale au lieu de contourner le registre audité. + +Aucune boucle de retry locale n'est ajoutée. Les cinq méthodes héritent de la politique centrale `RetrySafe` déjà attachée à leurs descripteurs. + +### `getBlockCommitment` + +Signature publique : + +```text +HttpTransportPool::get_block_commitment(role, slot) + -> Result +``` + +Paramètres wire : + +```json +[430000123] +``` + +Le DTO `SolanaBlockCommitment` introduit en `pre.002` est désormais réellement consommé par un wrapper runtime. Son helper `decode_wire` et la struct privée `WireBlockCommitment` sortent donc de `#[cfg(test)]`. + +Le champ nullable `commitment` reste représenté par `Option>`; `totalStake` reste `u64`. + +Conformément au fix précédent, **aucun autre** décodeur Blocks/Economics préparé en `pre.002` n'est activé prématurément. + +### `getBlockHeight` + +Signature publique : + +```text +HttpTransportPool::get_block_height(role, Option<&SolanaContextConfig>) + -> Result +``` + +La configuration réutilise le contrat partagé existant : + +```text +commitment +minContextSlot +``` + +Un `None` ou un `SolanaContextConfig::default()` produit une liste de paramètres vide : + +```json +[] +``` + +Une configuration non vide conserve exactement les champs fournis, par exemple : + +```json +[ + { + "commitment": "finalized", + "minContextSlot": 429000000 + } +] +``` + +Aucune valeur de commitment n'est inventée localement lorsque la configuration est absente. + +### `getBlockTime` + +Signature publique : + +```text +HttpTransportPool::get_block_time(role, slot) + -> Result> +``` + +Le wrapper conserve sans perte les deux états de résultat : + +```text +JSON i64 -> Some(timestamp) +JSON null -> None +``` + +Transport ne convertit pas le timestamp en date/heure applicative et ne synthétise aucune valeur pour `null`. + +### `getFirstAvailableBlock` + +Signature publique : + +```text +HttpTransportPool::get_first_available_block(role) + -> Result +``` + +Le wrapper envoie exactement : + +```json +[] +``` + +et retourne le slot `u64` fourni par le runtime. + +### `minimumLedgerSlot` + +Signature publique : + +```text +HttpTransportPool::minimum_ledger_slot(role) + -> Result +``` + +Le wrapper envoie exactement : + +```json +[] +``` + +et retourne le slot minimum `u64` fourni par le nœud, sans lui appliquer de sémantique de rétention locale. + +## Discipline `#[cfg(test)]` héritée de `pre.002-fix.001` + +La correction précédente est prolongée explicitement : + +```text +runtime en pre.003 + SolanaBlockCommitment::decode_wire + WireBlockCommitment + +reste test-only + SolanaGetBlockConfig::{is_empty,to_json_value} + SolanaBlockProductionRange::to_json_value + SolanaBlockProductionConfig::{is_empty,to_json_value} + SolanaBlockProduction::decode_wire + SolanaBlockReward::decode_wire + SolanaBlockTransaction::decode_wire + SolanaConfirmedBlock::decode_wire + SolanaPerformanceSample::decode_wire + helpers privés transaction/reward de bloc + autres WireBlock* + helpers/wires Economics +``` + +Aucun `#[allow(dead_code)]` n'est introduit. + +## Fixtures HTTP ajoutées + +```text +crates/ksp-onchain-transport-lib/fixtures/http/get_block_commitment.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_block_height.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.null.json +crates/ksp-onchain-transport-lib/fixtures/http/get_first_available_block.success.json +crates/ksp-onchain-transport-lib/fixtures/http/minimum_ledger_slot.success.json +``` + +Les fixtures sont des enveloppes JSON-RPC complètes et déterministes utilisées par les tests HTTP locaux. + +## Tests + +`unit_tests/rpc_blocks.rs` ajoute six tests async de wrapper : + +```text +typed_get_block_commitment_serializes_slot_and_preserves_distribution +typed_get_block_height_serializes_context_config +typed_get_block_height_omits_explicitly_empty_config +typed_get_block_time_preserves_timestamp_and_null +typed_get_first_available_block_has_no_params +typed_minimum_ledger_slot_has_no_params +``` + +Ils vérifient les requêtes HTTP réellement émises vers le serveur fixture local, et pas seulement des helpers de sérialisation isolés. + +`tests/public_api.rs` ajoute une canarie qui référence les cinq méthodes directement depuis `HttpTransportPool` afin de verrouiller leur disponibilité publique. + +`tests/release_completeness.rs` : + +- renomme la canarie globale `V0_2_4` qui parlait encore de l'état « before wrappers » ; +- ajoute une canarie `pre.003` qui verrouille exactement les cinq méthodes de cette tranche et leur classification `Read / RetrySafe`. + +Après application, les compteurs attendus pour la crate Transport sont donc au minimum : + +```text +unit tests 196 -> 202 +public API tests 20 -> 21 +release-completeness 15 -> 16 +smoke Devnet 1 ignoré comme prévu +``` + +Ces nombres sont des attentes dérivées des tests ajoutés ; les validations Cargo doivent être exécutées sur le checkout de développement. + +## Documentation de crate + +La rustdoc racine de `ksp-onchain-transport-lib` est mise à jour pour refléter l'état réel de `0.2.4-pre.003` : les primitives Blocks/Economics sont acquises et cinq wrappers Blocks sont désormais actifs. + +Le plan `011`, le ROADMAP et le CHANGELOG restent inchangés : aucun redécoupage de release ni jalon documentaire final n'est nécessaire dans cette tranche. + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/src/rpc_blocks.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs +crates/ksp-onchain-transport-lib/tests/public_api.rs +crates/ksp-onchain-transport-lib/tests/release_completeness.rs +``` + +## Fichiers ajoutés + +```text +crates/ksp-onchain-transport-lib/fixtures/http/get_block_commitment.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_block_height.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_block_time.null.json +crates/ksp-onchain-transport-lib/fixtures/http/get_first_available_block.success.json +crates/ksp-onchain-transport-lib/fixtures/http/minimum_ledger_slot.success.json +deltas/0.2.4/pre.003.md +``` + +## Fichiers supprimés + +Aucun. + +## Fichiers volontairement inchangés + +```text +crates/ksp-onchain-transport-lib/src/rpc_method.rs +crates/ksp-onchain-transport-lib/src/rpc_economics.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs +docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md +ROADMAP.md +CHANGELOG.md +deltas/0.2.4/pre.001.md +deltas/0.2.4/pre.001-fix.001.md +deltas/0.2.4/pre.002.md +deltas/0.2.4/pre.002-fix.001.md +``` + +Aucun delta historique n'est réécrit. + +## Validations exécutées dans le sandbox + +Le sandbox ne fournit pas le toolchain Rust. Les contrôles effectués sont donc statiques : + +- parsing des six nouvelles fixtures JSON ; +- contrôle du signal Cargo `0.2.4-pre.3` ; +- contrôle de présence exacte des cinq nouvelles méthodes publiques ; +- contrôle que seul le wire `getBlockCommitment` nécessaire à cette tranche sort de `#[cfg(test)]` ; +- contrôle qu'aucun `#[allow(dead_code)]` n'est introduit ; +- scan du nouveau code de production pour `unwrap`, `expect`, `panic!` et `unsafe` ; +- contrôle différentiel contre `0.2.4-pre.002-fix.001` ; +- contrôle des versions d'en-tête des fichiers modifiés. + +## Validations Cargo à exécuter + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +``` + +Le critère attendu est : aucune régression, aucun warning introduit par `pre.003`, et les nouveaux compteurs `202 / 21 / 16` si aucune autre modification locale n'ajoute de test. + +Le commit attendu après validation est : + +```text +v0.2.4-pre.003 +``` + +## Questions ouvertes + +Aucune pour cette tranche. + +## Suite + +`0.2.4-pre.004` reste dédiée aux ranges/performance : + +```text +getBlocks +getBlocksWithLimit +getRecentPerformanceSamples +``` + +avec les validations déterministes déjà cadrées dans le plan : plage maximale de 500 000 slots, `limit <= 500000`, `limit <= 720` pour les performance samples, et conservation des formes de réponse sans perte.