v0.2.5-pre.009

This commit is contained in:
2026-08-20 09:24:10 +02:00
parent 1125ade4c1
commit e91bf36e9e
83 changed files with 2467 additions and 1801 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/client.rs
// version: 5
// version: 6
/// Passive runtime availability reported for one logical HTTP endpoint or role.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -163,26 +163,6 @@ impl HttpEndpointSnapshot {
}
}
pub(crate) struct HttpEndpointHttpResponse {
status: u16,
retry_after: std::option::Option<std::time::Duration>,
body: std::vec::Vec<u8>,
}
impl HttpEndpointHttpResponse {
pub(crate) const fn status(&self) -> u16 {
return self.status;
}
pub(crate) const fn retry_after(&self) -> std::option::Option<std::time::Duration> {
return self.retry_after;
}
pub(crate) fn body(&self) -> &[u8] {
return self.body.as_slice();
}
}
/// Shareable logical HTTP endpoint client owned by KSP Transport.
///
/// The underlying `reqwest::Client` owns socket pooling. KSP keeps the configured URL private from diagnostics and exposes only safe routing metadata.
@@ -191,12 +171,6 @@ pub struct HttpEndpointClient {
inner: std::sync::Arc<HttpEndpointClientInner>,
}
struct HttpEndpointClientInner {
settings: crate::HttpEndpointSettings,
client: reqwest::Client,
role_runtimes: std::vec::Vec<std::sync::Arc<crate::resilience::HttpRoleRuntime>>,
}
impl HttpEndpointClient {
/// Builds one logical endpoint client from KSP-owned runtime settings.
pub fn new(settings: crate::HttpEndpointSettings) -> ksp_core_lib::Result<Self> {
@@ -221,7 +195,7 @@ impl HttpEndpointClient {
};
let mut role_runtimes = std::vec::Vec::with_capacity(settings.roles().len());
for role in settings.roles() {
role_runtimes.push(std::sync::Arc::new(crate::resilience::HttpRoleRuntime::new(role, std::sync::Arc::clone(&notify))));
role_runtimes.push(std::sync::Arc::new(crate::HttpRoleRuntime::new(role, std::sync::Arc::clone(&notify))));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
@@ -352,7 +326,7 @@ impl HttpEndpointClient {
&self,
role: &crate::HttpRoleName,
request_kind: &crate::HttpRequestKind,
) -> std::option::Option<(u32, std::sync::Arc<crate::resilience::HttpRoleRuntime>)> {
) -> std::option::Option<(u32, std::sync::Arc<crate::HttpRoleRuntime>)> {
if !self.enabled() {
return std::option::Option::None;
}
@@ -419,10 +393,36 @@ impl std::fmt::Debug for HttpEndpointClient {
}
}
pub(crate) struct HttpEndpointHttpResponse {
status: u16,
retry_after: std::option::Option<std::time::Duration>,
body: std::vec::Vec<u8>,
}
impl HttpEndpointHttpResponse {
pub(crate) const fn status(&self) -> u16 {
return self.status;
}
pub(crate) const fn retry_after(&self) -> std::option::Option<std::time::Duration> {
return self.retry_after;
}
pub(crate) fn body(&self) -> &[u8] {
return self.body.as_slice();
}
}
struct HttpEndpointClientInner {
settings: crate::HttpEndpointSettings,
client: reqwest::Client,
role_runtimes: std::vec::Vec<std::sync::Arc<crate::HttpRoleRuntime>>,
}
fn role_snapshot(
role: &crate::HttpEndpointRoleSettings,
request_kinds: std::vec::Vec<std::string::String>,
runtime: &crate::resilience::HttpRoleRuntime,
runtime: &crate::HttpRoleRuntime,
) -> crate::HttpEndpointRoleSnapshot {
let availability = if role.enabled() { runtime.availability(std::time::Instant::now()) } else { crate::HttpEndpointAvailability::Disabled };
return crate::HttpEndpointRoleSnapshot {

View File

@@ -1,29 +1,29 @@
// file: crates/ksp-onchain-transport-lib/src/error.rs
// version: 2
// version: 3
/// 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");
/// Error code used when typed Solana RPC parameters violate a locally enforceable method contract.
pub const ERROR_CODE_INVALID_RPC_PARAMETERS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_rpc_parameters");
/// 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 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 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 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 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 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 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 transport deadline expires.
pub const ERROR_CODE_TIMEOUT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "timeout");

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 19
// version: 20
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -34,8 +35,6 @@ mod rpc_tokens;
mod rpc_transactions;
mod settings;
pub(crate) use self::constants::TRACING_TARGET;
/// Passive runtime availability reported for one logical HTTP endpoint.
pub use self::client::HttpEndpointAvailability;
/// Shareable logical HTTP endpoint client owned by KSP Transport.
@@ -296,3 +295,8 @@ pub use self::settings::HttpRoleLimits;
pub use self::settings::HttpRoleName;
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
pub use self::settings::HttpTransportSettings;
pub(crate) use self::constants::TRACING_TARGET;
pub(crate) use self::resilience::HttpConcurrencyPermit;
pub(crate) use self::resilience::HttpRoleRuntime;
pub(crate) use self::resilience::RoleAdmissionAttempt;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/pool.rs
// version: 5
// version: 6
/// Safe snapshot of the logical HTTP endpoint pool.
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -74,8 +74,8 @@ impl HttpEndpointSelection {
pub struct HttpRequestPermit {
selection: crate::HttpEndpointSelection,
deadline: std::time::Instant,
role_runtime: std::sync::Arc<crate::resilience::HttpRoleRuntime>,
_concurrency_permit: crate::resilience::HttpConcurrencyPermit,
role_runtime: std::sync::Arc<crate::HttpRoleRuntime>,
_concurrency_permit: crate::HttpConcurrencyPermit,
}
impl HttpRequestPermit {
@@ -165,14 +165,6 @@ pub struct HttpTransportPool {
inner: std::sync::Arc<HttpTransportPoolInner>,
}
struct HttpTransportPoolInner {
clients: std::vec::Vec<crate::HttpEndpointClient>,
retry: crate::HttpRetrySettings,
notify: std::sync::Arc<tokio::sync::Notify>,
request_ids: std::sync::atomic::AtomicU64,
cursors: std::sync::Mutex<std::collections::BTreeMap<(std::string::String, std::string::String, u32), usize>>,
}
impl HttpTransportPool {
/// Builds a logical endpoint pool after validating all Transport-owned runtime settings.
pub fn new(settings: crate::HttpTransportSettings) -> ksp_core_lib::Result<Self> {
@@ -369,7 +361,7 @@ impl HttpTransportPool {
let candidate = &candidates[position];
let admission = candidate.runtime.try_acquire(now);
match admission {
crate::resilience::RoleAdmissionAttempt::Ready(concurrency_permit) => {
crate::RoleAdmissionAttempt::Ready(concurrency_permit) => {
let selection_result = self.selection_from_runtime_candidate(role, request_kind, candidate);
let selection = match selection_result {
std::result::Result::Ok(value) => value,
@@ -391,13 +383,13 @@ impl HttpTransportPool {
_concurrency_permit: concurrency_permit,
});
},
crate::resilience::RoleAdmissionAttempt::BlockedUntil(ready_at) => {
crate::RoleAdmissionAttempt::BlockedUntil(ready_at) => {
earliest_ready = earlier_instant(earliest_ready, ready_at);
},
crate::resilience::RoleAdmissionAttempt::ConcurrencySaturated => {
crate::RoleAdmissionAttempt::ConcurrencySaturated => {
concurrency_saturated = true;
},
crate::resilience::RoleAdmissionAttempt::Unavailable => {},
crate::RoleAdmissionAttempt::Unavailable => {},
}
offset = offset.saturating_add(1);
}
@@ -548,6 +540,14 @@ impl std::fmt::Debug for HttpTransportPool {
}
}
struct HttpTransportPoolInner {
clients: std::vec::Vec<crate::HttpEndpointClient>,
retry: crate::HttpRetrySettings,
notify: std::sync::Arc<tokio::sync::Notify>,
request_ids: std::sync::atomic::AtomicU64,
cursors: std::sync::Mutex<std::collections::BTreeMap<(std::string::String, std::string::String, u32), usize>>,
}
#[derive(Clone, Copy)]
struct PoolCandidate {
client_index: usize,
@@ -557,7 +557,7 @@ struct PoolCandidate {
struct RuntimePoolCandidate {
client_index: usize,
priority: u32,
runtime: std::sync::Arc<crate::resilience::HttpRoleRuntime>,
runtime: std::sync::Arc<crate::HttpRoleRuntime>,
}
enum RuntimeSelectionAttempt {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/resilience.rs
// version: 1
// version: 2
const DEFAULT_RATE_LIMIT_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(1);
const MAX_PROVIDER_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(60);
@@ -71,61 +71,6 @@ impl HttpRetryDecision {
}
}
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
///
/// `completed_retries` counts retries already performed after the initial attempt. Provider `Retry-After` values are defensively bounded to sixty seconds
/// before they can extend the local exponential backoff. RPC application errors are never converted into transport retries.
#[must_use]
pub fn evaluate_transport_retry(
method: &crate::HttpRpcMethodDescriptor,
settings: &crate::HttpRetrySettings,
cause: crate::HttpRetryCause,
dispatch_state: crate::HttpDispatchState,
completed_retries: u32,
provider_retry_after: std::option::Option<std::time::Duration>,
) -> crate::HttpRetryDecision {
if completed_retries >= settings.max_retries() || !cause.is_retryable() {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NotApplicable {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NeverAfterDispatch && dispatch_state == crate::HttpDispatchState::DispatchedAmbiguous {
return crate::HttpRetryDecision::Stop;
}
let retry_number = completed_retries.saturating_add(1);
let mut delay = retry_backoff(settings, retry_number);
if cause == crate::HttpRetryCause::RateLimited
&& let std::option::Option::Some(provider_delay) = provider_retry_after
{
let bounded_provider_delay = std::cmp::min(provider_delay, MAX_PROVIDER_RETRY_AFTER);
if bounded_provider_delay > delay {
delay = bounded_provider_delay;
}
}
return crate::HttpRetryDecision::RetryAfter(delay);
}
pub(crate) fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std::time::Duration {
let mut delay = settings.initial_backoff();
if retry_number <= 1 {
return std::cmp::min(delay, settings.max_backoff());
}
let mut step = 1_u32;
while step < retry_number {
let doubled = match delay.checked_mul(2) {
std::option::Option::Some(value) => value,
std::option::Option::None => settings.max_backoff(),
};
delay = std::cmp::min(doubled, settings.max_backoff());
if delay >= settings.max_backoff() {
return settings.max_backoff();
}
step = step.saturating_add(1);
}
return delay;
}
pub(crate) struct HttpRoleRuntime {
limits: crate::HttpRoleLimits,
bucket: std::sync::Mutex<std::option::Option<HttpTokenBucketState>>,
@@ -216,21 +161,21 @@ impl HttpRoleRuntime {
return self.rate_limit_count.load(std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn try_acquire(self: &std::sync::Arc<Self>, now: std::time::Instant) -> RoleAdmissionAttempt {
pub(crate) fn try_acquire(self: &std::sync::Arc<Self>, now: std::time::Instant) -> crate::RoleAdmissionAttempt {
if let std::option::Option::Some(remaining) = self.cooldown_remaining_at(now) {
let ready_at = match now.checked_add(remaining) {
std::option::Option::Some(value) => value,
std::option::Option::None => now,
};
return RoleAdmissionAttempt::BlockedUntil(ready_at);
return crate::RoleAdmissionAttempt::BlockedUntil(ready_at);
}
let semaphore_permit = match &self.semaphore {
std::option::Option::Some(semaphore) => {
let permit_result = std::sync::Arc::clone(semaphore).try_acquire_owned();
match permit_result {
std::result::Result::Ok(permit) => std::option::Option::Some(permit),
std::result::Result::Err(tokio::sync::TryAcquireError::NoPermits) => return RoleAdmissionAttempt::ConcurrencySaturated,
std::result::Result::Err(tokio::sync::TryAcquireError::Closed) => return RoleAdmissionAttempt::Unavailable,
std::result::Result::Err(tokio::sync::TryAcquireError::NoPermits) => return crate::RoleAdmissionAttempt::ConcurrencySaturated,
std::result::Result::Err(tokio::sync::TryAcquireError::Closed) => return crate::RoleAdmissionAttempt::Unavailable,
}
},
std::option::Option::None => std::option::Option::None,
@@ -239,9 +184,9 @@ impl HttpRoleRuntime {
if let std::option::Option::Some(ready_at) = token_result {
drop(semaphore_permit);
self.notify.notify_one();
return RoleAdmissionAttempt::BlockedUntil(ready_at);
return crate::RoleAdmissionAttempt::BlockedUntil(ready_at);
}
return RoleAdmissionAttempt::Ready(HttpConcurrencyPermit { semaphore_permit, notify: std::sync::Arc::clone(&self.notify) });
return crate::RoleAdmissionAttempt::Ready(HttpConcurrencyPermit { semaphore_permit, notify: std::sync::Arc::clone(&self.notify) });
}
pub(crate) fn record_success(&self) {
@@ -385,6 +330,61 @@ impl HttpTokenBucketState {
}
}
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
///
/// `completed_retries` counts retries already performed after the initial attempt. Provider `Retry-After` values are defensively bounded to sixty seconds
/// before they can extend the local exponential backoff. RPC application errors are never converted into transport retries.
#[must_use]
pub fn evaluate_transport_retry(
method: &crate::HttpRpcMethodDescriptor,
settings: &crate::HttpRetrySettings,
cause: crate::HttpRetryCause,
dispatch_state: crate::HttpDispatchState,
completed_retries: u32,
provider_retry_after: std::option::Option<std::time::Duration>,
) -> crate::HttpRetryDecision {
if completed_retries >= settings.max_retries() || !cause.is_retryable() {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NotApplicable {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NeverAfterDispatch && dispatch_state == crate::HttpDispatchState::DispatchedAmbiguous {
return crate::HttpRetryDecision::Stop;
}
let retry_number = completed_retries.saturating_add(1);
let mut delay = retry_backoff(settings, retry_number);
if cause == crate::HttpRetryCause::RateLimited
&& let std::option::Option::Some(provider_delay) = provider_retry_after
{
let bounded_provider_delay = std::cmp::min(provider_delay, MAX_PROVIDER_RETRY_AFTER);
if bounded_provider_delay > delay {
delay = bounded_provider_delay;
}
}
return crate::HttpRetryDecision::RetryAfter(delay);
}
fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std::time::Duration {
let mut delay = settings.initial_backoff();
if retry_number <= 1 {
return std::cmp::min(delay, settings.max_backoff());
}
let mut step = 1_u32;
while step < retry_number {
let doubled = match delay.checked_mul(2) {
std::option::Option::Some(value) => value,
std::option::Option::None => settings.max_backoff(),
};
delay = std::cmp::min(doubled, settings.max_backoff());
if delay >= settings.max_backoff() {
return settings.max_backoff();
}
step = step.saturating_add(1);
}
return delay;
}
#[cfg(test)]
#[path = "../unit_tests/resilience.rs"]
mod tests;

View File

@@ -1,5 +1,9 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
// version: 4
// version: 5
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
const MAX_MEMCMP_BYTES: usize = 128;
/// Account-data encoding accepted by Solana HTTP account methods.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -592,10 +596,6 @@ pub enum SolanaProgramAccountsResult {
Context(crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>),
}
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
const MAX_MEMCMP_BYTES: usize = 128;
impl crate::HttpTransportPool {
/// Executes typed `getAccountInfo` through the common KSP HTTP transport path.
pub async fn get_account_info(
@@ -752,6 +752,58 @@ impl crate::HttpTransportPool {
}
}
#[derive(serde::Deserialize)]
struct WireRpcResponse<T> {
context: serde_json::Value,
value: T,
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum WireProgramAccountsResult {
Context(WireRpcResponse<std::vec::Vec<serde_json::Value>>),
Accounts(std::vec::Vec<serde_json::Value>),
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum WireAccountData {
LegacyBinary(std::string::String),
JsonParsed(WireParsedAccountData),
Encoded((std::string::String, std::string::String)),
}
#[derive(serde::Deserialize)]
struct WireParsedAccountData {
program: std::string::String,
parsed: serde_json::Value,
space: u64,
}
#[derive(serde::Deserialize)]
struct WireAccount {
lamports: u64,
data: serde_json::Value,
owner: std::string::String,
executable: bool,
#[serde(rename = "rentEpoch")]
rent_epoch: u64,
#[serde(default)]
space: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
struct WireKeyedAccount {
pubkey: std::string::String,
account: serde_json::Value,
}
#[derive(serde::Deserialize)]
struct WireAccountBalance {
address: std::string::String,
lamports: u64,
}
fn account_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
let descriptor = crate::find_http_rpc_method(method);
return match descriptor {
@@ -934,58 +986,6 @@ fn decode_keyed_accounts(method: &str, values: std::vec::Vec<serde_json::Value>)
return std::result::Result::Ok(decoded_values);
}
#[derive(serde::Deserialize)]
struct WireRpcResponse<T> {
context: serde_json::Value,
value: T,
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum WireProgramAccountsResult {
Context(WireRpcResponse<std::vec::Vec<serde_json::Value>>),
Accounts(std::vec::Vec<serde_json::Value>),
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum WireAccountData {
LegacyBinary(std::string::String),
JsonParsed(WireParsedAccountData),
Encoded((std::string::String, std::string::String)),
}
#[derive(serde::Deserialize)]
struct WireParsedAccountData {
program: std::string::String,
parsed: serde_json::Value,
space: u64,
}
#[derive(serde::Deserialize)]
struct WireAccount {
lamports: u64,
data: serde_json::Value,
owner: std::string::String,
executable: bool,
#[serde(rename = "rentEpoch")]
rent_epoch: u64,
#[serde(default)]
space: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
struct WireKeyedAccount {
pubkey: std::string::String,
account: serde_json::Value,
}
#[derive(serde::Deserialize)]
struct WireAccountBalance {
address: std::string::String,
lamports: u64,
}
#[cfg(test)]
#[path = "../unit_tests/rpc_accounts.rs"]
mod tests;

View File

@@ -1,5 +1,8 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
// version: 6
// version: 7
const MAX_GET_BLOCKS_RANGE: u64 = 500_000;
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720;
/// Transaction detail level accepted by modern `getBlock` requests.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
@@ -828,8 +831,85 @@ impl crate::HttpTransportPool {
}
}
const MAX_GET_BLOCKS_RANGE: u64 = 500_000;
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720;
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockCommitment {
commitment: std::option::Option<std::vec::Vec<u64>>,
total_stake: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProductionRange {
first_slot: u64,
last_slot: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProduction {
by_identity: std::collections::BTreeMap<std::string::String, (usize, usize)>,
range: WireBlockProductionRange,
}
#[derive(serde::Deserialize)]
struct WireBlockProductionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockReward {
pubkey: std::string::String,
lamports: i64,
post_balance: u64,
#[serde(default)]
reward_type: std::option::Option<std::string::String>,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockTransaction {
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedBlock {
previous_blockhash: std::string::String,
blockhash: std::string::String,
parent_slot: u64,
#[serde(default)]
transactions: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
signatures: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
rewards: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
num_reward_partitions: crate::SolanaWireField<u64>,
block_time: std::option::Option<i64>,
block_height: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePerformanceSample {
slot: u64,
num_transactions: u64,
#[serde(default)]
num_non_vote_transactions: std::option::Option<u64>,
num_slots: u64,
sample_period_secs: u16,
}
fn validate_blocks_context_commitment(method: &'static str, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result<()> {
if let std::option::Option::Some(config) = config
@@ -978,86 +1058,6 @@ fn decode_block_rewards(
return std::result::Result::Ok(crate::SolanaWireField::Value(rewards));
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockCommitment {
commitment: std::option::Option<std::vec::Vec<u64>>,
total_stake: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProductionRange {
first_slot: u64,
last_slot: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProduction {
by_identity: std::collections::BTreeMap<std::string::String, (usize, usize)>,
range: WireBlockProductionRange,
}
#[derive(serde::Deserialize)]
struct WireBlockProductionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockReward {
pubkey: std::string::String,
lamports: i64,
post_balance: u64,
#[serde(default)]
reward_type: std::option::Option<std::string::String>,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockTransaction {
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedBlock {
previous_blockhash: std::string::String,
blockhash: std::string::String,
parent_slot: u64,
#[serde(default)]
transactions: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
signatures: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
rewards: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
num_reward_partitions: crate::SolanaWireField<u64>,
block_time: std::option::Option<i64>,
block_height: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePerformanceSample {
slot: u64,
num_transactions: u64,
#[serde(default)]
num_non_vote_transactions: std::option::Option<u64>,
num_slots: u64,
sample_period_secs: u16,
}
#[cfg(test)]
#[path = "../unit_tests/rpc_blocks.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
// version: 4
// version: 5
const MAX_GET_SLOT_LEADERS: u64 = 5_000;
@@ -807,55 +807,6 @@ impl crate::HttpTransportPool {
}
}
fn push_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
if let std::option::Option::Some(config) = config
&& (config.commitment().is_some() || config.min_context_slot().is_some())
{
params.push((*config).to_json_value());
}
return;
}
fn decode_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(method, value);
let values = match decoded {
std::result::Result::Ok(values) => values,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut pubkeys = std::vec::Vec::with_capacity(values.len());
for value in values {
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
match pubkey {
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(pubkeys);
}
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
.with_context("rpc_method", method)
.with_context(field, value.to_string()),
);
}
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
let descriptor = crate::find_http_rpc_method(method);
return match descriptor {
std::option::Option::Some(descriptor)
if descriptor.category() == crate::HttpRpcCategory::Cluster && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
{
std::result::Result::Ok(descriptor)
},
_ => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Cluster descriptor is missing from the audited 0.2.2 registry")
.with_context("rpc_method", method),
),
};
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireClusterNode {
@@ -938,6 +889,55 @@ struct WireVoteAccountStatus {
delinquent: std::vec::Vec<serde_json::Value>,
}
fn push_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
if let std::option::Option::Some(config) = config
&& (config.commitment().is_some() || config.min_context_slot().is_some())
{
params.push((*config).to_json_value());
}
return;
}
fn decode_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(method, value);
let values = match decoded {
std::result::Result::Ok(values) => values,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut pubkeys = std::vec::Vec::with_capacity(values.len());
for value in values {
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
match pubkey {
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(pubkeys);
}
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
.with_context("rpc_method", method)
.with_context(field, value.to_string()),
);
}
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
let descriptor = crate::find_http_rpc_method(method);
return match descriptor {
std::option::Option::Some(descriptor)
if descriptor.category() == crate::HttpRpcCategory::Cluster && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
{
std::result::Result::Ok(descriptor)
},
_ => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Cluster descriptor is missing from the audited 0.2.2 registry")
.with_context("rpc_method", method),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/rpc_cluster.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
// version: 4
// version: 5
/// Commitment level accepted by typed Solana HTTP RPC adapters.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -155,6 +155,13 @@ impl<T> SolanaRpcResponse<T> {
}
}
#[derive(serde::Deserialize)]
struct WireRpcContext {
slot: u64,
#[serde(rename = "apiVersion", default)]
api_version: std::option::Option<std::string::String>,
}
/// Decodes one private serde wire type and maps shape failures to the shared Transport error domain.
pub(crate) fn decode_wire_json<T: serde::de::DeserializeOwned>(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<T> {
let decoded = serde_json::from_value::<T>(value);
@@ -181,13 +188,6 @@ pub(crate) fn parse_wire_pubkey(method: &str, field: &str, value: &str) -> ksp_c
};
}
#[derive(serde::Deserialize)]
struct WireRpcContext {
slot: u64,
#[serde(rename = "apiVersion", default)]
api_version: std::option::Option<std::string::String>,
}
#[cfg(test)]
#[path = "../unit_tests/rpc_common.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_economics.rs
// version: 5
// version: 6
/// Inflation-governor values returned by `getInflationGovernor`.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -441,6 +441,53 @@ impl crate::HttpTransportPool {
}
}
#[derive(serde::Deserialize)]
struct WireEconomicsRpcResponse<T> {
context: serde_json::Value,
value: T,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationGovernor {
initial: f64,
terminal: f64,
taper: f64,
foundation: f64,
foundation_term: f64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationRate {
total: f64,
validator: f64,
foundation: f64,
epoch: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationReward {
epoch: u64,
effective_slot: u64,
amount: u64,
post_balance: u64,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSupply {
total: u64,
circulating: u64,
non_circulating: u64,
non_circulating_accounts: std::vec::Vec<std::string::String>,
}
fn push_economics_commitment_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaCommitmentConfig>) {
if let std::option::Option::Some(config) = config
&& config.commitment().is_some()
@@ -539,53 +586,6 @@ fn economics_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::Ht
};
}
#[derive(serde::Deserialize)]
struct WireEconomicsRpcResponse<T> {
context: serde_json::Value,
value: T,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationGovernor {
initial: f64,
terminal: f64,
taper: f64,
foundation: f64,
foundation_term: f64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationRate {
total: f64,
validator: f64,
foundation: f64,
epoch: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationReward {
epoch: u64,
effective_slot: u64,
amount: u64,
post_balance: u64,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSupply {
total: u64,
circulating: u64,
non_circulating: u64,
non_circulating_accounts: std::vec::Vec<std::string::String>,
}
#[cfg(test)]
#[path = "../unit_tests/rpc_economics.rs"]
mod tests;

View File

@@ -1,281 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_method.rs
// version: 2
/// 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: crate::TRACING_TARGET,
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: crate::TRACING_TARGET,
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(());
}
}
// version: 3
const CURRENT_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 52] = [
crate::HttpRpcMethodDescriptor::new(
@@ -1075,6 +799,282 @@ const HISTORICAL_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 14] = [
),
];
/// 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: crate::TRACING_TARGET,
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: crate::TRACING_TARGET,
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(());
}
}
/// 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] {

View File

@@ -1,5 +1,9 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
// version: 8
// version: 9
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
const MAX_SIGNATURE_STATUSES: usize = 256;
/// Binary encoding accepted for serialized transaction input payloads.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -1083,10 +1087,6 @@ impl SolanaSimulateTransactionResult {
}
}
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
const MAX_SIGNATURE_STATUSES: usize = 256;
impl crate::HttpTransportPool {
/// Executes typed `getFeeForMessage` through the common KSP HTTP transport path.
pub async fn get_fee_for_message(
@@ -1469,6 +1469,104 @@ impl crate::HttpTransportPool {
}
}
#[derive(serde::Deserialize)]
struct WireTransactionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireLatestBlockhash {
blockhash: std::string::String,
last_valid_block_height: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePrioritizationFee {
slot: u64,
prioritization_fee: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureInfo {
signature: std::string::String,
slot: u64,
#[serde(default)]
err: std::option::Option<serde_json::Value>,
#[serde(default)]
memo: std::option::Option<std::string::String>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
#[serde(default)]
transaction_index: std::option::Option<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureStatus {
slot: u64,
#[serde(default)]
confirmations: std::option::Option<u64>,
status: serde_json::Value,
#[serde(default)]
err: std::option::Option<serde_json::Value>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedTransaction {
slot: u64,
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
transaction_index: crate::SolanaWireField<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSimulateTransactionResult {
#[serde(default)]
err: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
logs: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
accounts: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
#[serde(default)]
units_consumed: crate::SolanaWireField<u64>,
#[serde(default)]
loaded_accounts_data_size: crate::SolanaWireField<u32>,
#[serde(default)]
return_data: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
inner_instructions: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
replacement_blockhash: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
fee: crate::SolanaWireField<u64>,
#[serde(default)]
pre_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
post_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
pre_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
post_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
loaded_addresses: crate::SolanaWireField<serde_json::Value>,
}
fn push_transaction_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
if let std::option::Option::Some(config) = config
&& (config.commitment().is_some() || config.min_context_slot().is_some())
@@ -1584,12 +1682,6 @@ fn transaction_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::
};
}
#[derive(serde::Deserialize)]
struct WireTransactionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
fn invalid_transaction_wire(method: &str, field: &str, message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method).with_context("field", field);
}
@@ -1711,98 +1803,6 @@ fn decode_replacement_blockhash_field(
};
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireLatestBlockhash {
blockhash: std::string::String,
last_valid_block_height: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePrioritizationFee {
slot: u64,
prioritization_fee: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureInfo {
signature: std::string::String,
slot: u64,
#[serde(default)]
err: std::option::Option<serde_json::Value>,
#[serde(default)]
memo: std::option::Option<std::string::String>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
#[serde(default)]
transaction_index: std::option::Option<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureStatus {
slot: u64,
#[serde(default)]
confirmations: std::option::Option<u64>,
status: serde_json::Value,
#[serde(default)]
err: std::option::Option<serde_json::Value>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedTransaction {
slot: u64,
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
transaction_index: crate::SolanaWireField<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSimulateTransactionResult {
#[serde(default)]
err: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
logs: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
accounts: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
#[serde(default)]
units_consumed: crate::SolanaWireField<u64>,
#[serde(default)]
loaded_accounts_data_size: crate::SolanaWireField<u32>,
#[serde(default)]
return_data: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
inner_instructions: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
replacement_blockhash: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
fee: crate::SolanaWireField<u64>,
#[serde(default)]
pre_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
post_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
pre_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
post_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
loaded_addresses: crate::SolanaWireField<serde_json::Value>,
}
#[cfg(test)]
#[path = "../unit_tests/rpc_transactions.rs"]
mod tests;