v0.3.15-pre.002

This commit is contained in:
2026-09-13 06:07:42 +02:00
parent 84ef8bb1f1
commit 8ea261d3a4
7 changed files with 532 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
<!-- version: 26 -->
<!-- version: 27 -->
# Utilisation de `ksp-onchain-transport-lib`
@@ -82,7 +82,14 @@ let endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
ws_url,
ksp_onchain_transport_lib::WsSessionSettings::default(),
);
)
.with_subscription_capabilities(vec![ksp_onchain_transport_lib::WsSubscriptionKind::Logs]);
if !endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs) {
return Err(ksp_core_lib::Error::new(
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
"required WebSocket subscription capability is not declared",
));
}
let session = match ksp_onchain_transport_lib::WsSession::connect(endpoint).await {
Ok(value) => value,
Err(error) => return Err(error),
@@ -90,6 +97,8 @@ let session = match ksp_onchain_transport_lib::WsSession::connect(endpoint).awai
let snapshot = session.snapshot();
```
`with_subscription_capabilities(...)` porte une preuve explicite et typed des familles déclarées pour cet endpoint. Une déclaration vide, dupliquée ou incompatible avec `WsProtocolKind` est rejetée par `WsTransportSettings::validate()`. Un endpoint construit sans déclaration reste valide pour compatibilité, mais `subscription_capabilities()` retourne `None` et `supports_subscription(...)` retourne `false` : l'absence de déclaration n'est jamais une preuve implicite de support.
Deux appels `WsSession::connect` avec le même endpoint créent volontairement deux connexions physiques distinctes. Il n'existe encore aucun pool de sessions automatique.
Le socket brut et la primitive JSON-RPC générique ne sont pas publics. Le moteur générique de subscription typed reste `pub(crate)` ; il ne constitue donc pas une escape hatch provider-specific.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/ws_settings.rs
// version: 4
// version: 5
const DEFAULT_WS_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const DEFAULT_WS_COMMAND_QUEUE_CAPACITY: usize = 128;
@@ -401,6 +401,7 @@ pub struct WsEndpointSettings {
provider: crate::WsProviderName,
cluster: crate::WsClusterName,
protocol: crate::WsProtocolKind,
subscription_capabilities: std::option::Option<std::vec::Vec<crate::WsSubscriptionKind>>,
url: crate::WsEndpointUrl,
session: crate::WsSessionSettings,
}
@@ -417,7 +418,42 @@ impl WsEndpointSettings {
url: crate::WsEndpointUrl,
session: crate::WsSessionSettings,
) -> Self {
return Self { name: name.into(), enabled, provider, cluster, protocol, url, session };
return Self {
name: name.into(),
enabled,
provider,
cluster,
protocol,
subscription_capabilities: std::option::Option::None,
url,
session,
};
}
/// Declares the exact WebSocket subscription families supported by this endpoint.
///
/// An endpoint created without this builder remains valid for backward compatibility, but its capabilities are considered undeclared and therefore
/// never constitute positive support evidence for a subscription family. Validation rejects an explicitly declared empty set, duplicates and
/// capabilities that cannot belong to the selected protocol surface.
#[must_use]
pub fn with_subscription_capabilities(mut self, capabilities: std::vec::Vec<crate::WsSubscriptionKind>) -> Self {
self.subscription_capabilities = std::option::Option::Some(capabilities);
return self;
}
/// Returns the explicitly declared WebSocket subscription capabilities, or `None` when the endpoint still uses the legacy undeclared form.
#[must_use]
pub fn subscription_capabilities(&self) -> std::option::Option<&[crate::WsSubscriptionKind]> {
return self.subscription_capabilities.as_deref();
}
/// Returns whether this endpoint explicitly declares support for one WebSocket subscription family.
///
/// Undeclared legacy capability state is fail-closed and therefore returns `false`.
#[must_use]
pub fn supports_subscription(&self, kind: crate::WsSubscriptionKind) -> bool {
return ws_protocol_supports_subscription(self.protocol, kind)
&& self.subscription_capabilities().is_some_and(|capabilities| return capabilities.contains(&kind));
}
/// Returns the logical endpoint name.
@@ -532,6 +568,32 @@ fn validate_ws_endpoint(endpoint: &crate::WsEndpointSettings, endpoint_index: us
if let std::result::Result::Err(error) = validate_ws_descriptor(endpoint.cluster().as_str(), cluster_field.as_str()) {
return std::result::Result::Err(error);
}
if let std::result::Result::Some(capabilities) = endpoint.subscription_capabilities() {
if capabilities.is_empty() {
return ws_invalid_settings(
"explicit WebSocket subscription capabilities must not be empty",
format!("ws_endpoints[{endpoint_index}].subscription_capabilities").as_str(),
);
}
for (capability_index, capability) in capabilities.iter().copied().enumerate() {
let field = format!("ws_endpoints[{endpoint_index}].subscription_capabilities[{capability_index}]");
if !ws_protocol_supports_subscription(endpoint.protocol(), capability) {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket subscription capability is incompatible with endpoint protocol")
.with_context("field", field)
.with_context("subscription_kind", capability.as_str())
.with_context("protocol", endpoint.protocol().as_str()),
);
}
if capabilities[..capability_index].contains(&capability) {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket subscription capabilities must be unique")
.with_context("field", field)
.with_context("subscription_kind", capability.as_str()),
);
}
}
}
if let std::result::Result::Err(error) = endpoint.session().validate() {
return std::result::Result::Err(error);
}
@@ -547,6 +609,34 @@ fn validate_ws_endpoint(endpoint: &crate::WsEndpointSettings, endpoint_index: us
return std::result::Result::Ok(());
}
const fn ws_protocol_supports_subscription(protocol: crate::WsProtocolKind, kind: crate::WsSubscriptionKind) -> bool {
return match protocol {
crate::WsProtocolKind::SolanaStandard => matches!(
kind,
crate::WsSubscriptionKind::Account
| crate::WsSubscriptionKind::Block
| crate::WsSubscriptionKind::Logs
| crate::WsSubscriptionKind::Program
| crate::WsSubscriptionKind::Root
| crate::WsSubscriptionKind::Signature
| crate::WsSubscriptionKind::Slot
| crate::WsSubscriptionKind::SlotsUpdates
| crate::WsSubscriptionKind::Vote
),
crate::WsProtocolKind::HeliusLaserStream => matches!(
kind,
crate::WsSubscriptionKind::Account
| crate::WsSubscriptionKind::Logs
| crate::WsSubscriptionKind::Program
| crate::WsSubscriptionKind::Root
| crate::WsSubscriptionKind::Signature
| crate::WsSubscriptionKind::Slot
| crate::WsSubscriptionKind::SlotsUpdates
| crate::WsSubscriptionKind::HeliusTransaction
),
};
}
fn validate_ws_descriptor(value: &str, field: &str) -> ksp_core_lib::Result<()> {
if value.trim().is_empty() {
return ws_invalid_settings("WebSocket transport descriptor must not be empty", field);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 54
// version: 55
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -1057,3 +1057,21 @@ fn public_v0_3_13_pre_002_yellowstone_subscribe_identity_is_opaque_and_available
assert!(!debug.contains("bytes:"));
return;
}
#[test]
fn public_v0_3_15_pre_002_websocket_endpoint_capability_contract_is_available_from_crate_root() {
let endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
"typed_public",
true,
ksp_onchain_transport_lib::WsProviderName::new("public"),
ksp_onchain_transport_lib::WsClusterName::new("devnet"),
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
ksp_onchain_transport_lib::WsEndpointUrl::parse("wss://api.devnet.solana.com").expect("public WebSocket URL fixture must parse"),
ksp_onchain_transport_lib::WsSessionSettings::default(),
)
.with_subscription_capabilities(std::vec![ksp_onchain_transport_lib::WsSubscriptionKind::Logs, ksp_onchain_transport_lib::WsSubscriptionKind::Block,]);
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
assert!(!endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction));
assert_eq!(endpoint.subscription_capabilities().map(|capabilities| return capabilities.len()), std::option::Option::Some(2));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 46
// version: 47
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
@@ -1399,3 +1399,16 @@ fn release_v0_3_14_pre_004_native_replay_evidence_never_conflates_attempt_delive
assert!(!stream_source.contains("replay_coverage_proven_count"));
return;
}
#[test]
fn release_v0_3_15_pre_002_websocket_capabilities_are_endpoint_declared_typed_and_protocol_bounded() {
let settings_source = include_str!("../src/ws_settings.rs");
assert!(settings_source.contains("subscription_capabilities: std::option::Option<std::vec::Vec<crate::WsSubscriptionKind>>"));
assert!(settings_source.contains("pub fn with_subscription_capabilities"));
assert!(settings_source.contains("pub fn subscription_capabilities"));
assert!(settings_source.contains("pub fn supports_subscription"));
assert!(settings_source.contains("crate::WsProtocolKind::SolanaStandard => matches!("));
assert!(settings_source.contains("crate::WsProtocolKind::HeliusLaserStream => matches!("));
assert!(!settings_source.contains("provider().as_str() == \"helius\""));
assert!(!settings_source.contains("provider().as_str() == \"publicnode\""));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs
// version: 2
// version: 3
fn valid_endpoint(name: &str, url_text: &str) -> crate::WsEndpointSettings {
return crate::WsEndpointSettings::new(
@@ -148,3 +148,80 @@ fn websocket_transport_settings_debug_never_exposes_endpoint_url() {
assert!(!rendered.contains("provider.invalid"));
assert!(!rendered.contains("password"));
}
#[test]
fn v0_3_15_pre_002_undeclared_websocket_capabilities_fail_closed_without_breaking_legacy_settings() {
let endpoint = valid_endpoint("legacy", "wss://legacy.invalid/ws");
assert_eq!(endpoint.subscription_capabilities(), std::option::Option::None);
assert!(!endpoint.supports_subscription(crate::WsSubscriptionKind::Logs));
assert!(!endpoint.supports_subscription(crate::WsSubscriptionKind::Block));
let settings = crate::WsTransportSettings::new(std::vec![endpoint]);
assert!(settings.validate().is_ok());
}
#[test]
fn v0_3_15_pre_002_standard_endpoint_declares_exact_typed_subscription_capabilities() {
let endpoint = valid_endpoint("typed", "wss://typed.invalid/ws")
.with_subscription_capabilities(std::vec![crate::WsSubscriptionKind::Logs, crate::WsSubscriptionKind::Block,]);
assert_eq!(
endpoint.subscription_capabilities(),
std::option::Option::Some([crate::WsSubscriptionKind::Logs, crate::WsSubscriptionKind::Block].as_slice())
);
assert!(endpoint.supports_subscription(crate::WsSubscriptionKind::Logs));
assert!(endpoint.supports_subscription(crate::WsSubscriptionKind::Block));
assert!(!endpoint.supports_subscription(crate::WsSubscriptionKind::Vote));
assert!(!endpoint.supports_subscription(crate::WsSubscriptionKind::HeliusTransaction));
let settings = crate::WsTransportSettings::new(std::vec![endpoint]);
assert!(settings.validate().is_ok());
}
#[test]
fn v0_3_15_pre_002_explicit_capability_set_rejects_empty_and_duplicate_entries() {
let empty = valid_endpoint("empty", "wss://empty.invalid/ws").with_subscription_capabilities(std::vec![]);
let empty_error = crate::WsTransportSettings::new(std::vec![empty]).validate().expect_err("explicit empty capabilities must fail");
assert_eq!(empty_error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
let duplicate = valid_endpoint("duplicate_caps", "wss://duplicate.invalid/ws")
.with_subscription_capabilities(std::vec![crate::WsSubscriptionKind::Logs, crate::WsSubscriptionKind::Logs,]);
let duplicate_error = crate::WsTransportSettings::new(std::vec![duplicate]).validate().expect_err("duplicate capabilities must fail");
assert_eq!(duplicate_error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
}
#[test]
fn v0_3_15_pre_002_capabilities_must_belong_to_selected_protocol_surface() {
let standard =
valid_endpoint("standard_bad", "wss://standard.invalid/ws").with_subscription_capabilities(std::vec![crate::WsSubscriptionKind::HeliusTransaction]);
assert!(!standard.supports_subscription(crate::WsSubscriptionKind::HeliusTransaction));
let standard_error =
crate::WsTransportSettings::new(std::vec![standard]).validate().expect_err("standard protocol must reject Helius transaction capability");
assert_eq!(standard_error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
let helius_block = crate::WsEndpointSettings::new(
"helius_block",
true,
crate::WsProviderName::new("helius"),
crate::WsClusterName::new("mainnet"),
crate::WsProtocolKind::HeliusLaserStream,
crate::WsEndpointUrl::parse("wss://helius.invalid/ws").expect("test URL must parse"),
crate::WsSessionSettings::default(),
)
.with_subscription_capabilities(std::vec![crate::WsSubscriptionKind::Block]);
assert!(!helius_block.supports_subscription(crate::WsSubscriptionKind::Block));
let helius_block_error =
crate::WsTransportSettings::new(std::vec![helius_block]).validate().expect_err("Helius protocol must reject unsupported block capability");
assert_eq!(helius_block_error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
let helius_supported = crate::WsEndpointSettings::new(
"helius_supported",
true,
crate::WsProviderName::new("helius"),
crate::WsClusterName::new("mainnet"),
crate::WsProtocolKind::HeliusLaserStream,
crate::WsEndpointUrl::parse("wss://helius-supported.invalid/ws").expect("test URL must parse"),
crate::WsSessionSettings::default(),
)
.with_subscription_capabilities(std::vec![crate::WsSubscriptionKind::Logs, crate::WsSubscriptionKind::HeliusTransaction]);
assert!(helius_supported.supports_subscription(crate::WsSubscriptionKind::Logs));
assert!(helius_supported.supports_subscription(crate::WsSubscriptionKind::HeliusTransaction));
assert!(!helius_supported.supports_subscription(crate::WsSubscriptionKind::Block));
assert!(!helius_supported.supports_subscription(crate::WsSubscriptionKind::Vote));
let helius_settings = crate::WsTransportSettings::new(std::vec![helius_supported]);
assert!(helius_settings.validate().is_ok());
}