diff --git a/.env.example b/.env.example index a7f4116..753bdab 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # file: .env.example -# version: 4 +# version: 5 # KSP Logging root directory. Used by config/std.logging.json for relative log output paths. # The current Config document fallback is "logs" when neither the process environment nor .env defines this variable. @@ -22,10 +22,22 @@ KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com # The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent. KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com +# Optional public Solana Devnet WebSocket endpoint override used by config/std.transport.json. +# The committed Transport document falls back to wss://api.devnet.solana.com when this variable is absent. +KSP_PUBLIC_SOLANA_DEVNET_WS_URL=wss://api.devnet.solana.com + +# Optional public Solana Mainnet WebSocket endpoint override used by config/std.transport.json and its example. +# The committed Transport document falls back to wss://api.mainnet-beta.solana.com when this variable is absent. +KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet-beta.solana.com + # Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected. # Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON. # KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me +# Optional complete private-provider WebSocket endpoint URL used only by the Transport example when explicitly selected. +# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON. +# KSP_SECRET_SOLANA_WS_URL=wss://provider.example/?api-key=replace-me + # Fade-in duration in milliseconds used by the common KSP desk splash lifecycle. KSP_DESK_SPLASH_FADE_IN_MS=300 diff --git a/Cargo.toml b/Cargo.toml index fb2fadc..2cf765c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 194 +# version: 195 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.2.7-pre.2.fix.1" +version = "0.2.7-pre.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/config/examples/std.transport.example.json b/config/examples/std.transport.example.json index d87ffdf..76f9cff 100644 --- a/config/examples/std.transport.example.json +++ b/config/examples/std.transport.example.json @@ -1,10 +1,27 @@ { - "format_version": 1, + "format_version": 2, "retry": { "max_retries": 3, "initial_backoff_ms": 150, "max_backoff_ms": 3000 }, + "ws_defaults": { + "command_timeout_ms": 10000, + "close_timeout_ms": 5000, + "reconnect": { + "max_retries": 5, + "initial_backoff_ms": 250, + "max_backoff_ms": 5000 + }, + "resubscribe": "active_subscriptions", + "command_queue_capacity": 128, + "notification_queue_capacity": 256, + "max_active_subscriptions": 1024, + "max_pending_requests": 128, + "max_message_size_bytes": 67108864, + "max_frame_size_bytes": 16777216, + "max_write_buffer_size_bytes": 1048576 + }, "default_profile": "mainnet_mixed", "profiles": [ { @@ -23,7 +40,9 @@ { "role": "default", "enabled": true, - "request_kinds": ["*"], + "request_kinds": [ + "*" + ], "priority": 200, "limits": { "requests_per_second": 5, @@ -47,7 +66,9 @@ { "role": "default", "enabled": true, - "request_kinds": ["*"], + "request_kinds": [ + "*" + ], "priority": 100, "limits": { "requests_per_second": 20, @@ -58,6 +79,28 @@ } ] } + ], + "ws_endpoints": [ + { + "name": "mainnet_public_ws", + "enabled": true, + "provider": "solana-public", + "cluster": "mainnet-beta", + "kind": "solana_standard", + "url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}" + }, + { + "name": "mainnet_private_ws", + "enabled": true, + "provider": "private-provider", + "cluster": "mainnet-beta", + "kind": "solana_standard", + "url": "${KSP_SECRET_SOLANA_WS_URL:-wss://example.invalid}", + "session": { + "notification_queue_capacity": 512, + "max_active_subscriptions": 2048 + } + } ] } ] diff --git a/config/schemas/std.transport.schema.json b/config/schemas/std.transport.schema.json index 2515611..3b45cf2 100644 --- a/config/schemas/std.transport.schema.json +++ b/config/schemas/std.transport.schema.json @@ -1,20 +1,15 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:ksp:schema:std.transport:v1", - "title": "KSP standard HTTP Transport configuration", - "type": "object", - "additionalProperties": false, - "required": ["format_version", "retry", "default_profile", "profiles"], - "properties": { - "format_version": {"const": 1}, - "retry": {"$ref": "#/$defs/retry"}, - "default_profile": {"$ref": "#/$defs/profileId"}, - "profiles": { - "type": "array", - "minItems": 1, - "items": {"$ref": "#/$defs/profile"} + "$id": "urn:ksp:schema:std.transport:v2", + "title": "KSP standard HTTP + WebSocket Transport configuration", + "oneOf": [ + { + "$ref": "#/$defs/documentV1" + }, + { + "$ref": "#/$defs/documentV2" } - }, + ], "$defs": { "profileId": { "type": "string", @@ -35,50 +30,99 @@ "minimum": 1, "maximum": 4294967295 }, + "positiveUsize": { + "type": "integer", + "minimum": 1, + "maximum": 4294967295 + }, "retry": { "type": "object", "additionalProperties": false, - "required": ["max_retries", "initial_backoff_ms", "max_backoff_ms"], + "required": [ + "max_retries", + "initial_backoff_ms", + "max_backoff_ms" + ], "properties": { - "max_retries": {"type": "integer", "minimum": 0, "maximum": 100}, - "initial_backoff_ms": {"$ref": "#/$defs/positiveMs"}, - "max_backoff_ms": {"$ref": "#/$defs/positiveMs"} + "max_retries": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "initial_backoff_ms": { + "$ref": "#/$defs/positiveMs" + }, + "max_backoff_ms": { + "$ref": "#/$defs/positiveMs" + } } }, "limits": { "type": "object", "additionalProperties": false, "properties": { - "requests_per_second": {"$ref": "#/$defs/positiveU32"}, - "burst_capacity": {"$ref": "#/$defs/positiveU32"}, - "max_concurrent_requests": {"$ref": "#/$defs/positiveU32"}, - "pause_after_rate_limit_ms": {"$ref": "#/$defs/positiveMs"} + "requests_per_second": { + "$ref": "#/$defs/positiveU32" + }, + "burst_capacity": { + "$ref": "#/$defs/positiveU32" + }, + "max_concurrent_requests": { + "$ref": "#/$defs/positiveU32" + }, + "pause_after_rate_limit_ms": { + "$ref": "#/$defs/positiveMs" + } } }, "role": { "type": "object", "additionalProperties": false, - "required": ["role", "enabled", "request_kinds", "priority", "limits"], + "required": [ + "role", + "enabled", + "request_kinds", + "priority", + "limits" + ], "properties": { - "role": {"$ref": "#/$defs/descriptor"}, - "enabled": {"type": "boolean"}, + "role": { + "$ref": "#/$defs/descriptor" + }, + "enabled": { + "type": "boolean" + }, "request_kinds": { "type": "array", "minItems": 1, "uniqueItems": true, - "items": {"$ref": "#/$defs/descriptor"}, + "items": { + "$ref": "#/$defs/descriptor" + }, "allOf": [ { - "if": {"contains": {"const": "*"}}, - "then": {"maxItems": 1} + "if": { + "contains": { + "const": "*" + } + }, + "then": { + "maxItems": 1 + } } ] }, - "priority": {"type": "integer", "minimum": 0, "maximum": 4294967295}, - "limits": {"$ref": "#/$defs/limits"} + "priority": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + }, + "limits": { + "$ref": "#/$defs/limits" + } } }, - "endpoint": { + "httpEndpoint": { "type": "object", "additionalProperties": false, "required": [ @@ -92,31 +136,322 @@ "roles" ], "properties": { - "name": {"$ref": "#/$defs/descriptor"}, - "enabled": {"type": "boolean"}, - "provider": {"$ref": "#/$defs/descriptor"}, - "cluster": {"$ref": "#/$defs/descriptor"}, - "url": {"type": "string", "minLength": 1}, - "connect_timeout_ms": {"$ref": "#/$defs/positiveMs"}, - "request_timeout_ms": {"$ref": "#/$defs/positiveMs"}, - "max_idle_connections_per_host": {"type": "integer", "minimum": 1}, + "name": { + "$ref": "#/$defs/descriptor" + }, + "enabled": { + "type": "boolean" + }, + "provider": { + "$ref": "#/$defs/descriptor" + }, + "cluster": { + "$ref": "#/$defs/descriptor" + }, + "url": { + "type": "string", + "minLength": 1 + }, + "connect_timeout_ms": { + "$ref": "#/$defs/positiveMs" + }, + "request_timeout_ms": { + "$ref": "#/$defs/positiveMs" + }, + "max_idle_connections_per_host": { + "type": "integer", + "minimum": 1 + }, "roles": { "type": "array", "minItems": 1, - "items": {"$ref": "#/$defs/role"} + "items": { + "$ref": "#/$defs/role" + } } } }, - "profile": { + "wsReconnect": { "type": "object", "additionalProperties": false, - "required": ["profile_id", "endpoints"], + "required": [ + "max_retries", + "initial_backoff_ms", + "max_backoff_ms" + ], "properties": { - "profile_id": {"$ref": "#/$defs/profileId"}, + "max_retries": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "initial_backoff_ms": { + "$ref": "#/$defs/positiveMs" + }, + "max_backoff_ms": { + "$ref": "#/$defs/positiveMs" + } + } + }, + "wsReconnectOverride": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "max_retries": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "initial_backoff_ms": { + "$ref": "#/$defs/positiveMs" + }, + "max_backoff_ms": { + "$ref": "#/$defs/positiveMs" + } + } + }, + "wsSession": { + "type": "object", + "additionalProperties": false, + "required": [ + "command_timeout_ms", + "close_timeout_ms", + "reconnect", + "resubscribe", + "command_queue_capacity", + "notification_queue_capacity", + "max_active_subscriptions", + "max_pending_requests", + "max_message_size_bytes", + "max_frame_size_bytes", + "max_write_buffer_size_bytes" + ], + "properties": { + "command_timeout_ms": { + "$ref": "#/$defs/positiveMs" + }, + "close_timeout_ms": { + "$ref": "#/$defs/positiveMs" + }, + "reconnect": { + "$ref": "#/$defs/wsReconnect" + }, + "resubscribe": { + "enum": [ + "never", + "active_subscriptions" + ] + }, + "command_queue_capacity": { + "$ref": "#/$defs/positiveUsize" + }, + "notification_queue_capacity": { + "$ref": "#/$defs/positiveUsize" + }, + "max_active_subscriptions": { + "$ref": "#/$defs/positiveUsize" + }, + "max_pending_requests": { + "$ref": "#/$defs/positiveUsize" + }, + "max_message_size_bytes": { + "$ref": "#/$defs/positiveUsize" + }, + "max_frame_size_bytes": { + "$ref": "#/$defs/positiveUsize" + }, + "max_write_buffer_size_bytes": { + "$ref": "#/$defs/positiveUsize" + } + } + }, + "wsSessionOverride": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "command_timeout_ms": { + "$ref": "#/$defs/positiveMs" + }, + "close_timeout_ms": { + "$ref": "#/$defs/positiveMs" + }, + "reconnect": { + "$ref": "#/$defs/wsReconnectOverride" + }, + "resubscribe": { + "enum": [ + "never", + "active_subscriptions" + ] + }, + "command_queue_capacity": { + "$ref": "#/$defs/positiveUsize" + }, + "notification_queue_capacity": { + "$ref": "#/$defs/positiveUsize" + }, + "max_active_subscriptions": { + "$ref": "#/$defs/positiveUsize" + }, + "max_pending_requests": { + "$ref": "#/$defs/positiveUsize" + }, + "max_message_size_bytes": { + "$ref": "#/$defs/positiveUsize" + }, + "max_frame_size_bytes": { + "$ref": "#/$defs/positiveUsize" + }, + "max_write_buffer_size_bytes": { + "$ref": "#/$defs/positiveUsize" + } + } + }, + "wsEndpoint": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "enabled", + "provider", + "cluster", + "kind", + "url" + ], + "properties": { + "name": { + "$ref": "#/$defs/descriptor" + }, + "enabled": { + "type": "boolean" + }, + "provider": { + "$ref": "#/$defs/descriptor" + }, + "cluster": { + "$ref": "#/$defs/descriptor" + }, + "kind": { + "enum": [ + "solana_standard" + ] + }, + "url": { + "type": "string", + "minLength": 1 + }, + "session": { + "$ref": "#/$defs/wsSessionOverride" + } + } + }, + "profileV1": { + "type": "object", + "additionalProperties": false, + "required": [ + "profile_id", + "endpoints" + ], + "properties": { + "profile_id": { + "$ref": "#/$defs/profileId" + }, "endpoints": { "type": "array", "minItems": 1, - "items": {"$ref": "#/$defs/endpoint"} + "items": { + "$ref": "#/$defs/httpEndpoint" + } + } + } + }, + "profileV2": { + "type": "object", + "additionalProperties": false, + "required": [ + "profile_id", + "endpoints", + "ws_endpoints" + ], + "properties": { + "profile_id": { + "$ref": "#/$defs/profileId" + }, + "endpoints": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/httpEndpoint" + } + }, + "ws_endpoints": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/wsEndpoint" + } + } + } + }, + "documentV1": { + "type": "object", + "additionalProperties": false, + "required": [ + "format_version", + "retry", + "default_profile", + "profiles" + ], + "properties": { + "format_version": { + "const": 1 + }, + "retry": { + "$ref": "#/$defs/retry" + }, + "default_profile": { + "$ref": "#/$defs/profileId" + }, + "profiles": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/profileV1" + } + } + } + }, + "documentV2": { + "type": "object", + "additionalProperties": false, + "required": [ + "format_version", + "retry", + "ws_defaults", + "default_profile", + "profiles" + ], + "properties": { + "format_version": { + "const": 2 + }, + "retry": { + "$ref": "#/$defs/retry" + }, + "ws_defaults": { + "$ref": "#/$defs/wsSession" + }, + "default_profile": { + "$ref": "#/$defs/profileId" + }, + "profiles": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/profileV2" + } } } } diff --git a/config/std.transport.json b/config/std.transport.json index 4cbf365..eff4964 100644 --- a/config/std.transport.json +++ b/config/std.transport.json @@ -1,10 +1,27 @@ { - "format_version": 1, + "format_version": 2, "retry": { "max_retries": 2, "initial_backoff_ms": 100, "max_backoff_ms": 2000 }, + "ws_defaults": { + "command_timeout_ms": 10000, + "close_timeout_ms": 5000, + "reconnect": { + "max_retries": 5, + "initial_backoff_ms": 250, + "max_backoff_ms": 5000 + }, + "resubscribe": "active_subscriptions", + "command_queue_capacity": 128, + "notification_queue_capacity": 256, + "max_active_subscriptions": 1024, + "max_pending_requests": 128, + "max_message_size_bytes": 67108864, + "max_frame_size_bytes": 16777216, + "max_write_buffer_size_bytes": 1048576 + }, "default_profile": "devnet_public", "profiles": [ { @@ -23,7 +40,9 @@ { "role": "default", "enabled": true, - "request_kinds": ["*"], + "request_kinds": [ + "*" + ], "priority": 100, "limits": { "requests_per_second": 5, @@ -34,6 +53,16 @@ } ] } + ], + "ws_endpoints": [ + { + "name": "solana_devnet_public_ws", + "enabled": true, + "provider": "solana-public", + "cluster": "devnet", + "kind": "solana_standard", + "url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}" + } ] }, { @@ -52,7 +81,9 @@ { "role": "default", "enabled": true, - "request_kinds": ["*"], + "request_kinds": [ + "*" + ], "priority": 100, "limits": { "requests_per_second": 5, @@ -63,6 +94,16 @@ } ] } + ], + "ws_endpoints": [ + { + "name": "solana_mainnet_public_ws", + "enabled": true, + "provider": "solana-public", + "cluster": "mainnet-beta", + "kind": "solana_standard", + "url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}" + } ] } ] diff --git a/crates/ksp-config-lib/README.md b/crates/ksp-config-lib/README.md index 108c64c..ef2471d 100644 --- a/crates/ksp-config-lib/README.md +++ b/crates/ksp-config-lib/README.md @@ -1,5 +1,5 @@ - + # ksp-config-lib @@ -23,7 +23,7 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio - la classification `Public`, `Internal`, `Secret` ; - les représentations réelle et sûre/redacted ainsi que la provenance des valeurs résolues ; - l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ; -- l'adapter du document HTTP Transport effectif vers `ksp_onchain_transport_lib::HttpTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ; +- l'adapter du document Transport V1/V2 vers `HttpTransportSettings` et, en V2, `WsTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ; - la surface de management pour inspecter et réparer les sources Config enregistrées, modifier `std.logging.json`, consulter les rapports d'environnement, révéler explicitement une valeur réelle et modifier `.env` ; - les écritures atomiques JSON/`.env` et la protection des permissions `.env` ; - les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`. @@ -81,7 +81,7 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées. -Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON. +Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs HTTP et WebSocket : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON. ## Documentation @@ -89,7 +89,7 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur - [`TODO.md`](TODO.md) — points explicitement différés après `0.1.3` ; - [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ; - [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ; -- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ; +- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V2 HTTP + WebSocket, avec lecture backward du V1 HTTP-only ; - [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ; - [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ; - [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime. diff --git a/crates/ksp-config-lib/USAGE.md b/crates/ksp-config-lib/USAGE.md index 82f3631..d7ccfcf 100644 --- a/crates/ksp-config-lib/USAGE.md +++ b/crates/ksp-config-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-config-lib @@ -125,7 +125,7 @@ Un `logs_directory` relatif est ancré sur le current working directory du proce Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation. -### 4.1 Construire le Transport HTTP depuis Config +### 4.1 Construire le Transport HTTP + WebSocket depuis Config Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` : @@ -135,12 +135,18 @@ let transport = match engine.load_resolved_transport_config(std::option::Option: std::result::Result::Err(error) => return std::result::Result::Err(error), }; -let transport_settings = transport.into_settings(); +let http_settings = transport.http_settings(); +let ws_settings = transport.ws_settings(); +let _ = (http_settings, ws_settings); ``` -Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted. +`std.transport` V2 conserve `retry` et `profiles[].endpoints[]` pour HTTP, ajoute `ws_defaults` et `profiles[].ws_endpoints[]`, puis exige `kind = "solana_standard"` en `0.2.7`. Un `ws_endpoints[].session` optionnel surcharge seulement les paramètres génériques de `WsSessionSettings`. -La dépendance reste unidirectionnelle : Config connaît le contrat Transport pour le construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP. +Le même schema enregistré conserve la lecture stricte du V1 historique : dans ce cas `http_settings()` reste disponible et `ws_settings()` retourne `None`. Aucun `WsTransportSettings` vide n'est inventé pour simuler l'absence de WebSocket. + +Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs HTTP et WebSocket peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted. + +La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP. ### 4.2 Résoudre le répertoire Wallet depuis Config diff --git a/crates/ksp-config-lib/src/lib.rs b/crates/ksp-config-lib/src/lib.rs index 7219c86..f3aa9c8 100644 --- a/crates/ksp-config-lib/src/lib.rs +++ b/crates/ksp-config-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/src/lib.rs -// version: 16 +// version: 17 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -154,9 +154,9 @@ pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME; pub use self::registry::DEFAULT_STD_LOGGING_FILENAME; /// Default physical filename for the standard Logging JSON Schema document. pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME; -/// Default physical filename for the standard HTTP Transport configuration document. +/// Default physical filename for the standard HTTP + WebSocket Transport configuration document. pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME; -/// Default physical filename for the standard HTTP Transport JSON Schema document. +/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document. pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME; /// Default physical filename for the standard Wallet configuration document. pub use self::registry::DEFAULT_STD_WALLET_FILENAME; @@ -168,13 +168,13 @@ pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK; pub use self::registry::FILE_ID_SCHEMA_COMPOSITE; /// Logical file identifier for the standard Logging JSON Schema document. pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING; -/// Logical file identifier for the standard HTTP Transport JSON Schema document. +/// Logical file identifier for the standard HTTP + WebSocket Transport JSON Schema document. pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT; /// Logical file identifier for the standard Wallet JSON Schema document. pub use self::registry::FILE_ID_SCHEMA_STD_WALLET; /// Logical file identifier for the standard Logging configuration document. pub use self::registry::FILE_ID_STD_LOGGING; -/// Logical file identifier for the standard HTTP Transport configuration document. +/// Logical file identifier for the standard HTTP + WebSocket Transport configuration document. pub use self::registry::FILE_ID_STD_TRANSPORT; /// Logical file identifier for the standard Wallet configuration document. pub use self::registry::FILE_ID_STD_WALLET; @@ -188,7 +188,7 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE; pub use self::sensitivity::ResolvedConfigJson; /// One resolved Config string preserving real/safe representations and provenance. pub use self::sensitivity::ResolvedConfigText; -/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`. +/// Effective standard Transport configuration mapped to HTTP and optional WebSocket runtime settings. pub use self::transport::ResolvedTransportConfig; /// Effective standard Wallet configuration resolved to validated filesystem roots. pub use self::wallet::ResolvedWalletConfig; diff --git a/crates/ksp-config-lib/src/registry.rs b/crates/ksp-config-lib/src/registry.rs index 141ae5c..e78889e 100644 --- a/crates/ksp-config-lib/src/registry.rs +++ b/crates/ksp-config-lib/src/registry.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/src/registry.rs -// version: 8 +// version: 9 /// Bootstrap argument used to replace a known Config filename mapping. pub const ARG_FILE_MAP: &str = "--filemap"; @@ -11,9 +11,9 @@ pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json"; pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json"; /// Default physical filename for the standard Logging JSON Schema document. pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json"; -/// Default physical filename for the standard HTTP Transport configuration document. +/// Default physical filename for the standard HTTP + WebSocket Transport configuration document. pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json"; -/// Default physical filename for the standard HTTP Transport JSON Schema document. +/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document. pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json"; /// Default physical filename for the standard Wallet configuration document. pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json"; @@ -25,13 +25,13 @@ pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-w pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite"; /// Logical file identifier for the standard Logging JSON Schema document. pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging"; -/// Logical file identifier for the standard HTTP Transport JSON Schema document. +/// Logical file identifier for the standard HTTP + WebSocket Transport JSON Schema document. pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport"; /// Logical file identifier for the standard Wallet JSON Schema document. pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet"; /// Logical file identifier for the standard Logging configuration document. pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging"; -/// Logical file identifier for the standard HTTP Transport configuration document. +/// Logical file identifier for the standard HTTP + WebSocket Transport configuration document. pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport"; /// Logical file identifier for the standard Wallet configuration document. pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet"; diff --git a/crates/ksp-config-lib/src/transport.rs b/crates/ksp-config-lib/src/transport.rs index 9bcff72..0838258 100644 --- a/crates/ksp-config-lib/src/transport.rs +++ b/crates/ksp-config-lib/src/transport.rs @@ -1,7 +1,7 @@ // file: crates/ksp-config-lib/src/transport.rs -// version: 2 +// version: 3 -/// Effective standard HTTP Transport configuration resolved from Config and mapped to the Transport runtime contract. +/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP and optional WebSocket runtime contracts. #[derive(Clone, Eq, PartialEq)] pub struct ResolvedTransportConfig { file_id: crate::ConfigFileId, @@ -10,6 +10,7 @@ pub struct ResolvedTransportConfig { selection_source: crate::ConfigProfileSelectionSource, effective: crate::ResolvedConfigJson, settings: ksp_onchain_transport_lib::HttpTransportSettings, + ws_settings: std::option::Option, } impl ResolvedTransportConfig { @@ -47,16 +48,43 @@ impl ResolvedTransportConfig { } /// Returns the validated runtime HTTP Transport settings. + /// + /// This compatibility accessor keeps the HTTP contract introduced before Transport V2. #[must_use] pub const fn settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings { return &self.settings; } + /// Returns the validated runtime HTTP Transport settings. + #[must_use] + pub const fn http_settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings { + return &self.settings; + } + + /// Returns validated WebSocket Transport settings when the selected document uses format V2. + /// + /// Backward-compatible V1 HTTP-only documents return [`std::option::Option::None`]. + #[must_use] + pub fn ws_settings(&self) -> std::option::Option<&ksp_onchain_transport_lib::WsTransportSettings> { + return self.ws_settings.as_ref(); + } + /// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings. #[must_use] pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings { return self.settings; } + + /// Consumes this resolved Config and returns both HTTP and optional WebSocket runtime settings. + #[must_use] + pub fn into_transport_settings( + self, + ) -> ( + ksp_onchain_transport_lib::HttpTransportSettings, + std::option::Option, + ) { + return (self.settings, self.ws_settings); + } } impl std::fmt::Debug for ResolvedTransportConfig { @@ -68,16 +96,16 @@ impl std::fmt::Debug for ResolvedTransportConfig { .field("profile_id", &self.profile_id) .field("selection_source", &self.selection_source) .field("effective", &self.effective) + .field("has_ws_settings", &self.ws_settings.is_some()) .finish_non_exhaustive(); } } impl crate::ConfigDocumentEngine { - /// Loads the standard HTTP Transport document, selects a profile, resolves environment placeholders and maps it to Transport runtime settings. + /// Loads the standard Transport document, selects a profile, resolves environment placeholders and maps HTTP plus optional WebSocket runtime settings. /// /// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed - /// because the Transport URL wrapper owns runtime redaction. Invalid environment-resolved values are reported as - /// [`crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID`] without copying endpoint URL values into ordinary error context. + /// because Transport URL wrappers own runtime redaction. V1 documents remain HTTP-only; V2 documents require WebSocket defaults and endpoints. pub fn load_resolved_transport_config( &self, requested_profile: std::option::Option<&str>, @@ -96,7 +124,7 @@ impl crate::ConfigDocumentEngine { return resolve_transport_profile(&profile, environment); } - /// Maps an already resolved standard Transport profile to the runtime HTTP Transport adapter while preserving its selection provenance. + /// Maps an already resolved standard Transport profile to HTTP plus optional WebSocket runtime adapters while preserving selection provenance. /// /// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.transport`. pub fn resolve_transport_config_profile( @@ -121,7 +149,11 @@ struct EffectiveTransportSource { format_version: u32, profile_id: String, retry: EffectiveRetrySource, + #[serde(default)] + ws_defaults: std::option::Option, endpoints: std::vec::Vec, + #[serde(default)] + ws_endpoints: std::option::Option>, } #[derive(serde::Deserialize)] @@ -165,7 +197,69 @@ struct EffectiveLimitsSource { pause_after_rate_limit_ms: std::option::Option, } +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveWsSessionSource { + command_timeout_ms: u64, + close_timeout_ms: u64, + reconnect: EffectiveWsReconnectSource, + resubscribe: String, + command_queue_capacity: usize, + notification_queue_capacity: usize, + max_active_subscriptions: usize, + max_pending_requests: usize, + max_message_size_bytes: usize, + max_frame_size_bytes: usize, + max_write_buffer_size_bytes: usize, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveWsReconnectSource { + max_retries: u32, + initial_backoff_ms: u64, + max_backoff_ms: u64, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveWsEndpointSource { + name: String, + enabled: bool, + provider: String, + cluster: String, + kind: String, + url: String, + #[serde(default)] + session: std::option::Option, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveWsSessionOverrideSource { + command_timeout_ms: std::option::Option, + close_timeout_ms: std::option::Option, + reconnect: std::option::Option, + resubscribe: std::option::Option, + command_queue_capacity: std::option::Option, + notification_queue_capacity: std::option::Option, + max_active_subscriptions: std::option::Option, + max_pending_requests: std::option::Option, + max_message_size_bytes: std::option::Option, + max_frame_size_bytes: std::option::Option, + max_write_buffer_size_bytes: std::option::Option, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveWsReconnectOverrideSource { + max_retries: std::option::Option, + initial_backoff_ms: std::option::Option, + max_backoff_ms: std::option::Option, +} + fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result { + ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Transport Config profile"); let effective = profile.resolve_effective_environment_detailed(environment); let effective = match effective { std::result::Result::Ok(value) => value, @@ -180,12 +274,10 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment ); }, }; - if source.format_version != 1 { - return std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported")); - } if source.profile_id != profile.profile_id() { return std::result::Result::Err(effective_error(profile, "effective Transport profile_id does not match the selected profile")); } + let format_version = source.format_version; let retry = ksp_onchain_transport_lib::HttpRetrySettings::new( source.retry.max_retries, std::time::Duration::from_millis(source.retry.initial_backoff_ms), @@ -197,14 +289,24 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment std::result::Result::Err(error) => return std::result::Result::Err(error), }; let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(endpoints, retry); - let validation = settings.validate(); - if let std::result::Result::Err(error) = validation { - return std::result::Result::Err(transport_contract_error(profile, "effective Transport settings fail the Transport runtime contract", &error)); + if let std::result::Result::Err(error) = settings.validate() { + return std::result::Result::Err(transport_contract_error(profile, "effective HTTP Transport settings fail the Transport runtime contract", &error)); } + let ws_settings = map_optional_ws_settings(format_version, source.ws_defaults, source.ws_endpoints, profile); + let ws_settings = match ws_settings { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let ws_endpoint_count = match ws_settings.as_ref() { + std::option::Option::Some(value) => value.endpoints().len(), + std::option::Option::None => 0_usize, + }; ksp_logging_lib::debug!( target: crate::TRACING_TARGET, profile_id = profile.profile_id(), - endpoint_count = settings.endpoints().len(), + format_version, + http_endpoint_count = settings.endpoints().len(), + ws_endpoint_count, "mapped standard Transport Config to runtime settings" ); return std::result::Result::Ok(ResolvedTransportConfig { @@ -214,9 +316,49 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment selection_source: profile.selection_source(), effective, settings, + ws_settings, }); } +fn map_optional_ws_settings( + format_version: u32, + defaults: std::option::Option, + sources: std::option::Option>, + profile: &crate::ResolvedConfigProfile, +) -> ksp_core_lib::Result> { + return match format_version { + 1 => { + if defaults.is_some() || sources.is_some() { + std::result::Result::Err(effective_error(profile, "Transport V1 must remain HTTP-only")) + } else { + ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapped backward-compatible Transport V1 without WebSocket settings"); + std::result::Result::Ok(std::option::Option::None) + } + }, + 2 => { + let defaults = match defaults { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 requires ws_defaults")), + }; + let sources = match sources { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 profile requires ws_endpoints")), + }; + let endpoints = map_ws_endpoints(sources, &defaults, profile); + let endpoints = match endpoints { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let settings = ksp_onchain_transport_lib::WsTransportSettings::new(endpoints); + if let std::result::Result::Err(error) = settings.validate() { + return std::result::Result::Err(transport_contract_error(profile, "effective WebSocket Transport settings fail the Transport runtime contract", &error)); + } + std::result::Result::Ok(std::option::Option::Some(settings)) + }, + _ => std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported")), + }; +} + fn map_endpoints( sources: std::vec::Vec, profile: &crate::ResolvedConfigProfile, @@ -229,7 +371,7 @@ fn map_endpoints( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err( - transport_contract_error(profile, "effective endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name), + transport_contract_error(profile, "effective HTTP endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name), ); }, }; @@ -253,6 +395,171 @@ fn map_endpoints( return std::result::Result::Ok(endpoints); } +fn map_ws_endpoints( + sources: std::vec::Vec, + defaults: &EffectiveWsSessionSource, + profile: &crate::ResolvedConfigProfile, +) -> ksp_core_lib::Result> { + let mut endpoints = std::vec::Vec::::with_capacity(sources.len()); + for source in sources { + let endpoint_name = source.name.clone(); + let protocol = map_ws_protocol_kind(source.kind.as_str(), profile, endpoint_name.as_str()); + let protocol = match protocol { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let url = ksp_onchain_transport_lib::WsEndpointUrl::parse(source.url); + let url = match url { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err( + transport_contract_error(profile, "effective WebSocket endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name), + ); + }, + }; + let session = map_ws_session_settings(defaults, source.session.as_ref(), profile, endpoint_name.as_str()); + let session = match session { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + endpoints.push(ksp_onchain_transport_lib::WsEndpointSettings::new( + source.name, + source.enabled, + ksp_onchain_transport_lib::WsProviderName::new(source.provider), + ksp_onchain_transport_lib::WsClusterName::new(source.cluster), + protocol, + url, + session, + )); + } + return std::result::Result::Ok(endpoints); +} + +fn map_ws_protocol_kind( + value: &str, + profile: &crate::ResolvedConfigProfile, + endpoint_name: &str, +) -> ksp_core_lib::Result { + return match value { + "solana_standard" => std::result::Result::Ok(ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard), + _ => std::result::Result::Err( + effective_error(profile, "effective WebSocket protocol kind is unsupported") + .with_context("endpoint_name", endpoint_name) + .with_context("ws_kind", value), + ), + }; +} + +fn map_ws_session_settings( + defaults: &EffectiveWsSessionSource, + overrides: std::option::Option<&EffectiveWsSessionOverrideSource>, + profile: &crate::ResolvedConfigProfile, + endpoint_name: &str, +) -> ksp_core_lib::Result { + let mut command_timeout_ms = defaults.command_timeout_ms; + let mut close_timeout_ms = defaults.close_timeout_ms; + let mut reconnect_max_retries = defaults.reconnect.max_retries; + let mut reconnect_initial_backoff_ms = defaults.reconnect.initial_backoff_ms; + let mut reconnect_max_backoff_ms = defaults.reconnect.max_backoff_ms; + let mut resubscribe_text = defaults.resubscribe.clone(); + let mut command_queue_capacity = defaults.command_queue_capacity; + let mut notification_queue_capacity = defaults.notification_queue_capacity; + let mut max_active_subscriptions = defaults.max_active_subscriptions; + let mut max_pending_requests = defaults.max_pending_requests; + let mut max_message_size_bytes = defaults.max_message_size_bytes; + let mut max_frame_size_bytes = defaults.max_frame_size_bytes; + let mut max_write_buffer_size_bytes = defaults.max_write_buffer_size_bytes; + if let std::option::Option::Some(overrides) = overrides { + if let std::option::Option::Some(value) = overrides.command_timeout_ms { + command_timeout_ms = value; + } + if let std::option::Option::Some(value) = overrides.close_timeout_ms { + close_timeout_ms = value; + } + if let std::option::Option::Some(reconnect) = overrides.reconnect.as_ref() { + if let std::option::Option::Some(value) = reconnect.max_retries { + reconnect_max_retries = value; + } + if let std::option::Option::Some(value) = reconnect.initial_backoff_ms { + reconnect_initial_backoff_ms = value; + } + if let std::option::Option::Some(value) = reconnect.max_backoff_ms { + reconnect_max_backoff_ms = value; + } + } + if let std::option::Option::Some(value) = overrides.resubscribe.as_ref() { + resubscribe_text = value.clone(); + } + if let std::option::Option::Some(value) = overrides.command_queue_capacity { + command_queue_capacity = value; + } + if let std::option::Option::Some(value) = overrides.notification_queue_capacity { + notification_queue_capacity = value; + } + if let std::option::Option::Some(value) = overrides.max_active_subscriptions { + max_active_subscriptions = value; + } + if let std::option::Option::Some(value) = overrides.max_pending_requests { + max_pending_requests = value; + } + if let std::option::Option::Some(value) = overrides.max_message_size_bytes { + max_message_size_bytes = value; + } + if let std::option::Option::Some(value) = overrides.max_frame_size_bytes { + max_frame_size_bytes = value; + } + if let std::option::Option::Some(value) = overrides.max_write_buffer_size_bytes { + max_write_buffer_size_bytes = value; + } + } + let reconnect = ksp_onchain_transport_lib::WsReconnectSettings::new( + reconnect_max_retries, + std::time::Duration::from_millis(reconnect_initial_backoff_ms), + std::time::Duration::from_millis(reconnect_max_backoff_ms), + ); + let resubscribe = map_ws_resubscribe_policy(resubscribe_text.as_str(), profile, endpoint_name); + let resubscribe = match resubscribe { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let settings = ksp_onchain_transport_lib::WsSessionSettings::new( + std::time::Duration::from_millis(command_timeout_ms), + std::time::Duration::from_millis(close_timeout_ms), + reconnect, + resubscribe, + command_queue_capacity, + notification_queue_capacity, + max_active_subscriptions, + max_pending_requests, + max_message_size_bytes, + max_frame_size_bytes, + max_write_buffer_size_bytes, + ); + if let std::result::Result::Err(error) = settings.validate() { + return std::result::Result::Err( + transport_contract_error(profile, "effective WebSocket session settings fail the Transport runtime contract", &error) + .with_context("endpoint_name", endpoint_name), + ); + } + return std::result::Result::Ok(settings); +} + +fn map_ws_resubscribe_policy( + value: &str, + profile: &crate::ResolvedConfigProfile, + endpoint_name: &str, +) -> ksp_core_lib::Result { + return match value { + "never" => std::result::Result::Ok(ksp_onchain_transport_lib::WsResubscribePolicy::Never), + "active_subscriptions" => std::result::Result::Ok(ksp_onchain_transport_lib::WsResubscribePolicy::ActiveSubscriptions), + _ => std::result::Result::Err( + effective_error(profile, "effective WebSocket resubscribe policy is unsupported") + .with_context("endpoint_name", endpoint_name) + .with_context("resubscribe", value), + ), + }; +} + fn map_roles( sources: std::vec::Vec, profile: &crate::ResolvedConfigProfile, diff --git a/crates/ksp-config-lib/tests/public_api.rs b/crates/ksp-config-lib/tests/public_api.rs index 9647642..1c2452a 100644 --- a/crates/ksp-config-lib/tests/public_api.rs +++ b/crates/ksp-config-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/tests/public_api.rs -// version: 21 +// version: 22 //! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, //! Logging/Transport adapters and management contracts. @@ -252,6 +252,9 @@ fn transport_adapter_contract_is_available_from_crate_root() { let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_transport_config; let _composite_loader = ksp_config_lib::ConfigDocumentEngine::resolve_transport_config_profile; assert!(std::mem::size_of::() > 0); + let _http_settings = ksp_config_lib::ResolvedTransportConfig::http_settings; + let _ws_settings = ksp_config_lib::ResolvedTransportConfig::ws_settings; + let _into_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_transport_settings; assert_eq!(ksp_config_lib::FILE_ID_STD_TRANSPORT, "cfg.std.transport"); assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT, "schema.std.transport"); assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json"); diff --git a/crates/ksp-config-lib/unit_tests/fixtures/std.transport.json b/crates/ksp-config-lib/unit_tests/fixtures/std.transport.json index f5ac990..324243a 100644 --- a/crates/ksp-config-lib/unit_tests/fixtures/std.transport.json +++ b/crates/ksp-config-lib/unit_tests/fixtures/std.transport.json @@ -1,10 +1,27 @@ { - "format_version": 1, + "format_version": 2, "retry": { "max_retries": 4, "initial_backoff_ms": 125, "max_backoff_ms": 2500 }, + "ws_defaults": { + "command_timeout_ms": 8000, + "close_timeout_ms": 4000, + "reconnect": { + "max_retries": 6, + "initial_backoff_ms": 200, + "max_backoff_ms": 4000 + }, + "resubscribe": "active_subscriptions", + "command_queue_capacity": 64, + "notification_queue_capacity": 96, + "max_active_subscriptions": 256, + "max_pending_requests": 48, + "max_message_size_bytes": 33554432, + "max_frame_size_bytes": 8388608, + "max_write_buffer_size_bytes": 524288 + }, "default_profile": "secret_test", "profiles": [ { @@ -23,7 +40,9 @@ { "role": "default", "enabled": true, - "request_kinds": ["*"], + "request_kinds": [ + "*" + ], "priority": 7, "limits": { "requests_per_second": 9, @@ -34,6 +53,25 @@ } ] } + ], + "ws_endpoints": [ + { + "name": "fixture_private_ws", + "enabled": true, + "provider": "fixture-provider", + "cluster": "fixture-cluster", + "kind": "solana_standard", + "url": "${KSP_SECRET_TRANSPORT_TEST_WS_URL:-wss://fallback.invalid}", + "session": { + "command_timeout_ms": 4500, + "reconnect": { + "max_retries": 7 + }, + "resubscribe": "never", + "notification_queue_capacity": 48, + "max_pending_requests": 24 + } + } ] } ] diff --git a/crates/ksp-config-lib/unit_tests/fixtures_v1/std.transport.json b/crates/ksp-config-lib/unit_tests/fixtures_v1/std.transport.json new file mode 100644 index 0000000..95567aa --- /dev/null +++ b/crates/ksp-config-lib/unit_tests/fixtures_v1/std.transport.json @@ -0,0 +1,42 @@ +{ + "format_version": 1, + "retry": { + "max_retries": 1, + "initial_backoff_ms": 90, + "max_backoff_ms": 900 + }, + "default_profile": "legacy_http", + "profiles": [ + { + "profile_id": "legacy_http", + "endpoints": [ + { + "name": "legacy_http", + "enabled": true, + "provider": "legacy-provider", + "cluster": "devnet", + "url": "https://legacy.invalid", + "connect_timeout_ms": 5000, + "request_timeout_ms": 2200, + "max_idle_connections_per_host": 2, + "roles": [ + { + "role": "default", + "enabled": true, + "request_kinds": [ + "*" + ], + "priority": 10, + "limits": { + "requests_per_second": 2, + "burst_capacity": 3, + "max_concurrent_requests": 2, + "pause_after_rate_limit_ms": 250 + } + } + ] + } + ] + } + ] +} diff --git a/crates/ksp-config-lib/unit_tests/transport.rs b/crates/ksp-config-lib/unit_tests/transport.rs index 6f78957..3b9a31c 100644 --- a/crates/ksp-config-lib/unit_tests/transport.rs +++ b/crates/ksp-config-lib/unit_tests/transport.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/unit_tests/transport.rs -// version: 2 +// version: 3 #[test] fn fixture_transport_profile_maps_complete_runtime_contract() { @@ -41,6 +41,49 @@ fn fixture_transport_profile_maps_complete_runtime_contract() { assert_eq!(role.limits().burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(12)); assert_eq!(role.limits().max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(4)); assert_eq!(role.limits().pause_after_rate_limit(), std::option::Option::Some(std::time::Duration::from_millis(650))); + let ws = resolved.ws_settings(); + assert!(ws.is_some(), "V2 fixture should expose WebSocket settings"); + if let std::option::Option::Some(ws) = ws { + assert_eq!(ws.endpoints().len(), 1); + let endpoint = &ws.endpoints()[0]; + assert_eq!(endpoint.name(), "fixture_private_ws"); + assert_eq!(endpoint.provider().as_str(), "fixture-provider"); + assert_eq!(endpoint.cluster().as_str(), "fixture-cluster"); + assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard); + assert_eq!(endpoint.url().as_str(), "wss://fallback.invalid"); + assert_eq!(endpoint.session().command_timeout(), std::time::Duration::from_millis(4500)); + assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(4000)); + assert_eq!(endpoint.session().reconnect().max_retries(), 7); + assert_eq!(endpoint.session().reconnect().initial_backoff(), std::time::Duration::from_millis(200)); + assert_eq!(endpoint.session().reconnect().max_backoff(), std::time::Duration::from_millis(4000)); + assert_eq!(endpoint.session().resubscribe(), ksp_onchain_transport_lib::WsResubscribePolicy::Never); + assert_eq!(endpoint.session().command_queue_capacity(), 64); + assert_eq!(endpoint.session().notification_queue_capacity(), 48); + assert_eq!(endpoint.session().max_active_subscriptions(), 256); + assert_eq!(endpoint.session().max_pending_requests(), 24); + assert_eq!(endpoint.session().max_message_size_bytes(), 33_554_432); + assert_eq!(endpoint.session().max_frame_size_bytes(), 8_388_608); + assert_eq!(endpoint.session().max_write_buffer_size_bytes(), 524_288); + } +} + +#[test] +fn v1_transport_fixture_remains_backward_readable_and_http_only() { + let engine = v1_fixture_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); + let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "strict Transport V1 fixture should remain readable: {resolved:?}"); + if let std::result::Result::Ok(resolved) = resolved { + assert_eq!(resolved.profile_id(), "legacy_http"); + assert_eq!(resolved.settings().retry().max_retries(), 1); + assert_eq!(resolved.settings().endpoints().len(), 1); + assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://legacy.invalid"); + assert!(resolved.ws_settings().is_none(), "V1 must not invent WebSocket runtime settings"); + } } #[test] @@ -59,12 +102,23 @@ fn committed_transport_document_maps_default_and_explicit_profiles() { assert_eq!(default.profile_id(), "devnet_public"); assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet"); assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com"); + let ws = default.ws_settings(); + assert!(ws.is_some(), "committed V2 Devnet profile should expose WebSocket settings"); + if let std::option::Option::Some(ws) = ws { + assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.devnet.solana.com"); + assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard); + } } if let std::result::Result::Ok(mainnet) = mainnet { assert_eq!(mainnet.profile_id(), "mainnet_public"); assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit); assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet-beta"); assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com"); + let ws = mainnet.ws_settings(); + assert!(ws.is_some(), "committed V2 Mainnet profile should expose WebSocket settings"); + if let std::option::Option::Some(ws) = ws { + assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet-beta.solana.com"); + } } } @@ -84,7 +138,9 @@ fn transport_profile_preserves_global_and_profile_origin() { assert!(profile.is_ok(), "committed Transport profile should resolve: {profile:?}"); if let std::result::Result::Ok(profile) = profile { assert_eq!(profile.origin("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global)); + assert_eq!(profile.origin("ws_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global)); assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile)); + assert_eq!(profile.origin("ws_endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile)); assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global)); } } @@ -149,6 +205,36 @@ fn secret_transport_url_is_runtime_available_but_safe_projection_is_redacted() { assert!(debug.contains(crate::REDACTED_CONFIG_VALUE)); } +#[test] +fn secret_websocket_url_is_runtime_available_but_safe_projection_is_redacted() { + let engine = fixture_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let canary = "wss://user:pass@secret-provider.invalid/path?api-key=transport-ws-secret-canary"; + let mut process = std::collections::BTreeMap::::new(); + process.insert("KSP_SECRET_TRANSPORT_TEST_WS_URL".to_owned(), canary.to_owned()); + let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); + let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "secret WebSocket endpoint should map without being exposed: {resolved:?}"); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let ws = resolved.ws_settings(); + assert!(ws.is_some(), "V2 fixture should expose WebSocket settings"); + if let std::option::Option::Some(ws) = ws { + assert_eq!(ws.endpoints()[0].url().as_str(), canary); + } + let safe_url = resolved.effective().safe_value().pointer("/ws_endpoints/0/url").and_then(serde_json::Value::as_str); + assert_eq!(safe_url, std::option::Option::Some(crate::REDACTED_CONFIG_VALUE)); + let debug = format!("{resolved:?}"); + assert!(!debug.contains("transport-ws-secret-canary")); + assert!(!debug.contains("user:pass")); + assert!(debug.contains(crate::REDACTED_CONFIG_VALUE)); +} + #[test] fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() { let engine = fixture_engine(); @@ -216,6 +302,22 @@ fn fixture_engine() -> ksp_core_lib::Result { return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry)); } +fn v1_fixture_engine() -> ksp_core_lib::Result { + let workspace = workspace_root(); + let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures_v1"); + let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture_root, workspace.join("config/schemas")); + let bootstrap = match bootstrap { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let registry = crate::ConfigFileRegistry::defaults(); + let registry = match registry { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry)); +} + fn committed_engine() -> ksp_core_lib::Result { let workspace = workspace_root(); let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas")); diff --git a/deltas/0.2.7/pre.003.md b/deltas/0.2.7/pre.003.md new file mode 100644 index 0000000..1c0769c --- /dev/null +++ b/deltas/0.2.7/pre.003.md @@ -0,0 +1,225 @@ + + + +# Delta `0.2.7-pre.003` — `std.transport` V2 HTTP + WebSocket + +## 1. Base requise + +```text +0.2.7-pre.002-fix.001 appliquée +workspace.package.version = 0.2.7-pre.2.fix.1 +``` + +Le checkpoint opérateur reçu avant cette tranche est entièrement vert : `cargo fmt`, audit Python, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, tests Transport ciblés et `cargo test --workspace`. + +## 2. Signal technique + +Cette prerelease non-fix modifie configuration exécutable, Rust et tests. Conformément à `VER-ID-009` : + +```text +livraison = 0.2.7-pre.003 +workspace.package.version = 0.2.7-pre.3 +commit = v0.2.7-pre.003 +``` + +Aucun tag prerelease. + +## 3. `std.transport` V2 + +Le document standard livré devient : + +```text +format_version = 2 +retry +ws_defaults +default_profile +profiles[] + profile_id + endpoints[] + ws_endpoints[] + kind + session? +``` + +Le HTTP existant reste inchangé dans `endpoints[]`. `ws_defaults` contient les defaults génériques de `WsSessionSettings`; `ws_endpoints[].session` peut surcharger seulement les paramètres génériques nécessaires à un endpoint. + +Le discriminateur est obligatoire : + +```text +kind = solana_standard +``` + +Toute autre famille est rejetée en `0.2.7`. Aucun paramètre Helius, LaserStream ou autre provider-specific n'est pré-implémenté. + +## 4. Backward V1 strict + +Le schema enregistré passe à : + +```text +urn:ksp:schema:std.transport:v2 +``` + +Il conserve deux branches strictes discriminées par `format_version` : + +```text +V1 -> HTTP-only historique +V2 -> HTTP + ws_defaults + ws_endpoints +``` + +Le V1 n'est pas rendu compatible par un relâchement de `additionalProperties`. Une fixture V1 dédiée prouve le chemin historique. + +Dans l'adapter : + +```text +V1 -> HttpTransportSettings + ws_settings = None +V2 -> HttpTransportSettings + Some(WsTransportSettings) +``` + +`WsTransportSettings` conserve donc son invariant `pre.002` : il n'existe jamais comme faux conteneur vide. + +## 5. Adapter Config -> Transport + +`ResolvedTransportConfig` conserve `settings()` pour compatibilité HTTP et ajoute : + +```text +http_settings() +ws_settings() -> Option<&WsTransportSettings> +into_transport_settings() -> (HttpTransportSettings, Option) +``` + +Le mapper V2 construit : + +```text +WsProviderName +WsClusterName +WsProtocolKind::SolanaStandard +WsEndpointUrl +WsReconnectSettings +WsResubscribePolicy +WsSessionSettings +WsEndpointSettings +WsTransportSettings +``` + +La direction reste strictement `Config -> Transport`. Aucun import de Config n'est ajouté à `ksp-onchain-transport-lib`. + +## 6. Overrides session + +Les valeurs de `ws_defaults` correspondent aux defaults KSP matérialisés en `pre.002`. Un endpoint peut surcharger indépendamment : + +```text +command_timeout_ms +close_timeout_ms +reconnect.{max_retries, initial_backoff_ms, max_backoff_ms} +resubscribe +command_queue_capacity +notification_queue_capacity +max_active_subscriptions +max_pending_requests +max_message_size_bytes +max_frame_size_bytes +max_write_buffer_size_bytes +``` + +La session résultante est toujours validée par `WsSessionSettings::validate()`. + +## 7. Secrets et logging + +Les URLs HTTP et WebSocket peuvent provenir de `KSP_SECRET_*`; les valeurs réelles restent disponibles au runtime mais la projection Config safe les redacted. Les erreurs d'adaptation ne recopient pas l'URL. + +Le mapping Config utilise exclusivement `ksp-logging-lib` avec le `TRACING_TARGET` existant de `ksp-config-lib` : + +```text +trace -> début mapping et chemin backward V1 +debug -> version, compteurs HTTP/WS après validation +``` + +Aucun `tracing` direct n'est ajouté. + +## 8. Environment inventory + +Ajouts `.env.example` : + +```text +KSP_PUBLIC_SOLANA_DEVNET_WS_URL +KSP_PUBLIC_SOLANA_MAINNET_WS_URL +# KSP_SECRET_SOLANA_WS_URL +``` + +Les URLs provider privées restent des valeurs complètes gérées via `KSP_SECRET_*`. + +## 9. Tests ajoutés/étendus + +```text +V2 fixture HTTP + WS complète +ws_defaults + endpoint overrides +V1 strict toujours chargeable +V1 n'invente pas de WsTransportSettings vide +committed Devnet/Mainnet V2 mappe HTTP + WS +provenance ws_defaults globale et ws_endpoints profil +secret WebSocket URL disponible au runtime mais redacted en safe/Debug +public API canary pour les nouveaux accessors +``` + +## 10. Fichiers principaux modifiés + +```text +Cargo.toml +.env.example +config/std.transport.json +config/examples/std.transport.example.json +config/schemas/std.transport.schema.json +crates/ksp-config-lib/src/transport.rs +crates/ksp-config-lib/src/lib.rs +crates/ksp-config-lib/src/registry.rs +crates/ksp-config-lib/unit_tests/transport.rs +crates/ksp-config-lib/unit_tests/fixtures/std.transport.json +crates/ksp-config-lib/tests/public_api.rs +crates/ksp-config-lib/README.md +crates/ksp-config-lib/USAGE.md +docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md +docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md +``` + +Nouveau : + +```text +crates/ksp-config-lib/unit_tests/fixtures_v1/std.transport.json +deltas/0.2.7/pre.003.md +``` + +Aucune dépendance externe n'est ajoutée. + +## 11. Validation de préparation + +Le sandbox de génération ne fournit pas Cargo. Sont exécutés ici : + +```text +python3 scripts/audit_rust_workspace_rules.py +validation JSON des documents/schema +inspection absence de tracing direct +inspection version workspace +inspection archive delta +``` + +Les gates compilés restent opérateur-only. + +## 12. Gates opérateur avant commit + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-config-lib +cargo test -p ksp-onchain-transport-lib +cargo test --workspace +``` + +Si le checkpoint est vert : + +```text +commit = v0.2.7-pre.003 +``` + +La tranche suivante est `0.2.7-pre.004` : dépendances WebSocket, actor physique, handshake/read/write, pending JSON-RPC et serveur local déterministe. diff --git a/docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md b/docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md index 898f3d2..ea56292 100644 --- a/docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md +++ b/docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md @@ -1,9 +1,9 @@ - + # Plan `0.2.7` — WebSocket Solana standard -> **Statut : actif, `0.2.7-pre.002`.** Le gate `pre.001` est clos. `pre.002` matérialise les settings WebSocket Transport, la redaction URL, le discriminateur de protocole, les identités locales, les états lifecycle et les snapshots sûrs. Le socket physique reste différé à `pre.004`. +> **Statut : actif, `0.2.7-pre.003`.** Les gates `pre.001` et `pre.002` sont clos. `pre.003` matérialise `std.transport` V2 HTTP + WebSocket, la lecture backward V1 HTTP-only et l'adapter `Config -> WsTransportSettings`. Le socket physique reste différé à `pre.004`. ## 1. Objet et base vérifiée @@ -110,9 +110,9 @@ Aucun gate Cargo n'est déclaré réussi. Une validation opérateur est obligato La surface WebSocket doit être additive : aucune renormalisation invasive des types `Http*` n'est requise dans `0.2.7`. -### 4.2 Config actuel est explicitement HTTP V1 +### 4.2 Config hérité et évolution V2 -`config/std.transport.json`, son schema et `ksp-config-lib/src/transport.rs` décrivent un document V1 HTTP : +À l’ouverture de `0.2.7`, `config/std.transport.json`, son schema et `ksp-config-lib/src/transport.rs` décrivaient un document V1 HTTP : ```text format_version = 1 @@ -124,7 +124,7 @@ format_version != 1 rejeté Décision : **ne pas ajouter silencieusement des clés WebSocket au V1**. -`0.2.7` fera évoluer le même composant standard `std.transport` vers un **format V2 explicitement HTTP + WebSocket**, tout en conservant la lecture du V1 HTTP-only pour compatibilité. Config reste propriétaire du parsing/résolution et adapte ensuite vers les settings publics Transport. +`pre.003` fait évoluer le même composant standard `std.transport` vers un **format V2 explicitement HTTP + WebSocket**, tout en conservant la lecture du V1 HTTP-only pour compatibilité. Config reste propriétaire du parsing/résolution et adapte ensuite vers les settings publics Transport. Shape cible minimale : @@ -146,7 +146,21 @@ Décision d'extensibilité : **le conteneur V2 ne doit pas supposer qu'il n'exis Cette préparation ne promet aucune option Helius dans `0.2.7`. L'audit provider-specific de la release suivante décidera la relation exacte entre Helius Enhanced WebSocket, LaserStream et le moteur standard avant d'ajouter des variantes/configurations concrètes. -Le V1 chargé sous `0.2.7` produit une configuration HTTP valide et une collection WebSocket vide. Le V2 devient le format livré par les fixtures Config dès la tranche qui matérialise le WebSocket. +Le V1 chargé sous `0.2.7` produit une configuration HTTP valide et aucune instance `WsTransportSettings`. Le V2 devient le format livré par les fixtures Config dès la tranche qui matérialise le WebSocket. + +Checkpoint `pre.003` matérialisé : + +```text +std.transport schema id = urn:ksp:schema:std.transport:v2 +V1 strict HTTP-only -> toujours accepté +V2 strict HTTP + WS -> format livré +ws_defaults -> session defaults génériques +ws_endpoints[].session -> overrides génériques optionnels +ws_endpoints[].kind -> solana_standard uniquement en 0.2.7 +ResolvedTransportConfig -> HTTP toujours présent + WS Option pour backward V1 +``` + +Le schema V2 utilise deux branches strictes discriminées par `format_version`; la compatibilité V1 n'est donc pas obtenue en relâchant `additionalProperties`. Les overrides endpoint ne contiennent que des paramètres génériques de `WsSessionSettings`, jamais des options Helius/provider-specific. ## 5. Audit historique bot3 @@ -853,7 +867,7 @@ L'inventaire officiel n'impose que 9 familles de subscriptions, mais le lifecycl ```text pre.001 audit interne/externe + matrice 18 méthodes + bot3 + dependencies + threat model + plan/sizing pre.002 DONE — settings WS Transport + URL redaction + IDs/states/snapshots + tests de settings -pre.003 std.transport V2 HTTP+WS + backward V1 + discriminateur WS + schema/fixtures + Config -> WsTransportSettings +pre.003 DONE — std.transport V2 HTTP+WS + backward V1 + discriminateur WS + schema/fixtures + Config -> WsTransportSettings pre.004 deps tokio-tungstenite/futures-util + actor physique + handshake/read/write + pending JSON-RPC + serveur local pre.005 limites frame/message/request + control frames + cancellation/close/shutdown + adversarial socket tests pre.006 registry subscriptions + IDs locaux + generic subscribe/unsubscribe engine + channels typed bounded diff --git a/docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md b/docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md index 5f82f54..527d204 100644 --- a/docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md +++ b/docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md @@ -1,9 +1,9 @@ - + # Validation `0.2.7` — WebSocket Solana standard -> **Statut : matrice active, `0.2.7-pre.002`.** Le gate normatif `pre.001` est clos ; les settings, identités, états lifecycle et snapshots sûrs sont maintenant matérialisés. Les preuves runtime socket/subscription restent ouvertes. +> **Statut : matrice active, `0.2.7-pre.003`.** Les settings/lifecycle `pre.002` et la composition Config V2 `pre.003` sont matérialisés. Les preuves runtime socket/subscription restent ouvertes. ## 1. Baseline normative @@ -253,7 +253,7 @@ Aucune de ces dependencies n'est ajoutée par `pre.001`; le graphe Cargo stable ## 9. Config compliance initiale -V1 actuel : HTTP-only strict. Décision : +V1 hérité : HTTP-only strict. Contrat de compatibilité : ```text V1 -> support de lecture conservé, WS vide @@ -266,7 +266,24 @@ Transport -X-> Config Le type Transport correspondant au discriminateur est prévu `#[non_exhaustive]`. L'objectif est de pouvoir ajouter ultérieurement une famille telle que Helius Enhanced WebSocket sans créer un nouveau conteneur Config ni injecter des options provider-specific dans les settings Solana standard. Toute valeur inconnue reste explicitement rejetée en `0.2.7`; aucun paramètre Helius n'est implémenté par anticipation. -La schema V1 n'est pas assouplie. Une schema V2 explicite remplace la fixture standard au moment où l'adapter est matérialisé. +`pre.003` remplace le schema enregistré par `urn:ksp:schema:std.transport:v2`, avec deux branches strictes : V1 HTTP-only et V2 HTTP + WebSocket. La compatibilité V1 ne relâche donc ni `additionalProperties`, ni la shape historique. + +Preuves `pre.003` : + +```text +config/std.transport.json format_version = 2 +config/examples/std.transport.example.json format_version = 2 +V1 fixture dédiée load + HTTP mapping OK attendu +V1 ws_settings = None +V2 ws_settings = Some(validated) +ws_defaults globals +profiles[].ws_endpoints profile-local +ws_endpoints[].kind enum schema solana_standard +ws_endpoints[].session overrides génériques optionnels +Config -> Transport seule direction de dépendance +``` + +`ResolvedTransportConfig::settings()` reste l'accesseur HTTP historique. `http_settings()` l'explicite et `ws_settings()` expose `Option<&WsTransportSettings>` afin que le V1 backward ne force jamais un faux `WsTransportSettings` vide. `into_transport_settings()` permet de consommer les deux contrats ensemble. ## 9.1 Checkpoint settings/lifecycle `pre.002` @@ -359,7 +376,7 @@ N subscriptions same session prouvé reconnect/resubscribe/backpressure/shutdown gates verts unstable warnings centralisés HTTP 52+14 non régressé -Config V1 backward + V2 WS validés +Config V1 backward + V2 WS validés (**Done `pre.003`**, compilation opérateur requise) smoke live opt-in documenté cargo tree inspecté cargo test --workspace vert