v0.2.1-pre.002

This commit is contained in:
2026-08-17 18:02:18 +02:00
parent d98d152f08
commit 0cff0406ab
15 changed files with 2989 additions and 10 deletions

View File

@@ -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");

View File

@@ -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<serde_json::Value>,
}
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<std::string::String>, params: std::vec::Vec<serde_json::Value>) -> ksp_core_lib::Result<Self> {
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<std::string::String> {
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<serde_json::Value>,
}
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", &"<redacted>")
.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", &"<omitted>").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<serde_json::Value> {
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<crate::JsonRpcResponse> {
let decode_result = serde_json::from_str::<serde_json::Value>(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<crate::JsonRpcResponse> {
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::<crate::JsonRpcErrorObject>(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<crate::JsonRpcResponse> {
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;

View File

@@ -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;

File diff suppressed because it is too large Load Diff

View File

@@ -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<std::string::String>) -> ksp_core_lib::Result<Self> {
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(<redacted>)");
}
}
/// 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<std::string::String>) -> 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<std::string::String>) -> 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<std::string::String>) -> 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<std::string::String>) -> 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<std::num::NonZeroU32>,
burst_capacity: std::option::Option<std::num::NonZeroU32>,
max_concurrent_requests: std::option::Option<std::num::NonZeroU32>,
pause_after_rate_limit: std::option::Option<std::time::Duration>,
}
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<std::num::NonZeroU32>,
burst_capacity: std::option::Option<std::num::NonZeroU32>,
max_concurrent_requests: std::option::Option<std::num::NonZeroU32>,
pause_after_rate_limit: std::option::Option<std::time::Duration>,
) -> 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<std::num::NonZeroU32> {
return self.requests_per_second;
}
/// Returns the configured token-bucket burst capacity.
#[must_use]
pub const fn burst_capacity(&self) -> std::option::Option<std::num::NonZeroU32> {
return self.burst_capacity;
}
/// Returns the configured maximum concurrent request count.
#[must_use]
pub const fn max_concurrent_requests(&self) -> std::option::Option<std::num::NonZeroU32> {
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<std::time::Duration> {
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<crate::HttpRequestKind>,
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<crate::HttpRequestKind>,
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<usize>,
roles: std::vec::Vec<crate::HttpEndpointRoleSettings>,
}
impl HttpEndpointSettings {
/// Creates explicit runtime settings for one logical HTTP endpoint.
#[must_use]
pub fn new(
name: impl std::convert::Into<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<usize>,
roles: std::vec::Vec<crate::HttpEndpointRoleSettings>,
) -> 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<usize> {
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<crate::HttpEndpointSettings>,
retry: crate::HttpRetrySettings,
}
impl HttpTransportSettings {
/// Creates complete HTTP transport runtime settings.
#[must_use]
pub fn new(endpoints: std::vec::Vec<crate::HttpEndpointSettings>, 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;