v0.3.15-pre.002
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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\""));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user