v0.2.5-pre.009
This commit is contained in:
@@ -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(¬ify))));
|
||||
role_runtimes.push(std::sync::Arc::new(crate::HttpRoleRuntime::new(role, std::sync::Arc::clone(¬ify))));
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/client.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
|
||||
let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse");
|
||||
@@ -25,7 +25,7 @@ fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
|
||||
|
||||
#[test]
|
||||
fn endpoint_client_snapshot_never_contains_url_or_secret_material() {
|
||||
let client = super::HttpEndpointClient::new(endpoint(true, "https://provider.invalid/rpc?api-key=SECRET-CANARY")).expect("client must build");
|
||||
let client = crate::HttpEndpointClient::new(endpoint(true, "https://provider.invalid/rpc?api-key=SECRET-CANARY")).expect("client must build");
|
||||
let snapshot = client.snapshot();
|
||||
let rendered = format!("{snapshot:?} {client:?}");
|
||||
assert_eq!(snapshot.availability(), crate::HttpEndpointAvailability::Available);
|
||||
@@ -36,21 +36,21 @@ fn endpoint_client_snapshot_never_contains_url_or_secret_material() {
|
||||
|
||||
#[test]
|
||||
fn disabled_endpoint_client_is_visible_but_not_selectable() {
|
||||
let client = super::HttpEndpointClient::new(endpoint(false, "https://api.devnet.solana.com")).expect("disabled client must still build");
|
||||
let client = crate::HttpEndpointClient::new(endpoint(false, "https://api.devnet.solana.com")).expect("disabled client must still build");
|
||||
assert_eq!(client.snapshot().availability(), crate::HttpEndpointAvailability::Disabled);
|
||||
assert!(!client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_client_matches_exact_and_wildcard_capabilities() {
|
||||
let client = super::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
let client = crate::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
assert!(client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
|
||||
assert!(!client.supports(&crate::HttpRoleName::new("write"), &crate::HttpRequestKind::new("get_balance")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_role_snapshot_exposes_safe_resilience_state() {
|
||||
let client = super::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
let client = crate::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
let snapshot = client.snapshot();
|
||||
let role = &snapshot.roles()[0];
|
||||
assert_eq!(role.availability(), crate::HttpEndpointAvailability::Available);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn request_serialization_matches_json_rpc_2_0_shape() {
|
||||
let request = super::JsonRpcRequest::new(7, "getBalance", std::vec![serde_json::json!("Address111"), serde_json::json!({"commitment":"confirmed"})])
|
||||
let request = crate::JsonRpcRequest::new(7, "getBalance", std::vec![serde_json::json!("Address111"), serde_json::json!({"commitment":"confirmed"})])
|
||||
.expect("valid request must construct");
|
||||
let encoded = request.to_json_string().expect("serializable request must encode");
|
||||
let value: serde_json::Value = serde_json::from_str(encoded.as_str()).expect("encoded request must remain JSON");
|
||||
@@ -15,28 +15,28 @@ fn request_serialization_matches_json_rpc_2_0_shape() {
|
||||
|
||||
#[test]
|
||||
fn request_rejects_empty_or_untrimmed_method() {
|
||||
assert!(super::JsonRpcRequest::new(1, "", std::vec![]).is_err());
|
||||
assert!(super::JsonRpcRequest::new(1, " getHealth", std::vec![]).is_err());
|
||||
assert!(crate::JsonRpcRequest::new(1, "", std::vec![]).is_err());
|
||||
assert!(crate::JsonRpcRequest::new(1, " getHealth", std::vec![]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_preserves_null_success_result() {
|
||||
let response = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":null,"id":9}"#, 9).expect("null result is a valid success payload");
|
||||
assert!(matches!(&response, super::JsonRpcResponse::Success(_)), "success response must not parse as error");
|
||||
if let super::JsonRpcResponse::Success(success) = response {
|
||||
let response = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":null,"id":9}"#, 9).expect("null result is a valid success payload");
|
||||
assert!(matches!(&response, crate::JsonRpcResponse::Success(_)), "success response must not parse as error");
|
||||
if let crate::JsonRpcResponse::Success(success) = response {
|
||||
assert!(success.result().is_null());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_preserves_rpc_error_payload() {
|
||||
let response = super::parse_json_rpc_response_text(
|
||||
let response = crate::parse_json_rpc_response_text(
|
||||
r#"{"jsonrpc":"2.0","error":{"code":-32005,"message":"Node is unhealthy","data":{"numSlotsBehind":12}},"id":4}"#,
|
||||
4,
|
||||
)
|
||||
.expect("valid JSON-RPC error envelope must parse");
|
||||
assert!(matches!(&response, super::JsonRpcResponse::Error(_)), "RPC error response must not parse as success");
|
||||
if let super::JsonRpcResponse::Error(error_response) = response {
|
||||
assert!(matches!(&response, crate::JsonRpcResponse::Error(_)), "RPC error response must not parse as success");
|
||||
if let crate::JsonRpcResponse::Error(error_response) = response {
|
||||
assert_eq!(error_response.error().code(), -32005);
|
||||
assert_eq!(error_response.error().message(), "Node is unhealthy");
|
||||
assert_eq!(error_response.error().data(), std::option::Option::Some(&serde_json::json!({"numSlotsBehind":12})));
|
||||
@@ -45,38 +45,38 @@ fn response_parser_preserves_rpc_error_payload() {
|
||||
|
||||
#[test]
|
||||
fn response_parser_rejects_id_mismatch() {
|
||||
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","id":2}"#, 1).expect_err("mismatched id must fail");
|
||||
let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","id":2}"#, 1).expect_err("mismatched id must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_rejects_wrong_protocol_version() {
|
||||
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"1.0","result":"ok","id":1}"#, 1).expect_err("wrong version must fail");
|
||||
let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"1.0","result":"ok","id":1}"#, 1).expect_err("wrong version must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_rejects_both_result_and_error() {
|
||||
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","error":{"code":-1,"message":"bad"},"id":1}"#, 1)
|
||||
let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","error":{"code":-1,"message":"bad"},"id":1}"#, 1)
|
||||
.expect_err("mutually exclusive fields must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_rejects_missing_result_and_error() {
|
||||
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","id":1}"#, 1).expect_err("missing outcome must fail");
|
||||
let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","id":1}"#, 1).expect_err("missing outcome must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_distinguishes_invalid_json_from_protocol_error() {
|
||||
let error = super::parse_json_rpc_response_text("not-json", 1).expect_err("invalid JSON must fail decoding");
|
||||
let error = crate::parse_json_rpc_response_text("not-json", 1).expect_err("invalid JSON must fail decoding");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_DECODE_FAILED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_error_maps_to_shared_ksp_error_without_copying_remote_payload_into_context() {
|
||||
let response = super::parse_json_rpc_response_text(
|
||||
let response = crate::parse_json_rpc_response_text(
|
||||
r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"SECRET-CANARY","data":{"payload":"SECRET-DATA"}},"id":1}"#,
|
||||
1,
|
||||
)
|
||||
@@ -90,7 +90,7 @@ fn rpc_error_maps_to_shared_ksp_error_without_copying_remote_payload_into_contex
|
||||
|
||||
#[test]
|
||||
fn request_debug_omits_parameter_payloads() {
|
||||
let request = super::JsonRpcRequest::new(1, "sendTransaction", std::vec![serde_json::json!("SIGNED-TRANSACTION-SECRET-CANARY")])
|
||||
let request = crate::JsonRpcRequest::new(1, "sendTransaction", std::vec![serde_json::json!("SIGNED-TRANSACTION-SECRET-CANARY")])
|
||||
.expect("test request must construct");
|
||||
let rendered = format!("{request:?}");
|
||||
assert!(rendered.contains("sendTransaction"));
|
||||
@@ -100,11 +100,11 @@ fn request_debug_omits_parameter_payloads() {
|
||||
|
||||
#[test]
|
||||
fn response_debug_omits_result_and_remote_error_payloads() {
|
||||
let success = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":{"secret":"RESULT-SECRET-CANARY"},"id":1}"#, 1)
|
||||
let success = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":{"secret":"RESULT-SECRET-CANARY"},"id":1}"#, 1)
|
||||
.expect("test success response must parse");
|
||||
let success_rendered = format!("{success:?}");
|
||||
assert!(!success_rendered.contains("RESULT-SECRET-CANARY"));
|
||||
let failure = super::parse_json_rpc_response_text(
|
||||
let failure = crate::parse_json_rpc_response_text(
|
||||
r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"MESSAGE-SECRET-CANARY","data":{"secret":"DATA-SECRET-CANARY"}},"id":2}"#,
|
||||
2,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings {
|
||||
return crate::HttpEndpointRoleSettings::new(
|
||||
@@ -66,7 +66,7 @@ fn settings(endpoints: std::vec::Vec<crate::HttpEndpointSettings>) -> crate::Htt
|
||||
|
||||
#[test]
|
||||
fn pool_prefers_lowest_priority_tier() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("secondary", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("primary", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
@@ -80,7 +80,7 @@ fn pool_prefers_lowest_priority_tier() {
|
||||
|
||||
#[test]
|
||||
fn pool_round_robins_fairly_inside_best_priority_tier() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("one", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("two", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
@@ -98,7 +98,7 @@ fn pool_round_robins_fairly_inside_best_priority_tier() {
|
||||
|
||||
#[test]
|
||||
fn disabled_best_priority_endpoint_falls_back_to_next_tier() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("disabled-primary", false, 1, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
@@ -111,7 +111,7 @@ fn disabled_best_priority_endpoint_falls_back_to_next_tier() {
|
||||
|
||||
#[test]
|
||||
fn pool_filters_role_and_capability_before_priority() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("wrong-capability", true, 1, std::vec![crate::HttpRequestKind::new("send_transaction")]),
|
||||
endpoint("matching", true, 50, std::vec![crate::HttpRequestKind::new("get_balance")]),
|
||||
]))
|
||||
@@ -124,7 +124,7 @@ fn pool_filters_role_and_capability_before_priority() {
|
||||
|
||||
#[test]
|
||||
fn pool_returns_structured_error_when_no_endpoint_matches() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("read-only", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("read-only", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
.expect("pool must build");
|
||||
let error = pool
|
||||
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("send_transaction"))
|
||||
@@ -135,7 +135,7 @@ fn pool_returns_structured_error_when_no_endpoint_matches() {
|
||||
|
||||
#[test]
|
||||
fn standard_method_selection_uses_registry_request_kind() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("balance", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("balance", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
.expect("pool must build");
|
||||
let method = crate::find_http_rpc_method("getBalance").expect("audited method must exist");
|
||||
let selection = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect("standard method must route");
|
||||
@@ -144,7 +144,7 @@ fn standard_method_selection_uses_registry_request_kind() {
|
||||
|
||||
#[test]
|
||||
fn pool_snapshot_is_safe_and_preserves_disabled_endpoints() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("enabled", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("disabled", false, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
@@ -185,7 +185,7 @@ fn disabled_role_is_excluded_before_priority_selection() {
|
||||
base.max_idle_connections_per_host(),
|
||||
std::vec![disabled_role, enabled_non_matching_role],
|
||||
);
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
disabled_role_endpoint,
|
||||
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
@@ -198,7 +198,7 @@ fn disabled_role_is_excluded_before_priority_selection() {
|
||||
|
||||
#[test]
|
||||
fn removed_standard_method_is_rejected_before_endpoint_routing() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("wildcard", true, 10, std::vec![crate::HttpRequestKind::wildcard()],)]))
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("wildcard", true, 10, std::vec![crate::HttpRequestKind::wildcard()],)]))
|
||||
.expect("pool must build");
|
||||
let method = crate::find_http_rpc_method("confirmTransaction").expect("historical method must exist");
|
||||
let error = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect_err("removed standard method must be rejected before routing");
|
||||
@@ -207,7 +207,7 @@ fn removed_standard_method_is_rejected_before_endpoint_routing() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_concurrency_saturation_falls_back_to_lower_priority_tier() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
limited_endpoint("primary", 1, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None),
|
||||
limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None),
|
||||
]))
|
||||
@@ -222,7 +222,7 @@ async fn runtime_concurrency_saturation_falls_back_to_lower_priority_tier() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_token_bucket_exhaustion_falls_back_without_busy_waiting() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
limited_endpoint("primary", 1, std::option::Option::Some(1), std::option::Option::Some(1), std::option::Option::None, std::option::Option::None),
|
||||
limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
]))
|
||||
@@ -238,7 +238,7 @@ async fn runtime_token_bucket_exhaustion_falls_back_without_busy_waiting() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_cooldown_excludes_rate_limited_role_and_uses_fallback() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
@@ -263,7 +263,7 @@ async fn provider_cooldown_excludes_rate_limited_role_and_uses_fallback() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn admission_waits_for_released_concurrency_without_holding_a_sync_mutex_across_await() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
@@ -289,7 +289,7 @@ async fn admission_waits_for_released_concurrency_without_holding_a_sync_mutex_a
|
||||
|
||||
#[tokio::test]
|
||||
async fn admission_timeout_is_bounded_when_concurrency_never_becomes_available() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
@@ -310,7 +310,7 @@ async fn admission_timeout_is_bounded_when_concurrency_never_becomes_available()
|
||||
|
||||
#[tokio::test]
|
||||
async fn passive_health_snapshot_moves_from_degraded_back_to_available_after_success() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/resilience.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||
return std::num::NonZeroU32::new(value).expect("test limit must be non-zero");
|
||||
@@ -46,46 +46,46 @@ fn retry_backoff_is_exponential_and_bounded() {
|
||||
#[test]
|
||||
fn retry_safe_timeout_is_retried_until_budget_is_exhausted() {
|
||||
let settings = retry_settings();
|
||||
let first = super::evaluate_transport_retry(
|
||||
let first = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
super::HttpRetryCause::Timeout,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::Timeout,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(first, super::HttpRetryDecision::RetryAfter(std::time::Duration::from_millis(100)));
|
||||
let exhausted = super::evaluate_transport_retry(
|
||||
assert_eq!(first, crate::HttpRetryDecision::RetryAfter(std::time::Duration::from_millis(100)));
|
||||
let exhausted = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
super::HttpRetryCause::Timeout,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::Timeout,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
settings.max_retries(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(exhausted, super::HttpRetryDecision::Stop);
|
||||
assert_eq!(exhausted, crate::HttpRetryDecision::Stop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_submission_never_retries_after_ambiguous_dispatch() {
|
||||
let decision = super::evaluate_transport_retry(
|
||||
let decision = crate::evaluate_transport_retry(
|
||||
method("sendTransaction"),
|
||||
&retry_settings(),
|
||||
super::HttpRetryCause::Connection,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::Connection,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(decision, super::HttpRetryDecision::Stop);
|
||||
assert_eq!(decision, crate::HttpRetryDecision::Stop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_submission_can_retry_when_transport_proves_no_dispatch() {
|
||||
let decision = super::evaluate_transport_retry(
|
||||
let decision = crate::evaluate_transport_retry(
|
||||
method("sendTransaction"),
|
||||
&retry_settings(),
|
||||
super::HttpRetryCause::Connection,
|
||||
super::HttpDispatchState::NotDispatched,
|
||||
crate::HttpRetryCause::Connection,
|
||||
crate::HttpDispatchState::NotDispatched,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
@@ -94,36 +94,36 @@ fn write_submission_can_retry_when_transport_proves_no_dispatch() {
|
||||
|
||||
#[test]
|
||||
fn rpc_application_and_invalid_response_are_not_transport_retries() {
|
||||
for cause in [super::HttpRetryCause::RpcApplication, super::HttpRetryCause::InvalidResponse, super::HttpRetryCause::Request] {
|
||||
let decision = super::evaluate_transport_retry(
|
||||
for cause in [crate::HttpRetryCause::RpcApplication, crate::HttpRetryCause::InvalidResponse, crate::HttpRetryCause::Request] {
|
||||
let decision = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&retry_settings(),
|
||||
cause,
|
||||
super::HttpDispatchState::NotDispatched,
|
||||
crate::HttpDispatchState::NotDispatched,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(decision, super::HttpRetryDecision::Stop);
|
||||
assert_eq!(decision, crate::HttpRetryDecision::Stop);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_retry_after_can_extend_backoff_but_is_defensively_bounded() {
|
||||
let settings = retry_settings();
|
||||
let extended = super::evaluate_transport_retry(
|
||||
let extended = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
super::HttpRetryCause::RateLimited,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::RateLimited,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::Some(std::time::Duration::from_secs(3)),
|
||||
);
|
||||
assert_eq!(extended.delay(), std::option::Option::Some(std::time::Duration::from_secs(3)));
|
||||
let bounded = super::evaluate_transport_retry(
|
||||
let bounded = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
super::HttpRetryCause::RateLimited,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::RateLimited,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::Some(std::time::Duration::from_secs(600)),
|
||||
);
|
||||
@@ -144,29 +144,29 @@ fn token_bucket_consumes_burst_then_refills_from_elapsed_time() {
|
||||
#[test]
|
||||
fn absent_burst_capacity_defaults_to_one_second_of_rps_capacity() {
|
||||
let role = role_limits(std::option::Option::Some(2), std::option::Option::None, std::option::Option::None, std::option::Option::None);
|
||||
let runtime = std::sync::Arc::new(super::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let runtime = std::sync::Arc::new(crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let now = std::time::Instant::now();
|
||||
let first = runtime.try_acquire(now);
|
||||
let second = runtime.try_acquire(now);
|
||||
let third = runtime.try_acquire(now);
|
||||
assert!(matches!(first, super::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(second, super::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(third, super::RoleAdmissionAttempt::BlockedUntil(_)));
|
||||
assert!(matches!(first, crate::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(second, crate::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(third, crate::RoleAdmissionAttempt::BlockedUntil(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrency_semaphore_releases_capacity_when_permit_is_dropped() {
|
||||
let role = role_limits(std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None);
|
||||
let runtime = std::sync::Arc::new(super::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let runtime = std::sync::Arc::new(crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let now = std::time::Instant::now();
|
||||
let first = runtime.try_acquire(now);
|
||||
let held = match first {
|
||||
super::RoleAdmissionAttempt::Ready(permit) => permit,
|
||||
crate::RoleAdmissionAttempt::Ready(permit) => permit,
|
||||
_ => panic!("first concurrency permit must be available"),
|
||||
};
|
||||
assert!(matches!(runtime.try_acquire(now), super::RoleAdmissionAttempt::ConcurrencySaturated));
|
||||
assert!(matches!(runtime.try_acquire(now), crate::RoleAdmissionAttempt::ConcurrencySaturated));
|
||||
drop(held);
|
||||
assert!(matches!(runtime.try_acquire(now), super::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(runtime.try_acquire(now), crate::RoleAdmissionAttempt::Ready(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -177,7 +177,7 @@ fn rate_limit_cooldown_marks_role_and_caps_provider_delay() {
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(std::time::Duration::from_millis(10)),
|
||||
);
|
||||
let runtime = super::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new()));
|
||||
let runtime = crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new()));
|
||||
let pause = runtime.record_rate_limited(std::option::Option::Some(std::time::Duration::from_secs(600)));
|
||||
assert_eq!(pause, std::time::Duration::from_secs(60));
|
||||
assert_eq!(runtime.rate_limit_count(), 1);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn audited_registry_has_expected_current_and_historical_counts() {
|
||||
assert_eq!(super::current_http_rpc_methods().len(), 52);
|
||||
assert_eq!(super::historical_http_rpc_methods().len(), 14);
|
||||
assert_eq!(crate::current_http_rpc_methods().len(), 52);
|
||||
assert_eq!(crate::historical_http_rpc_methods().len(), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audited_registry_method_names_are_unique() {
|
||||
let mut names = std::collections::BTreeSet::<&str>::new();
|
||||
for descriptor in super::current_http_rpc_methods() {
|
||||
for descriptor in crate::current_http_rpc_methods() {
|
||||
assert!(names.insert(descriptor.method()), "duplicate current method {}", descriptor.method());
|
||||
}
|
||||
for descriptor in super::historical_http_rpc_methods() {
|
||||
for descriptor in crate::historical_http_rpc_methods() {
|
||||
assert!(names.insert(descriptor.method()), "duplicate historical method {}", descriptor.method());
|
||||
}
|
||||
assert_eq!(names.len(), 66);
|
||||
@@ -26,13 +26,13 @@ fn coverage_release_counts_match_recalibrated_matrix() {
|
||||
let mut transactions = 0_usize;
|
||||
let mut blocks_economics = 0_usize;
|
||||
let mut historical = 0_usize;
|
||||
for descriptor in super::current_http_rpc_methods() {
|
||||
for descriptor in crate::current_http_rpc_methods() {
|
||||
match descriptor.coverage_release() {
|
||||
super::HttpRpcCoverageRelease::V0_2_1 => foundation += 1,
|
||||
super::HttpRpcCoverageRelease::V0_2_2 => accounts_tokens_cluster += 1,
|
||||
super::HttpRpcCoverageRelease::V0_2_3 => transactions += 1,
|
||||
super::HttpRpcCoverageRelease::V0_2_4 => blocks_economics += 1,
|
||||
super::HttpRpcCoverageRelease::Historical => historical += 1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_1 => foundation += 1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_2 => accounts_tokens_cluster += 1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_3 => transactions += 1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_4 => blocks_economics += 1,
|
||||
crate::HttpRpcCoverageRelease::Historical => historical += 1,
|
||||
}
|
||||
}
|
||||
assert_eq!(historical, 0);
|
||||
@@ -45,8 +45,8 @@ fn coverage_release_counts_match_recalibrated_matrix() {
|
||||
#[test]
|
||||
fn foundation_canary_assignment_is_exact() {
|
||||
let mut names = std::vec::Vec::<&str>::new();
|
||||
for descriptor in super::current_http_rpc_methods() {
|
||||
if descriptor.coverage_release() == super::HttpRpcCoverageRelease::V0_2_1 {
|
||||
for descriptor in crate::current_http_rpc_methods() {
|
||||
if descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_1 {
|
||||
names.push(descriptor.method());
|
||||
}
|
||||
}
|
||||
@@ -56,62 +56,62 @@ fn foundation_canary_assignment_is_exact() {
|
||||
|
||||
#[test]
|
||||
fn historical_methods_are_deprecated_removed_and_not_retryable() {
|
||||
for descriptor in super::historical_http_rpc_methods() {
|
||||
assert_eq!(descriptor.documentation_status(), super::RpcDocumentationStatus::Deprecated);
|
||||
assert_eq!(descriptor.runtime_status(), super::RpcRuntimeStatus::Removed);
|
||||
assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NotApplicable);
|
||||
assert_eq!(descriptor.coverage_release(), super::HttpRpcCoverageRelease::Historical);
|
||||
for descriptor in crate::historical_http_rpc_methods() {
|
||||
assert_eq!(descriptor.documentation_status(), crate::RpcDocumentationStatus::Deprecated);
|
||||
assert_eq!(descriptor.runtime_status(), crate::RpcRuntimeStatus::Removed);
|
||||
assert_eq!(descriptor.transport_retry_class(), crate::TransportRetryClass::NotApplicable);
|
||||
assert_eq!(descriptor.coverage_release(), crate::HttpRpcCoverageRelease::Historical);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_transaction_and_get_block_track_deprecated_legacy_request_form() {
|
||||
let get_transaction = super::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
|
||||
let get_block = super::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist");
|
||||
let get_transaction = crate::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
|
||||
let get_block = crate::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist");
|
||||
assert!(get_transaction.request_form_status().has_deprecated_legacy());
|
||||
assert!(get_block.request_form_status().has_deprecated_legacy());
|
||||
let get_balance = super::find_http_rpc_method("getBalance").expect("getBalance descriptor must exist");
|
||||
let get_balance = crate::find_http_rpc_method("getBalance").expect("getBalance descriptor must exist");
|
||||
assert!(!get_balance.request_form_status().has_deprecated_legacy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_submission_methods_are_never_retry_after_ambiguous_dispatch() {
|
||||
for method in ["sendTransaction", "requestAirdrop"] {
|
||||
let descriptor = super::find_http_rpc_method(method).expect("write descriptor must exist");
|
||||
assert_eq!(descriptor.operation_kind(), super::RpcOperationKind::WriteSubmission);
|
||||
assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NeverAfterDispatch);
|
||||
let descriptor = crate::find_http_rpc_method(method).expect("write descriptor must exist");
|
||||
assert_eq!(descriptor.operation_kind(), crate::RpcOperationKind::WriteSubmission);
|
||||
assert_eq!(descriptor.transport_retry_class(), crate::TransportRetryClass::NeverAfterDispatch);
|
||||
}
|
||||
let simulation = super::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must exist");
|
||||
assert_eq!(simulation.operation_kind(), super::RpcOperationKind::Simulation);
|
||||
assert_eq!(simulation.transport_retry_class(), super::TransportRetryClass::RetrySafe);
|
||||
let simulation = crate::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must exist");
|
||||
assert_eq!(simulation.operation_kind(), crate::RpcOperationKind::Simulation);
|
||||
assert_eq!(simulation.transport_retry_class(), crate::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_method_support_check_returns_method_removed_error() {
|
||||
let descriptor = super::find_http_rpc_method("confirmTransaction").expect("historical descriptor must exist");
|
||||
let descriptor = crate::find_http_rpc_method("confirmTransaction").expect("historical descriptor must exist");
|
||||
let error = descriptor.ensure_runtime_supported().expect_err("removed method must not be callable");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_METHOD_REMOVED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_supported_method_passes_runtime_support_check() {
|
||||
let descriptor = super::find_http_rpc_method("getHealth").expect("current descriptor must exist");
|
||||
let descriptor = crate::find_http_rpc_method("getHealth").expect("current descriptor must exist");
|
||||
assert!(descriptor.ensure_runtime_supported().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_unstable_descriptor_executes_central_warning_path() {
|
||||
let descriptor = super::HttpRpcMethodDescriptor::new(
|
||||
let descriptor = crate::HttpRpcMethodDescriptor::new(
|
||||
"experimentalMethod",
|
||||
super::HttpRpcCategory::Cluster,
|
||||
crate::HttpRpcCategory::Cluster,
|
||||
"experimental_method",
|
||||
super::RpcDocumentationStatus::Unstable,
|
||||
super::RpcRuntimeStatus::Supported,
|
||||
super::RpcRequestFormStatus::Stable,
|
||||
super::RpcOperationKind::Read,
|
||||
super::TransportRetryClass::RetrySafe,
|
||||
crate::RpcDocumentationStatus::Unstable,
|
||||
crate::RpcRuntimeStatus::Supported,
|
||||
crate::RpcRequestFormStatus::Stable,
|
||||
crate::RpcOperationKind::Read,
|
||||
crate::TransportRetryClass::RetrySafe,
|
||||
std::option::Option::None,
|
||||
super::HttpRpcCoverageRelease::V0_2_1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_1,
|
||||
);
|
||||
assert!(descriptor.requires_method_usage_warning());
|
||||
assert!(descriptor.ensure_runtime_supported().is_ok());
|
||||
@@ -119,5 +119,5 @@ fn supported_unstable_descriptor_executes_central_warning_path() {
|
||||
|
||||
#[test]
|
||||
fn lookup_rejects_unknown_method_without_affecting_raw_provider_extensions() {
|
||||
assert!(super::find_http_rpc_method("providerCustomMethod").is_none());
|
||||
assert!(crate::find_http_rpc_method("providerCustomMethod").is_none());
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/settings.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||
return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero");
|
||||
}
|
||||
|
||||
fn valid_settings(url_text: &str) -> super::HttpTransportSettings {
|
||||
let url = super::HttpEndpointUrl::parse(url_text).expect("test URL must be valid");
|
||||
let limits = super::HttpRoleLimits::new(
|
||||
fn valid_settings(url_text: &str) -> crate::HttpTransportSettings {
|
||||
let url = crate::HttpEndpointUrl::parse(url_text).expect("test URL must be valid");
|
||||
let limits = crate::HttpRoleLimits::new(
|
||||
std::option::Option::Some(non_zero(10)),
|
||||
std::option::Option::Some(non_zero(20)),
|
||||
std::option::Option::Some(non_zero(4)),
|
||||
std::option::Option::Some(std::time::Duration::from_millis(500)),
|
||||
);
|
||||
let role = super::HttpEndpointRoleSettings::new(super::HttpRoleName::new("default"), true, std::vec![super::HttpRequestKind::wildcard()], 100, limits);
|
||||
let endpoint = super::HttpEndpointSettings::new(
|
||||
let role = crate::HttpEndpointRoleSettings::new(crate::HttpRoleName::new("default"), true, std::vec![crate::HttpRequestKind::wildcard()], 100, limits);
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"devnet_public",
|
||||
true,
|
||||
super::HttpProviderName::new("solana-public"),
|
||||
super::HttpClusterName::new("devnet"),
|
||||
crate::HttpProviderName::new("solana-public"),
|
||||
crate::HttpClusterName::new("devnet"),
|
||||
url,
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(15),
|
||||
std::option::Option::Some(8),
|
||||
std::vec![role],
|
||||
);
|
||||
return super::HttpTransportSettings::new(
|
||||
return crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
super::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)),
|
||||
crate::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_url_accepts_http_and_https() {
|
||||
assert!(super::HttpEndpointUrl::parse("https://api.devnet.solana.com").is_ok());
|
||||
assert!(super::HttpEndpointUrl::parse("http://127.0.0.1:8899").is_ok());
|
||||
assert!(crate::HttpEndpointUrl::parse("https://api.devnet.solana.com").is_ok());
|
||||
assert!(crate::HttpEndpointUrl::parse("http://127.0.0.1:8899").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_url_rejects_non_http_schemes() {
|
||||
let result = super::HttpEndpointUrl::parse("ws://api.devnet.solana.com");
|
||||
let result = crate::HttpEndpointUrl::parse("ws://api.devnet.solana.com");
|
||||
let error = result.expect_err("WebSocket URL must not be accepted by HTTP settings");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_url_debug_redacts_secret_material() {
|
||||
let url = super::HttpEndpointUrl::parse("https://provider.invalid/rpc?api-key=SECRET-CANARY").expect("test URL must parse");
|
||||
let url = crate::HttpEndpointUrl::parse("https://provider.invalid/rpc?api-key=SECRET-CANARY").expect("test URL must parse");
|
||||
let rendered = format!("{url:?}");
|
||||
assert!(rendered.contains("<redacted>"));
|
||||
assert!(!rendered.contains("SECRET-CANARY"));
|
||||
@@ -70,28 +70,28 @@ fn transport_settings_debug_does_not_leak_endpoint_url() {
|
||||
|
||||
#[test]
|
||||
fn transport_settings_require_one_enabled_endpoint() {
|
||||
let url = super::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("test URL must parse");
|
||||
let role = super::HttpEndpointRoleSettings::new(
|
||||
super::HttpRoleName::new("default"),
|
||||
let url = crate::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("test URL must parse");
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![super::HttpRequestKind::wildcard()],
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
100,
|
||||
super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let endpoint = super::HttpEndpointSettings::new(
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"disabled",
|
||||
false,
|
||||
super::HttpProviderName::new("provider"),
|
||||
super::HttpClusterName::new("devnet"),
|
||||
crate::HttpProviderName::new("provider"),
|
||||
crate::HttpClusterName::new("devnet"),
|
||||
url,
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::option::Option::None,
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
super::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||
crate::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||
);
|
||||
let error = settings.validate().expect_err("all-disabled settings must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
@@ -101,7 +101,7 @@ fn transport_settings_require_one_enabled_endpoint() {
|
||||
fn transport_settings_reject_duplicate_endpoint_names() {
|
||||
let first = valid_settings("https://one.invalid");
|
||||
let second = valid_settings("https://two.invalid");
|
||||
let settings = super::HttpTransportSettings::new(std::vec![first.endpoints()[0].clone(), second.endpoints()[0].clone()], first.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![first.endpoints()[0].clone(), second.endpoints()[0].clone()], first.retry().clone());
|
||||
let error = settings.validate().expect_err("duplicate endpoint names must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
}
|
||||
@@ -110,7 +110,7 @@ fn transport_settings_reject_duplicate_endpoint_names() {
|
||||
fn transport_settings_reject_duplicate_roles() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let duplicated_endpoint = super::HttpEndpointSettings::new(
|
||||
let duplicated_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
@@ -121,7 +121,7 @@ fn transport_settings_reject_duplicate_roles() {
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
std::vec![endpoint.roles()[0].clone(), endpoint.roles()[0].clone()],
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(std::vec![duplicated_endpoint], base.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![duplicated_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
@@ -129,14 +129,14 @@ fn transport_settings_reject_duplicate_roles() {
|
||||
fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let role = super::HttpEndpointRoleSettings::new(
|
||||
super::HttpRoleName::new("default"),
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![super::HttpRequestKind::wildcard(), super::HttpRequestKind::new("get_balance")],
|
||||
std::vec![crate::HttpRequestKind::wildcard(), crate::HttpRequestKind::new("get_balance")],
|
||||
100,
|
||||
endpoint.roles()[0].limits().clone(),
|
||||
);
|
||||
let modified_endpoint = super::HttpEndpointSettings::new(
|
||||
let modified_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
@@ -147,7 +147,7 @@ fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
@@ -155,14 +155,14 @@ fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
|
||||
fn transport_settings_reject_burst_without_rps() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let role = super::HttpEndpointRoleSettings::new(
|
||||
super::HttpRoleName::new("default"),
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![super::HttpRequestKind::wildcard()],
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
100,
|
||||
super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::Some(non_zero(2)), std::option::Option::None, std::option::Option::None),
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::Some(non_zero(2)), std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let modified_endpoint = super::HttpEndpointSettings::new(
|
||||
let modified_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
@@ -173,16 +173,16 @@ fn transport_settings_reject_burst_without_rps() {
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_reject_reversed_retry_backoff() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let settings = super::HttpTransportSettings::new(
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
base.endpoints().to_vec(),
|
||||
super::HttpRetrySettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
|
||||
crate::HttpRetrySettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
|
||||
);
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
@@ -191,7 +191,7 @@ fn transport_settings_reject_reversed_retry_backoff() {
|
||||
fn transport_settings_reject_zero_request_timeout() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let modified_endpoint = super::HttpEndpointSettings::new(
|
||||
let modified_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
@@ -202,6 +202,6 @@ fn transport_settings_reject_zero_request_timeout() {
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
endpoint.roles().to_vec(),
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user