v0.5.1-pre.008
This commit is contained in:
@@ -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 d’inclure une URL résolue. Les corps HTTP non-success et messages JSON-RPC distants ne sont plus recopiés dans les erreurs/logs afin qu’un fournisseur ne puisse pas y réinjecter une URL ou un credential. Le getter d’URL 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 d’endpoint 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 d’exécution sur le déplacement des autorisations `*_send_enabled` vers `ExecutionConfig` ; le comportement RPC reste inchangé.
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user