v0.5.1-pre.008

This commit is contained in:
2026-08-10 11:21:07 +02:00
parent a2820062eb
commit 0e05c660c6
90 changed files with 2490 additions and 1613 deletions

View File

@@ -1,8 +1,15 @@
<!-- file: ks-onchain-transport/CHANGELOG.md -->
<!-- version: 15 -->
<!-- version: 21 -->
# CHANGELOG — ks-onchain-transport
## `0.5.1-pre.008`
- Fix: retire complètement les URLs résolues des snapshots HTTP/WS/session, conserve ces snapshots backend-only et non sérialisables directement, sanitise le `Debug` de `WsSession`/`RpcEndpoint`, et ne recopie plus les erreurs brutes de connexion HTTP/WS susceptibles dinclure une URL résolue. Les corps HTTP non-success et messages JSON-RPC distants ne sont plus recopiés dans les erreurs/logs afin quun fournisseur ne puisse pas y réinjecter une URL ou un credential. Le getter dURL HTTP inutilisé est supprimé et le getter WS nécessaire à la session devient `pub(crate)`.
- remplace les dérivations `Debug` de `HttpClient` et `WsClient` par des implémentations manuelles sanitisées qui ne recopient aucune chaîne issue de la configuration dendpoint et utilisent un marqueur `<redacted>` ;
- conserve ainsi `Debug` sur les clients et pools sans réintroduire `Debug` sur `ks_config::HttpEndpointConfig` / `WsEndpointConfig`, et ajoute des tests canaris HTTP/WS de non-divulgation.
## `0.5.1-pre.007`
- aligne les fixtures dexécution sur le déplacement des autorisations `*_send_enabled` vers `ExecutionConfig` ; le comportement RPC reste inchangé.

View File

@@ -1,10 +1,10 @@
// file: ks-onchain-transport/src/client.rs
// version: 4
// version: 5
//! RPC client scaffold for Solana ingestion.
/// RPC endpoint configuration.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Eq, PartialEq)]
pub struct RpcEndpoint {
/// HTTP RPC URL.
pub http_url: std::string::String,
@@ -12,6 +12,17 @@ pub struct RpcEndpoint {
pub ws_url: std::option::Option<std::string::String>,
}
impl std::fmt::Debug for crate::RpcEndpoint {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ws_state = if self.ws_url.is_some() { "configured" } else { "missing" };
return formatter
.debug_struct("RpcEndpoint")
.field("http_url", &"<redacted>")
.field("ws_url", &ws_state)
.finish();
}
}
/// Minimal Solana RPC client abstraction.
pub trait SolanaRpcClient {
/// Fetches a raw transaction payload by signature.
@@ -20,3 +31,22 @@ pub trait SolanaRpcClient {
signature: &ks_lib::MdSignature,
) -> ks_core::Result<std::option::Option<std::string::String>>;
}
#[cfg(test)]
mod tests {
#[test]
fn rpc_endpoint_debug_omits_resolved_urls() {
let endpoint = crate::RpcEndpoint {
http_url: "https://HTTP-RPC-SECRET-CANARY.invalid/?api-key=secret".to_string(),
ws_url: std::option::Option::Some(
"wss://WS-RPC-SECRET-CANARY.invalid/?api-key=secret".to_string(),
),
};
let rendered = format!("{endpoint:?}");
assert!(rendered.contains("<redacted>"));
assert!(rendered.contains("configured"));
assert!(!rendered.contains("HTTP-RPC-SECRET-CANARY"));
assert!(!rendered.contains("WS-RPC-SECRET-CANARY"));
assert!(!rendered.contains("api-key=secret"));
}
}

View File

@@ -1,5 +1,5 @@
// file: ks-onchain-transport/src/http_client.rs
// version: 14
// version: 20
//! HTTP JSON-RPC client for standard Solana RPC endpoints.
@@ -15,21 +15,30 @@ pub enum HttpMethodClass {
}
/// Snapshot of one pooled HTTP endpoint.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Clone, Eq, PartialEq)]
pub struct HttpPoolClientSnapshot {
/// Logical endpoint name.
pub endpoint_name: std::string::String,
/// Provider name.
pub provider: std::string::String,
/// Endpoint URL.
pub endpoint_url: std::string::String,
/// Supported roles.
pub roles: std::vec::Vec<crate::EndpointRoleSnapshot>,
/// Status string.
pub status: std::string::String,
}
impl std::fmt::Debug for crate::HttpPoolClientSnapshot {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("HttpPoolClientSnapshot")
.field("endpoint_name", &self.endpoint_name)
.field("provider", &self.provider)
.field("roles", &self.roles)
.field("status", &self.status)
.finish();
}
}
#[derive(Debug)]
struct HttpRequestLimitState {
available_tokens: f64,
@@ -138,7 +147,7 @@ impl HttpRequestLimiter {
}
/// HTTP JSON-RPC client bound to one configured endpoint.
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct HttpClient {
endpoint: ks_config::HttpEndpointConfig,
client: reqwest::Client,
@@ -147,6 +156,16 @@ pub struct HttpClient {
selected_role: std::option::Option<std::string::String>,
}
impl std::fmt::Debug for crate::HttpClient {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("HttpClient")
.field("endpoint", &"<redacted>")
.field("selected_role_configured", &self.selected_role.is_some())
.finish_non_exhaustive();
}
}
impl crate::HttpClient {
/// Creates a new HTTP client bound to one endpoint.
pub fn new(endpoint: ks_config::HttpEndpointConfig) -> ks_core::Result<Self> {
@@ -207,11 +226,6 @@ impl crate::HttpClient {
return self.endpoint.provider.as_str();
}
/// Returns the endpoint URL.
pub fn endpoint_url(&self) -> &str {
return self.endpoint.url.as_str();
}
/// Returns the endpoint configuration.
pub fn endpoint_config(&self) -> &ks_config::HttpEndpointConfig {
return &self.endpoint;
@@ -230,7 +244,7 @@ impl crate::HttpClient {
return false;
}
/// Returns a serializable endpoint snapshot.
/// Returns a backend-only endpoint snapshot without resolved URL material.
pub fn snapshot(&self) -> crate::HttpPoolClientSnapshot {
let mut roles = std::vec::Vec::new();
for role in &self.endpoint.roles {
@@ -239,7 +253,6 @@ impl crate::HttpClient {
return crate::HttpPoolClientSnapshot {
endpoint_name: self.endpoint.name.clone(),
provider: self.endpoint.provider.clone(),
endpoint_url: self.endpoint.url.clone(),
roles,
status: "active".to_string(),
};
@@ -342,9 +355,10 @@ impl crate::HttpClient {
let response = match response_result {
std::result::Result::Ok(response) => response,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, retry_index, error = %error, "HTTP JSON-RPC transport failed");
let error_kind = reqwest_error_kind(&error);
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, retry_index, error_kind, "HTTP JSON-RPC transport failed");
return std::result::Result::Err(ks_core::Error::http(format!(
"http json-rpc request '{}' failed on endpoint '{}': {error}",
"http json-rpc request '{}' failed on endpoint '{}' ({error_kind})",
method, self.endpoint.name
)));
},
@@ -355,9 +369,10 @@ impl crate::HttpClient {
let text = match text_result {
std::result::Result::Ok(text) => text,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, retry_index, error = %error, "HTTP JSON-RPC response body read failed");
let error_kind = reqwest_error_kind(&error);
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, retry_index, error_kind, "HTTP JSON-RPC response body read failed");
return std::result::Result::Err(ks_core::Error::http(format!(
"cannot read http json-rpc response '{}' from endpoint '{}': {error}",
"cannot read http json-rpc response '{}' from endpoint '{}' ({error_kind})",
method, self.endpoint.name
)));
},
@@ -383,8 +398,8 @@ impl crate::HttpClient {
if !status.is_success() {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, retry_index, response_byte_length = text.len(), "HTTP JSON-RPC endpoint returned non-success status");
return std::result::Result::Err(ks_core::Error::http(format!(
"http json-rpc endpoint '{}' returned status {} after {} retries: {}",
self.endpoint.name, status, retry_index, text
"http json-rpc endpoint '{}' returned status {} after {} retries",
self.endpoint.name, status, retry_index
)));
}
let parsed = match crate::parse_json_rpc_text(&text) {
@@ -412,7 +427,7 @@ impl crate::HttpClient {
},
std::option::Option::None => configured_pause_ms,
};
tracing::warn!(target: crate::TRACING_TARGET, action = "retry_http_json_rpc_after_rate_limit", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, rpc_error_code = error_response.error.code, rpc_error_message = %error_response.error.message, retry_index, pause_ms, "HTTP JSON-RPC endpoint returned a rate-limit RPC error; retrying with bounded backoff");
tracing::warn!(target: crate::TRACING_TARGET, action = "retry_http_json_rpc_after_rate_limit", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, rpc_error_code = error_response.error.code, retry_index, pause_ms, "HTTP JSON-RPC endpoint returned a rate-limit RPC error; retrying with bounded backoff");
if let std::option::Option::Some(limiter) = &request_limiter {
limiter.block_for(pause_ms).await;
}
@@ -420,13 +435,10 @@ impl crate::HttpClient {
continue;
},
crate::JsonRpcResponse::Error(error_response) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, retry_index, rpc_error_code = error_response.error.code, rpc_error_message = %error_response.error.message, "HTTP JSON-RPC endpoint returned an RPC error");
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, retry_index, rpc_error_code = error_response.error.code, "HTTP JSON-RPC endpoint returned an RPC error");
return std::result::Result::Err(ks_core::Error::http(format!(
"json-rpc error {} from '{}' after {} retries: {}",
error_response.error.code,
self.endpoint.name,
retry_index,
error_response.error.message
"json-rpc error {} from '{}' after {} retries",
error_response.error.code, self.endpoint.name, retry_index
)));
},
crate::JsonRpcResponse::Notification(_) => {
@@ -515,6 +527,25 @@ impl crate::HttpClient {
}
}
fn reqwest_error_kind(error: &reqwest::Error) -> &'static str {
if error.is_timeout() {
return "timeout";
}
if error.is_connect() {
return "connect";
}
if error.is_request() {
return "request";
}
if error.is_body() {
return "body";
}
if error.is_decode() {
return "decode";
}
return "transport";
}
#[cfg(test)]
mod tests {
fn role_config(
@@ -577,6 +608,27 @@ mod tests {
assert!(client.can_handle("http_any", "send_transaction"));
}
#[test]
fn debug_output_omits_endpoint_url() {
let mut endpoint = endpoint(true);
endpoint.url = "https://HTTP-SECRET-CANARY.invalid/?api-key=secret".to_string();
let client = match crate::HttpClient::new(endpoint) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
let rendered = format!("{client:?}");
assert!(rendered.contains("<redacted>"));
assert!(rendered.contains("selected_role_configured: false"));
assert!(!rendered.contains("HTTP-SECRET-CANARY"));
assert!(!rendered.contains("api-key=secret"));
let snapshot = client.snapshot();
let snapshot_rendered = format!("{snapshot:?}");
assert!(!snapshot_rendered.contains("endpoint_url"));
assert!(!snapshot_rendered.contains("endpointUrl"));
assert!(!snapshot_rendered.contains("HTTP-SECRET-CANARY"));
assert!(!snapshot_rendered.contains("api-key=secret"));
}
#[test]
fn snapshot_preserves_endpoint_metadata() {
let client = match crate::HttpClient::new(endpoint(true)) {
@@ -586,7 +638,6 @@ mod tests {
let snapshot = client.snapshot();
assert_eq!(snapshot.endpoint_name, "http_a");
assert_eq!(snapshot.provider, "test");
assert_eq!(snapshot.endpoint_url, "https://example.invalid");
assert_eq!(snapshot.roles.len(), 3);
}

View File

@@ -1,5 +1,5 @@
// file: ks-onchain-transport/src/http_pool.rs
// version: 10
// version: 11
//! HTTP endpoint pool and role-based routing.
@@ -53,7 +53,7 @@ impl crate::HttpEndpointPool {
});
}
/// Returns a serializable snapshot of every endpoint in the pool.
/// Returns backend-only endpoint metadata without resolved URL material.
pub fn snapshot(&self) -> std::vec::Vec<crate::HttpPoolClientSnapshot> {
let mut snapshots = std::vec::Vec::new();
for client in &self.clients {

View File

@@ -1,5 +1,5 @@
// file: ks-onchain-transport/src/ws_client.rs
// version: 9
// version: 15
//! Standard Solana WebSocket client helpers.
@@ -7,28 +7,46 @@ use futures_util::SinkExt; // rust-rules: trait-import
use futures_util::StreamExt; // rust-rules: trait-import
/// Snapshot of one pooled WebSocket endpoint.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Clone, Eq, PartialEq)]
pub struct WsPoolClientSnapshot {
/// Logical endpoint name.
pub endpoint_name: std::string::String,
/// Provider name.
pub provider: std::string::String,
/// Endpoint URL.
pub endpoint_url: std::string::String,
/// Supported roles.
pub roles: std::vec::Vec<crate::EndpointRoleSnapshot>,
/// Status string.
pub status: std::string::String,
}
impl std::fmt::Debug for crate::WsPoolClientSnapshot {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("WsPoolClientSnapshot")
.field("endpoint_name", &self.endpoint_name)
.field("provider", &self.provider)
.field("roles", &self.roles)
.field("status", &self.status)
.finish();
}
}
/// Standard Solana WebSocket client bound to one configured endpoint.
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct WsClient {
endpoint: ks_config::WsEndpointConfig,
next_request_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
impl std::fmt::Debug for crate::WsClient {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("WsClient")
.field("endpoint", &"<redacted>")
.finish_non_exhaustive();
}
}
impl crate::WsClient {
/// Creates a new WebSocket client bound to one endpoint.
pub fn new(endpoint: ks_config::WsEndpointConfig) -> ks_core::Result<Self> {
@@ -56,8 +74,7 @@ impl crate::WsClient {
return self.endpoint.provider.as_str();
}
/// Returns the endpoint URL.
pub fn endpoint_url(&self) -> &str {
pub(crate) fn endpoint_url(&self) -> &str {
return self.endpoint.url.as_str();
}
@@ -79,7 +96,7 @@ impl crate::WsClient {
return false;
}
/// Returns a serializable endpoint snapshot.
/// Returns a backend-only endpoint snapshot without resolved URL material.
pub fn snapshot(&self) -> crate::WsPoolClientSnapshot {
let mut roles = std::vec::Vec::new();
for role in &self.endpoint.roles {
@@ -88,7 +105,6 @@ impl crate::WsClient {
return crate::WsPoolClientSnapshot {
endpoint_name: self.endpoint.name.clone(),
provider: self.endpoint.provider.clone(),
endpoint_url: self.endpoint.url.clone(),
roles,
status: "idle".to_string(),
};
@@ -156,10 +172,10 @@ impl crate::WsClient {
};
let (mut stream, _response) = match connect_result {
std::result::Result::Ok(pair) => pair,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "connect_ws_endpoint", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, error = %error, "WebSocket endpoint connection failed");
std::result::Result::Err(_error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "connect_ws_endpoint", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, error_kind = "connect", "WebSocket endpoint connection failed");
return std::result::Result::Err(ks_core::Error::ws(format!(
"cannot connect websocket endpoint '{}': {error}",
"cannot connect websocket endpoint '{}'",
self.endpoint.name
)));
},
@@ -217,7 +233,7 @@ impl crate::WsClient {
match parse_result {
std::result::Result::Ok(response) => {
if let crate::JsonRpcResponse::Error(error_response) = &response {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_ws_json_rpc_once", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, response_kind = response.kind_name(), rpc_error_code = error_response.error.code, rpc_error_message = %error_response.error.message, "WebSocket JSON-RPC endpoint returned an RPC error");
tracing::error!(target: crate::TRACING_TARGET, action = "execute_ws_json_rpc_once", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, response_kind = response.kind_name(), rpc_error_code = error_response.error.code, "WebSocket JSON-RPC endpoint returned an RPC error");
} else {
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_ws_json_rpc_once", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, response_kind = response.kind_name(), "one-shot WebSocket JSON-RPC request completed");
}
@@ -334,6 +350,26 @@ mod tests {
assert!(client.can_handle("ws_any", "logs_subscribe_mentions"));
}
#[test]
fn debug_output_omits_endpoint_url() {
let mut endpoint = endpoint(true);
endpoint.url = "wss://WS-SECRET-CANARY.invalid/?api-key=secret".to_string();
let client = match crate::WsClient::new(endpoint) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
let rendered = format!("{client:?}");
assert!(rendered.contains("<redacted>"));
assert!(!rendered.contains("WS-SECRET-CANARY"));
assert!(!rendered.contains("api-key=secret"));
let snapshot = client.snapshot();
let snapshot_rendered = format!("{snapshot:?}");
assert!(!snapshot_rendered.contains("endpoint_url"));
assert!(!snapshot_rendered.contains("endpointUrl"));
assert!(!snapshot_rendered.contains("WS-SECRET-CANARY"));
assert!(!snapshot_rendered.contains("api-key=secret"));
}
#[test]
fn snapshot_preserves_endpoint_metadata() {
let client = match crate::WsClient::new(endpoint(true)) {
@@ -343,7 +379,6 @@ mod tests {
let snapshot = client.snapshot();
assert_eq!(snapshot.endpoint_name, "ws_a");
assert_eq!(snapshot.provider, "test");
assert_eq!(snapshot.endpoint_url, "wss://example.invalid");
assert_eq!(snapshot.roles.len(), 3);
}

View File

@@ -1,5 +1,5 @@
// file: ks-onchain-transport/src/ws_pool.rs
// version: 8
// version: 9
//! WebSocket endpoint pool and role-based routing.
@@ -53,7 +53,7 @@ impl crate::WsEndpointPool {
});
}
/// Returns a serializable snapshot of every endpoint in the pool.
/// Returns backend-only endpoint metadata without resolved URL material.
pub fn snapshot(&self) -> std::vec::Vec<crate::WsPoolClientSnapshot> {
let mut snapshots = std::vec::Vec::new();
for client in &self.clients {

View File

@@ -1,5 +1,5 @@
// file: ks-onchain-transport/src/ws_session.rs
// version: 6
// version: 9
//! Persistent multiplexed WebSocket session with bounded reconnect and resubscription.
@@ -100,15 +100,12 @@ pub struct WsSubscriptionSnapshot {
}
/// Current snapshot of one persistent WebSocket session.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Clone, Eq, PartialEq)]
pub struct WsSessionSnapshot {
/// Endpoint name.
pub endpoint_name: std::string::String,
/// Provider name.
pub provider: std::string::String,
/// Endpoint URL.
pub endpoint_url: std::string::String,
/// Current lifecycle state.
pub state: crate::WsSessionState,
/// Number of successful reconnects since session creation.
@@ -119,6 +116,20 @@ pub struct WsSessionSnapshot {
pub subscriptions: std::vec::Vec<crate::WsSubscriptionSnapshot>,
}
impl std::fmt::Debug for crate::WsSessionSnapshot {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("WsSessionSnapshot")
.field("endpoint_name", &self.endpoint_name)
.field("provider", &self.provider)
.field("state", &self.state)
.field("reconnect_count", &self.reconnect_count)
.field("capabilities", &self.capabilities)
.field("subscriptions", &self.subscriptions)
.finish();
}
}
/// Successful subscription acknowledgement.
#[derive(Clone, Debug, PartialEq)]
pub struct WsSubscriptionAck {
@@ -202,7 +213,6 @@ impl std::fmt::Debug for crate::WsSession {
.debug_struct("WsSession")
.field("endpoint_name", &self.endpoint.name)
.field("provider", &self.endpoint.provider)
.field("endpoint_url", &self.endpoint.url)
.finish_non_exhaustive();
}
}
@@ -238,7 +248,6 @@ impl crate::WsSession {
let snapshot = std::sync::Arc::new(tokio::sync::RwLock::new(crate::WsSessionSnapshot {
endpoint_name: endpoint.name.clone(),
provider: endpoint.provider.clone(),
endpoint_url: endpoint.url.clone(),
state: crate::WsSessionState::Connected,
reconnect_count: 0,
capabilities: initial_capabilities,
@@ -788,10 +797,8 @@ impl WsSessionRuntime {
async fn handle_error_response(&mut self, error_response: crate::JsonRpcErrorResponse) {
let request_id = error_response.id.as_u64();
let error = ks_core::Error::ws(format!(
"WebSocket JSON-RPC error {}: {}",
error_response.error.code, error_response.error.message
));
let error =
ks_core::Error::ws(format!("WebSocket JSON-RPC error {}", error_response.error.code));
if let std::option::Option::Some(request_id) = request_id {
if let std::option::Option::Some(pending) = self.pending.remove(&request_id) {
match pending {
@@ -852,10 +859,8 @@ impl WsSessionRuntime {
self.emit_diagnostic(
"ws_capability_disabled",
format!(
"endpoint '{}' rejected unstable method '{method}'; capability disabled for this session: JSON-RPC {} {}",
self.client.endpoint_name(),
error_response.error.code,
error_response.error.message
"endpoint '{}' rejected unstable method '{method}'; capability disabled for this session: JSON-RPC {}",
self.client.endpoint_name(), error_response.error.code
),
);
}
@@ -1180,8 +1185,8 @@ async fn connect_stream(client: &crate::WsClient) -> ks_core::Result<WsStream> {
};
return match connect_result {
std::result::Result::Ok((stream, _response)) => std::result::Result::Ok(stream),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::ws(format!(
"cannot connect WebSocket endpoint '{}': {error}",
std::result::Result::Err(_error) => std::result::Result::Err(ks_core::Error::ws(format!(
"cannot connect WebSocket endpoint '{}'",
client.endpoint_name()
))),
};
@@ -1281,6 +1286,21 @@ mod tests {
);
}
#[test]
fn session_snapshot_debug_contains_no_endpoint_url_field() {
let snapshot = crate::WsSessionSnapshot {
endpoint_name: "helius".to_string(),
provider: "helius".to_string(),
state: crate::WsSessionState::Connected,
reconnect_count: 0,
capabilities: crate::StandardWsCapabilities::default(),
subscriptions: std::vec::Vec::new(),
};
let rendered = format!("{snapshot:?}");
assert!(!rendered.contains("endpoint_url"));
assert!(!rendered.contains("endpointUrl"));
}
#[tokio::test]
async fn persistent_session_multiplexes_notification_and_explicit_unsubscribe() {
let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await {