diff --git a/Cargo.toml b/Cargo.toml index 31ef9ed..d1692a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 297 +# version: 298 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.2.12-pre.8.fix.3" +version = "0.2.12-pre.9" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-app-solprices-desk/package.json b/crates/ksp-app-solprices-desk/package.json index 14367f0..397b357 100644 --- a/crates/ksp-app-solprices-desk/package.json +++ b/crates/ksp-app-solprices-desk/package.json @@ -1,7 +1,7 @@ { "name": "ksp-app-solprices-desk", "private": true, - "version": "0.2.12-pre.7", + "version": "0.2.12-pre.9", "type": "module", "scripts": { "dev": "vite", diff --git a/crates/ksp-app-solprices-desk/src/market_price_runtime.rs b/crates/ksp-app-solprices-desk/src/market_price_runtime.rs index 12b72e6..fa48e48 100644 --- a/crates/ksp-app-solprices-desk/src/market_price_runtime.rs +++ b/crates/ksp-app-solprices-desk/src/market_price_runtime.rs @@ -1,10 +1,12 @@ // file: crates/ksp-app-solprices-desk/src/market_price_runtime.rs -// version: 3 +// version: 4 //! Provider-neutral market-price presentation runtime owned by SOL Prices Desk. use ts_rs::TS; // rust-rules: trait-import +const MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS: usize = 64; + /// Safe provider-neutral row projected to the SOL Prices Desk frontend. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] @@ -534,6 +536,16 @@ fn lock_presentation( } fn parse_provider_ids(provider_ids: std::vec::Vec) -> ksp_core_lib::Result> { + if provider_ids.is_empty() || provider_ids.len() > MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS { + return std::result::Result::Err( + ksp_core_lib::Error::new( + crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID, + "SOL Prices Desk market-price refresh selection has an invalid provider count", + ) + .with_context("provider_count", provider_ids.len().to_string()) + .with_context("max_provider_count", MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS.to_string()), + ); + } let mut parsed = std::vec::Vec::with_capacity(provider_ids.len()); for provider_id in provider_ids { let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new(provider_id); diff --git a/crates/ksp-app-solprices-desk/tauri.conf.json b/crates/ksp-app-solprices-desk/tauri.conf.json index dd0c32b..c652459 100644 --- a/crates/ksp-app-solprices-desk/tauri.conf.json +++ b/crates/ksp-app-solprices-desk/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "KSP SOL Prices Desk", - "version": "0.2.12-pre.7", + "version": "0.2.12-pre.9", "identifier": "com.sasedev.ksp-app-solprices-desk", "build": { "beforeDevCommand": { diff --git a/crates/ksp-app-solprices-desk/tests/desktop_contract.rs b/crates/ksp-app-solprices-desk/tests/desktop_contract.rs index f8b0a37..427082f 100644 --- a/crates/ksp-app-solprices-desk/tests/desktop_contract.rs +++ b/crates/ksp-app-solprices-desk/tests/desktop_contract.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs -// version: 7 +// version: 8 //! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`. @@ -80,6 +80,7 @@ fn pre_002_shell_uses_reserved_ports_windows_and_owned_backend_paths() { let package_version = package.pointer("/version").and_then(serde_json::Value::as_str); assert_packaged_version_floor(package_version, "package.json"); assert_eq!(tauri_version, package_version, "desktop package metadata versions must remain synchronized"); + assert_eq!(tauri_version, std::option::Option::Some(env!("CARGO_PKG_VERSION")), "SOL Prices Desk package metadata must match the current Cargo release"); assert_eq!(tauri.pointer("/build/devUrl").and_then(serde_json::Value::as_str), std::option::Option::Some("http://localhost:1434")); assert_eq!(tauri.pointer("/build/beforeDevCommand/script").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run dev")); assert_eq!(tauri.pointer("/build/beforeDevCommand/cwd").and_then(serde_json::Value::as_str), std::option::Option::Some(".")); diff --git a/crates/ksp-app-solprices-desk/tests/desktop_security.rs b/crates/ksp-app-solprices-desk/tests/desktop_security.rs index ea57acc..3629e48 100644 --- a/crates/ksp-app-solprices-desk/tests/desktop_security.rs +++ b/crates/ksp-app-solprices-desk/tests/desktop_security.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-solprices-desk/tests/desktop_security.rs -// version: 7 +// version: 8 //! Security, ownership and frontend instrumentation canaries for SOL Prices Desk. @@ -186,3 +186,16 @@ fn pre_007_remains_manual_without_browser_persistence_or_price_scheduler() { assert!(!main.contains(forbidden), "pre.007 must remain manual and backend-owned: {forbidden}"); } } + +#[test] +fn pre_009_hardening_bounds_hostile_batches_and_preserves_manual_backend_owned_refresh() { + let root = app_root(); + let runtime = read_text(root.join("src/market_price_runtime.rs").as_path()); + assert!(runtime.contains("const MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS: usize = 64;")); + assert!(runtime.contains("provider_ids.is_empty() || provider_ids.len() > MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS")); + assert!(runtime.contains("ERROR_CODE_MARKET_PRICE_SELECTION_INVALID")); + let main = read_text(root.join("frontend/ts/main.ts").as_path()); + for forbidden in ["setInterval(", "setTimeout(", "requestAnimationFrame(", "fetch(", "XMLHttpRequest", "localStorage", "sessionStorage"] { + assert!(!main.contains(forbidden), "release hardening must preserve manual backend-owned refresh: {forbidden}"); + } +} diff --git a/crates/ksp-app-solprices-desk/tests/market_price_composition_live_smoke.rs b/crates/ksp-app-solprices-desk/tests/market_price_composition_live_smoke.rs new file mode 100644 index 0000000..bb3fa8d --- /dev/null +++ b/crates/ksp-app-solprices-desk/tests/market_price_composition_live_smoke.rs @@ -0,0 +1,80 @@ +// file: crates/ksp-app-solprices-desk/tests/market_price_composition_live_smoke.rs +// version: 1 + +//! Opt-in live smoke proving SOL Prices Desk composite -> Config -> generic MarketPriceService composition. + +fn workspace_engine() -> ksp_core_lib::Result { + let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + 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(); + return match registry { + std::result::Result::Ok(value) => std::result::Result::Ok(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, value)), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; +} + +#[tokio::test(flavor = "current_thread")] +#[ignore = "opt-in live SOL Prices Desk composition smoke; resolves the committed public_keyless composite and performs external SOL/USD HTTPS refreshes"] +async fn committed_solprices_composite_refreshes_at_least_one_keyless_provider() { + let engine = workspace_engine(); + assert!(engine.is_ok(), "workspace Config engine should be constructible"); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK); + assert!(file_id.is_ok(), "SOL Prices Desk composite file_id should be valid"); + let file_id = match file_id { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let composite = engine.load_resolved_composite(&file_id, std::option::Option::None); + assert!(composite.is_ok(), "committed SOL Prices Desk composite should resolve"); + let composite = match composite { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(composite.profile_id(), "public_keyless"); + let component = composite.component("offchain_transport"); + assert!(component.is_some(), "SOL Prices Desk composite should contain Off-chain Transport"); + let component = match component { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let environment = ksp_config_lib::ConfigEnvironment::load(); + assert!(environment.is_ok(), "Config-owned environment snapshot should load"); + let environment = match environment { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let resolved = engine.resolve_offchain_transport_config_profile(component.resolved(), &environment); + assert!(resolved.is_ok(), "composite-selected public_keyless Off-chain Transport should map"); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(resolved.profile_id(), "public_keyless"); + assert_eq!(resolved.selection_source(), ksp_config_lib::ConfigProfileSelectionSource::Composite); + let registry = resolved.service().registry(); + assert_eq!(registry.len(), 8); + let outcomes = resolved.service().refresh_all().await; + assert!(outcomes.is_ok(), "generic public_keyless refresh_all should complete without provider-specific application branching"); + let outcomes = match outcomes { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(outcomes.len(), registry.len()); + let refreshed_count = outcomes.iter().filter(|outcome| return outcome.refreshed()).count(); + assert!(refreshed_count > 0, "at least one public_keyless provider should produce one live SOL/USD observation"); + for outcome in outcomes { + assert!(registry.entry(outcome.provider_id()).is_some(), "live outcome provider must belong to the generic registry"); + if let std::option::Option::Some(observation) = outcome.observation() { + assert_eq!(observation.pair(), ksp_offchain_transport_lib::MarketPricePair::SolUsd); + assert_eq!(observation.provider_id(), outcome.provider_id()); + } + } +} diff --git a/crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs b/crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs index e361f6b..ee97501 100644 --- a/crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs +++ b/crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs -// version: 3 +// version: 4 fn descriptor() -> std::option::Option { let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new("test-provider"); @@ -278,3 +278,25 @@ fn batch_loading_validation_rejects_in_flight_provider_without_marking_other_row } return std::result::Result::Ok(()); } + +#[test] +fn hostile_batch_size_is_rejected_before_provider_id_parsing() { + let provider_ids = (0..=super::MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS) + .map(|index| return std::format!("invalid provider payload {index}")) + .collect::>(); + let result = super::parse_provider_ids(provider_ids); + assert!(result.is_err()); + if let std::result::Result::Err(error) = result { + assert_eq!(error.code(), crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID); + assert_eq!(error.context().len(), 2); + } +} + +#[test] +fn empty_batch_is_rejected_at_the_application_boundary() { + let result = super::parse_provider_ids(std::vec::Vec::new()); + assert!(result.is_err()); + if let std::result::Result::Err(error) = result { + assert_eq!(error.code(), crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID); + } +} diff --git a/crates/ksp-app-wallet-desk/package.json b/crates/ksp-app-wallet-desk/package.json index 8e9870f..01c1201 100644 --- a/crates/ksp-app-wallet-desk/package.json +++ b/crates/ksp-app-wallet-desk/package.json @@ -1,7 +1,7 @@ { "name": "ksp-app-wallet-desk", "private": true, - "version": "0.2.12-pre.8.fix.3", + "version": "0.2.12-pre.9", "type": "module", "scripts": { "dev": "vite", diff --git a/crates/ksp-app-wallet-desk/tauri.conf.json b/crates/ksp-app-wallet-desk/tauri.conf.json index 00254d8..dc4434e 100644 --- a/crates/ksp-app-wallet-desk/tauri.conf.json +++ b/crates/ksp-app-wallet-desk/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "KSP Wallet Desk", - "version": "0.2.12-pre.8.fix.3", + "version": "0.2.12-pre.9", "identifier": "com.sasedev.ksp-app-wallet-desk", "build": { "beforeDevCommand": { diff --git a/crates/ksp-app-wallet-desk/tests/desktop_contract.rs b/crates/ksp-app-wallet-desk/tests/desktop_contract.rs index 41f942d..8964729 100644 --- a/crates/ksp-app-wallet-desk/tests/desktop_contract.rs +++ b/crates/ksp-app-wallet-desk/tests/desktop_contract.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs -// version: 28 +// version: 29 //! Desktop build, shell and Config-status contract audits for Wallet Desk. @@ -77,6 +77,10 @@ fn tauri_shell_uses_reserved_wallet_desk_ports_and_template_windows() { assert_eq!(tauri.pointer("/build/beforeDevCommand/cwd").and_then(serde_json::Value::as_str), std::option::Option::Some(".")); assert_eq!(tauri.pointer("/build/beforeBuildCommand/script").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run build")); assert_eq!(tauri.pointer("/build/beforeBuildCommand/cwd").and_then(serde_json::Value::as_str), std::option::Option::Some(".")); + assert_eq!( + tauri.pointer("/build/frontendDist").and_then(serde_json::Value::as_str), + std::option::Option::Some("../../../builds/khadhroony-solana-project/ksp-app-wallet-desk/dist"), + ); assert_eq!(tauri.pointer("/app/security/csp"), std::option::Option::Some(&serde_json::Value::Null)); let manifest = read_text(root.join("Cargo.toml").as_path()); assert!(manifest.contains("name = \"ksp-app-wallet-desk\"")); @@ -465,6 +469,7 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi let package_version = package.pointer("/version").and_then(serde_json::Value::as_str); assert_packaged_version_floor(package_version, "package.json"); assert_eq!(tauri_version, package_version, "desktop package metadata versions must remain synchronized"); + assert_eq!(tauri_version, std::option::Option::Some(env!("CARGO_PKG_VERSION")), "Wallet Desk package metadata must match the current Cargo release"); let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object); assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist"); if let std::option::Option::Some(resources) = resources { diff --git a/crates/ksp-app-wallet-desk/tests/release_compliance.rs b/crates/ksp-app-wallet-desk/tests/release_compliance.rs index 7689ced..58c47b3 100644 --- a/crates/ksp-app-wallet-desk/tests/release_compliance.rs +++ b/crates/ksp-app-wallet-desk/tests/release_compliance.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/tests/release_compliance.rs -// version: 8 +// version: 9 //! Release-wide deterministic compliance canaries for Wallet Desk. @@ -215,3 +215,29 @@ fn packaged_resources_include_only_registered_config_sources_and_schemas() { } } } + +#[test] +fn v0_2_12_wallet_market_price_projection_remains_provider_neutral_and_exact() { + let root = app_root(); + let source = read_rust_source_tree(root.join("src").as_path()); + for forbidden in [ + "MarketPriceBirdeye", + "MarketPriceCoinbaseExchange", + "MarketPriceCoinGecko", + "MarketPriceCoinMarketCap", + "MarketPriceCoinPaprika", + "MarketPriceDexScreener", + "MarketPriceJupiter", + "MarketPriceKraken", + ] { + assert!(!source.contains(forbidden), "Wallet Desk must not branch on concrete market-price providers: {forbidden}"); + } + let balance = read_text(root.join("src/wallet_balance.rs").as_path()); + assert!(!balance.contains("f64")); + assert!(!balance.contains("f32")); + let main = read_text(root.join("frontend/ts/main.ts").as_path()); + assert!(!main.contains("parseFloat(")); + assert!(!main.contains("Number(balance.sol")); + assert!(!main.contains("providerId")); + assert!(!main.contains("refresh_market_price")); +} diff --git a/deltas/0.2.12/pre.009.md b/deltas/0.2.12/pre.009.md new file mode 100644 index 0000000..17f990f --- /dev/null +++ b/deltas/0.2.12/pre.009.md @@ -0,0 +1,168 @@ + + + +# Delta `0.2.12-pre.009` — hardening transversal et gate technique/live final + +## Base requise + +```text +0.2.12-pre.008-fix.003 +``` + +Le gate opérateur de cette base est propre : `cargo fmt --all`, audits Rust/Markdown, `cargo check --workspace`, Clippy `--all-targets`, `cargo test -p ksp-app-wallet-desk` et `cargo test --workspace` passent. Le parcours fonctionnel Wallet avait déjà confirmé le refresh balance, la moyenne SOL/USD, l'équivalent USD et la version dynamique. + +## Objectif + +Fermer le couloir technique de `0.2.12` sans ouvrir de nouvelle fonctionnalité métier : + +```text +borne batch hostile Price Desk avant parsing provider ID +canaris packages/CWD/resources/provider-neutralité cross-app +smoke live keyless de composition applicative générique +synchronisation npm/Tauri des deux apps modifiées sur pre.9 +préparation du gate workspace + deux builds Tauri finaux +``` + +## Version technique + +```text +delivery 0.2.12-pre.009 +workspace Cargo 0.2.12-pre.9 +SOL Prices package/Tauri 0.2.12-pre.9 +Wallet package/Tauri 0.2.12-pre.9 +``` + +## Hardening Price Desk + +`MarketPriceRuntime::refresh_many` conserve le dispatch générique existant mais rejette désormais avant parsing des IDs opaques une sélection : + +```text +vide +ou +> 64 provider IDs +``` + +La borne applicative est volontairement indépendante de tout provider concret. La valeur `64` correspond au plafond V1 déjà défendu par `MarketPriceService`; le Desk peut rester plus strict qu'une évolution future du service tant qu'une révision explicite de l'IPC n'a pas été faite. + +Les doublons, IDs inconnus et conflits `in_flight` restent validés comme avant. Aucun scheduler, sleep, fallback, consensus, provider branch ou réseau frontend n'est ajouté. + +## Smoke live de composition + +Ajout du test opt-in : + +```text +crates/ksp-app-solprices-desk/tests/market_price_composition_live_smoke.rs +``` + +Commande opérateur : + +```bash +cargo test -p ksp-app-solprices-desk --test market_price_composition_live_smoke -- --ignored --nocapture +``` + +Le smoke ne réaudite pas les wire adapters. Il prouve seulement le chemin applicatif attendu : + +```text +cfg.composite.ksp-app-solprices-desk / public_keyless +-> ConfigDocumentEngine +-> ResolvedOffchainTransportConfig +-> MarketPriceService registry = 8 +-> refresh_all +-> au moins une observation SOL/USD live +``` + +Aucun secret n'est requis par ce profil. + +## Canaris finaux renforcés + +```text +SOL Prices package.json == tauri.conf.json == CARGO_PKG_VERSION +Wallet package.json == tauri.conf.json == CARGO_PKG_VERSION +Wallet frontendDist reste le dist Wallet dédié +Price Desk capabilities restent core + tracing uniquement +Price Desk frontend reste sans fetch/persistence/scheduler +Wallet src reste sans type provider concret +Wallet moyenne/équivalent restent sans f64/f32 +Wallet frontend reste sans calcul flottant prix +resources Config restent bornées au registry packagé +``` + +## Fichiers ajoutés + +```text +crates/ksp-app-solprices-desk/tests/market_price_composition_live_smoke.rs +deltas/0.2.12/pre.009.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-app-solprices-desk/package.json +crates/ksp-app-solprices-desk/tauri.conf.json +crates/ksp-app-solprices-desk/src/market_price_runtime.rs +crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs +crates/ksp-app-solprices-desk/tests/desktop_contract.rs +crates/ksp-app-solprices-desk/tests/desktop_security.rs +crates/ksp-app-wallet-desk/package.json +crates/ksp-app-wallet-desk/tauri.conf.json +crates/ksp-app-wallet-desk/tests/desktop_contract.rs +crates/ksp-app-wallet-desk/tests/release_compliance.rs +docs/plans/019-V0_2_12_SOL_PRICES_DESK_PLAN.md +docs/validation/015-V0_2_12_SOL_PRICES_DESK.md +``` + +## Fichiers supprimés + +```text +aucun +``` + +## Hors scope préservé + +```text +nouveau provider +modification wire/provider adapters +nouvelle commande prix Wallet +panneau provider détaillé Wallet +auto-refresh/polling/scheduler +fallback ou provider canonique +historique/OHLCV/chart +Store/worker prix +réconciliation README/USAGE finale +CHANGELOG/ROADMAP/prompt de release suivante +``` + +## Validations exécutées dans le sandbox de préparation + +```text +python3 scripts/audit_rust_workspace_rules.py PASS +python3 scripts/audit_markdown_tables.py README.md ... deltas PASS +validation JSON package/tauri PASS +contrôles statiques versions/frontières PASS +cargo fmt/check/clippy/test/live/tauri build NOT RUN — cargo indisponible dans le sandbox +``` + +## Gate opérateur attendu + +Exécuter dans cet ordre : + +```bash +cargo fmt --all +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.2.12 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-app-solprices-desk +cargo test -p ksp-app-wallet-desk +cargo test --workspace +cargo test -p ksp-app-solprices-desk --test market_price_composition_live_smoke -- --ignored --nocapture +(cd crates/ksp-app-solprices-desk && cargo tauri build) +(cd crates/ksp-app-wallet-desk && cargo tauri build) +``` + +Les deux `cargo tauri build` sont volontairement les dernières opérations lourdes conformément à `KSP-APP-034`. + +## Critère de fermeture + +`pre.009` est fermée lorsque le gate workspace, le smoke live de composition et les deux builds Tauri production sont verts. À ce stade, `pre.010` peut rester strictement documentaire/mécanique avant `rel.001`. diff --git a/docs/plans/019-V0_2_12_SOL_PRICES_DESK_PLAN.md b/docs/plans/019-V0_2_12_SOL_PRICES_DESK_PLAN.md index d9623b0..2450909 100644 --- a/docs/plans/019-V0_2_12_SOL_PRICES_DESK_PLAN.md +++ b/docs/plans/019-V0_2_12_SOL_PRICES_DESK_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.2.12` — SOL Prices Desk + intégration prix Wallet Desk @@ -250,9 +250,12 @@ Le périmètre Wallet Desk est volontairement réduit après validation complèt Wallet Desk existing shell -> Dashboard / Wallet courant -> Balance SOL existante - -> une seule case “SOL/USD moyen” + -> “SOL/USD moyen” -> valeur moyenne dérivée du refresh courant -> N.A. si aucune observation exploitable + -> “Équivalent USD” + -> balance SOL × moyenne du refresh courant, calcul exact côté Rust + -> N.A. si le prix auxiliaire est indisponible -> bouton Refresh balance existant -> getBalance On-chain -> refresh_all Off-chain des providers éligibles @@ -262,7 +265,7 @@ Wallet Desk existing shell Aucun inventaire provider, aucune availability détaillée et aucun bouton prix supplémentaire ne sont dupliqués dans Wallet Desk. Les fonctions avancées restent la responsabilité de `ksp-app-solprices-desk`. Le prix auxiliaire suit le lifecycle du refresh de balance existant et n'ajoute aucun nouveau droit VIEW/OWNER. -**Valorisation USD du solde : toujours hors scope `0.2.12`.** La moyenne est seulement affichée comme information SOL/USD ; aucun calcul `balance SOL × prix` n'est introduit. +**Équivalent USD retenu par décision opérateur.** Il reste une projection auxiliaire strictement dérivée du refresh courant : calcul côté Rust sans `f64`, aucun prix canonique/fallback et aucune dépendance supplémentaire du frontend aux providers. ## 9. DTO map retenue @@ -342,8 +345,8 @@ refresh_wallet_balance -> en parallèle : MarketPriceService::refresh_all -> ne garder que les observations réussies du refresh courant -> moyenne arithmétique backend sans f64, arrondie à 8 décimales maximum - -> WalletBalanceDto { ..., sol_usd_average: Option } - -> une seule case UI “SOL/USD moyen” ou “N.A.” + -> WalletBalanceDto { ..., sol_usd_average: Option, sol_usd_equivalent: Option } + -> UI compacte “SOL/USD moyen” + “Équivalent USD”, ou “N.A.” ``` La branche prix est **auxiliaire/best-effort** : un runtime Off-chain indisponible, zéro observation ou un échec structurel prix ne rend pas `getBalance` invalide. La balance SOL reste autoritative et la projection prix devient `None`. @@ -444,7 +447,7 @@ Canari obligatoire : `ksp-app-solprices-desk` référence son propre `package.js | TS-RS | bindings exportés par tests; prix exact en String | | workspace | fmt + audits + check + clippy + test | | SOL Prices Desk dev | parcours splash -> table -> row/many/all sur composition réelle | -| Wallet Desk dev | refresh balance + une case moyenne SOL/USD/N.A. + non-régression Wallet | +| Wallet Desk dev | refresh balance + moyenne SOL/USD + équivalent USD/N.A. + non-régression Wallet | | live composition | un smoke keyless applicatif opt-in suffit; ne réaudite pas les 8 wires | | production build | `cargo tauri build` crate-local pour SOL Prices Desk puis Wallet Desk | @@ -459,7 +462,7 @@ OHLCV/historique/chart SSE/WebSocket prix consensus/fallback automatique provider canonique -valorisation USD du solde Wallet +panneau providers détaillé ou commandes prix dédiées dans Wallet Desk auto-refresh/polling périodique Store/worker/job prix éditeur Config dans Price Desk @@ -555,7 +558,7 @@ Finalisation de la table responsive sans changer le contrat de dispatch : résum ### `pre.008` — Intégration Wallet minimale + recalibrage de clôture -**Statut : réalisé ; corrigé par `pre.008-fix.001`, `pre.008-fix.002` puis `pre.008-fix.003`, gate opérateur final à confirmer.** +**Statut : réalisé ; corrigé par `pre.008-fix.001`, `pre.008-fix.002` puis `pre.008-fix.003`, gate opérateur final PASS.** Absorption de l'ancien couloir Wallet `pre.009`–`pre.011` dans une seule tranche : ajout `offchain_transport/public_keyless` au composite Wallet, runtime Off-chain auxiliaire, `refresh_all` lors de `refresh_wallet_balance`, moyenne SOL/USD backend sans `f64` et projection compacte dans Wallet Desk. La correction `fix.002` complète le besoin opérateur réellement attendu : `WalletBalanceDto` expose également un équivalent USD exact calculé côté Rust à partir des lamports et de la moyenne du refresh courant ; Dashboard et Details affichent le prix moyen et l'équivalent ou `N.A.`. Aucun panneau provider, aucune command prix Wallet et aucun provider ID frontend ne sont ajoutés. La tranche réconcilie également le forecast avec cette réduction de scope. @@ -573,15 +576,15 @@ Correction du dernier écart fonctionnel de la tranche : le refresh produisait b #### `pre.008-fix.003` — Correction du littéral de canari version dynamique -**Statut : implémenté ; gate opérateur à confirmer.** +**Statut : réalisé ; gate opérateur Cargo/workspace intégralement PASS.** Le gate de `pre.008-fix.002` confirme le fonctionnement runtime et visuel attendu : Dashboard et Details affichent la moyenne SOL/USD, l'équivalent USD et la version `0.2.12-pre.8.fix.2`. `cargo fmt`, les audits KSP et `cargo check --workspace` passent. Clippy `--all-targets` s'arrête uniquement sur le canari `desktop_contract.rs` : la raw string `r#"..."#` se ferme prématurément sur le fragment `"#appVersionBadge"`. Le fix remplace cette assertion par une chaîne Rust standard correctement échappée, sans modifier le runtime, le frontend ou le calcul prix. ### `pre.009` — Hardening transversal + gate technique/live final -**Statut : planifié.** +**Statut : implémenté ; gate opérateur live/build à confirmer.** -Fusion des anciens hardenings `pre.008`/`pre.012` et du gate `pre.013` : canaris batch hostile/capabilities/resources, non-régression des deux Desks, workspace complet, smoke live keyless de composition, et `cargo tauri build` final des deux applications modifiées. Aucun nouveau comportement métier n'est ouvert. +Fusion des anciens hardenings `pre.008`/`pre.012` et du gate `pre.013` : la sélection batch Price Desk est bornée à `1..=64` avant parsing des IDs opaques, les canaris capabilities/resources/CWD/version/package et provider-neutralité Wallet sont renforcés, et un smoke live opt-in prouve le chemin composite SOL Prices Desk -> Config -> `MarketPriceService::refresh_all` avec au moins une observation keyless. Les métadonnées npm/Tauri des deux Desks modifiés sont synchronisées avec `0.2.12-pre.9` pour les builds de production finaux. Aucun nouveau comportement métier n'est ouvert ; les seuls gates restant opérateur sont le workspace complet, le smoke live et les deux `cargo tauri build` crate-local. ### `pre.010` — Réconciliation documentaire + préparation de publication diff --git a/docs/validation/015-V0_2_12_SOL_PRICES_DESK.md b/docs/validation/015-V0_2_12_SOL_PRICES_DESK.md index 5606532..51193ea 100644 --- a/docs/validation/015-V0_2_12_SOL_PRICES_DESK.md +++ b/docs/validation/015-V0_2_12_SOL_PRICES_DESK.md @@ -1,5 +1,5 @@ - + # Validation `0.2.12` — SOL Prices Desk + intégration Wallet Desk @@ -39,8 +39,8 @@ NOT RUN commande non exécutée dans le contexte indiqué | threat model desktop | PASS | plan 019 section 16 | | Wallet integration map | PASS | plan 019 section 12 | | auto-refresh | OUT | hors scope confirmé | -| valorisation USD du solde | OUT | aucun prix canonique; hors scope confirmé | -| forecast souple recalibré | PASS | 15 prereleases + rel; plan 019 section 19 | +| équivalent USD du solde | PASS | demandé opérateur; calcul backend exact depuis moyenne du refresh | +| forecast souple recalibré | PASS | clôture condensée jusqu’à `pre.010` + `rel.001` | | scope clôturable | PASS | pas de redécoupage requis avant `pre.002` | | code/scaffold fonctionnel ajouté en `pre.001` | OUT | interdit par le gate | @@ -58,9 +58,9 @@ NOT RUN commande non exécutée dans le contexte indiqué | refresh selected/many | `MarketPriceService::refresh_many` | PASS | | refresh all | `MarketPriceService::refresh_all` | PASS | | Diagnostics | profils/counts sûrs seulement | PASS | -| Wallet prix moyen | une case `SOL/USD moyen` / `N.A.` | PASS | +| Wallet prix moyen | `SOL/USD moyen` / `N.A.` | PASS | | auto-refresh | aucun scheduler/polling | OUT | -| Wallet USD valuation | aucune multiplication balance × prix | OUT | +| Wallet équivalent USD | calcul Rust exact / `N.A.` | PASS | ## 4. DTO/command matrix @@ -69,7 +69,7 @@ NOT RUN commande non exécutée dans le contexte indiqué | `MarketPriceRuntimeStatusDto` | profiles/counts sans secret | PASS | | `MarketPriceProviderRowDto` | ID/display/pair/semantics/auth/availability/price/timestamps/retry | PASS | | single refresh direct | provider_id opaque seulement | PASS | -| `MarketPriceRefreshManyRequestDto` | liste d’IDs opaques ; borne hostile à durcir en `pre.009` | PASS | +| `MarketPriceRefreshManyRequestDto` | liste d’IDs opaques ; garde app `1..=64` avant parsing | PASS | | `MarketPriceRefreshResultDto` | rows/outcome classifiés | PASS | | runtime/list/refresh/many/all commands | aucun URL/header/API key/body | PASS | | TS-RS exports | bindings générés non versionnés | PASS | @@ -78,20 +78,20 @@ NOT RUN commande non exécutée dans le contexte indiqué ## 5. Config/composite matrix -| Contrat | Valeur attendue | Statut | -|---------------------------------------|--------------------------------------------------------------|---------| -| Price Desk composite file | `config/composite.ksp-app-solprices-desk.json` | PASS | -| Price Desk file_id | `cfg.composite.ksp-app-solprices-desk` | PASS | -| schema | `schema.composite` existant | PASS | -| components | `logging`, `offchain_transport` | PASS | -| default profile | `public_keyless` | PASS | -| optional profile | `all_free` | PASS | -| test profile | `tests` -> offchain `public_keyless` | PASS | -| Wallet composite | ajoute offchain `public_keyless` à devnet/temporary/tests | PENDING | -| registry/resource count | 11 après nouveau composite | PASS | -| registry + resources atomiques | Config + Config Desk + Wallet Desk + Price Desk même tranche | PASS | -| nouveau schema Config | inutile | OUT | -| modification `std.offchain_transport` | pas de lacune identifiée | OUT | +| Contrat | Valeur attendue | Statut | +|---------------------------------------|--------------------------------------------------------------|--------| +| Price Desk composite file | `config/composite.ksp-app-solprices-desk.json` | PASS | +| Price Desk file_id | `cfg.composite.ksp-app-solprices-desk` | PASS | +| schema | `schema.composite` existant | PASS | +| components | `logging`, `offchain_transport` | PASS | +| default profile | `public_keyless` | PASS | +| optional profile | `all_free` | PASS | +| test profile | `tests` -> offchain `public_keyless` | PASS | +| Wallet composite | ajoute offchain `public_keyless` à devnet/temporary/tests | PASS | +| registry/resource count | 11 après nouveau composite | PASS | +| registry + resources atomiques | Config + Config Desk + Wallet Desk + Price Desk même tranche | PASS | +| nouveau schema Config | inutile | OUT | +| modification `std.offchain_transport` | pas de lacune identifiée | OUT | ## 6. Provider-agnostic boundary matrix @@ -109,22 +109,22 @@ NOT RUN commande non exécutée dans le contexte indiqué ## 7. Security matrix -| Risque/canari | Attendu | Statut | -|--------------------------------|-------------------------------------------------|---------| -| URL arbitraire depuis frontend | impossible par contrat command | PASS | -| extraction API key/header | aucune projection/log | PASS | -| provider_id inconnu | erreur KSP sûre | PASS | -| batch duplicate | rejet avant dispatch | PASS | -| batch oversized | borne hostile applicative à durcir en `pre.009` | PENDING | -| refresh spam | limites/cooldown Off-chain toujours actifs | PASS | -| stale observation | timestamps/état distingués | PASS | -| double-click race | in-flight/order contrôlés | PASS | -| close pendant refresh | aucune persistance/sûreté frontend requise | PENDING | -| CWD/resource confusion | canaris propres à chaque app | PENDING | -| Price Desk capabilities | `core:default`, `tracing:default` uniquement | PASS | -| plugin dialog Price Desk | absent | PASS | -| frontend sensitive logging | action/ID/count/code seulement | PASS | -| Wallet VIEW/OWNER/secrets | non-régression | PENDING | +| Risque/canari | Attendu | Statut | +|--------------------------------|-------------------------------------------------------|--------| +| URL arbitraire depuis frontend | impossible par contrat command | PASS | +| extraction API key/header | aucune projection/log | PASS | +| provider_id inconnu | erreur KSP sûre | PASS | +| batch duplicate | rejet avant dispatch | PASS | +| batch oversized | garde app `1..=64` avant parsing/dispatch | PASS | +| refresh spam | limites/cooldown Off-chain toujours actifs | PASS | +| stale observation | timestamps/état distingués | PASS | +| double-click race | in-flight/order contrôlés | PASS | +| close pendant refresh | état uniquement mémoire ; aucune persistance frontend | PASS | +| CWD/resource confusion | frontendDist/cwd/ports/resources canarisés par app | PASS | +| Price Desk capabilities | `core:default`, `tracing:default` uniquement | PASS | +| plugin dialog Price Desk | absent | PASS | +| frontend sensitive logging | action/ID/count/code seulement | PASS | +| Wallet VIEW/OWNER/secrets | suite Wallet + compliance inchangées | PASS | ## 8. Wallet integration matrix @@ -134,16 +134,16 @@ NOT RUN commande non exécutée dans le contexte indiqué | Config component | Wallet composite -> `cfg.std.offchain_transport/public_keyless` | PASS | | runtime | Config -> `ResolvedOffchainTransportConfig` -> `MarketPriceService` | PASS | | command | `refresh_wallet_balance` existante, aucun input prix/provider | PASS | -| DTO | `WalletBalanceDto.sol_usd_average: Option` | PASS | +| DTO | moyenne + équivalent USD en `Option` exacts | PASS | | moyenne | observations réussies du refresh courant, backend sans `f64`, 8 déc. max | PASS | -| UI | une seule case `SOL/USD moyen` / `N.A.` | PASS | +| UI | résumé compact `SOL/USD moyen` + `Équivalent USD` / `N.A.` | PASS | | balance HTTP On-chain | `getBalance` inchangé et toujours autoritatif | PASS | | dégradation prix | échec/absence prix -> `N.A.` sans faire échouer la balance | PASS | | WalletSession VIEW/OWNER | inchangée | PASS | | Wallet secret projections | aucune nouvelle | PASS | | provider ID frontend | aucun | PASS | | command prix Wallet | aucune | PASS | -| USD balance multiplication | hors scope | OUT | +| USD balance multiplication | calcul Rust exact depuis lamports + moyenne du refresh courant | PASS | ## 9. Tauri/package matrix @@ -158,6 +158,7 @@ NOT RUN commande non exécutée dans le contexte indiqué | backend cross-app | aucune référence Wallet par erreur | PASS | | resources | registry complet packagé | PASS | | bundle | targets/icons cohérents | PASS | +| versions apps modifiées | npm/Tauri = Cargo `0.2.12-pre.9` | PASS | | Tauri production build Price Desk | crate-local PASS attendu | PENDING | | Tauri production build Wallet Desk | crate-local PASS attendu | PENDING | @@ -171,8 +172,8 @@ NOT RUN commande non exécutée dans le contexte indiqué | `cargo check --workspace` | PENDING | | `cargo clippy --workspace --all-targets` | PENDING | | `cargo test --workspace` | PENDING | -| Price Desk functional dev path | PENDING | -| Wallet Desk functional price path | PENDING | +| Price Desk functional dev path | PASS | +| Wallet Desk functional price path | PASS | | keyless app-composition live smoke | PENDING | | Price Desk `cargo tauri build` | PENDING | | Wallet Desk `cargo tauri build` | PENDING | @@ -726,3 +727,70 @@ cargo test -p ksp-app-wallet-desk cargo test --workspace ``` +## 28. Gate `0.2.12-pre.009` — hardening transversal et gate final + +Le gate opérateur de `pre.008-fix.003` ferme définitivement la tranche Wallet condensée : + +```text +cargo fmt --all PASS +python3 scripts/audit_rust_workspace_rules.py PASS +python3 scripts/audit_markdown_tables.py README.md ... deltas PASS +cargo check --workspace PASS +cargo clippy --workspace --all-targets PASS +cargo test -p ksp-app-wallet-desk PASS +cargo test --workspace PASS +``` + +Le parcours fonctionnel avait déjà confirmé sous `fix.002` puis `fix.003` la moyenne SOL/USD, l'équivalent USD, la version dynamique et le refresh balance réel. + +`pre.009` ajoute les hardenings finaux suivants sans ouvrir de nouvelle fonctionnalité : + +```text +Price Desk batch IPC garde 1..=64 avant parsing des provider IDs +batch vide rejet KSP sûr +batch hostile 65 IDs rejet KSP sûr avant parsing provider +Price Desk manual refresh aucun scheduler/fetch/browser persistence +Price Desk package metadata npm/Tauri == CARGO_PKG_VERSION +Wallet package metadata npm/Tauri == CARGO_PKG_VERSION +Wallet frontendDist chemin Wallet dédié canarisé +Wallet provider-neutrality aucun type provider concret dans src/ +Wallet exactness aucun f64/f32 backend, aucun calcul flottant frontend +keyless live composition nouveau smoke opt-in SOL Prices Desk +``` + +Le smoke live dédié est : + +```bash +cargo test -p ksp-app-solprices-desk --test market_price_composition_live_smoke -- --ignored --nocapture +``` + +Validations sandbox après matérialisation de la tranche : + +```text +python3 scripts/audit_rust_workspace_rules.py PASS +python3 scripts/audit_markdown_tables.py README.md ... deltas PASS +JSON package/tauri des deux apps modifiées PASS +contrôles statiques versions/resources/frontières PASS +cargo fmt/check/clippy/test/live/tauri build NOT RUN — cargo indisponible +``` + +Il doit résoudre le composite committed `public_keyless`, mapper `ResolvedOffchainTransportConfig`, conserver exactement les huit entrées génériques du registry, exécuter `MarketPriceService::refresh_all` et obtenir au moins une observation SOL/USD. Il ne réaudite pas les huit wire adapters individuellement. + +Gate opérateur final à exécuter, dans cet ordre : + +```bash +cargo fmt --all +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.2.12 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-app-solprices-desk +cargo test -p ksp-app-wallet-desk +cargo test --workspace +cargo test -p ksp-app-solprices-desk --test market_price_composition_live_smoke -- --ignored --nocapture +(cd crates/ksp-app-solprices-desk && cargo tauri build) +(cd crates/ksp-app-wallet-desk && cargo tauri build) +``` + +Conformément à `KSP-APP-034`, les deux `cargo tauri build` restent les dernières opérations lourdes. +