diff --git a/Cargo.toml b/Cargo.toml index ac74b3b..be2b1fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 602 +# version: 603 [workspace] resolver = "3" members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"] [workspace.package] -version = "0.3.15-pre.6" +version = "0.3.15-pre.6.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/config/composite.ksp-app-raw-transaction-ingest-desk.json b/config/composite.ksp-app-raw-transaction-ingest-desk.json index 3f3f4db..7465030 100644 --- a/config/composite.ksp-app-raw-transaction-ingest-desk.json +++ b/config/composite.ksp-app-raw-transaction-ingest-desk.json @@ -1,9 +1,9 @@ { "format_version": 1, - "default_profile": "devnet_public", + "default_profile": "devnet", "profiles": [ { - "profile_id": "devnet_public", + "profile_id": "devnet", "documents": [ { "component_id": "logging", @@ -16,42 +16,12 @@ "profile_id": "devnet_public" }, { - "component_id": "store", - "file_id": "cfg.std.store", - "profile_id": "devnet" - } - ] - }, - { - "profile_id": "helius_devnet", - "documents": [ - { - "component_id": "logging", - "file_id": "cfg.std.logging", - "profile_id": "supertrace" - }, - { - "component_id": "transport", + "component_id": "transport.helius", "file_id": "cfg.std.transport", "profile_id": "helius_devnet" }, { - "component_id": "store", - "file_id": "cfg.std.store", - "profile_id": "devnet" - } - ] - }, - { - "profile_id": "orbitflare_devnet", - "documents": [ - { - "component_id": "logging", - "file_id": "cfg.std.logging", - "profile_id": "supertrace" - }, - { - "component_id": "transport", + "component_id": "transport.yellowstone", "file_id": "cfg.std.transport", "profile_id": "orbitflare_devnet" }, @@ -63,7 +33,7 @@ ] }, { - "profile_id": "mainnet_public", + "profile_id": "mainnet", "documents": [ { "component_id": "logging", @@ -76,42 +46,12 @@ "profile_id": "mainnet_public" }, { - "component_id": "store", - "file_id": "cfg.std.store", - "profile_id": "mainnet" - } - ] - }, - { - "profile_id": "helius_mainnet", - "documents": [ - { - "component_id": "logging", - "file_id": "cfg.std.logging", - "profile_id": "supertrace" - }, - { - "component_id": "transport", + "component_id": "transport.helius", "file_id": "cfg.std.transport", "profile_id": "helius_mainnet" }, { - "component_id": "store", - "file_id": "cfg.std.store", - "profile_id": "mainnet" - } - ] - }, - { - "profile_id": "publicnode_mainnet", - "documents": [ - { - "component_id": "logging", - "file_id": "cfg.std.logging", - "profile_id": "supertrace" - }, - { - "component_id": "transport", + "component_id": "transport.yellowstone", "file_id": "cfg.std.transport", "profile_id": "publicnode_mainnet" }, @@ -123,7 +63,7 @@ ] }, { - "profile_id": "publicnode_testnet", + "profile_id": "testnet", "documents": [ { "component_id": "logging", @@ -133,6 +73,11 @@ { "component_id": "transport", "file_id": "cfg.std.transport", + "profile_id": "testnet_public" + }, + { + "component_id": "transport.yellowstone", + "file_id": "cfg.std.transport", "profile_id": "publicnode_testnet" }, { diff --git a/config/std.transport.json b/config/std.transport.json index 075bf88..ae93276 100644 --- a/config/std.transport.json +++ b/config/std.transport.json @@ -472,6 +472,55 @@ } ] }, + { + "profile_id": "testnet_public", + "endpoints": [ + { + "name": "solana_testnet_public", + "enabled": true, + "provider": "solana-public", + "cluster": "testnet", + "url": "https://api.testnet.solana.com", + "connect_timeout_ms": 5000, + "request_timeout_ms": 15000, + "max_idle_connections_per_host": 8, + "roles": [ + { + "role": "default", + "enabled": true, + "request_kinds": [ + "*" + ], + "priority": 100, + "limits": { + "requests_per_second": 5, + "burst_capacity": 10, + "max_concurrent_requests": 8, + "pause_after_rate_limit_ms": 1000 + } + } + ] + } + ], + "ws_endpoints": [ + { + "name": "solana_testnet_public_ws", + "enabled": true, + "provider": "solana-public", + "cluster": "testnet", + "kind": "solana_standard", + "url": "wss://api.testnet.solana.com", + "capabilities": [ + "account", + "logs", + "program", + "root", + "signature", + "slot" + ] + } + ] + }, { "profile_id": "publicnode_testnet", "endpoints": [ diff --git a/crates/ksp-app-raw-transaction-ingest-desk/README.md b/crates/ksp-app-raw-transaction-ingest-desk/README.md index 85649ae..53753dd 100644 --- a/crates/ksp-app-raw-transaction-ingest-desk/README.md +++ b/crates/ksp-app-raw-transaction-ingest-desk/README.md @@ -1,5 +1,5 @@ - + # `ksp-app-raw-transaction-ingest-desk` @@ -27,6 +27,8 @@ Worker -> acquisition continue et lifecycle d'exécution L'inventaire de routes est calculé côté Rust à partir du composite Config validé et des settings Transport/Store résolus. Il ne lance aucun probe réseau et n'ouvre ni Store ni Worker. +Les profils applicatifs sont réseau-centriques (`devnet`, `mainnet`, `testnet`). Un profil peut agréger plusieurs profils Transport du même réseau : le provider reste une source de capability et ne devient jamais une identité réseau ni un choix de profil applicatif. + Le frontend ne reçoit aucune URL, credential, URI Store, metadata secrète, source key, handle runtime ou payload `RawTransaction`. ## Routes V1 diff --git a/crates/ksp-app-raw-transaction-ingest-desk/USAGE.md b/crates/ksp-app-raw-transaction-ingest-desk/USAGE.md index 3fc4590..017eb23 100644 --- a/crates/ksp-app-raw-transaction-ingest-desk/USAGE.md +++ b/crates/ksp-app-raw-transaction-ingest-desk/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de `ksp-app-raw-transaction-ingest-desk` @@ -17,7 +17,7 @@ Le runtime prépare les ressources Config nécessaires, charge le composite déd ## 2. Vue Routes -La vue Routes charge un inventaire déterministe construit côté Rust depuis le composite Config validé. Le sélecteur présente les profils logiques du composite et la vue projette, pour chaque route V1 : +La vue Routes charge un inventaire déterministe construit côté Rust depuis le composite Config validé. Le sélecteur présente les réseaux logiques `devnet`, `mainnet` et `testnet`, jamais les providers. Chaque réseau agrège les profils Transport same-network configurés pour ses capabilities. La vue projette, pour chaque route V1 : ```text route_id @@ -31,7 +31,7 @@ génération d'inventaire `Configured` signifie uniquement que toutes les requirements Config nécessaires à la route sont cohérentes. Aucun probe réseau, aucune ouverture de Store et aucun Worker ne sont déclenchés par l'inventaire. -Un refresh reconstruit l'inventaire depuis Config et publie une nouvelle génération. La sélection visuelle d'un profil ne choisit aucune URL ou ressource physique et ne démarre aucune acquisition. +Un refresh reconstruit l'inventaire depuis Config et publie une nouvelle génération. La sélection visuelle d'un réseau ne choisit aucune URL, provider ou ressource physique et ne démarre aucune acquisition. Une capability optionnelle dont le secret n'est pas résolu rend uniquement la route qui en dépend indisponible ; les routes satisfaites par le Transport de base du même réseau restent composables. Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker, URI Store, source keys et payloads RAW restent backend-only. diff --git a/crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html b/crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html index 308d2fe..342abf5 100644 --- a/crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html +++ b/crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html @@ -1,5 +1,5 @@ - + @@ -59,8 +59,8 @@
- - + +
Réseau
@@ -74,7 +74,7 @@
Configured signifie uniquement que Config prouve la composabilité. Les connexions, le Store et le Worker seront revalidés au Start dans les tranches suivantes.
-
Profil affiché :
+
Réseau affiché :
diff --git a/crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts b/crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts index 1acd0e3..6c95b30 100644 --- a/crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts +++ b/crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts @@ -1,5 +1,5 @@ // file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts -// version: 3 +// version: 4 import "bootstrap"; import ResizeObserver from "resize-observer-polyfill"; @@ -226,7 +226,7 @@ function bindRefreshControls(): void { return; } selectedProfileId = selector.value; - frontendDebug("main", "Raw Transaction Ingest Desk logical profile selected", { profileId: selectedProfileId }); + frontendDebug("main", "Raw Transaction Ingest Desk logical network selected", { profileId: selectedProfileId }); renderSelectedProfile(); }); } diff --git a/crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs b/crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs index 0f9749b..044edfd 100644 --- a/crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs +++ b/crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs -// version: 2 +// version: 3 //! Application-owned tracing targets and Config component identifiers. @@ -9,6 +9,12 @@ pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging"; pub(crate) const COMPOSITE_COMPONENT_ID_STORE: &str = "store"; /// Composite component identifier for Transport. pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport"; +/// Composite-local optional Transport source for Helius WebSocket capabilities. +pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS: &str = "transport.helius"; +/// Prefix used by optional same-network Transport capability sources. +pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX: &str = "transport."; +/// Composite-local optional Transport source for Yellowstone gRPC capabilities. +pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE: &str = "transport.yellowstone"; /// Structured domain used while bootstrapping Config and Logging. pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "raw_transaction_ingest.bootstrap"; /// Structured domain used by technical frontend events. diff --git a/crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs b/crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs index 12a2f95..1fc4d72 100644 --- a/crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs +++ b/crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs -// version: 2 +// version: 3 //! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes. @@ -44,6 +44,12 @@ pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING; pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_STORE; /// Composite-local identifier for the standard Transport component. pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT; +/// Composite-local optional Transport source for Helius WebSocket capabilities. +pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS; +/// Prefix used by optional same-network Transport capability sources. +pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX; +/// Composite-local optional Transport source for Yellowstone gRPC capabilities. +pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE; /// Structured domain used while bootstrapping Config and Logging. pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP; /// Structured domain used by technical frontend events. diff --git a/crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs b/crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs index 33fce7f..a45121e 100644 --- a/crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs +++ b/crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs -// version: 1 +// version: 2 //! Config-only logical route inventory for Raw Transaction Ingest Desk. @@ -8,28 +8,74 @@ struct CompositeProfileCatalog { profile_ids: std::vec::Vec, } +#[derive(Default)] +struct RouteCapabilities { + has_get_transaction: bool, + has_http_block_scan: bool, + has_yellowstone: bool, + has_standard_logs: bool, + has_standard_block: bool, + has_helius_transaction: bool, + helius_source_missing_secret: bool, + yellowstone_source_missing_secret: bool, +} + +impl RouteCapabilities { + fn absorb_transport(&mut self, transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) { + self.has_get_transaction |= http_role_supports_methods(transport.http_settings(), &["getTransaction"]); + self.has_http_block_scan |= http_role_supports_methods(transport.http_settings(), &["getSlot", "getBlocksWithLimit", "getBlock"]); + self.has_yellowstone |= has_yellowstone_grpc(transport, network); + self.has_standard_logs |= has_ws_capability( + transport, + network, + ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, + ksp_onchain_transport_lib::WsSubscriptionKind::Logs, + ); + self.has_standard_block |= has_ws_capability( + transport, + network, + ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, + ksp_onchain_transport_lib::WsSubscriptionKind::Block, + ); + self.has_helius_transaction |= has_ws_capability( + transport, + network, + ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream, + ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction, + ); + } +} + /// Rebuilds the complete safe route inventory from validated Config without opening any network, Store or Worker resource. pub(crate) fn build_route_inventory(management: &ksp_config_lib::ConfigManagement, generation: u32) -> ksp_core_lib::Result { - let catalog = composite_profile_catalog(management); - let catalog = match catalog { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = match environment { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; + return build_route_inventory_with_environment(management, &environment, generation); +} + +fn build_route_inventory_with_environment( + management: &ksp_config_lib::ConfigManagement, + environment: &ksp_config_lib::ConfigEnvironment, + generation: u32, +) -> ksp_core_lib::Result { + let catalog = composite_profile_catalog(management); + let catalog = match catalog { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; let mut profiles = std::vec::Vec::with_capacity(catalog.profile_ids.len()); for profile_id in &catalog.profile_ids { - profiles.push(build_profile_inventory(management, &environment, profile_id.as_str(), profile_id == &catalog.default_profile)); + profiles.push(build_profile_inventory(management, environment, profile_id.as_str(), profile_id == &catalog.default_profile)); } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_INVENTORY, generation, profile_count = profiles.len(), - "rebuilt Raw Transaction Ingest Desk Config-only route inventory" + "rebuilt Raw Transaction Ingest Desk network-centric Config-only route inventory" ); return std::result::Result::Ok(crate::RawIngestRouteInventoryDto { default_profile: catalog.default_profile, generation, profiles }); } @@ -92,14 +138,6 @@ fn build_profile_inventory( return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved); }, }; - let transport_profile = - crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT); - let transport_profile = match transport_profile { - std::result::Result::Ok(value) => value, - std::result::Result::Err(_) => { - return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved); - }, - }; let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE); let store_profile = match store_profile { std::result::Result::Ok(value) => value, @@ -107,91 +145,152 @@ fn build_profile_inventory( return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved); }, }; - let transport = management.engine().resolve_transport_config_profile(&transport_profile, environment); let store = management.engine().resolve_store_config_profile(&store_profile, environment); - let network_hint = resolved_network_hint(&transport, &store); - if transport.is_err() || store.is_err() { - let reason = resolution_failure_reason(&transport, &store); - return unavailable_profile(profile_id, is_default, network_hint.as_deref(), reason); - } - let transport = match transport { - std::result::Result::Ok(value) => value, - std::result::Result::Err(_) => { - return unavailable_profile(profile_id, is_default, network_hint.as_deref(), crate::RawIngestRouteUnavailableReason::ProfileUnresolved); - }, - }; let store = match store { std::result::Result::Ok(value) => value, - std::result::Result::Err(_) => { - return unavailable_profile(profile_id, is_default, network_hint.as_deref(), crate::RawIngestRouteUnavailableReason::ProfileUnresolved); + std::result::Result::Err(error) => { + let reason = resolution_error_reason(&error); + return unavailable_profile(profile_id, is_default, std::option::Option::None, reason); }, }; - let store_network = store.settings().network().as_str().to_owned(); - if !transport_matches_network(&transport, store_network.as_str()) { + let network = store.settings().network().as_str().to_owned(); + let base_transport_profile = + crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT); + let base_transport_profile = match base_transport_profile { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return unavailable_profile( + profile_id, + is_default, + std::option::Option::Some(network.as_str()), + crate::RawIngestRouteUnavailableReason::ProfileUnresolved, + ); + }, + }; + let base_transport = management.engine().resolve_transport_config_profile(&base_transport_profile, environment); + let base_transport = match base_transport { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return unavailable_profile(profile_id, is_default, std::option::Option::Some(network.as_str()), resolution_error_reason(&error)); + }, + }; + if !transport_matches_network(&base_transport, network.as_str()) { return unavailable_profile( profile_id, is_default, - std::option::Option::Some(store_network.as_str()), + std::option::Option::Some(network.as_str()), crate::RawIngestRouteUnavailableReason::NetworkMismatch, ); } - let routes = build_configured_profile_routes(&transport, store_network.as_str()); - return crate::RawIngestProfileInventoryDto { - is_default, - network: std::option::Option::Some(store_network), - profile_id: profile_id.to_owned(), - routes, - }; + let mut capabilities = RouteCapabilities::default(); + capabilities.absorb_transport(&base_transport, network.as_str()); + for (component_id, component) in composite.components() { + if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT { + continue; + } + if !component_id.starts_with(crate::COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX) { + continue; + } + if component_id != crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS && component_id != crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE { + return unavailable_profile( + profile_id, + is_default, + std::option::Option::Some(network.as_str()), + crate::RawIngestRouteUnavailableReason::ProfileUnresolved, + ); + } + if component.resolved().file_id().as_str() != ksp_config_lib::FILE_ID_STD_TRANSPORT { + return unavailable_profile( + profile_id, + is_default, + std::option::Option::Some(network.as_str()), + crate::RawIngestRouteUnavailableReason::ProfileUnresolved, + ); + } + let resolved = management.engine().resolve_transport_config_profile(component.resolved(), environment); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) if error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING => { + mark_optional_source_missing_secret(&mut capabilities, component_id.as_str()); + continue; + }, + std::result::Result::Err(_) => { + return unavailable_profile( + profile_id, + is_default, + std::option::Option::Some(network.as_str()), + crate::RawIngestRouteUnavailableReason::ProfileUnresolved, + ); + }, + }; + if !transport_matches_network(&resolved, network.as_str()) { + return unavailable_profile( + profile_id, + is_default, + std::option::Option::Some(network.as_str()), + crate::RawIngestRouteUnavailableReason::NetworkMismatch, + ); + } + capabilities.absorb_transport(&resolved, network.as_str()); + } + let routes = build_configured_profile_routes(&capabilities, network.as_str()); + return crate::RawIngestProfileInventoryDto { is_default, network: std::option::Option::Some(network), profile_id: profile_id.to_owned(), routes }; } -fn build_configured_profile_routes(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> std::vec::Vec { - let has_get_transaction = http_role_supports_methods(transport.http_settings(), &["getTransaction"]); - let has_http_block_scan = http_role_supports_methods(transport.http_settings(), &["getSlot", "getBlocksWithLimit", "getBlock"]); - let has_yellowstone = has_yellowstone_grpc(transport, network); - let has_standard_logs = - has_ws_capability(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Logs); - let has_standard_block = - has_ws_capability(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Block); - let has_helius_transaction = has_ws_capability( - transport, - network, - ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream, - ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction, - ); +fn mark_optional_source_missing_secret(capabilities: &mut RouteCapabilities, component_id: &str) { + if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS { + capabilities.helius_source_missing_secret = true; + } + if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE { + capabilities.yellowstone_source_missing_secret = true; + } +} + +fn build_configured_profile_routes(capabilities: &RouteCapabilities, network: &str) -> std::vec::Vec { + let yellowstone_reason = if capabilities.yellowstone_source_missing_secret { + crate::RawIngestRouteUnavailableReason::MissingRequiredSecret + } else { + crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc + }; + let helius_reason = if capabilities.helius_source_missing_secret { + crate::RawIngestRouteUnavailableReason::MissingRequiredSecret + } else { + crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability + }; return vec![ route_with_requirements( crate::RawIngestRouteId::YellowstoneHydrated, network, &[ - (has_yellowstone, crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc), - (has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction), + (capabilities.has_yellowstone, yellowstone_reason), + (capabilities.has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction), ], ), route_with_requirements( crate::RawIngestRouteId::StandardLogsHydrated, network, &[ - (has_standard_logs, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability), - (has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction), + (capabilities.has_standard_logs, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability), + (capabilities.has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction), ], ), route_with_requirements( crate::RawIngestRouteId::StandardBlockDirect, network, - &[(has_standard_block, crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability)], + &[(capabilities.has_standard_block, crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability)], ), route_with_requirements( crate::RawIngestRouteId::HeliusTransactionHydrated, network, &[ - (has_helius_transaction, crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability), - (has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction), + (capabilities.has_helius_transaction, helius_reason), + (capabilities.has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction), ], ), route_with_requirements( crate::RawIngestRouteId::HttpBlockPolling, network, - &[(has_http_block_scan, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)], + &[(capabilities.has_http_block_scan, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)], ), ]; } @@ -222,40 +321,13 @@ fn unavailable_profile( return crate::RawIngestProfileInventoryDto { is_default, network: network.map(str::to_owned), profile_id: profile_id.to_owned(), routes }; } -fn resolution_failure_reason( - transport: &ksp_core_lib::Result, - store: &ksp_core_lib::Result, -) -> crate::RawIngestRouteUnavailableReason { - let transport_missing_secret = match transport { - std::result::Result::Err(error) => error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING, - std::result::Result::Ok(_) => false, - }; - let store_missing_secret = match store { - std::result::Result::Err(error) => error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING, - std::result::Result::Ok(_) => false, - }; - if transport_missing_secret || store_missing_secret { +fn resolution_error_reason(error: &ksp_core_lib::Error) -> crate::RawIngestRouteUnavailableReason { + if error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING { return crate::RawIngestRouteUnavailableReason::MissingRequiredSecret; } return crate::RawIngestRouteUnavailableReason::ProfileUnresolved; } -fn resolved_network_hint( - transport: &ksp_core_lib::Result, - store: &ksp_core_lib::Result, -) -> std::option::Option { - if let std::result::Result::Ok(store) = store { - return std::option::Option::Some(store.settings().network().as_str().to_owned()); - } - if let std::result::Result::Ok(transport) = transport { - let networks = enabled_transport_networks(transport); - if networks.len() == 1 { - return networks.into_iter().next(); - } - } - return std::option::Option::None; -} - fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool { let networks = enabled_transport_networks(transport); return networks.len() == 1 && networks.contains(network); diff --git a/crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs b/crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs index d9ebbbb..9e68f3c 100644 --- a/crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs +++ b/crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs -// version: 1 +// version: 2 #[test] fn pre_006_route_requirement_projection_is_fail_closed_and_ordered() { @@ -27,14 +27,10 @@ fn pre_006_route_requirement_projection_is_fail_closed_and_ordered() { #[test] fn pre_006_unavailable_profile_projects_all_five_routes_without_physical_material() { - let profile = super::unavailable_profile( - "provider_profile", - false, - std::option::Option::Some("testnet"), - crate::RawIngestRouteUnavailableReason::MissingRequiredSecret, - ); + let profile = + super::unavailable_profile("mainnet", false, std::option::Option::Some("mainnet"), crate::RawIngestRouteUnavailableReason::MissingRequiredSecret); assert_eq!(profile.routes.len(), 5); - assert_eq!(profile.network.as_deref(), std::option::Option::Some("testnet")); + assert_eq!(profile.network.as_deref(), std::option::Option::Some("mainnet")); for route in profile.routes { assert!(!route.selectable); assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret)); @@ -42,60 +38,163 @@ fn pre_006_unavailable_profile_projects_all_five_routes_without_physical_materia } #[test] -fn pre_006_committed_devnet_public_inventory_proves_only_config_composability() { - let arguments = std::vec::Vec::::new(); - let management = crate::config_management(arguments.as_slice()); +fn pre_006_fix_001_network_profiles_aggregate_same_network_transport_capabilities() { + let management = committed_management(); assert!(management.is_ok()); let management = match management { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; - let inventory = crate::build_route_inventory(&management, 7); + let environment = all_optional_source_secrets(); + let inventory = super::build_route_inventory_with_environment(&management, &environment, 7); assert!(inventory.is_ok()); let inventory = match inventory { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(inventory.generation, 7); - assert_eq!(inventory.default_profile, "devnet_public"); - let profile = inventory.profiles.iter().find(|profile| return profile.profile_id == "devnet_public"); - assert!(profile.is_some()); - let profile = match profile { - std::option::Option::Some(value) => value, - std::option::Option::None => return, - }; - assert!(profile.is_default); - assert_eq!(profile.network.as_deref(), std::option::Option::Some("devnet")); - assert_eq!(profile.routes.len(), 5); - assert_route( - profile, - crate::RawIngestRouteId::YellowstoneHydrated, - false, - std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc), + assert_eq!(inventory.default_profile, "devnet"); + assert_eq!( + inventory.profiles.iter().map(|profile| return profile.profile_id.as_str()).collect::>(), + vec!["devnet", "mainnet", "testnet"] ); - assert_route(profile, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None); - assert_route( - profile, - crate::RawIngestRouteId::StandardBlockDirect, - false, - std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability), - ); - assert_route( - profile, - crate::RawIngestRouteId::HeliusTransactionHydrated, - false, - std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability), - ); - assert_route(profile, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None); + let devnet = profile(&inventory, "devnet"); + assert!(devnet.is_some()); + if let std::option::Option::Some(devnet) = devnet { + assert!(devnet.is_default); + assert_eq!(devnet.network.as_deref(), std::option::Option::Some("devnet")); + assert_route(devnet, crate::RawIngestRouteId::YellowstoneHydrated, true, std::option::Option::None); + assert_route(devnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None); + assert_route( + devnet, + crate::RawIngestRouteId::StandardBlockDirect, + false, + std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability), + ); + assert_route(devnet, crate::RawIngestRouteId::HeliusTransactionHydrated, true, std::option::Option::None); + assert_route(devnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None); + } + let mainnet = profile(&inventory, "mainnet"); + assert!(mainnet.is_some()); + if let std::option::Option::Some(mainnet) = mainnet { + assert_eq!(mainnet.network.as_deref(), std::option::Option::Some("mainnet")); + assert_route(mainnet, crate::RawIngestRouteId::YellowstoneHydrated, true, std::option::Option::None); + assert_route(mainnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None); + assert_route(mainnet, crate::RawIngestRouteId::HeliusTransactionHydrated, true, std::option::Option::None); + assert_route(mainnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None); + } + let testnet = profile(&inventory, "testnet"); + assert!(testnet.is_some()); + if let std::option::Option::Some(testnet) = testnet { + assert_eq!(testnet.network.as_deref(), std::option::Option::Some("testnet")); + assert_route(testnet, crate::RawIngestRouteId::YellowstoneHydrated, true, std::option::Option::None); + assert_route(testnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None); + assert_route( + testnet, + crate::RawIngestRouteId::HeliusTransactionHydrated, + false, + std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability), + ); + assert_route(testnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None); + } let serialized = serde_json::to_string(&inventory); assert!(serialized.is_ok()); if let std::result::Result::Ok(serialized) = serialized { - for forbidden in ["http://", "https://", "ws://", "wss://", "api-key", "x-token", "connection_uri", "source_key", "endpoint_name"] { - assert!(!serialized.contains(forbidden), "safe inventory leaked forbidden marker {forbidden}"); + for forbidden in [ + "http://", + "https://", + "ws://", + "wss://", + "api-key", + "x-token", + "connection_uri", + "source_key", + "endpoint_name", + "publicnode_mainnet", + "helius_mainnet", + ] { + assert!(!serialized.contains(forbidden), "safe network inventory leaked forbidden source marker {forbidden}"); } } } +#[test] +fn pre_006_fix_001_missing_optional_source_secret_does_not_disable_base_network_routes() { + let management = committed_management(); + assert!(management.is_ok()); + let management = match management { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let environment = ksp_config_lib::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); + let inventory = super::build_route_inventory_with_environment(&management, &environment, 11); + assert!(inventory.is_ok()); + let inventory = match inventory { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let mainnet = profile(&inventory, "mainnet"); + assert!(mainnet.is_some()); + if let std::option::Option::Some(mainnet) = mainnet { + assert_route(mainnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None); + assert_route(mainnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None); + assert_route( + mainnet, + crate::RawIngestRouteId::YellowstoneHydrated, + false, + std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret), + ); + assert_route( + mainnet, + crate::RawIngestRouteId::HeliusTransactionHydrated, + false, + std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret), + ); + } +} + +fn committed_management() -> ksp_core_lib::Result { + let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let crates_root = manifest.parent(); + let crates_root = match crates_root { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, "test manifest has no crates parent")); + }, + }; + let workspace = crates_root.parent(); + let workspace = match workspace { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, "test crates root has no workspace parent")); + }, + }; + let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), 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 = ksp_config_lib::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(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry))); +} + +fn all_optional_source_secrets() -> ksp_config_lib::ConfigEnvironment { + let mut process = std::collections::BTreeMap::::new(); + process.insert("KSP_SECRET_HELIUS_API_KEY".to_owned(), "helius-route-inventory-canary".to_owned()); + process.insert("KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN".to_owned(), "orbitflare-route-inventory-canary".to_owned()); + process.insert("KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN".to_owned(), "publicnode-mainnet-route-inventory-canary".to_owned()); + process.insert("KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN".to_owned(), "publicnode-testnet-route-inventory-canary".to_owned()); + return ksp_config_lib::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); +} + +fn profile<'a>(inventory: &'a crate::RawIngestRouteInventoryDto, profile_id: &str) -> std::option::Option<&'a crate::RawIngestProfileInventoryDto> { + return inventory.profiles.iter().find(|profile| return profile.profile_id == profile_id); +} + fn assert_route( profile: &crate::RawIngestProfileInventoryDto, route_id: crate::RawIngestRouteId, diff --git a/crates/ksp-config-lib/README.md b/crates/ksp-config-lib/README.md index a6232bd..3142d72 100644 --- a/crates/ksp-config-lib/README.md +++ b/crates/ksp-config-lib/README.md @@ -1,5 +1,5 @@ - + # ksp-config-lib @@ -104,7 +104,7 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur - [`../../config/std.store.json`](../../config/std.store.json) — targets Store PostgreSQL Devnet/Mainnet/Testnet et settings runtime bornés ; - [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ; - [`../../config/composite.ksp-app-backfill-desk.json`](../../config/composite.ksp-app-backfill-desk.json) — composition Logging/Transport/Store de Backfill Desk ; -- [`../../config/composite.ksp-app-raw-transaction-ingest-desk.json`](../../config/composite.ksp-app-raw-transaction-ingest-desk.json) — profils cohérents Logging/Transport/Store du Raw Transaction Ingest Desk ; +- [`../../config/composite.ksp-app-raw-transaction-ingest-desk.json`](../../config/composite.ksp-app-raw-transaction-ingest-desk.json) — profils réseau-centriques Logging/Transport/Store du Raw Transaction Ingest Desk, capables d’agréger plusieurs sources Transport same-network ; - [`../../config/composite.ksp-app-solprices-desk.json`](../../config/composite.ksp-app-solprices-desk.json) — composition Logging/Off-chain Transport de SOL Prices Desk ; - [`../../config/composite.ksp-app-store-desk.json`](../../config/composite.ksp-app-store-desk.json) — composition Logging/Store de Store Desk ; - [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Off-chain Transport/On-chain Transport/Wallet de Wallet Desk ; diff --git a/crates/ksp-config-lib/USAGE.md b/crates/ksp-config-lib/USAGE.md index 68f392d..296ea06 100644 --- a/crates/ksp-config-lib/USAGE.md +++ b/crates/ksp-config-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-config-lib @@ -158,13 +158,13 @@ secret_metadata = metadata dont la valeur doit avoir une provenance KSP_SECRET_* `protocol` n'est pas un nouveau `WsProtocolKind` et ne transforme pas PublicNode en protocole. Config résout les placeholders, vérifie la classe de sensibilité des metadata puis construit `YellowstoneGrpcTransportSettings`. Transport ne lit jamais l'environnement. Les valeurs de `secret_metadata` sont disponibles au runtime mais redacted dans `safe_value` et dans les représentations `Debug`. -Le provider affiche `solana-yellowstone-grpc.publicnode.com:443` ; le profil versionné `publicnode_mainnet` le représente sous la forme URL TLS requise par Transport, `https://solana-yellowstone-grpc.publicnode.com:443`, sans credential. Aucun hostname Testnet n'est versionné tant que sa valeur exacte n'est pas confirmée depuis une source opérateur/officielle. +Les profils versionnés `publicnode_mainnet` et `publicnode_testnet` représentent leurs endpoints Yellowstone sous la forme URL TLS requise par Transport et conservent le `x-token` dans `secret_metadata`. Ils restent des sources Transport network-scoped et ne définissent jamais l’identité du réseau applicatif. L'accesseur historique `into_transport_settings()` conserve volontairement son tuple `(HTTP, Option)`. Un consumer ayant besoin des trois backends utilise `into_all_transport_settings()` ou les accesseurs séparés afin de ne pas casser silencieusement les consumers V2. Les protocoles WebSocket restent `kind = "solana_standard"` et `kind = "helius_laserstream"`. Chaque endpoint peut en plus déclarer `capabilities`, liste de descriptors `WsSubscriptionKind` (`account`, `block`, `logs`, `program`, `root`, `signature`, `slot`, `slots_updates`, `vote`, `helius_transaction`). L'absence de cette liste reste backward-compatible mais ne constitue aucune preuve de support ; une liste présente est mappée explicitement vers Transport puis validée contre le protocole. Config ne déduit jamais une capability depuis `provider`. -Les profils Solana standard versionnés déclarent conservativement les six familles stables `account/logs/program/root/signature/slot` et ne déclarent donc pas `block`. Les profils `helius_devnet` et `helius_mainnet` déclarent les sept familles Helius déjà supportées par la façade (`account/logs/program/root/signature/slot/slots_updates`) plus `helius_transaction`, sans `block` ni `vote`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC. +Les profils Solana standard versionnés déclarent conservativement les six familles stables `account/logs/program/root/signature/slot` et ne déclarent donc pas `block`. Le profil `testnet_public` fournit le socle HTTP + WS standard Testnet sans credential ; `publicnode_testnet` reste une source Yellowstone optionnelle séparée. Les profils `helius_devnet` et `helius_mainnet` déclarent les sept familles Helius déjà supportées par la façade (`account/logs/program/root/signature/slot/slots_updates`) plus `helius_transaction`, sans `block` ni `vote`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC. Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. 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. @@ -347,6 +347,8 @@ Elles n'altèrent jamais l'environnement hérité du processus. Une valeur proce Sur Unix, un nouveau `.env` est créé avec des permissions privées `0600`; les permissions existantes sont préservées lors des remplacements atomiques. +Le composite `cfg.composite.ksp-app-raw-transaction-ingest-desk` expose des profils applicatifs `devnet`, `mainnet` et `testnet`. Un même profil peut référencer `transport`, `transport.helius` et `transport.yellowstone` tant que tous les profils Transport résolus appartiennent au même réseau que Store. Le provider identifie une source de capability, jamais un réseau logique. + ## 8. `.env.example` `/.env.example` est l'inventaire versionné. `/.env` reste local et ignoré. diff --git a/crates/ksp-config-lib/unit_tests/composite.rs b/crates/ksp-config-lib/unit_tests/composite.rs index 0d6a847..fdc0973 100644 --- a/crates/ksp-config-lib/unit_tests/composite.rs +++ b/crates/ksp-config-lib/unit_tests/composite.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/unit_tests/composite.rs -// version: 5 +// version: 6 const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json"; const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test"; @@ -129,7 +129,7 @@ fn duplicate_component_ids_are_rejected_inside_one_composite_profile() { } #[test] -fn v0_3_15_pre_003_raw_ingest_composite_keeps_transport_and_store_networks_coherent() { +fn v0_3_15_pre_006_fix_001_raw_ingest_composite_is_network_centric_and_keeps_all_transport_sources_coherent() { let workspace = workspace_root(); let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas")); assert!(bootstrap.is_ok(), "committed Config roots should be valid: {bootstrap:?}"); @@ -148,47 +148,58 @@ fn v0_3_15_pre_003_raw_ingest_composite_keeps_transport_and_store_networks_coher process.insert("KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN".to_owned(), "publicnode-mainnet-composite-canary".to_owned()); process.insert("KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN".to_owned(), "publicnode-testnet-composite-canary".to_owned()); let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); - for (profile_id, expected_network) in [ - ("devnet_public", "devnet"), - ("helius_devnet", "devnet"), - ("orbitflare_devnet", "devnet"), - ("mainnet_public", "mainnet"), - ("helius_mainnet", "mainnet"), - ("publicnode_mainnet", "mainnet"), - ("publicnode_testnet", "testnet"), + for (profile_id, expected_network, expected_transport_profiles) in [ + ("devnet", "devnet", vec!["devnet_public", "helius_devnet", "orbitflare_devnet"]), + ("mainnet", "mainnet", vec!["mainnet_public", "helius_mainnet", "publicnode_mainnet"]), + ("testnet", "testnet", vec!["testnet_public", "publicnode_testnet"]), ] { let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some(profile_id)); - assert!(composite.is_ok(), "Raw Ingest composite profile {profile_id} should resolve: {composite:?}"); + assert!(composite.is_ok(), "Raw Ingest network profile {profile_id} should resolve: {composite:?}"); let composite = match composite { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => continue, }; assert_eq!(composite.profile_id(), profile_id); - let transport_component = composite.component("transport"); let store_component = composite.component("store"); - assert!(transport_component.is_some(), "profile {profile_id} should contain Transport"); assert!(store_component.is_some(), "profile {profile_id} should contain Store"); - let (transport_component, store_component) = match (transport_component, store_component) { - (std::option::Option::Some(transport), std::option::Option::Some(store)) => (transport, store), - _ => continue, + let store_component = match store_component { + std::option::Option::Some(value) => value, + std::option::Option::None => continue, }; - assert_eq!(transport_component.resolved().profile_id(), profile_id); assert_eq!(store_component.resolved().profile_id(), expected_network); - let transport = engine.resolve_transport_config_profile(transport_component.resolved(), &environment); let store = engine.resolve_store_config_profile(store_component.resolved(), &environment); - assert!(transport.is_ok(), "Transport profile {profile_id} should map: {transport:?}"); assert!(store.is_ok(), "Store profile {expected_network} should map: {store:?}"); - let (transport, store) = match (transport, store) { - (std::result::Result::Ok(transport), std::result::Result::Ok(store)) => (transport, store), - _ => continue, - }; - assert_eq!(store.settings().network().as_str(), expected_network); - assert!(transport.http_settings().endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network)); - if let std::option::Option::Some(ws) = transport.ws_settings() { - assert!(ws.endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network)); + if let std::result::Result::Ok(store) = store { + assert_eq!(store.settings().network().as_str(), expected_network); } - if let std::option::Option::Some(grpc) = transport.grpc_settings() { - assert!(grpc.endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network)); + let transport_components = composite + .components() + .iter() + .filter(|(component_id, component)| { + return component_id.starts_with("transport") && component.resolved().file_id().as_str() == crate::FILE_ID_STD_TRANSPORT; + }) + .map(|(_, component)| return component) + .collect::>(); + assert_eq!(transport_components.len(), expected_transport_profiles.len()); + let actual_profiles = transport_components.iter().map(|component| return component.resolved().profile_id()).collect::>(); + for expected_transport_profile in expected_transport_profiles { + assert!( + actual_profiles.contains(&expected_transport_profile), + "network profile {profile_id} should include Transport source {expected_transport_profile}" + ); + } + for component in transport_components { + let transport = engine.resolve_transport_config_profile(component.resolved(), &environment); + assert!(transport.is_ok(), "Transport source {} should map: {transport:?}", component.resolved().profile_id()); + if let std::result::Result::Ok(transport) = transport { + assert!(transport.http_settings().endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network)); + if let std::option::Option::Some(ws) = transport.ws_settings() { + assert!(ws.endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network)); + } + if let std::option::Option::Some(grpc) = transport.grpc_settings() { + assert!(grpc.endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network)); + } + } } } } diff --git a/crates/ksp-config-lib/unit_tests/fixtures/composite.ksp-app-raw-transaction-ingest-desk.json b/crates/ksp-config-lib/unit_tests/fixtures/composite.ksp-app-raw-transaction-ingest-desk.json index d6eff9a..aab147f 100644 --- a/crates/ksp-config-lib/unit_tests/fixtures/composite.ksp-app-raw-transaction-ingest-desk.json +++ b/crates/ksp-config-lib/unit_tests/fixtures/composite.ksp-app-raw-transaction-ingest-desk.json @@ -1,9 +1,9 @@ { "format_version": 1, - "default_profile": "devnet_public", + "default_profile": "devnet", "profiles": [ { - "profile_id": "devnet_public", + "profile_id": "devnet", "documents": [ { "component_id": "logging", @@ -16,42 +16,12 @@ "profile_id": "secret_test" }, { - "component_id": "store", - "file_id": "cfg.std.store", - "profile_id": "devnet" - } - ] - }, - { - "profile_id": "helius_devnet", - "documents": [ - { - "component_id": "logging", - "file_id": "cfg.std.logging", - "profile_id": "local_dev" - }, - { - "component_id": "transport", + "component_id": "transport.helius", "file_id": "cfg.std.transport", "profile_id": "helius_devnet" }, { - "component_id": "store", - "file_id": "cfg.std.store", - "profile_id": "devnet" - } - ] - }, - { - "profile_id": "orbitflare_devnet", - "documents": [ - { - "component_id": "logging", - "file_id": "cfg.std.logging", - "profile_id": "local_dev" - }, - { - "component_id": "transport", + "component_id": "transport.yellowstone", "file_id": "cfg.std.transport", "profile_id": "secret_test" }, @@ -63,7 +33,7 @@ ] }, { - "profile_id": "mainnet_public", + "profile_id": "mainnet", "documents": [ { "component_id": "logging", @@ -75,6 +45,16 @@ "file_id": "cfg.std.transport", "profile_id": "secret_test" }, + { + "component_id": "transport.helius", + "file_id": "cfg.std.transport", + "profile_id": "helius_devnet" + }, + { + "component_id": "transport.yellowstone", + "file_id": "cfg.std.transport", + "profile_id": "secret_test" + }, { "component_id": "store", "file_id": "cfg.std.store", @@ -83,7 +63,7 @@ ] }, { - "profile_id": "helius_mainnet", + "profile_id": "testnet", "documents": [ { "component_id": "logging", @@ -96,42 +76,7 @@ "profile_id": "secret_test" }, { - "component_id": "store", - "file_id": "cfg.std.store", - "profile_id": "mainnet" - } - ] - }, - { - "profile_id": "publicnode_mainnet", - "documents": [ - { - "component_id": "logging", - "file_id": "cfg.std.logging", - "profile_id": "local_dev" - }, - { - "component_id": "transport", - "file_id": "cfg.std.transport", - "profile_id": "secret_test" - }, - { - "component_id": "store", - "file_id": "cfg.std.store", - "profile_id": "mainnet" - } - ] - }, - { - "profile_id": "publicnode_testnet", - "documents": [ - { - "component_id": "logging", - "file_id": "cfg.std.logging", - "profile_id": "local_dev" - }, - { - "component_id": "transport", + "component_id": "transport.yellowstone", "file_id": "cfg.std.transport", "profile_id": "secret_test" }, diff --git a/crates/ksp-config-lib/unit_tests/transport.rs b/crates/ksp-config-lib/unit_tests/transport.rs index bc7a889..c06e181 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: 14 +// version: 15 #[test] fn fixture_transport_profile_maps_complete_runtime_contract() { @@ -211,6 +211,33 @@ fn committed_v3_orbitflare_devnet_profile_maps_provider_neutral_yellowstone_grpc } } +#[test] +fn v0_3_15_pre_006_fix_001_testnet_public_is_keyless_standard_transport_without_yellowstone() { + let engine = committed_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::Some("testnet_public"), &environment); + assert!(resolved.is_ok(), "committed keyless Testnet Transport profile should map without provider credentials: {resolved:?}"); + if let std::result::Result::Ok(resolved) = resolved { + assert_eq!(resolved.profile_id(), "testnet_public"); + assert_eq!(resolved.settings().endpoints().len(), 1); + assert_eq!(resolved.settings().endpoints()[0].cluster().as_str(), "testnet"); + let ws = resolved.ws_settings(); + assert!(ws.is_some()); + if let std::option::Option::Some(ws) = ws { + assert_eq!(ws.endpoints().len(), 1); + assert_eq!(ws.endpoints()[0].cluster().as_str(), "testnet"); + assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard); + assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs)); + assert!(!ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block)); + } + assert!(resolved.grpc_settings().is_none(), "keyless Testnet base profile must not conflate PublicNode Yellowstone with the network itself"); + } +} + #[test] fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_with_network_scoped_secret_x_tokens() { let engine = committed_engine(); diff --git a/deltas/0.3.15/pre.006-fix.001.md b/deltas/0.3.15/pre.006-fix.001.md new file mode 100644 index 0000000..125097c --- /dev/null +++ b/deltas/0.3.15/pre.006-fix.001.md @@ -0,0 +1,159 @@ + + + +# Delta `0.3.15-pre.006-fix.001` — classification réseau des capabilities Raw Ingest + +## Base requise + +```text +0.3.15-pre.006 +workspace.package.version = 0.3.15-pre.6 +deltas/0.3.15/pre.006.md présent +``` + +## Défauts observés + +Le gate opérateur de `pre.006` révèle deux défauts distincts. + +Le canari `pre_006_committed_devnet_public_inventory_proves_only_config_composability` échoue car il construit `ConfigManagement` depuis les racines utilisateur courantes au lieu d'utiliser une racine Config de test déterministe. + +Le contrôle fonctionnel du Desk révèle surtout une mauvaise frontière de classification : `Yellowstone + HTTP hydration` sur Mainnet n'est `Configured` que lorsque le profil applicatif sélectionné est `publicnode_mainnet`. Ce comportement confond le profil Transport d'un provider avec l'identité du réseau logique. + +## Correction du modèle applicatif + +Le composite Raw Transaction Ingest Desk devient réseau-centrique : + +```text +devnet +mainnet +testnet +``` + +Chaque profil applicatif possède un Transport de base et peut référencer des sources Transport optionnelles du même réseau : + +```text +transport +transport.helius +transport.yellowstone +``` + +Le provider reste une propriété de la source Transport. Il ne devient ni un réseau ni un profil opérateur du Desk. + +Ainsi, pour `mainnet`, les capabilities sont agrégées depuis : + +```text +mainnet_public +helius_mainnet +publicnode_mainnet +``` + +Lorsque `KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN` est résolu, Yellowstone gRPC est une capability de `mainnet`; le choix d'un pseudo-profil `publicnode_mainnet` n'existe plus dans l'UI applicative. + +Le même principe s'applique à Devnet avec `devnet_public`, `helius_devnet` et `orbitflare_devnet`. + +## Testnet indépendant du token PublicNode + +Un nouveau profil Transport `testnet_public` fournit le socle HTTP + WebSocket Solana standard sans secret. + +`publicnode_testnet` reste une source Yellowstone optionnelle. L'absence du token PublicNode Testnet ne rend donc plus les routes HTTP/WS standard Testnet indisponibles. + +## Secrets optionnels + +Une erreur `ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING` sur une source Transport optionnelle ne rend plus tout le profil réseau indisponible. + +Elle affecte uniquement la route qui dépend de cette source : + +```text +transport.helius -> Helius Transaction + HTTP hydration +transport.yellowstone -> Yellowstone + HTTP hydration +``` + +Les routes satisfaites par le Transport de base restent `Configured`. + +Toute source Transport effectivement résolue doit néanmoins prouver exactement le même réseau que Store. Un mismatch reste fail-closed. + +## Canaris déterministes + +Les tests du Desk n'utilisent plus les racines Config utilisateur. Ils construisent un `ConfigDocumentEngine` sur les fichiers committed du workspace et injectent explicitement l'environnement nécessaire. + +Les nouveaux canaris vérifient notamment : + +```text +inventaire applicatif exact = devnet/mainnet/testnet ; +agrégation Helius/Yellowstone same-network ; +mainnet Yellowstone configuré lorsque le token PublicNode Mainnet est résolu ; +routes standard Mainnet toujours configurées lorsque les secrets optionnels sont absents ; +absence des identités provider dans la projection frontend ; +Testnet standard utilisable sans token PublicNode. +``` + +## Frontières conservées + +```text +aucun probe réseau ; +aucune ouverture Store ; +aucune session Transport ; +aucun Worker ; +aucun Start/Stop ; +aucune inférence depuis provider ; +aucune URL, credential, metadata secrète, URI Store ou handle projeté au frontend. +``` + +## Fichiers ajoutés + +```text +deltas/0.3.15/pre.006-fix.001.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +config/composite.ksp-app-raw-transaction-ingest-desk.json +config/std.transport.json +crates/ksp-app-raw-transaction-ingest-desk/README.md +crates/ksp-app-raw-transaction-ingest-desk/USAGE.md +crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html +crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts +crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs +crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs +crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs +crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs +crates/ksp-config-lib/README.md +crates/ksp-config-lib/USAGE.md +crates/ksp-config-lib/unit_tests/composite.rs +crates/ksp-config-lib/unit_tests/fixtures/composite.ksp-app-raw-transaction-ingest-desk.json +crates/ksp-config-lib/unit_tests/transport.rs +docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md +docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md +``` + +## Fichiers supprimés + +```text +aucun +``` + +## Version Cargo + +```text +header Cargo.toml : 602 -> 603 +workspace.package.version : 0.3.15-pre.6 -> 0.3.15-pre.6.fix.1 +``` + +## Gate opérateur attendu + +```bash +cargo fmt --all +cargo fmt --all -- --check +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.15 +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p ksp-config-lib --all-targets --all-features +cargo test -p ksp-app-raw-transaction-ingest-desk --all-targets --all-features +cargo test --workspace --all-targets --all-features +(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev) +``` + +Aucun `npm run build` direct n'appartient à ce gate ; Tauri possède le cycle frontend via ses hooks. diff --git a/docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md b/docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md index ec135df..6376e0d 100644 --- a/docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md +++ b/docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md @@ -1,5 +1,5 @@ - + # Plan v0.3.15 — Raw Transaction Ingest Desk @@ -199,16 +199,18 @@ Aucun gap Store n'est identifié. L'application ne dépend jamais de `ksp-store- | `mainnet_public` | mainnet | HTTP + WS Solana standard | non | oui | capabilities stables déclarées ; `Logs` oui ; `Block` non | | `helius_mainnet` | mainnet | HTTP public + WS Helius | API key Helius | oui | `HeliusTransaction` oui ; `Block`/`Vote` non | | `mainnet_backfill_pool` | mainnet | plusieurs HTTP + WS standard | selon endpoints | oui | `Logs` déclaré ; profil conservé hors composite Raw Ingest Desk | -| `publicnode_mainnet` | mainnet | HTTP + WS standard + Yellowstone gRPC | x-token secret | oui | `Logs` déclaré ; gRPC composable seulement si secret résolu | -| `publicnode_testnet` | testnet | HTTP + WS standard + Yellowstone gRPC | x-token secret | oui | `Logs` déclaré ; gRPC composable seulement si secret résolu | +| `publicnode_mainnet` | mainnet | HTTP + WS standard + Yellowstone gRPC | x-token secret | oui | source Yellowstone optionnelle du profil applicatif `mainnet` | +| `testnet_public` | testnet | HTTP + WS Solana standard | non | oui | socle keyless du profil applicatif `testnet` | +| `publicnode_testnet` | testnet | HTTP + WS standard + Yellowstone gRPC | x-token secret | oui | source Yellowstone optionnelle du profil applicatif `testnet` | Conséquences : ```text un fichier sous config/examples/ ne rend aucune route disponible une capability provider documentée sur le Web ne rend aucune route disponible -un secret requis mais non résolu rend le profil concerné non composable +un secret requis par une source optionnelle ne rend indisponible que la route qui dépend de cette source le Desk ne synthétise pas une capability à partir de provider == helius/publicnode/orbitflare +les profils applicatifs du Desk sont des réseaux logiques, pas des providers ``` ## 7. Modèle de route V1 @@ -726,10 +728,14 @@ Restauration du template Desk KSP clair, bornage de la grille de routes et corre Résoudre Config côté Rust et projeter les routes composables/non composables avec raisons sûres et génération d'inventaire, sans endpoint, URL, secret ou source key côté frontend. -**État : implémenté côté assemblage.** Le Desk ouvre uniquement la dépendance directe `ksp-onchain-transport-lib` nécessaire aux capabilities typées. Le backend énumère les profils du composite validé, résout Transport et Store via `ksp-config-lib`, exige un réseau logique unique identique au Store, puis calcule les cinq routes sans I/O réseau. HTTP est prouvé directement par rôles + `request_kinds` sur les settings typés, sans construire de pool/client ; WS repose sur `WsProtocolKind` + `WsSubscriptionKind`; Yellowstone exige un endpoint gRPC activé. L'inventaire expose profil, réseau, génération, route/state/selectable et reason codes sûrs. Il n'ouvre ni Store, ni session Transport, ni Worker et ne projette aucune ressource physique. +**État : implémenté puis corrigé par `pre.006-fix.001`.** Le Desk ouvre uniquement la dépendance directe `ksp-onchain-transport-lib` nécessaire aux capabilities typées. Le composite applicatif est réseau-centrique (`devnet`, `mainnet`, `testnet`) et peut référencer plusieurs profils Transport du même réseau. Le backend agrège leurs capabilities sans inférer quoi que ce soit du provider : PublicNode Mainnet Yellowstone est donc une capability de `mainnet`, pas un pseudo-réseau ni un profil opérateur séparé. Les sources optionnelles Helius/Yellowstone dont le secret manque ne désactivent que la route qui dépend de cette source. HTTP est prouvé directement par rôles + `request_kinds`, WS par `WsProtocolKind` + `WsSubscriptionKind`, Yellowstone par présence d'un endpoint gRPC activé. Aucun Store, session Transport ou Worker n'est ouvert. Budget cible : une tranche composition. +#### `pre.006-fix.001` — profils réseau-centriques et agrégation des capabilities + +Correction du modèle de classification : les providers redeviennent des sources Transport same-network et le sélecteur applicatif ne porte plus que `devnet`, `mainnet` et `testnet`. Ajout d'un profil `testnet_public` sans secret pour dissocier le Transport standard Testnet de PublicNode Yellowstone. Les canaris d'inventaire utilisent désormais des racines Config committées et un environnement injecté, sans dépendre du Config utilisateur de la machine. + ### `pre.007` — reconstruction Start des ressources Transport Revalider Config au Start et reconstruire les ressources Transport exactes nécessaires aux cinq routes, sans lancer encore le Worker. Le choix des endpoints physiques reste Transport-owned. diff --git a/docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md b/docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md index 373fee8..e1e4f20 100644 --- a/docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md +++ b/docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md @@ -1,5 +1,5 @@ - + # Validation v0.3.15 — Raw Transaction Ingest Desk @@ -190,19 +190,21 @@ HTTP : rôles et `request_kinds` sont déjà projetables côté Rust. WS : le protocol kind est projetable, mais `WsEndpointSettings` ne porte pas de liste de `WsSubscriptionKind`. C'est le gap déterminant de `pre.001`. -Profils committed vs exemple : +Profils Transport sources audités (ils ne constituent pas les profils applicatifs du Desk) : -| Profil/config | Réseau | Surfaces | Secret requis | Committed ? | Observation | -|-------------------------|------------------------------|---------------------------------------|-----------------------|----------------------|-------------------------------------------| -| `devnet_public` | devnet | HTTP + WS Solana standard | non | oui | pas de capability WS fine aujourd’hui | -| `orbitflare_devnet` | devnet | HTTP + WS standard + Yellowstone gRPC | token gRPC secret | conditionnel | profil complet seulement si secret résolu | -| `mainnet_public` | mainnet | HTTP + WS Solana standard | non | oui | pas de capability WS fine aujourd’hui | -| `mainnet_backfill_pool` | mainnet | plusieurs HTTP + WS standard | selon endpoint/config | oui si profil résolu | priorités/rôles restent Transport-owned | -| `publicnode_mainnet` | mainnet | HTTP + WS standard + Yellowstone gRPC | x-token secret | conditionnel | gRPC provider-gated | -| `publicnode_testnet` | testnet | HTTP + WS standard + Yellowstone gRPC | x-token secret | conditionnel | gRPC provider-gated | -| Helius exemple | mainnet/devnet selon exemple | WS `helius_laserstream` | API key secret | non committed | exemple != profil actif | +| Profil/config | Réseau | Surfaces | Secret requis | Committed ? | Observation | +|-------------------------|---------|---------------------------------------|-----------------------|----------------------|-------------------------------------------| +| `devnet_public` | devnet | HTTP + WS Solana standard | non | oui | pas de capability WS fine aujourd’hui | +| `orbitflare_devnet` | devnet | HTTP + WS standard + Yellowstone gRPC | token gRPC secret | conditionnel | profil complet seulement si secret résolu | +| `mainnet_public` | mainnet | HTTP + WS Solana standard | non | oui | pas de capability WS fine aujourd’hui | +| `mainnet_backfill_pool` | mainnet | plusieurs HTTP + WS standard | selon endpoint/config | oui si profil résolu | priorités/rôles restent Transport-owned | +| `publicnode_mainnet` | mainnet | HTTP + WS standard + Yellowstone gRPC | x-token secret | conditionnel | source Yellowstone same-network | +| `testnet_public` | testnet | HTTP + WS Solana standard | non | oui | socle keyless Testnet | +| `publicnode_testnet` | testnet | HTTP + WS standard + Yellowstone gRPC | x-token secret | conditionnel | source Yellowstone same-network | +| `helius_devnet` | devnet | HTTP public + WS Helius | API key secret | conditionnel | source Helius same-network | +| `helius_mainnet` | mainnet | HTTP public + WS Helius | API key secret | conditionnel | source Helius same-network | -Aucun profil Helius WS n'est committed dans `config/std.transport.json`; sa présence dans un exemple ne constitue pas une availability V1. +Ces profils sont des sources Transport. Le composite applicatif Raw Ingest les agrège désormais sous `devnet`, `mainnet` et `testnet`; un provider n'est jamais une identité réseau. ## 9. Audit Store @@ -360,7 +362,11 @@ Le forecast durable est porté par la section 20 du plan `036` sous forme hiéra ### `pre.006` — inventaire composable -**Statut : réalisé côté assemblage ; gate opérateur requis.** Le backend énumère les profils du composite Config validé et reconstruit une génération complète d'inventaire sans probe réseau. Transport et Store sont résolus par `ksp-config-lib`; la cohérence impose un réseau unique identique au Store. Les capabilities HTTP sont vérifiées par rôle/méthodes sans dispatch, les capabilities WS par `WsProtocolKind` + `WsSubscriptionKind`, et Yellowstone par présence d'un endpoint gRPC activé. Les cinq routes produisent uniquement `Configured` ou `Unavailable` avec reason codes sûrs. Le frontend reçoit seulement profil logique, réseau, génération, IDs/familles/labels, state/selectable/reason ; aucun endpoint, URL, secret, metadata, Store URI, source key ou handle runtime n'est projeté. Aucun Store, session Transport ou Worker n'est démarré. +**Statut : gate opérateur de `pre.006` en échec fonctionnel, corrigé par `pre.006-fix.001`.** Le premier inventaire classait encore les capabilities par profil Transport fournisseur : `Yellowstone + HTTP hydration` Mainnet n'était donc `Configured` que sous `publicnode_mainnet`. Le fix remplace cette projection par trois profils réseau (`devnet`, `mainnet`, `testnet`) qui agrègent les sources Transport same-network. PublicNode, Helius et OrbitFlare restent des sources de capabilities et ne deviennent jamais des identités réseau. Un secret optionnel absent n'invalide que la route dépendante. Le canari qui dépendait du Config utilisateur de la machine est remplacé par un moteur Config committé et un environnement injecté. Aucun I/O runtime n'est ajouté. + +#### `pre.006-fix.001` — correction classification réseau + +Attendus : `mainnet` expose Yellowstone dès que la source PublicNode Mainnet est effectivement résolue, quelle que soit la source HTTP/WS standard retenue dans le même profil réseau ; `mainnet` reste utilisable pour Standard Logs et HTTP Block Polling lorsque les secrets Helius/PublicNode sont absents. Le même principe s'applique à Devnet et Testnet. ### `pre.007` — ressources Start