From 0cff0406abfd1684d13486447d1669c2484c379e Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Mon, 17 Aug 2026 18:02:18 +0200 Subject: [PATCH] v0.2.1-pre.002 --- Cargo.toml | 7 +- ROADMAP.md | 3 +- crates/ksp-onchain-transport-lib/Cargo.toml | 18 + crates/ksp-onchain-transport-lib/src/error.rs | 27 + .../ksp-onchain-transport-lib/src/json_rpc.rs | 280 +++++ crates/ksp-onchain-transport-lib/src/lib.rs | 97 ++ .../src/rpc_method.rs | 1108 +++++++++++++++++ .../ksp-onchain-transport-lib/src/settings.rs | 578 +++++++++ .../tests/dependency_boundary.rs | 14 + .../tests/public_api.rs | 61 + .../unit_tests/json_rpc.rs | 117 ++ .../unit_tests/rpc_method.rs | 121 ++ .../unit_tests/settings.rs | 207 +++ deltas/0.2.1/pre.002.md | 333 +++++ docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md | 28 +- 15 files changed, 2989 insertions(+), 10 deletions(-) create mode 100644 crates/ksp-onchain-transport-lib/Cargo.toml create mode 100644 crates/ksp-onchain-transport-lib/src/error.rs create mode 100644 crates/ksp-onchain-transport-lib/src/json_rpc.rs create mode 100644 crates/ksp-onchain-transport-lib/src/lib.rs create mode 100644 crates/ksp-onchain-transport-lib/src/rpc_method.rs create mode 100644 crates/ksp-onchain-transport-lib/src/settings.rs create mode 100644 crates/ksp-onchain-transport-lib/tests/dependency_boundary.rs create mode 100644 crates/ksp-onchain-transport-lib/tests/public_api.rs create mode 100644 crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs create mode 100644 crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs create mode 100644 crates/ksp-onchain-transport-lib/unit_tests/settings.rs create mode 100644 deltas/0.2.1/pre.002.md diff --git a/Cargo.toml b/Cargo.toml index 8d82c69..8fd257b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 96 +# version: 97 [workspace] resolver = "3" -members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"] +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.1-pre.1" +version = "0.2.1-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" @@ -18,6 +18,7 @@ fs2 = { version = "^0.4" } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } jsonschema = { version = "^0.49", default-features = false } +reqwest = { version = "^0.13", default-features = false } solana-pubkey = { version = "^4.3", default-features = false } tracing = { version = "^0.1", default-features = false, features = ["std"] } tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] } diff --git a/ROADMAP.md b/ROADMAP.md index a354c46..297e994 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # Roadmap KSP @@ -47,6 +47,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U - [X] `0.2.0-rel.001` — Publication stable du cadrage `0.2.x`; prochaine release : `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation`. - [X] `0.2.1-pre.001` — Audit HTTP officiel/bot3, matrice exhaustive, architecture et gate de sizing : scope monolithique refusé, premier split de la couverture HTTP sur `0.2.1`–`0.2.6`, `0.2.1` réduite déclarée raisonnablement clôturable. - [X] `0.2.1-pre.001-fix.001` — Recalibrer le split HTTP sur `0.2.1`–`0.2.4` : `0.2.2` Accounts + Tokens + Cluster, `0.2.3` Transactions, `0.2.4` Blocks + Economics + compliance finale ; autoriser l'enchaînement de plusieurs releases complètement clôturées dans une même session lorsque le sizing le permet. +- [X] `0.2.1-pre.002` — Créer la crate `ksp-onchain-transport-lib`, fixer ses settings runtime publics/validation, les envelopes JSON-RPC HTTP, les codes d'erreur, le registre central 52 current + 14 historiques et la base de warnings KSP de statut. ### Releases fonctionnelles décidées/pressenties diff --git a/crates/ksp-onchain-transport-lib/Cargo.toml b/crates/ksp-onchain-transport-lib/Cargo.toml new file mode 100644 index 0000000..811a779 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/Cargo.toml @@ -0,0 +1,18 @@ +# file: crates/ksp-onchain-transport-lib/Cargo.toml +# version: 1 + +[package] +name = "ksp-onchain-transport-lib" +version.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +ksp-core-lib = { path = "../ksp-core-lib" } +ksp-logging-lib = { path = "../ksp-logging-lib" } +reqwest.workspace = true +serde.workspace = true +serde_json.workspace = true + +[lints] +workspace = true diff --git a/crates/ksp-onchain-transport-lib/src/error.rs b/crates/ksp-onchain-transport-lib/src/error.rs new file mode 100644 index 0000000..04dc2e7 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/error.rs @@ -0,0 +1,27 @@ +// file: crates/ksp-onchain-transport-lib/src/error.rs +// version: 1 + +/// Error code used when HTTP transport runtime settings are invalid. +pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_settings"); +/// Error code used when no logical endpoint can satisfy a request. +pub const ERROR_CODE_ENDPOINT_SELECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "endpoint_selection_failed"); +/// Error code used when an HTTP connection cannot be established. +pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_connection_failed"); +/// Error code used when an HTTP request fails after a connection exists. +pub const ERROR_CODE_HTTP_REQUEST_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_request_failed"); +/// Error code used when a transport deadline expires. +pub const ERROR_CODE_TIMEOUT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "timeout"); +/// Error code used when an endpoint or provider rate-limits a request. +pub const ERROR_CODE_RATE_LIMITED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rate_limited"); +/// Error code used when a JSON-RPC request cannot be encoded. +pub const ERROR_CODE_JSON_ENCODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_encode_failed"); +/// Error code used when an HTTP JSON-RPC payload cannot be decoded as JSON. +pub const ERROR_CODE_JSON_DECODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_decode_failed"); +/// Error code used when a decoded JSON-RPC envelope violates protocol invariants. +pub const ERROR_CODE_JSON_RPC_PROTOCOL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_rpc_protocol_invalid"); +/// Error code used when a remote JSON-RPC endpoint returns an application-level RPC error. +pub const ERROR_CODE_RPC_APPLICATION_ERROR: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rpc_application_error"); +/// Error code used when a historically documented RPC method is no longer supported by the targeted runtime. +pub const ERROR_CODE_METHOD_REMOVED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "method_removed"); +/// Error code used when a decoded response cannot satisfy the KSP transport contract expected by the caller. +pub const ERROR_CODE_INVALID_RESPONSE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_response"); diff --git a/crates/ksp-onchain-transport-lib/src/json_rpc.rs b/crates/ksp-onchain-transport-lib/src/json_rpc.rs new file mode 100644 index 0000000..09edf79 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/json_rpc.rs @@ -0,0 +1,280 @@ +// file: crates/ksp-onchain-transport-lib/src/json_rpc.rs +// version: 1 + +const JSON_RPC_VERSION: &str = "2.0"; + +/// JSON-RPC 2.0 HTTP request envelope emitted by KSP. +#[derive(Clone, PartialEq, serde::Serialize)] +pub struct JsonRpcRequest { + jsonrpc: &'static str, + id: u64, + method: std::string::String, + params: std::vec::Vec, +} + +impl JsonRpcRequest { + /// Creates a JSON-RPC 2.0 request with a KSP-owned numeric identifier. + pub fn new(id: u64, method: impl std::convert::Into, params: std::vec::Vec) -> ksp_core_lib::Result { + let method = method.into(); + if method.trim().is_empty() || method.trim() != method { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID, "JSON-RPC method must be a non-empty trimmed string") + .with_context("field", "method"), + ); + } + return std::result::Result::Ok(Self { jsonrpc: JSON_RPC_VERSION, id, method, params }); + } + + /// Returns the numeric request identifier. + #[must_use] + pub const fn id(&self) -> u64 { + return self.id; + } + + /// Returns the RPC method name. + #[must_use] + pub fn method(&self) -> &str { + return self.method.as_str(); + } + + /// Returns ordered request parameters. + #[must_use] + pub fn params(&self) -> &[serde_json::Value] { + return self.params.as_slice(); + } + + /// Serializes the request into compact JSON text. + pub fn to_json_string(&self) -> ksp_core_lib::Result { + let serialization_result = serde_json::to_string(self); + return match serialization_result { + std::result::Result::Ok(text) => std::result::Result::Ok(text), + std::result::Result::Err(error) => std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_ENCODE_FAILED, "cannot encode JSON-RPC request") + .with_context("method", self.method()) + .with_source(error), + ), + }; + } +} + +impl std::fmt::Debug for JsonRpcRequest { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("JsonRpcRequest") + .field("jsonrpc", &self.jsonrpc) + .field("id", &self.id) + .field("method", &self.method) + .field("param_count", &self.params.len()) + .finish(); + } +} + +/// JSON-RPC 2.0 error payload returned by a remote Solana endpoint. +#[derive(Clone, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct JsonRpcErrorObject { + code: i64, + message: std::string::String, + #[serde(default)] + data: std::option::Option, +} + +impl JsonRpcErrorObject { + /// Returns the remote JSON-RPC application error code. + #[must_use] + pub const fn code(&self) -> i64 { + return self.code; + } + + /// Returns the remote human-readable RPC error message. + #[must_use] + pub fn message(&self) -> &str { + return self.message.as_str(); + } + + /// Returns optional provider-supplied error data. + #[must_use] + pub const fn data(&self) -> std::option::Option<&serde_json::Value> { + return self.data.as_ref(); + } +} + +impl std::fmt::Debug for JsonRpcErrorObject { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("JsonRpcErrorObject") + .field("code", &self.code) + .field("message", &"") + .field("data", &if self.data.is_some() { "present" } else { "absent" }) + .finish(); + } +} + +/// Validated JSON-RPC 2.0 success response. +#[derive(Clone, PartialEq)] +pub struct JsonRpcSuccessResponse { + id: u64, + result: serde_json::Value, +} + +impl JsonRpcSuccessResponse { + /// Returns the echoed request identifier. + #[must_use] + pub const fn id(&self) -> u64 { + return self.id; + } + + /// Returns the raw JSON result, including JSON `null` when the method legitimately returns it. + #[must_use] + pub const fn result(&self) -> &serde_json::Value { + return &self.result; + } + + /// Consumes the response and returns its raw JSON result. + #[must_use] + pub fn into_result(self) -> serde_json::Value { + return self.result; + } +} + +impl std::fmt::Debug for JsonRpcSuccessResponse { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.debug_struct("JsonRpcSuccessResponse").field("id", &self.id).field("result", &"").finish(); + } +} + +/// Validated JSON-RPC 2.0 error response. +#[derive(Clone, Debug, PartialEq)] +pub struct JsonRpcErrorResponse { + id: u64, + error: crate::JsonRpcErrorObject, +} + +impl JsonRpcErrorResponse { + /// Returns the echoed request identifier. + #[must_use] + pub const fn id(&self) -> u64 { + return self.id; + } + + /// Returns the remote JSON-RPC application error payload. + #[must_use] + pub const fn error(&self) -> &crate::JsonRpcErrorObject { + return &self.error; + } +} + +/// Validated JSON-RPC 2.0 HTTP response preserving success and application-error payloads separately. +#[derive(Clone, Debug, PartialEq)] +pub enum JsonRpcResponse { + /// Successful response containing a raw method result. + Success(crate::JsonRpcSuccessResponse), + /// Application-level JSON-RPC error returned by the remote endpoint. + Error(crate::JsonRpcErrorResponse), +} + +impl JsonRpcResponse { + /// Converts a validated response into the raw success result or a KSP application-error classification. + pub fn into_result(self) -> ksp_core_lib::Result { + return match self { + Self::Success(success) => std::result::Result::Ok(success.into_result()), + Self::Error(error_response) => std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_RPC_APPLICATION_ERROR, "Solana JSON-RPC endpoint returned an application error") + .with_context("rpc_code", error_response.error().code().to_string()), + ), + }; + } +} + +/// Parses and validates a JSON-RPC HTTP response from UTF-8 JSON text. +pub fn parse_json_rpc_response_text(text: &str, expected_id: u64) -> ksp_core_lib::Result { + let decode_result = serde_json::from_str::(text); + let value = match decode_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_DECODE_FAILED, "cannot decode JSON-RPC response as JSON").with_source(error), + ); + }, + }; + return crate::parse_json_rpc_response_value(value, expected_id); +} + +/// Validates a decoded JSON value as one JSON-RPC HTTP response for the expected KSP request identifier. +pub fn parse_json_rpc_response_value(value: serde_json::Value, expected_id: u64) -> ksp_core_lib::Result { + let object = match value.as_object() { + std::option::Option::Some(object) => object, + std::option::Option::None => { + return protocol_error("JSON-RPC response must be an object", "response"); + }, + }; + let version = match object.get("jsonrpc") { + std::option::Option::Some(serde_json::Value::String(version)) => version.as_str(), + std::option::Option::Some(_) => { + return protocol_error("JSON-RPC version must be a string", "jsonrpc"); + }, + std::option::Option::None => { + return protocol_error("JSON-RPC response is missing its version", "jsonrpc"); + }, + }; + if version != JSON_RPC_VERSION { + return protocol_error("JSON-RPC version must be exactly 2.0", "jsonrpc"); + } + let response_id = match object.get("id") { + std::option::Option::Some(id) => match id.as_u64() { + std::option::Option::Some(id) => id, + std::option::Option::None => { + return protocol_error("JSON-RPC response id must be an unsigned integer", "id"); + }, + }, + std::option::Option::None => { + return protocol_error("JSON-RPC response is missing its id", "id"); + }, + }; + if response_id != expected_id { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID, "JSON-RPC response id does not match the request") + .with_context("expected_id", expected_id.to_string()) + .with_context("actual_id", response_id.to_string()), + ); + } + let has_result = object.contains_key("result"); + let has_error = object.contains_key("error"); + if has_result == has_error { + return protocol_error("JSON-RPC response must contain exactly one of result or error", "response"); + } + if has_result { + let result = match object.get("result") { + std::option::Option::Some(result) => result.clone(), + std::option::Option::None => { + return protocol_error("JSON-RPC result field disappeared during validation", "result"); + }, + }; + return std::result::Result::Ok(crate::JsonRpcResponse::Success(crate::JsonRpcSuccessResponse { id: response_id, result })); + } + let error_value = match object.get("error") { + std::option::Option::Some(error) => error.clone(), + std::option::Option::None => { + return protocol_error("JSON-RPC error field disappeared during validation", "error"); + }, + }; + let error_decode_result = serde_json::from_value::(error_value); + let error = match error_decode_result { + std::result::Result::Ok(error) => error, + std::result::Result::Err(error) => { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID, "JSON-RPC error object is invalid") + .with_context("field", "error") + .with_source(error), + ); + }, + }; + return std::result::Result::Ok(crate::JsonRpcResponse::Error(crate::JsonRpcErrorResponse { id: response_id, error })); +} + +fn protocol_error(message: &str, field: &str) -> ksp_core_lib::Result { + return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID, message).with_context("field", field)); +} + +#[cfg(test)] +#[path = "../unit_tests/json_rpc.rs"] +mod tests; diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs new file mode 100644 index 0000000..e6ac517 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/lib.rs @@ -0,0 +1,97 @@ +// file: crates/ksp-onchain-transport-lib/src/lib.rs +// version: 1 +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! KSP-owned Solana on-chain transport foundation. +//! +//! This crate owns runtime HTTP transport settings, Solana HTTP JSON-RPC envelopes and the audited standard method registry. It deliberately remains +//! independent from `ksp-config-lib`, Store and Program layers. Config may later construct these public settings through a one-way adapter, while network +//! clients, pools, resilience and typed Solana method adapters are introduced by subsequent `0.2.1` prereleases. + +mod error; +mod json_rpc; +mod rpc_method; +mod settings; + +/// Error code used when no logical endpoint can satisfy a request. +pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED; +/// Error code used when an HTTP connection cannot be established. +pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED; +/// Error code used when an HTTP request fails after connection establishment. +pub use self::error::ERROR_CODE_HTTP_REQUEST_FAILED; +/// Error code used when a decoded response cannot satisfy the expected KSP transport contract. +pub use self::error::ERROR_CODE_INVALID_RESPONSE; +/// Error code used when HTTP transport runtime settings are invalid. +pub use self::error::ERROR_CODE_INVALID_SETTINGS; +/// Error code used when an HTTP JSON-RPC payload cannot be decoded as JSON. +pub use self::error::ERROR_CODE_JSON_DECODE_FAILED; +/// Error code used when a JSON-RPC request cannot be encoded. +pub use self::error::ERROR_CODE_JSON_ENCODE_FAILED; +/// Error code used when a decoded JSON-RPC envelope violates protocol invariants. +pub use self::error::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID; +/// Error code used when a historically documented RPC method has been removed from the targeted runtime. +pub use self::error::ERROR_CODE_METHOD_REMOVED; +/// Error code used when an endpoint or provider rate-limits a request. +pub use self::error::ERROR_CODE_RATE_LIMITED; +/// Error code used when a remote endpoint returns an application-level JSON-RPC error. +pub use self::error::ERROR_CODE_RPC_APPLICATION_ERROR; +/// Error code used when a transport deadline expires. +pub use self::error::ERROR_CODE_TIMEOUT; +/// JSON-RPC 2.0 error payload returned by a remote Solana endpoint. +pub use self::json_rpc::JsonRpcErrorObject; +/// Validated JSON-RPC 2.0 error response. +pub use self::json_rpc::JsonRpcErrorResponse; +/// JSON-RPC 2.0 HTTP request envelope emitted by KSP. +pub use self::json_rpc::JsonRpcRequest; +/// Validated JSON-RPC 2.0 HTTP response. +pub use self::json_rpc::JsonRpcResponse; +/// Validated JSON-RPC 2.0 success response. +pub use self::json_rpc::JsonRpcSuccessResponse; +/// Parses and validates a JSON-RPC HTTP response from UTF-8 JSON text. +pub use self::json_rpc::parse_json_rpc_response_text; +/// Validates a decoded JSON value as one JSON-RPC HTTP response. +pub use self::json_rpc::parse_json_rpc_response_value; +/// Functional category used by the audited Solana HTTP JSON-RPC registry. +pub use self::rpc_method::HttpRpcCategory; +/// Release that owns typed KSP coverage for one audited HTTP RPC method. +pub use self::rpc_method::HttpRpcCoverageRelease; +/// Immutable audited descriptor for one Solana HTTP JSON-RPC method. +pub use self::rpc_method::HttpRpcMethodDescriptor; +/// Documentation lifecycle status of one audited RPC method. +pub use self::rpc_method::RpcDocumentationStatus; +/// Technical operation kind used to separate reads, simulations and submissions. +pub use self::rpc_method::RpcOperationKind; +/// Request-form policy attached to a stable RPC method. +pub use self::rpc_method::RpcRequestFormStatus; +/// Runtime availability status of one audited RPC method. +pub use self::rpc_method::RpcRuntimeStatus; +/// HTTP transport retry classification attached to an RPC method descriptor. +pub use self::rpc_method::TransportRetryClass; +/// Returns all current Solana HTTP RPC method descriptors audited for the `0.2.1`–`0.2.4` coverage sequence. +pub use self::rpc_method::current_http_rpc_methods; +/// Finds a current or historical standard Solana HTTP RPC descriptor by exact method name. +pub use self::rpc_method::find_http_rpc_method; +/// Returns historically documented deprecated HTTP RPC descriptors retained for compliance history. +pub use self::rpc_method::historical_http_rpc_methods; +/// Open cluster or network descriptor used by HTTP endpoint settings. +pub use self::settings::HttpClusterName; +/// Runtime settings for one role declared by an HTTP endpoint. +pub use self::settings::HttpEndpointRoleSettings; +/// Runtime settings for one named Solana HTTP endpoint. +pub use self::settings::HttpEndpointSettings; +/// Runtime HTTP endpoint URL with redacted diagnostics. +pub use self::settings::HttpEndpointUrl; +/// Open provider descriptor used by HTTP endpoint settings. +pub use self::settings::HttpProviderName; +/// Open request-kind descriptor used by logical endpoint capabilities. +pub use self::settings::HttpRequestKind; +/// Bounded retry settings owned by the HTTP transport runtime. +pub use self::settings::HttpRetrySettings; +/// Local limits attached to one logical HTTP endpoint role. +pub use self::settings::HttpRoleLimits; +/// Open logical endpoint role descriptor. +pub use self::settings::HttpRoleName; +/// Complete runtime settings consumed by the Solana HTTP transport foundation. +pub use self::settings::HttpTransportSettings; diff --git a/crates/ksp-onchain-transport-lib/src/rpc_method.rs b/crates/ksp-onchain-transport-lib/src/rpc_method.rs new file mode 100644 index 0000000..c6ed50b --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/rpc_method.rs @@ -0,0 +1,1108 @@ +// file: crates/ksp-onchain-transport-lib/src/rpc_method.rs +// version: 1 + +/// Functional category used by the audited Solana HTTP JSON-RPC registry. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum HttpRpcCategory { + /// Account state and rent queries. + Accounts, + /// SPL token-oriented RPC queries exposed by the standard Solana HTTP surface. + Tokens, + /// Transaction, signature, fee, simulation and submission methods. + Transactions, + /// Block, slot-history and performance-sample methods. + Blocks, + /// Cluster identity, epoch, leader, health and validator methods. + Cluster, + /// Supply, inflation and stake-economics methods. + Economics, + /// Historically documented methods removed from the targeted Agave runtime generation. + Historical, +} + +/// Documentation lifecycle status of one audited RPC method. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RpcDocumentationStatus { + /// The method is documented as stable. + Stable, + /// The method is documented as deprecated or obsolete. + Deprecated, + /// The method is documented as unstable or experimental. + Unstable, +} + +impl RpcDocumentationStatus { + /// Returns the stable machine-readable status code. + #[must_use] + pub const fn code(self) -> &'static str { + return match self { + Self::Stable => "stable", + Self::Deprecated => "deprecated", + Self::Unstable => "unstable", + }; + } +} + +/// Runtime availability status of one audited RPC method. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RpcRuntimeStatus { + /// The targeted runtime generation still supports the method. + Supported, + /// The method is historically documented but removed from the targeted runtime generation. + Removed, +} + +impl RpcRuntimeStatus { + /// Returns the stable machine-readable runtime status code. + #[must_use] + pub const fn code(self) -> &'static str { + return match self { + Self::Supported => "supported", + Self::Removed => "removed", + }; + } +} + +/// Request-form policy attached to a stable RPC method. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RpcRequestFormStatus { + /// Only the currently documented stable request form is tracked by KSP. + Stable, + /// The method is stable but also has a documented deprecated legacy request form that must warn when explicitly used. + StableWithDeprecatedLegacy, +} + +impl RpcRequestFormStatus { + /// Returns whether the method has a documented deprecated legacy request form. + #[must_use] + pub const fn has_deprecated_legacy(self) -> bool { + return match self { + Self::Stable => false, + Self::StableWithDeprecatedLegacy => true, + }; + } +} + +/// Technical operation kind used to separate reads, simulations and submissions. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RpcOperationKind { + /// Read-only RPC operation. + Read, + /// Simulation operation that does not submit a transaction for execution. + Simulation, + /// Technical write/submission operation whose ambiguous post-dispatch outcome must not be resent automatically. + WriteSubmission, +} + +/// HTTP transport retry classification attached to an RPC method descriptor. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum TransportRetryClass { + /// The identical transport request may be retried when the transport failure is classified as retryable. + RetrySafe, + /// The request must not be resent automatically after an ambiguous dispatch. + NeverAfterDispatch, + /// Retry classification does not apply because the method is not callable on the targeted runtime. + NotApplicable, +} + +/// Release that owns the typed KSP coverage for one audited current HTTP method. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum HttpRpcCoverageRelease { + /// `0.2.1` HTTP foundation and four canary methods. + V0_2_1, + /// `0.2.2` Accounts + Tokens + remaining Cluster methods. + V0_2_2, + /// `0.2.3` Transactions methods. + V0_2_3, + /// `0.2.4` Blocks + Economics methods and final HTTP compliance. + V0_2_4, + /// Historical registry entry with no callable typed release. + Historical, +} + +/// Immutable audited descriptor for one Solana HTTP JSON-RPC method. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct HttpRpcMethodDescriptor { + method: &'static str, + category: crate::HttpRpcCategory, + request_kind: &'static str, + documentation_status: crate::RpcDocumentationStatus, + runtime_status: crate::RpcRuntimeStatus, + request_form_status: crate::RpcRequestFormStatus, + operation_kind: crate::RpcOperationKind, + transport_retry_class: crate::TransportRetryClass, + replacement: std::option::Option<&'static str>, + coverage_release: crate::HttpRpcCoverageRelease, +} + +impl HttpRpcMethodDescriptor { + const fn new( + method: &'static str, + category: crate::HttpRpcCategory, + request_kind: &'static str, + documentation_status: crate::RpcDocumentationStatus, + runtime_status: crate::RpcRuntimeStatus, + request_form_status: crate::RpcRequestFormStatus, + operation_kind: crate::RpcOperationKind, + transport_retry_class: crate::TransportRetryClass, + replacement: std::option::Option<&'static str>, + coverage_release: crate::HttpRpcCoverageRelease, + ) -> Self { + return Self { + method, + category, + request_kind, + documentation_status, + runtime_status, + request_form_status, + operation_kind, + transport_retry_class, + replacement, + coverage_release, + }; + } + + /// Returns the exact JSON-RPC method name. + #[must_use] + pub const fn method(&self) -> &'static str { + return self.method; + } + + /// Returns the audited functional category. + #[must_use] + pub const fn category(&self) -> crate::HttpRpcCategory { + return self.category; + } + + /// Returns the stable open request-kind descriptor used by future endpoint role matching. + #[must_use] + pub const fn request_kind(&self) -> &'static str { + return self.request_kind; + } + + /// Returns the method documentation lifecycle status. + #[must_use] + pub const fn documentation_status(&self) -> crate::RpcDocumentationStatus { + return self.documentation_status; + } + + /// Returns runtime availability on the targeted Agave generation. + #[must_use] + pub const fn runtime_status(&self) -> crate::RpcRuntimeStatus { + return self.runtime_status; + } + + /// Returns the request-form policy, including whether a documented deprecated legacy form exists. + #[must_use] + pub const fn request_form_status(&self) -> crate::RpcRequestFormStatus { + return self.request_form_status; + } + + /// Returns the technical operation kind. + #[must_use] + pub const fn operation_kind(&self) -> crate::RpcOperationKind { + return self.operation_kind; + } + + /// Returns the transport retry classification. + #[must_use] + pub const fn transport_retry_class(&self) -> crate::TransportRetryClass { + return self.transport_retry_class; + } + + /// Returns the documented replacement or migration direction when one exists. + #[must_use] + pub const fn replacement(&self) -> std::option::Option<&'static str> { + return self.replacement; + } + + /// Returns the release assigned to typed KSP coverage. + #[must_use] + pub const fn coverage_release(&self) -> crate::HttpRpcCoverageRelease { + return self.coverage_release; + } + + /// Returns whether calling the method itself must emit a lifecycle warning when runtime support exists. + #[must_use] + pub const fn requires_method_usage_warning(&self) -> bool { + return match self.runtime_status { + crate::RpcRuntimeStatus::Removed => false, + crate::RpcRuntimeStatus::Supported => match self.documentation_status { + crate::RpcDocumentationStatus::Stable => false, + crate::RpcDocumentationStatus::Deprecated | crate::RpcDocumentationStatus::Unstable => true, + }, + }; + } + + /// Checks runtime support and centrally emits the KSP warning required for deprecated, unstable or removed methods. + /// + /// Removed methods return [`crate::ERROR_CODE_METHOD_REMOVED`] and are never presented as callable standard RPC operations. + pub fn ensure_runtime_supported(&self) -> ksp_core_lib::Result<()> { + if self.runtime_status == crate::RpcRuntimeStatus::Removed { + let replacement = match self.replacement { + std::option::Option::Some(replacement) => replacement, + std::option::Option::None => "none", + }; + ksp_logging_lib::warn!( + target: env!("CARGO_PKG_NAME"), + rpc_method = self.method, + documentation_status = self.documentation_status.code(), + runtime_status = self.runtime_status.code(), + replacement, + "removed Solana HTTP RPC method requested" + ); + let mut error = ksp_core_lib::Error::new(crate::ERROR_CODE_METHOD_REMOVED, "Solana HTTP RPC method is removed from the targeted runtime") + .with_context("rpc_method", self.method) + .with_context("documentation_status", self.documentation_status.code()); + if let std::option::Option::Some(replacement) = self.replacement { + error = error.with_context("replacement", replacement); + } + return std::result::Result::Err(error); + } + if self.requires_method_usage_warning() { + let replacement = match self.replacement { + std::option::Option::Some(replacement) => replacement, + std::option::Option::None => "none", + }; + ksp_logging_lib::warn!( + target: env!("CARGO_PKG_NAME"), + rpc_method = self.method, + documentation_status = self.documentation_status.code(), + runtime_status = self.runtime_status.code(), + replacement, + "non-stable Solana HTTP RPC method requested" + ); + } + return std::result::Result::Ok(()); + } +} + +const CURRENT_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 52] = [ + crate::HttpRpcMethodDescriptor::new( + "getAccountInfo", + crate::HttpRpcCategory::Accounts, + "get_account_info", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getBalance", + crate::HttpRpcCategory::Accounts, + "get_balance", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_1, + ), + crate::HttpRpcMethodDescriptor::new( + "getLargestAccounts", + crate::HttpRpcCategory::Accounts, + "get_largest_accounts", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getMinimumBalanceForRentExemption", + crate::HttpRpcCategory::Accounts, + "get_minimum_balance_for_rent_exemption", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getMultipleAccounts", + crate::HttpRpcCategory::Accounts, + "get_multiple_accounts", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getProgramAccounts", + crate::HttpRpcCategory::Accounts, + "get_program_accounts", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getTokenAccountBalance", + crate::HttpRpcCategory::Tokens, + "get_token_account_balance", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getTokenAccountsByDelegate", + crate::HttpRpcCategory::Tokens, + "get_token_accounts_by_delegate", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getTokenAccountsByOwner", + crate::HttpRpcCategory::Tokens, + "get_token_accounts_by_owner", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getTokenLargestAccounts", + crate::HttpRpcCategory::Tokens, + "get_token_largest_accounts", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getTokenSupply", + crate::HttpRpcCategory::Tokens, + "get_token_supply", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getFeeForMessage", + crate::HttpRpcCategory::Transactions, + "get_fee_for_message", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "getLatestBlockhash", + crate::HttpRpcCategory::Transactions, + "get_latest_blockhash", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "getRecentPrioritizationFees", + crate::HttpRpcCategory::Transactions, + "get_recent_prioritization_fees", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "getSignaturesForAddress", + crate::HttpRpcCategory::Transactions, + "get_signatures_for_address", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "getSignatureStatuses", + crate::HttpRpcCategory::Transactions, + "get_signature_statuses", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "getTransaction", + crate::HttpRpcCategory::Transactions, + "get_transaction", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::StableWithDeprecatedLegacy, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "getTransactionCount", + crate::HttpRpcCategory::Transactions, + "get_transaction_count", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "isBlockhashValid", + crate::HttpRpcCategory::Transactions, + "is_blockhash_valid", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "requestAirdrop", + crate::HttpRpcCategory::Transactions, + "request_airdrop", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::WriteSubmission, + crate::TransportRetryClass::NeverAfterDispatch, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "sendTransaction", + crate::HttpRpcCategory::Transactions, + "send_transaction", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::WriteSubmission, + crate::TransportRetryClass::NeverAfterDispatch, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "simulateTransaction", + crate::HttpRpcCategory::Transactions, + "simulate_transaction", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Simulation, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_3, + ), + crate::HttpRpcMethodDescriptor::new( + "getBlock", + crate::HttpRpcCategory::Blocks, + "get_block", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::StableWithDeprecatedLegacy, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getBlockCommitment", + crate::HttpRpcCategory::Blocks, + "get_block_commitment", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getBlockHeight", + crate::HttpRpcCategory::Blocks, + "get_block_height", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getBlockProduction", + crate::HttpRpcCategory::Blocks, + "get_block_production", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getBlocks", + crate::HttpRpcCategory::Blocks, + "get_blocks", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getBlocksWithLimit", + crate::HttpRpcCategory::Blocks, + "get_blocks_with_limit", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getBlockTime", + crate::HttpRpcCategory::Blocks, + "get_block_time", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getFirstAvailableBlock", + crate::HttpRpcCategory::Blocks, + "get_first_available_block", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getRecentPerformanceSamples", + crate::HttpRpcCategory::Blocks, + "get_recent_performance_samples", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "minimumLedgerSlot", + crate::HttpRpcCategory::Blocks, + "minimum_ledger_slot", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getClusterNodes", + crate::HttpRpcCategory::Cluster, + "get_cluster_nodes", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getEpochInfo", + crate::HttpRpcCategory::Cluster, + "get_epoch_info", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getEpochSchedule", + crate::HttpRpcCategory::Cluster, + "get_epoch_schedule", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getGenesisHash", + crate::HttpRpcCategory::Cluster, + "get_genesis_hash", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_1, + ), + crate::HttpRpcMethodDescriptor::new( + "getHealth", + crate::HttpRpcCategory::Cluster, + "get_health", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_1, + ), + crate::HttpRpcMethodDescriptor::new( + "getHighestSnapshotSlot", + crate::HttpRpcCategory::Cluster, + "get_highest_snapshot_slot", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getIdentity", + crate::HttpRpcCategory::Cluster, + "get_identity", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getLeaderSchedule", + crate::HttpRpcCategory::Cluster, + "get_leader_schedule", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getMaxRetransmitSlot", + crate::HttpRpcCategory::Cluster, + "get_max_retransmit_slot", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getMaxShredInsertSlot", + crate::HttpRpcCategory::Cluster, + "get_max_shred_insert_slot", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getSlot", + crate::HttpRpcCategory::Cluster, + "get_slot", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getSlotLeader", + crate::HttpRpcCategory::Cluster, + "get_slot_leader", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getSlotLeaders", + crate::HttpRpcCategory::Cluster, + "get_slot_leaders", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getVersion", + crate::HttpRpcCategory::Cluster, + "get_version", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_1, + ), + crate::HttpRpcMethodDescriptor::new( + "getVoteAccounts", + crate::HttpRpcCategory::Cluster, + "get_vote_accounts", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_2, + ), + crate::HttpRpcMethodDescriptor::new( + "getInflationGovernor", + crate::HttpRpcCategory::Economics, + "get_inflation_governor", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getInflationRate", + crate::HttpRpcCategory::Economics, + "get_inflation_rate", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getInflationReward", + crate::HttpRpcCategory::Economics, + "get_inflation_reward", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getStakeMinimumDelegation", + crate::HttpRpcCategory::Economics, + "get_stake_minimum_delegation", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), + crate::HttpRpcMethodDescriptor::new( + "getSupply", + crate::HttpRpcCategory::Economics, + "get_supply", + crate::RpcDocumentationStatus::Stable, + crate::RpcRuntimeStatus::Supported, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::RetrySafe, + std::option::Option::None, + crate::HttpRpcCoverageRelease::V0_2_4, + ), +]; + +const HISTORICAL_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 14] = [ + crate::HttpRpcMethodDescriptor::new( + "confirmTransaction", + crate::HttpRpcCategory::Historical, + "confirm_transaction", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getSignatureStatuses"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getConfirmedBlock", + crate::HttpRpcCategory::Historical, + "get_confirmed_block", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getBlock"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getConfirmedBlocks", + crate::HttpRpcCategory::Historical, + "get_confirmed_blocks", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getBlocks"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getConfirmedBlocksWithLimit", + crate::HttpRpcCategory::Historical, + "get_confirmed_blocks_with_limit", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getBlocksWithLimit"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getConfirmedSignaturesForAddress2", + crate::HttpRpcCategory::Historical, + "get_confirmed_signatures_for_address2", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getSignaturesForAddress"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getConfirmedTransaction", + crate::HttpRpcCategory::Historical, + "get_confirmed_transaction", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getTransaction"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getFeeCalculatorForBlockhash", + crate::HttpRpcCategory::Historical, + "get_fee_calculator_for_blockhash", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("isBlockhashValid / getFeeForMessage"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getFeeRateGovernor", + crate::HttpRpcCategory::Historical, + "get_fee_rate_governor", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getFeeForMessage"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getFees", + crate::HttpRpcCategory::Historical, + "get_fees", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getFeeForMessage"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getRecentBlockhash", + crate::HttpRpcCategory::Historical, + "get_recent_blockhash", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getLatestBlockhash"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getSignatureConfirmation", + crate::HttpRpcCategory::Historical, + "get_signature_confirmation", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getSignatureStatuses"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getSignatureStatus", + crate::HttpRpcCategory::Historical, + "get_signature_status", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getSignatureStatuses"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getSnapshotSlot", + crate::HttpRpcCategory::Historical, + "get_snapshot_slot", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::Some("getHighestSnapshotSlot"), + crate::HttpRpcCoverageRelease::Historical, + ), + crate::HttpRpcMethodDescriptor::new( + "getStakeActivation", + crate::HttpRpcCategory::Historical, + "get_stake_activation", + crate::RpcDocumentationStatus::Deprecated, + crate::RpcRuntimeStatus::Removed, + crate::RpcRequestFormStatus::Stable, + crate::RpcOperationKind::Read, + crate::TransportRetryClass::NotApplicable, + std::option::Option::None, + crate::HttpRpcCoverageRelease::Historical, + ), +]; + +/// Returns all 52 current Solana HTTP RPC method descriptors audited for the `0.2.1`–`0.2.4` coverage sequence. +#[must_use] +pub const fn current_http_rpc_methods() -> &'static [crate::HttpRpcMethodDescriptor] { + return &CURRENT_HTTP_RPC_METHODS; +} + +/// Returns the 14 historically documented deprecated HTTP RPC descriptors retained for compliance history. +#[must_use] +pub const fn historical_http_rpc_methods() -> &'static [crate::HttpRpcMethodDescriptor] { + return &HISTORICAL_HTTP_RPC_METHODS; +} + +/// Finds a current or historical standard Solana HTTP RPC descriptor by exact JSON-RPC method name. +#[must_use] +pub fn find_http_rpc_method(method: &str) -> std::option::Option<&'static crate::HttpRpcMethodDescriptor> { + for descriptor in CURRENT_HTTP_RPC_METHODS.iter() { + if descriptor.method() == method { + return std::option::Option::Some(descriptor); + } + } + for descriptor in HISTORICAL_HTTP_RPC_METHODS.iter() { + if descriptor.method() == method { + return std::option::Option::Some(descriptor); + } + } + return std::option::Option::None; +} + +#[cfg(test)] +#[path = "../unit_tests/rpc_method.rs"] +mod tests; diff --git a/crates/ksp-onchain-transport-lib/src/settings.rs b/crates/ksp-onchain-transport-lib/src/settings.rs new file mode 100644 index 0000000..21cbfcb --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/settings.rs @@ -0,0 +1,578 @@ +// file: crates/ksp-onchain-transport-lib/src/settings.rs +// version: 1 + +/// Runtime HTTP endpoint URL owned by Transport. +/// +/// The actual URL can contain provider credentials. Its [`std::fmt::Debug`] implementation is intentionally redacted. +#[derive(Clone, Eq, PartialEq)] +pub struct HttpEndpointUrl { + value: std::string::String, +} + +impl HttpEndpointUrl { + /// Parses and validates one HTTP or HTTPS endpoint URL. + pub fn parse(value: impl std::convert::Into) -> ksp_core_lib::Result { + let value = value.into(); + let parsed_result = reqwest::Url::parse(value.as_str()); + let parsed = match parsed_result { + std::result::Result::Ok(parsed) => parsed, + std::result::Result::Err(error) => { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint URL is invalid") + .with_context("field", "endpoints.url") + .with_source(error), + ); + }, + }; + if parsed.scheme() != "http" && parsed.scheme() != "https" { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint URL must use http or https") + .with_context("field", "endpoints.url") + .with_context("scheme", parsed.scheme()), + ); + } + if parsed.host_str().is_none() { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint URL must contain a host").with_context("field", "endpoints.url"), + ); + } + return std::result::Result::Ok(Self { value }); + } + + /// Returns the sensitive runtime URL text. + /// + /// Callers must not write this value to logs, generic diagnostics or snapshots. + #[must_use] + pub fn as_str(&self) -> &str { + return self.value.as_str(); + } +} + +impl std::fmt::Debug for HttpEndpointUrl { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.write_str("HttpEndpointUrl()"); + } +} + +/// Open provider descriptor used by HTTP endpoint settings. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct HttpProviderName { + value: std::string::String, +} + +impl HttpProviderName { + /// Creates an open provider descriptor. Validation is performed by [`HttpTransportSettings::validate`]. + #[must_use] + pub fn new(value: impl std::convert::Into) -> Self { + return Self { value: value.into() }; + } + + /// Returns the provider descriptor text. + #[must_use] + pub fn as_str(&self) -> &str { + return self.value.as_str(); + } +} + +/// Open cluster or network descriptor used by HTTP endpoint settings. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct HttpClusterName { + value: std::string::String, +} + +impl HttpClusterName { + /// Creates an open cluster descriptor. Validation is performed by [`HttpTransportSettings::validate`]. + #[must_use] + pub fn new(value: impl std::convert::Into) -> Self { + return Self { value: value.into() }; + } + + /// Returns the cluster descriptor text. + #[must_use] + pub fn as_str(&self) -> &str { + return self.value.as_str(); + } +} + +/// Open logical endpoint role descriptor. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct HttpRoleName { + value: std::string::String, +} + +impl HttpRoleName { + /// Creates an open role descriptor. Validation is performed by [`HttpTransportSettings::validate`]. + #[must_use] + pub fn new(value: impl std::convert::Into) -> Self { + return Self { value: value.into() }; + } + + /// Returns the role descriptor text. + #[must_use] + pub fn as_str(&self) -> &str { + return self.value.as_str(); + } +} + +/// Open request-kind descriptor used by logical endpoint capabilities. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct HttpRequestKind { + value: std::string::String, +} + +impl HttpRequestKind { + /// Creates an open request-kind descriptor. `*` is reserved as the wildcard accepted by all standard request kinds. + #[must_use] + pub fn new(value: impl std::convert::Into) -> Self { + return Self { value: value.into() }; + } + + /// Creates the wildcard request-kind descriptor. + #[must_use] + pub fn wildcard() -> Self { + return Self::new("*"); + } + + /// Returns the request-kind descriptor text. + #[must_use] + pub fn as_str(&self) -> &str { + return self.value.as_str(); + } + + /// Returns whether this descriptor is the wildcard capability. + #[must_use] + pub fn is_wildcard(&self) -> bool { + return self.value == "*"; + } +} + +/// Local limits attached to one logical HTTP endpoint role. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct HttpRoleLimits { + requests_per_second: std::option::Option, + burst_capacity: std::option::Option, + max_concurrent_requests: std::option::Option, + pause_after_rate_limit: std::option::Option, +} + +impl HttpRoleLimits { + /// Creates explicit role limits. `None` leaves the corresponding limit unbounded by this KSP transport layer. + #[must_use] + pub const fn new( + requests_per_second: std::option::Option, + burst_capacity: std::option::Option, + max_concurrent_requests: std::option::Option, + pause_after_rate_limit: std::option::Option, + ) -> Self { + return Self { requests_per_second, burst_capacity, max_concurrent_requests, pause_after_rate_limit }; + } + + /// Returns the configured requests-per-second limit. + #[must_use] + pub const fn requests_per_second(&self) -> std::option::Option { + return self.requests_per_second; + } + + /// Returns the configured token-bucket burst capacity. + #[must_use] + pub const fn burst_capacity(&self) -> std::option::Option { + return self.burst_capacity; + } + + /// Returns the configured maximum concurrent request count. + #[must_use] + pub const fn max_concurrent_requests(&self) -> std::option::Option { + return self.max_concurrent_requests; + } + + /// Returns the configured cooldown applied after rate limiting. + #[must_use] + pub const fn pause_after_rate_limit(&self) -> std::option::Option { + return self.pause_after_rate_limit; + } +} + +/// Bounded retry settings owned by the HTTP transport runtime. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct HttpRetrySettings { + max_retries: u32, + initial_backoff: std::time::Duration, + max_backoff: std::time::Duration, +} + +impl HttpRetrySettings { + /// Creates bounded retry settings. + #[must_use] + pub const fn new(max_retries: u32, initial_backoff: std::time::Duration, max_backoff: std::time::Duration) -> Self { + return Self { max_retries, initial_backoff, max_backoff }; + } + + /// Returns the number of retries allowed after the initial attempt. + #[must_use] + pub const fn max_retries(&self) -> u32 { + return self.max_retries; + } + + /// Returns the initial retry backoff. + #[must_use] + pub const fn initial_backoff(&self) -> std::time::Duration { + return self.initial_backoff; + } + + /// Returns the maximum retry backoff. + #[must_use] + pub const fn max_backoff(&self) -> std::time::Duration { + return self.max_backoff; + } +} + +/// Runtime settings for one role declared by an HTTP endpoint. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct HttpEndpointRoleSettings { + role: crate::HttpRoleName, + enabled: bool, + request_kinds: std::vec::Vec, + priority: u32, + limits: crate::HttpRoleLimits, +} + +impl HttpEndpointRoleSettings { + /// Creates explicit settings for one logical endpoint role. + #[must_use] + pub fn new( + role: crate::HttpRoleName, + enabled: bool, + request_kinds: std::vec::Vec, + priority: u32, + limits: crate::HttpRoleLimits, + ) -> Self { + return Self { role, enabled, request_kinds, priority, limits }; + } + + /// Returns the open logical role descriptor. + #[must_use] + pub const fn role(&self) -> &crate::HttpRoleName { + return &self.role; + } + + /// Returns whether this role participates in endpoint selection. + #[must_use] + pub const fn enabled(&self) -> bool { + return self.enabled; + } + + /// Returns request kinds supported by this role. + #[must_use] + pub fn request_kinds(&self) -> &[crate::HttpRequestKind] { + return self.request_kinds.as_slice(); + } + + /// Returns the role priority where lower values are preferred. + #[must_use] + pub const fn priority(&self) -> u32 { + return self.priority; + } + + /// Returns local rate, burst and concurrency limits. + #[must_use] + pub const fn limits(&self) -> &crate::HttpRoleLimits { + return &self.limits; + } +} + +/// Runtime settings for one named Solana HTTP endpoint. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct HttpEndpointSettings { + name: std::string::String, + enabled: bool, + provider: crate::HttpProviderName, + cluster: crate::HttpClusterName, + url: crate::HttpEndpointUrl, + connect_timeout: std::time::Duration, + request_timeout: std::time::Duration, + max_idle_connections_per_host: std::option::Option, + roles: std::vec::Vec, +} + +impl HttpEndpointSettings { + /// Creates explicit runtime settings for one logical HTTP endpoint. + #[must_use] + pub fn new( + name: impl std::convert::Into, + enabled: bool, + provider: crate::HttpProviderName, + cluster: crate::HttpClusterName, + url: crate::HttpEndpointUrl, + connect_timeout: std::time::Duration, + request_timeout: std::time::Duration, + max_idle_connections_per_host: std::option::Option, + roles: std::vec::Vec, + ) -> Self { + return Self { + name: name.into(), + enabled, + provider, + cluster, + url, + connect_timeout, + request_timeout, + max_idle_connections_per_host, + roles, + }; + } + + /// Returns the endpoint identity used by selection and safe diagnostics. + #[must_use] + pub fn name(&self) -> &str { + return self.name.as_str(); + } + + /// Returns whether this endpoint participates in endpoint selection. + #[must_use] + pub const fn enabled(&self) -> bool { + return self.enabled; + } + + /// Returns the provider descriptor. + #[must_use] + pub const fn provider(&self) -> &crate::HttpProviderName { + return &self.provider; + } + + /// Returns the cluster descriptor. + #[must_use] + pub const fn cluster(&self) -> &crate::HttpClusterName { + return &self.cluster; + } + + /// Returns the sensitive endpoint URL wrapper. + #[must_use] + pub const fn url(&self) -> &crate::HttpEndpointUrl { + return &self.url; + } + + /// Returns the connection-establishment timeout. + #[must_use] + pub const fn connect_timeout(&self) -> std::time::Duration { + return self.connect_timeout; + } + + /// Returns the end-to-end request timeout used by this endpoint. + #[must_use] + pub const fn request_timeout(&self) -> std::time::Duration { + return self.request_timeout; + } + + /// Returns the optional per-host idle connection pool limit. + #[must_use] + pub const fn max_idle_connections_per_host(&self) -> std::option::Option { + return self.max_idle_connections_per_host; + } + + /// Returns endpoint roles in declaration order. + #[must_use] + pub fn roles(&self) -> &[crate::HttpEndpointRoleSettings] { + return self.roles.as_slice(); + } +} + +/// Complete runtime settings consumed by the Solana HTTP transport foundation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct HttpTransportSettings { + endpoints: std::vec::Vec, + retry: crate::HttpRetrySettings, +} + +impl HttpTransportSettings { + /// Creates complete HTTP transport runtime settings. + #[must_use] + pub fn new(endpoints: std::vec::Vec, retry: crate::HttpRetrySettings) -> Self { + return Self { endpoints, retry }; + } + + /// Returns configured endpoints in declaration order. + #[must_use] + pub fn endpoints(&self) -> &[crate::HttpEndpointSettings] { + return self.endpoints.as_slice(); + } + + /// Returns the bounded transport retry settings. + #[must_use] + pub const fn retry(&self) -> &crate::HttpRetrySettings { + return &self.retry; + } + + /// Validates structural runtime invariants without reading Config or environment state. + pub fn validate(&self) -> ksp_core_lib::Result<()> { + let retry_validation = validate_retry(self.retry()); + if let std::result::Result::Err(error) = retry_validation { + return std::result::Result::Err(error); + } + if self.endpoints.is_empty() { + return invalid_settings("at least one HTTP endpoint must be configured", "endpoints"); + } + let mut enabled_endpoint_count = 0_usize; + for (endpoint_index, endpoint) in self.endpoints.iter().enumerate() { + let endpoint_validation = validate_endpoint(endpoint, endpoint_index); + if let std::result::Result::Err(error) = endpoint_validation { + return std::result::Result::Err(error); + } + if endpoint.enabled() { + enabled_endpoint_count += 1; + } + for previous in &self.endpoints[..endpoint_index] { + if previous.name() == endpoint.name() { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint names must be unique") + .with_context("field", format!("endpoints[{endpoint_index}].name")) + .with_context("endpoint_name", endpoint.name()), + ); + } + } + } + if enabled_endpoint_count == 0 { + return invalid_settings("at least one HTTP endpoint must be enabled", "endpoints.enabled"); + } + ksp_logging_lib::debug!( + target: env!("CARGO_PKG_NAME"), + endpoint_count = self.endpoints.len(), + enabled_endpoint_count, + "validated HTTP transport settings" + ); + return std::result::Result::Ok(()); + } +} + +fn validate_retry(retry: &crate::HttpRetrySettings) -> ksp_core_lib::Result<()> { + if retry.initial_backoff().is_zero() { + return invalid_settings("initial retry backoff must be greater than zero", "retry.initial_backoff"); + } + if retry.max_backoff().is_zero() { + return invalid_settings("maximum retry backoff must be greater than zero", "retry.max_backoff"); + } + if retry.max_backoff() < retry.initial_backoff() { + return invalid_settings("maximum retry backoff must not be lower than initial retry backoff", "retry.max_backoff"); + } + return std::result::Result::Ok(()); +} + +fn validate_endpoint(endpoint: &crate::HttpEndpointSettings, endpoint_index: usize) -> ksp_core_lib::Result<()> { + let endpoint_name_validation = validate_descriptor(endpoint.name(), format!("endpoints[{endpoint_index}].name").as_str()); + if let std::result::Result::Err(error) = endpoint_name_validation { + return std::result::Result::Err(error); + } + let provider_validation = validate_descriptor(endpoint.provider().as_str(), format!("endpoints[{endpoint_index}].provider").as_str()); + if let std::result::Result::Err(error) = provider_validation { + return std::result::Result::Err(error); + } + let cluster_validation = validate_descriptor(endpoint.cluster().as_str(), format!("endpoints[{endpoint_index}].cluster").as_str()); + if let std::result::Result::Err(error) = cluster_validation { + return std::result::Result::Err(error); + } + if endpoint.connect_timeout().is_zero() { + return invalid_settings("HTTP connect timeout must be greater than zero", format!("endpoints[{endpoint_index}].connect_timeout").as_str()); + } + if endpoint.request_timeout().is_zero() { + return invalid_settings("HTTP request timeout must be greater than zero", format!("endpoints[{endpoint_index}].request_timeout").as_str()); + } + if let std::option::Option::Some(max_idle) = endpoint.max_idle_connections_per_host() { + if max_idle == 0 { + return invalid_settings( + "max idle connections per host must be greater than zero when configured", + format!("endpoints[{endpoint_index}].max_idle_connections_per_host").as_str(), + ); + } + } + if endpoint.roles().is_empty() { + return invalid_settings("HTTP endpoint must declare at least one role", format!("endpoints[{endpoint_index}].roles").as_str()); + } + let mut enabled_role_count = 0_usize; + for (role_index, role) in endpoint.roles().iter().enumerate() { + let role_validation = validate_role(role, endpoint_index, role_index); + if let std::result::Result::Err(error) = role_validation { + return std::result::Result::Err(error); + } + if role.enabled() { + enabled_role_count += 1; + } + for previous in &endpoint.roles()[..role_index] { + if previous.role() == role.role() { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint role names must be unique per endpoint") + .with_context("field", format!("endpoints[{endpoint_index}].roles[{role_index}].role")) + .with_context("endpoint_name", endpoint.name()) + .with_context("role", role.role().as_str()), + ); + } + } + } + if endpoint.enabled() && enabled_role_count == 0 { + return invalid_settings("enabled HTTP endpoint must expose at least one enabled role", format!("endpoints[{endpoint_index}].roles.enabled").as_str()); + } + return std::result::Result::Ok(()); +} + +fn validate_role(role: &crate::HttpEndpointRoleSettings, endpoint_index: usize, role_index: usize) -> ksp_core_lib::Result<()> { + let role_validation = validate_descriptor(role.role().as_str(), format!("endpoints[{endpoint_index}].roles[{role_index}].role").as_str()); + if let std::result::Result::Err(error) = role_validation { + return std::result::Result::Err(error); + } + if role.request_kinds().is_empty() { + return invalid_settings( + "HTTP endpoint role must declare at least one request kind", + format!("endpoints[{endpoint_index}].roles[{role_index}].request_kinds").as_str(), + ); + } + if role.request_kinds().len() > 1 && role.request_kinds().iter().any(crate::HttpRequestKind::is_wildcard) { + return invalid_settings("wildcard request kind must be used alone", format!("endpoints[{endpoint_index}].roles[{role_index}].request_kinds").as_str()); + } + for (request_kind_index, request_kind) in role.request_kinds().iter().enumerate() { + let request_kind_validation = + validate_descriptor(request_kind.as_str(), format!("endpoints[{endpoint_index}].roles[{role_index}].request_kinds[{request_kind_index}]").as_str()); + if let std::result::Result::Err(error) = request_kind_validation { + return std::result::Result::Err(error); + } + for previous in &role.request_kinds()[..request_kind_index] { + if previous == request_kind { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP request kinds must be unique per role") + .with_context("field", format!("endpoints[{endpoint_index}].roles[{role_index}].request_kinds[{request_kind_index}]")) + .with_context("request_kind", request_kind.as_str()), + ); + } + } + } + if role.limits().burst_capacity().is_some() && role.limits().requests_per_second().is_none() { + return invalid_settings( + "burst capacity requires a requests-per-second limit", + format!("endpoints[{endpoint_index}].roles[{role_index}].limits.burst_capacity").as_str(), + ); + } + if let std::option::Option::Some(pause) = role.limits().pause_after_rate_limit() { + if pause.is_zero() { + return invalid_settings( + "rate-limit cooldown must be greater than zero when configured", + format!("endpoints[{endpoint_index}].roles[{role_index}].limits.pause_after_rate_limit").as_str(), + ); + } + } + return std::result::Result::Ok(()); +} + +fn validate_descriptor(value: &str, field: &str) -> ksp_core_lib::Result<()> { + if value.trim().is_empty() { + return invalid_settings("transport descriptor must not be empty", field); + } + if value.trim() != value { + return invalid_settings("transport descriptor must not contain leading or trailing whitespace", field); + } + return std::result::Result::Ok(()); +} + +fn invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()> { + return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("field", field)); +} + +#[cfg(test)] +#[path = "../unit_tests/settings.rs"] +mod tests; diff --git a/crates/ksp-onchain-transport-lib/tests/dependency_boundary.rs b/crates/ksp-onchain-transport-lib/tests/dependency_boundary.rs new file mode 100644 index 0000000..900b413 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/tests/dependency_boundary.rs @@ -0,0 +1,14 @@ +// file: crates/ksp-onchain-transport-lib/tests/dependency_boundary.rs +// version: 1 + +#[test] +fn transport_manifest_preserves_ksp_dependency_firewall() { + let manifest_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"); + let manifest = std::fs::read_to_string(manifest_path).expect("transport manifest must be readable during integration tests"); + for forbidden in ["ksp-config-lib", "ksp-store-api", "ksp-store-lib", "ksp-program-api", "ksp-program-lib", "tracing =", "tracing."] { + assert!(!manifest.contains(forbidden), "forbidden direct transport dependency detected: {forbidden}"); + } + assert!(manifest.contains("ksp-core-lib")); + assert!(manifest.contains("ksp-logging-lib")); + assert!(manifest.contains("reqwest.workspace = true")); +} diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs new file mode 100644 index 0000000..af6dd24 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/tests/public_api.rs @@ -0,0 +1,61 @@ +// file: crates/ksp-onchain-transport-lib/tests/public_api.rs +// version: 1 + +#[test] +fn public_settings_contract_is_constructible_without_config_dependency() { + let url = ksp_onchain_transport_lib::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("public URL parser must accept Devnet endpoint"); + let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new( + ksp_onchain_transport_lib::HttpRoleName::new("default"), + true, + std::vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()], + 100, + ksp_onchain_transport_lib::HttpRoleLimits::new( + std::option::Option::None, + std::option::Option::None, + std::option::Option::None, + std::option::Option::None, + ), + ); + let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new( + "solana_devnet_public", + true, + ksp_onchain_transport_lib::HttpProviderName::new("solana-public"), + ksp_onchain_transport_lib::HttpClusterName::new("devnet"), + url, + std::time::Duration::from_secs(5), + std::time::Duration::from_secs(15), + std::option::Option::Some(8), + std::vec![role], + ); + let settings = ksp_onchain_transport_lib::HttpTransportSettings::new( + std::vec![endpoint], + ksp_onchain_transport_lib::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)), + ); + assert!(settings.validate().is_ok()); +} + +#[test] +fn public_json_rpc_contract_round_trips_foundation_shape() { + let request = ksp_onchain_transport_lib::JsonRpcRequest::new(1, "getHealth", std::vec![]).expect("public request constructor must succeed"); + let encoded = request.to_json_string().expect("public request must serialize"); + assert!(encoded.contains("\"jsonrpc\":\"2.0\"")); + let response = + ksp_onchain_transport_lib::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","id":1}"#, 1).expect("public parser must validate response"); + let result = response.into_result().expect("success response must return result"); + assert_eq!(result, serde_json::json!("ok")); +} + +#[test] +fn public_method_registry_exposes_current_and_historical_surfaces() { + assert_eq!(ksp_onchain_transport_lib::current_http_rpc_methods().len(), 52); + assert_eq!(ksp_onchain_transport_lib::historical_http_rpc_methods().len(), 14); + let removed = ksp_onchain_transport_lib::find_http_rpc_method("confirmTransaction").expect("historical method must be discoverable"); + assert_eq!(removed.runtime_status(), ksp_onchain_transport_lib::RpcRuntimeStatus::Removed); + assert_eq!(removed.ensure_runtime_supported().expect_err("removed method must fail").code(), ksp_onchain_transport_lib::ERROR_CODE_METHOD_REMOVED); +} + +#[test] +fn public_error_codes_share_the_core_error_domain() { + assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS.domain(), "onchain_transport"); + assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_RPC_APPLICATION_ERROR.domain(), "onchain_transport"); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs b/crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs new file mode 100644 index 0000000..887da22 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs @@ -0,0 +1,117 @@ +// file: crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs +// version: 1 + +#[test] +fn request_serialization_matches_json_rpc_2_0_shape() { + let request = super::JsonRpcRequest::new(7, "getBalance", std::vec![serde_json::json!("Address111"), serde_json::json!({"commitment":"confirmed"})]) + .expect("valid request must construct"); + let encoded = request.to_json_string().expect("serializable request must encode"); + let value: serde_json::Value = serde_json::from_str(encoded.as_str()).expect("encoded request must remain JSON"); + assert_eq!(value["jsonrpc"], serde_json::json!("2.0")); + assert_eq!(value["id"], serde_json::json!(7)); + assert_eq!(value["method"], serde_json::json!("getBalance")); + assert_eq!(value["params"].as_array().expect("params must be an array").len(), 2); +} + +#[test] +fn request_rejects_empty_or_untrimmed_method() { + assert!(super::JsonRpcRequest::new(1, "", std::vec![]).is_err()); + assert!(super::JsonRpcRequest::new(1, " getHealth", std::vec![]).is_err()); +} + +#[test] +fn response_parser_preserves_null_success_result() { + let response = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":null,"id":9}"#, 9).expect("null result is a valid success payload"); + match response { + super::JsonRpcResponse::Success(success) => assert!(success.result().is_null()), + super::JsonRpcResponse::Error(_) => assert!(false, "success response must not parse as error"), + } +} + +#[test] +fn response_parser_preserves_rpc_error_payload() { + let response = super::parse_json_rpc_response_text( + r#"{"jsonrpc":"2.0","error":{"code":-32005,"message":"Node is unhealthy","data":{"numSlotsBehind":12}},"id":4}"#, + 4, + ) + .expect("valid JSON-RPC error envelope must parse"); + match response { + super::JsonRpcResponse::Error(error_response) => { + assert_eq!(error_response.error().code(), -32005); + assert_eq!(error_response.error().message(), "Node is unhealthy"); + assert_eq!(error_response.error().data(), std::option::Option::Some(&serde_json::json!({"numSlotsBehind":12}))); + }, + super::JsonRpcResponse::Success(_) => assert!(false, "RPC error response must not parse as success"), + } +} + +#[test] +fn response_parser_rejects_id_mismatch() { + let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","id":2}"#, 1).expect_err("mismatched id must fail"); + assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); +} + +#[test] +fn response_parser_rejects_wrong_protocol_version() { + let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"1.0","result":"ok","id":1}"#, 1).expect_err("wrong version must fail"); + assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); +} + +#[test] +fn response_parser_rejects_both_result_and_error() { + let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","error":{"code":-1,"message":"bad"},"id":1}"#, 1) + .expect_err("mutually exclusive fields must fail"); + assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); +} + +#[test] +fn response_parser_rejects_missing_result_and_error() { + let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","id":1}"#, 1).expect_err("missing outcome must fail"); + assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); +} + +#[test] +fn response_parser_distinguishes_invalid_json_from_protocol_error() { + let error = super::parse_json_rpc_response_text("not-json", 1).expect_err("invalid JSON must fail decoding"); + assert_eq!(error.code(), crate::ERROR_CODE_JSON_DECODE_FAILED); +} + +#[test] +fn rpc_error_maps_to_shared_ksp_error_without_copying_remote_payload_into_context() { + let response = super::parse_json_rpc_response_text( + r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"SECRET-CANARY","data":{"payload":"SECRET-DATA"}},"id":1}"#, + 1, + ) + .expect("valid error envelope must parse"); + let error = response.into_result().expect_err("RPC application error must map to KSP error"); + assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR); + let rendered = format!("{error:?}"); + assert!(!rendered.contains("SECRET-CANARY")); + assert!(!rendered.contains("SECRET-DATA")); +} + +#[test] +fn request_debug_omits_parameter_payloads() { + let request = super::JsonRpcRequest::new(1, "sendTransaction", std::vec![serde_json::json!("SIGNED-TRANSACTION-SECRET-CANARY")]) + .expect("test request must construct"); + let rendered = format!("{request:?}"); + assert!(rendered.contains("sendTransaction")); + assert!(rendered.contains("param_count")); + assert!(!rendered.contains("SIGNED-TRANSACTION-SECRET-CANARY")); +} + +#[test] +fn response_debug_omits_result_and_remote_error_payloads() { + let success = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":{"secret":"RESULT-SECRET-CANARY"},"id":1}"#, 1) + .expect("test success response must parse"); + let success_rendered = format!("{success:?}"); + assert!(!success_rendered.contains("RESULT-SECRET-CANARY")); + let failure = super::parse_json_rpc_response_text( + r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"MESSAGE-SECRET-CANARY","data":{"secret":"DATA-SECRET-CANARY"}},"id":2}"#, + 2, + ) + .expect("test error response must parse"); + let failure_rendered = format!("{failure:?}"); + assert!(!failure_rendered.contains("MESSAGE-SECRET-CANARY")); + assert!(!failure_rendered.contains("DATA-SECRET-CANARY")); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs new file mode 100644 index 0000000..541a5fe --- /dev/null +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs @@ -0,0 +1,121 @@ +// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs +// version: 1 + +#[test] +fn audited_registry_has_expected_current_and_historical_counts() { + assert_eq!(super::current_http_rpc_methods().len(), 52); + assert_eq!(super::historical_http_rpc_methods().len(), 14); +} + +#[test] +fn audited_registry_method_names_are_unique() { + let mut names = std::collections::BTreeSet::<&str>::new(); + for descriptor in super::current_http_rpc_methods() { + assert!(names.insert(descriptor.method()), "duplicate current method {}", descriptor.method()); + } + for descriptor in super::historical_http_rpc_methods() { + assert!(names.insert(descriptor.method()), "duplicate historical method {}", descriptor.method()); + } + assert_eq!(names.len(), 66); +} + +#[test] +fn coverage_release_counts_match_recalibrated_matrix() { + let mut foundation = 0_usize; + let mut accounts_tokens_cluster = 0_usize; + let mut transactions = 0_usize; + let mut blocks_economics = 0_usize; + for descriptor in super::current_http_rpc_methods() { + match descriptor.coverage_release() { + super::HttpRpcCoverageRelease::V0_2_1 => foundation += 1, + super::HttpRpcCoverageRelease::V0_2_2 => accounts_tokens_cluster += 1, + super::HttpRpcCoverageRelease::V0_2_3 => transactions += 1, + super::HttpRpcCoverageRelease::V0_2_4 => blocks_economics += 1, + super::HttpRpcCoverageRelease::Historical => assert!(false, "current method must not be historical"), + } + } + assert_eq!(foundation, 4); + assert_eq!(accounts_tokens_cluster, 22); + assert_eq!(transactions, 11); + assert_eq!(blocks_economics, 15); +} + +#[test] +fn foundation_canary_assignment_is_exact() { + let mut names = std::vec::Vec::<&str>::new(); + for descriptor in super::current_http_rpc_methods() { + if descriptor.coverage_release() == super::HttpRpcCoverageRelease::V0_2_1 { + names.push(descriptor.method()); + } + } + names.sort_unstable(); + assert_eq!(names, std::vec!["getBalance", "getGenesisHash", "getHealth", "getVersion"]); +} + +#[test] +fn historical_methods_are_deprecated_removed_and_not_retryable() { + for descriptor in super::historical_http_rpc_methods() { + assert_eq!(descriptor.documentation_status(), super::RpcDocumentationStatus::Deprecated); + assert_eq!(descriptor.runtime_status(), super::RpcRuntimeStatus::Removed); + assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NotApplicable); + assert_eq!(descriptor.coverage_release(), super::HttpRpcCoverageRelease::Historical); + } +} + +#[test] +fn get_transaction_and_get_block_track_deprecated_legacy_request_form() { + let get_transaction = super::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist"); + let get_block = super::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist"); + assert!(get_transaction.request_form_status().has_deprecated_legacy()); + assert!(get_block.request_form_status().has_deprecated_legacy()); + let get_balance = super::find_http_rpc_method("getBalance").expect("getBalance descriptor must exist"); + assert!(!get_balance.request_form_status().has_deprecated_legacy()); +} + +#[test] +fn write_submission_methods_are_never_retry_after_ambiguous_dispatch() { + for method in ["sendTransaction", "requestAirdrop"] { + let descriptor = super::find_http_rpc_method(method).expect("write descriptor must exist"); + assert_eq!(descriptor.operation_kind(), super::RpcOperationKind::WriteSubmission); + assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NeverAfterDispatch); + } + let simulation = super::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must exist"); + assert_eq!(simulation.operation_kind(), super::RpcOperationKind::Simulation); + assert_eq!(simulation.transport_retry_class(), super::TransportRetryClass::RetrySafe); +} + +#[test] +fn removed_method_support_check_returns_method_removed_error() { + let descriptor = super::find_http_rpc_method("confirmTransaction").expect("historical descriptor must exist"); + let error = descriptor.ensure_runtime_supported().expect_err("removed method must not be callable"); + assert_eq!(error.code(), crate::ERROR_CODE_METHOD_REMOVED); +} + +#[test] +fn stable_supported_method_passes_runtime_support_check() { + let descriptor = super::find_http_rpc_method("getHealth").expect("current descriptor must exist"); + assert!(descriptor.ensure_runtime_supported().is_ok()); +} + +#[test] +fn supported_unstable_descriptor_executes_central_warning_path() { + let descriptor = super::HttpRpcMethodDescriptor::new( + "experimentalMethod", + super::HttpRpcCategory::Cluster, + "experimental_method", + super::RpcDocumentationStatus::Unstable, + super::RpcRuntimeStatus::Supported, + super::RpcRequestFormStatus::Stable, + super::RpcOperationKind::Read, + super::TransportRetryClass::RetrySafe, + std::option::Option::None, + super::HttpRpcCoverageRelease::V0_2_1, + ); + assert!(descriptor.requires_method_usage_warning()); + assert!(descriptor.ensure_runtime_supported().is_ok()); +} + +#[test] +fn lookup_rejects_unknown_method_without_affecting_raw_provider_extensions() { + assert!(super::find_http_rpc_method("providerCustomMethod").is_none()); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/settings.rs b/crates/ksp-onchain-transport-lib/unit_tests/settings.rs new file mode 100644 index 0000000..0d497b7 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/unit_tests/settings.rs @@ -0,0 +1,207 @@ +// file: crates/ksp-onchain-transport-lib/unit_tests/settings.rs +// version: 1 + +fn non_zero(value: u32) -> std::num::NonZeroU32 { + return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero"); +} + +fn valid_settings(url_text: &str) -> super::HttpTransportSettings { + let url = super::HttpEndpointUrl::parse(url_text).expect("test URL must be valid"); + let limits = super::HttpRoleLimits::new( + std::option::Option::Some(non_zero(10)), + std::option::Option::Some(non_zero(20)), + std::option::Option::Some(non_zero(4)), + std::option::Option::Some(std::time::Duration::from_millis(500)), + ); + let role = super::HttpEndpointRoleSettings::new(super::HttpRoleName::new("default"), true, std::vec![super::HttpRequestKind::wildcard()], 100, limits); + let endpoint = super::HttpEndpointSettings::new( + "devnet_public", + true, + super::HttpProviderName::new("solana-public"), + super::HttpClusterName::new("devnet"), + url, + std::time::Duration::from_secs(5), + std::time::Duration::from_secs(15), + std::option::Option::Some(8), + std::vec![role], + ); + return super::HttpTransportSettings::new( + std::vec![endpoint], + super::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)), + ); +} + +#[test] +fn endpoint_url_accepts_http_and_https() { + assert!(super::HttpEndpointUrl::parse("https://api.devnet.solana.com").is_ok()); + assert!(super::HttpEndpointUrl::parse("http://127.0.0.1:8899").is_ok()); +} + +#[test] +fn endpoint_url_rejects_non_http_schemes() { + let result = super::HttpEndpointUrl::parse("ws://api.devnet.solana.com"); + let error = result.expect_err("WebSocket URL must not be accepted by HTTP settings"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS); +} + +#[test] +fn endpoint_url_debug_redacts_secret_material() { + let url = super::HttpEndpointUrl::parse("https://provider.invalid/rpc?api-key=SECRET-CANARY").expect("test URL must parse"); + let rendered = format!("{url:?}"); + assert!(rendered.contains("")); + assert!(!rendered.contains("SECRET-CANARY")); + assert!(!rendered.contains("provider.invalid")); +} + +#[test] +fn valid_transport_settings_pass_validation() { + let settings = valid_settings("https://api.devnet.solana.com"); + assert!(settings.validate().is_ok()); +} + +#[test] +fn transport_settings_debug_does_not_leak_endpoint_url() { + let settings = valid_settings("https://provider.invalid/rpc?api-key=SECRET-CANARY"); + let rendered = format!("{settings:?}"); + assert!(!rendered.contains("SECRET-CANARY")); + assert!(!rendered.contains("provider.invalid")); + assert!(rendered.contains("HttpEndpointUrl()")); +} + +#[test] +fn transport_settings_require_one_enabled_endpoint() { + let url = super::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("test URL must parse"); + let role = super::HttpEndpointRoleSettings::new( + super::HttpRoleName::new("default"), + true, + std::vec![super::HttpRequestKind::wildcard()], + 100, + super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), + ); + let endpoint = super::HttpEndpointSettings::new( + "disabled", + false, + super::HttpProviderName::new("provider"), + super::HttpClusterName::new("devnet"), + url, + std::time::Duration::from_secs(1), + std::time::Duration::from_secs(1), + std::option::Option::None, + std::vec![role], + ); + let settings = super::HttpTransportSettings::new( + std::vec![endpoint], + super::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)), + ); + let error = settings.validate().expect_err("all-disabled settings must fail"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS); +} + +#[test] +fn transport_settings_reject_duplicate_endpoint_names() { + let first = valid_settings("https://one.invalid"); + let second = valid_settings("https://two.invalid"); + let settings = super::HttpTransportSettings::new(std::vec![first.endpoints()[0].clone(), second.endpoints()[0].clone()], first.retry().clone()); + let error = settings.validate().expect_err("duplicate endpoint names must fail"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS); +} + +#[test] +fn transport_settings_reject_duplicate_roles() { + let base = valid_settings("https://api.devnet.solana.com"); + let endpoint = &base.endpoints()[0]; + let duplicated_endpoint = super::HttpEndpointSettings::new( + endpoint.name(), + true, + endpoint.provider().clone(), + endpoint.cluster().clone(), + endpoint.url().clone(), + endpoint.connect_timeout(), + endpoint.request_timeout(), + endpoint.max_idle_connections_per_host(), + std::vec![endpoint.roles()[0].clone(), endpoint.roles()[0].clone()], + ); + let settings = super::HttpTransportSettings::new(std::vec![duplicated_endpoint], base.retry().clone()); + assert!(settings.validate().is_err()); +} + +#[test] +fn transport_settings_reject_wildcard_mixed_with_specific_kind() { + let base = valid_settings("https://api.devnet.solana.com"); + let endpoint = &base.endpoints()[0]; + let role = super::HttpEndpointRoleSettings::new( + super::HttpRoleName::new("default"), + true, + std::vec![super::HttpRequestKind::wildcard(), super::HttpRequestKind::new("get_balance")], + 100, + endpoint.roles()[0].limits().clone(), + ); + let modified_endpoint = super::HttpEndpointSettings::new( + endpoint.name(), + true, + endpoint.provider().clone(), + endpoint.cluster().clone(), + endpoint.url().clone(), + endpoint.connect_timeout(), + endpoint.request_timeout(), + endpoint.max_idle_connections_per_host(), + std::vec![role], + ); + let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone()); + assert!(settings.validate().is_err()); +} + +#[test] +fn transport_settings_reject_burst_without_rps() { + let base = valid_settings("https://api.devnet.solana.com"); + let endpoint = &base.endpoints()[0]; + let role = super::HttpEndpointRoleSettings::new( + super::HttpRoleName::new("default"), + true, + std::vec![super::HttpRequestKind::wildcard()], + 100, + super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::Some(non_zero(2)), std::option::Option::None, std::option::Option::None), + ); + let modified_endpoint = super::HttpEndpointSettings::new( + endpoint.name(), + true, + endpoint.provider().clone(), + endpoint.cluster().clone(), + endpoint.url().clone(), + endpoint.connect_timeout(), + endpoint.request_timeout(), + endpoint.max_idle_connections_per_host(), + std::vec![role], + ); + let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone()); + assert!(settings.validate().is_err()); +} + +#[test] +fn transport_settings_reject_reversed_retry_backoff() { + let base = valid_settings("https://api.devnet.solana.com"); + let settings = super::HttpTransportSettings::new( + base.endpoints().to_vec(), + super::HttpRetrySettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)), + ); + assert!(settings.validate().is_err()); +} + +#[test] +fn transport_settings_reject_zero_request_timeout() { + let base = valid_settings("https://api.devnet.solana.com"); + let endpoint = &base.endpoints()[0]; + let modified_endpoint = super::HttpEndpointSettings::new( + endpoint.name(), + true, + endpoint.provider().clone(), + endpoint.cluster().clone(), + endpoint.url().clone(), + endpoint.connect_timeout(), + std::time::Duration::ZERO, + endpoint.max_idle_connections_per_host(), + endpoint.roles().to_vec(), + ); + let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone()); + assert!(settings.validate().is_err()); +} diff --git a/deltas/0.2.1/pre.002.md b/deltas/0.2.1/pre.002.md new file mode 100644 index 0000000..cdcf28e --- /dev/null +++ b/deltas/0.2.1/pre.002.md @@ -0,0 +1,333 @@ + + + +# Delta `0.2.1-pre.002` — fondation crate/settings/JSON-RPC/descriptors + +## Base requise + +```text +release : 0.2.1 +prerelease : pre.002 +identifiant de commit attendu : v0.2.1-pre.002 +workspace.package.version : 0.2.1-pre.2 +base : v0.2.1-pre.001-fix.001 +``` + +Le user a confirmé avoir commité `v0.2.1-pre.001` puis `v0.2.1-pre.001-fix.001` avant l'ouverture de cette tranche. + +## Objectif + +Matérialiser la fondation Rust de `ksp-onchain-transport-lib` sans ouvrir encore le client/pool HTTP : + +- ajouter la crate au workspace ; +- stabiliser les codes d'erreur Transport ; +- posséder les settings runtime publics et leur validation ; +- posséder les envelopes JSON-RPC 2.0 HTTP ; +- matérialiser le registre central des méthodes/statuts issu de la matrice `pre.001` ; +- installer la base de warnings KSP de statut sans dépendance directe à `tracing` ; +- ajouter les tests unitaires/intégration correspondant à ces contrats. + +## Version Cargo + +Conformément à `VER-ID-009` : + +```text +0.2.1-pre.1 -> 0.2.1-pre.2 +``` + +Toutes les crates membres continuent d'hériter `version.workspace = true`. + +## Workspace et dépendances + +Nouveau membre : + +```text +crates/ksp-onchain-transport-lib +``` + +Dépendances directes de la crate : + +```text +ksp-core-lib +ksp-logging-lib +reqwest.workspace = true +serde.workspace = true +serde_json.workspace = true +``` + +`reqwest` reste centralisé au workspace : + +```toml +reqwest = { version = "^0.13", default-features = false } +``` + +Le `pre.001` avait vérifié le 2026-08-17 la génération `reqwest 0.13` et observé `0.13.4`. Cette tranche n'active volontairement aucune feature TLS/JSON/client : `reqwest` est utilisé uniquement pour `Url::parse` dans la validation de `HttpEndpointUrl`. Conformément à `RUST-DEP-001` / `RUST-DEP-003`, les features réseau et Tokio seront ajoutés seulement lorsque `pre.003` introduira un vrai client HTTP async. + +Aucune dépendance directe vers : + +```text +ksp-config-lib +ksp-store-api +ksp-store-lib +ksp-program-api +ksp-program-lib +tracing +``` + +## Contrats publics Transport + +### Settings runtime + +La crate expose : + +```text +HttpTransportSettings +HttpEndpointSettings +HttpEndpointRoleSettings +HttpRoleLimits +HttpRetrySettings +HttpEndpointUrl +HttpProviderName +HttpClusterName +HttpRoleName +HttpRequestKind +``` + +Décisions : + +- Transport reste totalement constructible/testable sans Config ; +- `provider`, `cluster`, `role` et `request_kind` sont des descriptors ouverts, pas des enums fermées ; +- `HttpEndpointUrl` accepte uniquement HTTP/HTTPS, conserve la valeur runtime réelle mais redacted systématiquement son `Debug` ; +- `HttpTransportSettings::validate()` vérifie notamment endpoints activés, identités/roles uniques, timeouts positifs, request kinds, wildcard, limites et backoff cohérents ; +- `NonZeroU32` empêche structurellement les zéros sur RPS/burst/concurrence lorsqu'ils sont configurés ; +- aucun accès direct à `.env` ou `std::env::var*` n'existe dans Transport. + +### Codes d'erreur + +Le domaine unique reste : + +```text +onchain_transport +``` + +Codes stabilisés/réservés : + +```text +invalid_settings +endpoint_selection_failed +http_connection_failed +http_request_failed +timeout +rate_limited +json_encode_failed +json_decode_failed +json_rpc_protocol_invalid +rpc_application_error +method_removed +invalid_response +``` + +Ils utilisent exclusivement `ksp_core_lib::Error` / `ErrorCode`. + +## JSON-RPC HTTP + +La fondation expose : + +```text +JsonRpcRequest +JsonRpcSuccessResponse +JsonRpcErrorObject +JsonRpcErrorResponse +JsonRpcResponse +parse_json_rpc_response_text +parse_json_rpc_response_value +``` + +Invariants : + +- request ids KSP numériques `u64` ; +- `jsonrpc` exactement `"2.0"` ; +- id de réponse exactement égal à celui de la requête ; +- présence exclusive de `result` ou `error` ; +- JSON `null` conservé comme résultat valide ; +- erreur JSON syntaxique distincte d'une violation protocolaire ; +- application error RPC préservée dans `JsonRpcResponse::Error`, puis mappable vers `rpc_application_error` ; +- le mapping KSP ne copie pas le message/data distant dans le contexte générique ; +- `Debug` des requests masque les paramètres ; `Debug` des succès masque le résultat ; `Debug` des erreurs masque message/data distants. + +Ce dernier point empêche un log/debug accidentel d'une transaction, d'un token provider ou d'une réponse massive tout en laissant les getters explicites accessibles au consumer légitime. + +## Registre central des méthodes + +`rpc_method.rs` matérialise exactement : + +```text +52 méthodes current +14 méthodes historiques Deprecated / Removed +``` + +Chaque descriptor possède : + +```text +method +category +request_kind +documentation_status +runtime_status +request_form_status +operation_kind +transport_retry_class +replacement +coverage_release +``` + +La distribution de couverture est encodée et testée : + +```text +0.2.1 : 4 +0.2.2 : 22 +0.2.3 : 11 +0.2.4 : 15 + ---- + 52 current +``` + +Cas structurants déjà encodés : + +```text +getTransaction -> StableWithDeprecatedLegacy +getBlock -> StableWithDeprecatedLegacy +sendTransaction -> WriteSubmission / NeverAfterDispatch +requestAirdrop -> WriteSubmission / NeverAfterDispatch +simulateTransaction -> Simulation / RetrySafe +14 historiques -> Deprecated / Removed / NotApplicable +``` + +`HttpRpcMethodDescriptor::ensure_runtime_supported()` centralise le comportement de statut : + +- Stable + Supported : succès silencieux ; +- Deprecated/Unstable + Supported : `warn` via `ksp-logging-lib` ; +- Removed : `warn` puis erreur `method_removed`, sans simuler un appel supporté. + +Le target utilisé est toujours le nom Cargo réel via `env!("CARGO_PKG_NAME")`; aucune dépendance directe `tracing` n'est introduite. + +## Tests ajoutés + +40 tests Rust sont définis : + +- validation des settings et erreurs structurées ; +- HTTP/HTTPS uniquement ; +- redaction URL et `Debug` global settings ; +- endpoint enabled, doublons endpoint/rôle, wildcard, burst/RPS, timeouts, backoff ; +- sérialisation JSON-RPC request ; +- parse success/error/null ; +- id mismatch, version, `result xor error`, invalid JSON ; +- redaction des payloads request/result/error dans `Debug` ; +- mapping RPC application error ; +- registre 52 + 14 et unicité ; +- distribution 4/22/11/15 ; +- canaris exacts `getBalance/getGenesisHash/getHealth/getVersion` ; +- request forms deprecated de `getTransaction/getBlock` ; +- no-resend descriptors write ; +- warning path Unstable supporté ; +- erreur `method_removed` ; +- tests d'intégration de la façade publique ; +- canary de firewall des dépendances directes du manifest. + +Les vrais tests unitaires sont physiquement sous `unit_tests/` et rattachés aux modules de production via `#[cfg(test)]` + `#[path = ...]`. `tests/` est réservé aux tests d'intégration publics. + +## Fichiers ajoutés + +```text +crates/ksp-onchain-transport-lib/Cargo.toml +crates/ksp-onchain-transport-lib/src/error.rs +crates/ksp-onchain-transport-lib/src/json_rpc.rs +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/src/rpc_method.rs +crates/ksp-onchain-transport-lib/src/settings.rs +crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs +crates/ksp-onchain-transport-lib/unit_tests/settings.rs +crates/ksp-onchain-transport-lib/tests/dependency_boundary.rs +crates/ksp-onchain-transport-lib/tests/public_api.rs +deltas/0.2.1/pre.002.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +ROADMAP.md +docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md +``` + +## Fichiers supprimés + +Aucun. + +## Validations exécutées dans l'environnement d'échange + +Le conteneur ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Les validations exécutables disponibles ont donc été limitées aux contrôles statiques suivants : + +- parsing TOML du `Cargo.toml` racine et du manifest de la nouvelle crate ; +- contrôle `workspace.package.version = 0.2.1-pre.2` ; +- contrôle présence du nouveau membre workspace ; +- contrôle des headers `file:` et newline finale sur tous les nouveaux fichiers ; +- scan production : aucun `use`, `?`, `unwrap`, `expect`, `panic!`, `pub mod` ; +- scan manifest : aucune dépendance directe Config/Store/Program/`tracing` ; +- scan production : aucune lecture directe `std::env::var*` ; +- audit heuristique de rustdoc sur les éléments publics ; +- comparaison automatique du registre Rust avec la matrice `008` : 52 current identiques, dans le même ordre, et 14 historiques identiques ; +- contrôle de distribution de release : `4 / 22 / 11 / 15` ; +- contrôle des deux seuls marqueurs `StableWithDeprecatedLegacy` : `getTransaction`, `getBlock` ; +- contrôle des fences Markdown et headers des documents modifiés ; +- contrôle des lignes Rust > 160 colonnes après formatage manuel : aucune. + +## Validations non exécutées + +Obligatoires sur le checkout de développement avant commit : + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +cargo test --workspace + +cargo tree -p ksp-onchain-transport-lib +cargo tree -p ksp-onchain-transport-lib -d +cargo tree -p ksp-onchain-transport-lib -e features +cargo tree -p ksp-onchain-transport-lib -e normal +``` + +Les `cargo tree` sont désormais obligatoires parce que `reqwest` entre réellement dans le graphe à cette tranche. Les doublons significatifs doivent être examinés avant validation du commit. + +Aucun build Tauri n'est requis : aucune application Tauri/frontend n'est modifiée. + +## Décisions prises + +- conserver un identifiant JSON-RPC KSP numérique `u64` pour les requêtes émises ; +- préserver les payloads JSON dynamiques via `serde_json::Value` sans introduire de DTO Solana SDK massif ; +- redacter les `Debug` wire susceptibles de contenir des secrets/payloads massifs ; +- encoder dès maintenant la couverture release et la retry class dans le registre central ; +- ne pas activer de features réseau `reqwest` ni Tokio avant leur premier usage réel ; +- ne pas introduire Config, Store, Program, Solana SDK haut niveau, base64, bs58, WebSocket ou gRPC. + +## Questions ouvertes / reports explicites + +Aucune question bloquante pour `pre.003`. + +À décider avec l'implémentation réelle de `pre.003` : + +- features `reqwest 0.13` exactes nécessaires au client async/TLS ; +- ownership précis du `reqwest::Client` par endpoint logique ; +- structures runtime du pool, fairness et snapshots ; +- première utilisation effective de Tokio et des primitives `sync` si nécessaires. + +## Suite + +```text +0.2.1-pre.003 +``` + +Mission prévue : endpoint client + pool logique + rôles/capabilities + priorité/fairness/fallback + snapshots sûrs, sans encore ouvrir rate-limit/concurrence/retry effectif de `pre.004` au-delà des structures strictement nécessaires. diff --git a/docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md b/docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md index cd24e51..b9442e8 100644 --- a/docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md +++ b/docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md @@ -1,5 +1,5 @@ - + # `0.2.1-pre.001` — plan `ksp-onchain-transport-lib` HTTP Solana foundation @@ -696,18 +696,17 @@ Audit du 2026-08-17 : - `serde` / `serde_json` sont déjà des dépendances workspace `^1.0` ; - bot3 utilisait également `base64` et `bs58`, à différer jusqu'au premier besoin d'encoding explicite. -Décision de principe pour `pre.002` : +Décision concrétisée par `pre.002` : ```text -reqwest = ^0.13, features minimales async/json/TLS à revérifier dans le manifest officiel avant ajout +reqwest = ^0.13, default-features = false au workspace serde.workspace = true serde_json.workspace = true -tokio.workspace = true (+ feature sync si Semaphore retenu) ksp-core-lib path/workspace ksp-logging-lib path/workspace ``` -`reqwest` `0.13` utilise un backend TLS rustls par défaut dans la génération actuelle ; la sélection exacte `default-features`/features doit être lue depuis le manifest/docs de la version réellement ajoutée, puis justifiée dans le delta. +`pre.002` utilise `reqwest` uniquement pour le parsing/validation robuste de `HttpEndpointUrl`; aucune requête réseau n'est encore créée. Conformément à `RUST-DEP-001` / `RUST-DEP-003`, aucune feature TLS/JSON/client ni dépendance Tokio locale n'est activée par anticipation. Les features réellement nécessaires au client async sont décidées et ajoutées dans `pre.003`, lorsque le chemin de compilation HTTP existe. Le même principe reporte `tokio` à la première tranche qui utilise effectivement ses primitives runtime/sync. Ne pas ajouter : @@ -859,7 +858,7 @@ La release peut devenir stable seulement si : | Tranche | Objectif | |-----------|----------------------------------------------------------------------------------------------------------------------| | `pre.001` | audit KSP + bot3 + docs officielles, matrice 52+14, architecture, split et sizing | -| `pre.002` | crate/workspace, codes erreur, settings/validation, JSON-RPC, descriptors/status, base Logging | +| `pre.002` | **réalisé** : crate/workspace, codes erreur, settings/validation, JSON-RPC, descriptors/status, base Logging | | `pre.003` | endpoint client + pool logique + rôles/capabilities + priorité/fairness/fallback + snapshots sûrs | | `pre.004` | RPS/burst/concurrence/cooldown + timeout + retry/backoff + classification retry/no-resend | | `pre.005` | méthodes canari `getHealth`, `getVersion`, `getGenesisHash`, `getBalance` + fixtures déterministes | @@ -868,6 +867,23 @@ La release peut devenir stable seulement si : Ce découpage est révisable si une tranche dépasse le budget ; la release réduite, contrairement au scope initial, reste raisonnablement clôturable dans la session. +### 22.1 État après `0.2.1-pre.002` + +`pre.002` matérialise la fondation sans ouvrir le client/pool de `pre.003` : + +- nouvelle crate `ksp-onchain-transport-lib`, membre du workspace, `workspace.package.version = 0.2.1-pre.2` ; +- dépendances directes : `ksp-core-lib`, `ksp-logging-lib`, `serde`, `serde_json`, `reqwest` sans features par défaut ; aucune dépendance Config/Store/Program/`tracing` directe ; +- settings runtime publics : URL redacted, provider/cluster/role/request-kind ouverts, endpoints/rôles/limites/retry et validation structurelle ; +- douze codes d'erreur `onchain_transport` réservés/stabilisés selon la stratégie du §14 ; +- JSON-RPC HTTP KSP : request numérique KSP, sérialisation, parse success/error, validation `jsonrpc = 2.0`, id exact et exclusivité `result`/`error`, avec `null` résultat préservé ; +- `Debug` des requests/réponses n'expose pas les params, résultats ou payloads d'erreur distants par défaut ; +- registre central exact de 52 méthodes current + 14 historiques, avec catégorie, request kind, statut documentaire/runtime, forme legacy, operation kind, retry class, remplacement et release de couverture ; +- `getTransaction` et `getBlock` portent explicitement `StableWithDeprecatedLegacy`; `sendTransaction` et `requestAirdrop` portent `WriteSubmission / NeverAfterDispatch`; +- contrôle central `ensure_runtime_supported()` : warning KSP pour surfaces non stables supportées, warning + `method_removed` pour les historiques supprimées ; +- 40 tests Rust ajoutés (unitaires + intégration), dont redaction, invariants JSON-RPC, matrice 4/22/11/15 et canary de firewall du manifest. + +Les éléments suivants restent volontairement à `pre.003+` : construction `reqwest::Client`, clients logiques, pool, sélection/rôles/fallback, Tokio runtime/sync, TLS, rate limiting, concurrence et retry effectif. + ## 23. Séquence `0.2.x` recalibrée ```text