v0.2.12-pre.008

This commit is contained in:
2026-08-27 14:41:08 +02:00
parent 2042482592
commit f1e365ec97
22 changed files with 631 additions and 129 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 294
# version: 295
[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.7"
version = "0.2.12-pre.8"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -10,6 +10,11 @@
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "offchain_transport",
"file_id": "cfg.std.offchain_transport",
"profile_id": "public_keyless"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
@@ -30,6 +35,11 @@
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "offchain_transport",
"file_id": "cfg.std.offchain_transport",
"profile_id": "public_keyless"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
@@ -50,6 +60,11 @@
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "offchain_transport",
"file_id": "cfg.std.offchain_transport",
"profile_id": "public_keyless"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",

View File

@@ -10,6 +10,11 @@
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "offchain_transport",
"file_id": "cfg.std.offchain_transport",
"profile_id": "public_keyless"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
@@ -30,6 +35,11 @@
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "offchain_transport",
"file_id": "cfg.std.offchain_transport",
"profile_id": "public_keyless"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
@@ -50,6 +60,11 @@
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "offchain_transport",
"file_id": "cfg.std.offchain_transport",
"profile_id": "public_keyless"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-wallet-desk/Cargo.toml
# version: 5
# version: 6
[package]
name = "ksp-app-wallet-desk"
@@ -27,6 +27,7 @@ fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-offchain-transport-lib = { path = "../ksp-offchain-transport-lib" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
ksp-wallet-lib = { path = "../ksp-wallet-lib" }
serde = { workspace = true, features = ["derive"] }

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-wallet-desk/frontend/main.html -->
<!-- version: 5 -->
<!-- version: 6 -->
<!DOCTYPE html>
<html lang="fr">
<head>
@@ -97,6 +97,7 @@
<dt class="col-5">Alias</dt><dd id="currentWalletAlias" class="col-7 text-break"></dd>
<dt class="col-5">Notes</dt><dd id="currentWalletNotes" class="col-7 text-break"></dd>
<dt class="col-5">Balance SOL</dt><dd id="currentWalletBalanceSol" class="col-7"></dd>
<dt class="col-5">SOL/USD moyen</dt><dd id="currentWalletSolUsdAverage" class="col-7">N.A.</dd>
</dl>
</div>
</div>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 16
// version: 17
import { Modal } from "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -378,6 +378,7 @@ function clearStrongViewSensitiveInputs(): void {
function clearBalanceProjection(): void {
setText("#currentWalletBalanceSol", "—");
setText("#currentWalletSolUsdAverage", "N.A.");
setText("#detailsBalanceLamports", "—");
setText("#detailsBalanceSol", "—");
setText("#detailsBalanceSlot", "—");
@@ -1379,13 +1380,14 @@ function renderWalletBalance(balance: WalletBalanceDto): void {
return;
}
setText("#currentWalletBalanceSol", balance.sol);
setText("#currentWalletSolUsdAverage", balance.solUsdAverage ? `${balance.solUsdAverage} USD` : "N.A.");
setText("#detailsBalanceLamports", balance.lamports);
setText("#detailsBalanceSol", balance.sol);
setText("#detailsBalanceSlot", balance.slot.toString());
setText("#detailsBalanceApiVersion", balance.apiVersion ?? "—");
setText("#detailsBalanceTransport", `${balance.transportProfile} / ${balance.transportRole}`);
setText("#balanceStatus", "Balance réseau actualisée.");
frontendInfo("main", "Wallet balance rendered", { lamports: balance.lamports, slot: balance.slot, walletId: balance.walletId });
setText("#balanceStatus", "Balance réseau et prix moyen SOL/USD actualisés.");
frontendInfo("main", "Wallet balance rendered", { averageAvailable: balance.solUsdAverage !== null, lamports: balance.lamports, slot: balance.slot, walletId: balance.walletId });
}
async function refreshWalletBalance(): Promise<void> {
@@ -1393,8 +1395,8 @@ async function refreshWalletBalance(): Promise<void> {
return;
}
const walletId = activeWalletId;
setText("#balanceStatus", "Interrogation getBalance en cours…");
frontendDebug("main", "Wallet balance refresh requested", { walletId });
setText("#balanceStatus", "Interrogation getBalance et refresh SOL/USD en cours…");
frontendDebug("main", "Wallet balance and SOL/USD refresh requested", { walletId });
try {
const balance = await invokeKsp<WalletBalanceDto>("main", "refresh_wallet_balance");
renderWalletBalance(balance);

View File

@@ -1,7 +1,7 @@
{
"name": "ksp-app-wallet-desk",
"private": true,
"version": "0.2.6",
"version": "0.2.12-pre.8",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 19
// version: 20
//! Shared backend state owned by the Wallet Desk Tauri application.
@@ -7,6 +7,7 @@
pub(crate) struct AppState {
config_management: ksp_config_lib::ConfigManagement,
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
offchain_runtime: std::option::Option<crate::OffchainTransportRuntime>,
splash_settings: crate::SplashSettings,
splash_sequence_started: std::sync::atomic::AtomicBool,
transport_runtime: crate::TransportRuntime,
@@ -41,6 +42,20 @@ impl AppState {
return std::result::Result::Err(error);
},
};
let offchain_runtime = crate::initialize_offchain_transport(&config_management);
let offchain_runtime = match offchain_runtime {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Wallet Desk Off-chain Transport is unavailable; SOL/USD average will be N.A."
);
std::option::Option::None
},
};
let wallet_config_startup = crate::initialize_wallet_config(&config_management);
let wallet_config_startup = match wallet_config_startup {
std::result::Result::Ok(value) => value,
@@ -66,6 +81,7 @@ impl AppState {
fallback_active: logging_startup.fallback_active,
startup_diagnostic: logging_startup.startup_diagnostic,
}),
offchain_runtime,
splash_settings,
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
transport_runtime,
@@ -1126,8 +1142,10 @@ impl AppState {
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
std::option::Option::None,
);
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_TRANSPORT, wallet_id = context.wallet_id.as_str(), capability = context.capability, transport_profile = self.transport_runtime.profile_id(), transport_role = self.transport_runtime.role().as_str(), "Wallet balance refresh started");
let result = self.transport_runtime.pool().get_balance(self.transport_runtime.role(), &context.pubkey, std::option::Option::Some(&config)).await;
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_TRANSPORT, wallet_id = context.wallet_id.as_str(), capability = context.capability, transport_profile = self.transport_runtime.profile_id(), transport_role = self.transport_runtime.role().as_str(), "Wallet balance and auxiliary SOL/USD refresh started");
let balance_future = self.transport_runtime.pool().get_balance(self.transport_runtime.role(), &context.pubkey, std::option::Option::Some(&config));
let price_future = self.refresh_sol_usd_average();
let (result, sol_usd_average) = tokio::join!(balance_future, price_future);
let result = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -1147,14 +1165,52 @@ impl AppState {
lamports: lamports.to_string(),
slot: result.context().slot(),
sol: crate::format_lamports_as_sol(lamports),
sol_usd_average,
transport_profile: self.transport_runtime.profile_id().to_owned(),
transport_role: self.transport_runtime.role().as_str().to_owned(),
wallet_id: context.wallet_id.clone(),
};
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_TRANSPORT, wallet_id = context.wallet_id.as_str(), capability = context.capability, lamports, slot = dto.slot, "Wallet balance refresh succeeded");
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_TRANSPORT, wallet_id = context.wallet_id.as_str(), capability = context.capability, lamports, slot = dto.slot, sol_usd_average_available = dto.sol_usd_average.is_some(), "Wallet balance refresh succeeded");
return std::result::Result::Ok(dto);
}
async fn refresh_sol_usd_average(&self) -> std::option::Option<String> {
let runtime = match &self.offchain_runtime {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let outcomes = runtime.service().refresh_all().await;
let outcomes = match outcomes {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
offchain_profile = runtime.profile_id(),
error_domain = error.code().domain(),
error_code = error.code().code(),
"Wallet Desk SOL/USD provider refresh failed; average is unavailable"
);
return std::option::Option::None;
},
};
let prices = outcomes
.iter()
.filter_map(|outcome| return outcome.observation().map(|observation| return observation.price()))
.collect::<std::vec::Vec<_>>();
let average = crate::average_sol_usd_prices(prices.as_slice());
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
offchain_profile = runtime.profile_id(),
provider_count = outcomes.len(),
observed_provider_count = prices.len(),
average_available = average.is_some(),
"Wallet Desk SOL/USD refresh completed"
);
return average;
}
fn authorized_balance_context(&self) -> ksp_core_lib::Result<WalletBalanceContext> {
let session = self.wallet_session.lock();
let session = match session {

View File

@@ -1,10 +1,12 @@
// file: crates/ksp-app-wallet-desk/src/constants.rs
// version: 8
// version: 9
//! Logging targets, domains and composite component identifiers owned by Wallet Desk.
/// Composite-local identifier for the Logging standard document.
pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
/// Composite-local identifier for the Off-chain Transport standard document.
pub(crate) const COMPOSITE_COMPONENT_ID_OFFCHAIN_TRANSPORT: &str = "offchain_transport";
/// Composite-local identifier for the HTTP Transport standard document.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
/// Composite-local identifier for the Wallet standard document.
@@ -13,6 +15,8 @@ pub(crate) const COMPOSITE_COMPONENT_ID_WALLET: &str = "wallet";
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "wallet.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by Wallet Desk Off-chain Transport and SOL/USD average operations.
pub(crate) const TRACING_DOMAIN_OFFCHAIN_TRANSPORT: &str = "wallet.offchain_transport";
/// Structured domain used by the Wallet Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell";
/// Structured domain used by Wallet Desk HTTP Transport and balance operations.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/lib.rs
// version: 13
// version: 14
//! Tauri desktop application shell for KSP Wallet management and inspection.
@@ -14,6 +14,7 @@ mod dto_common;
mod errors;
mod frontend_logging;
mod logging_runtime;
mod offchain_runtime;
mod splash;
mod tauri;
mod transport_runtime;
@@ -47,6 +48,8 @@ pub(crate) use self::bootstrap::load_wallet_desk_composite;
pub(crate) use self::bootstrap::required_composite_component_profile;
/// Composite-local identifier for the Logging standard document.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
/// Composite-local identifier for the Off-chain Transport standard document.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_OFFCHAIN_TRANSPORT;
/// Composite-local identifier for the HTTP Transport standard document.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
/// Composite-local identifier for the Wallet standard document.
@@ -55,6 +58,8 @@ pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_WALLET;
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain used by Wallet Desk Off-chain Transport and SOL/USD average operations.
pub(crate) use self::constants::TRACING_DOMAIN_OFFCHAIN_TRANSPORT;
/// Structured domain used by the Wallet Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Wallet Desk HTTP Transport and balance operations.
@@ -137,6 +142,10 @@ pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
pub(crate) use self::frontend_logging::emit_frontend_log_event;
/// Creates the stable runtime identity for this Wallet Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Provider-neutral Off-chain Transport runtime used by the Wallet balance projection.
pub(crate) use self::offchain_runtime::OffchainTransportRuntime;
/// Resolves the optional composite-selected Off-chain Transport runtime.
pub(crate) use self::offchain_runtime::initialize_offchain_transport;
/// Command emitted by Rust to the splash frontend.
pub(crate) use self::splash::SplashOrderDto;
/// Runtime timings used by the common desk splash lifecycle.
@@ -155,6 +164,8 @@ pub(crate) use self::tw_splash::require_splash_window;
pub(crate) use self::tw_splash::splash_frontend_ready_service;
/// Safe balance snapshot for the currently authorized Wallet.
pub(crate) use self::wallet_balance::WalletBalanceDto;
/// Computes the Wallet Desk SOL/USD arithmetic mean without floating point.
pub(crate) use self::wallet_balance::average_sol_usd_prices;
/// Formats exact lamports as a decimal SOL string without floating point.
pub(crate) use self::wallet_balance::format_lamports_as_sol;
/// Resolved Wallet Config and directory preparation status captured during application bootstrap.

View File

@@ -0,0 +1,60 @@
// file: crates/ksp-app-wallet-desk/src/offchain_runtime.rs
// version: 1
//! Optional composite-selected Off-chain Transport runtime used by Wallet Desk SOL/USD presentation.
/// Safe provider-neutral Off-chain Transport runtime retained by Wallet Desk when Config resolution succeeds.
pub(crate) struct OffchainTransportRuntime {
resolved: ksp_config_lib::ResolvedOffchainTransportConfig,
}
impl OffchainTransportRuntime {
/// Returns the generic market-price service built and owned by Config.
#[must_use]
pub(crate) const fn service(&self) -> &ksp_offchain_transport_lib::MarketPriceService {
return self.resolved.service();
}
/// Returns the composite-selected Off-chain Transport profile identifier.
#[must_use]
pub(crate) fn profile_id(&self) -> &str {
return self.resolved.profile_id();
}
}
/// Resolves the Wallet Desk Off-chain Transport component through Config.
pub(crate) fn initialize_offchain_transport(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<OffchainTransportRuntime> {
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),
};
let composite = crate::load_wallet_desk_composite(management);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = crate::required_composite_component_profile(
&composite,
crate::COMPOSITE_COMPONENT_ID_OFFCHAIN_TRANSPORT,
ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT,
);
let profile = match profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = management.engine().resolve_offchain_transport_config_profile(&profile, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider_count = resolved.service().registry().len();
ksp_logging_lib::info!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
offchain_profile = resolved.profile_id(),
provider_count,
"initialized Wallet Desk Off-chain Transport from composite-managed configuration"
);
return std::result::Result::Ok(OffchainTransportRuntime { resolved });
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/wallet_balance.rs
// version: 1
// version: 2
//! Safe Wallet balance projection produced by the typed HTTP Transport path.
@@ -18,6 +18,8 @@ pub(crate) struct WalletBalanceDto {
pub(crate) slot: u64,
/// Exact decimal SOL presentation derived from lamports without floating point.
pub(crate) sol: String,
/// Arithmetic mean of successful SOL/USD observations produced by this refresh, or unavailable when no provider returned one.
pub(crate) sol_usd_average: std::option::Option<String>,
/// Composite-selected Transport profile used for the RPC.
pub(crate) transport_profile: String,
/// Logical Transport role used for the RPC.
@@ -34,6 +36,98 @@ pub(crate) fn format_lamports_as_sol(lamports: u64) -> String {
return format!("{whole}.{fractional:09}");
}
/// Computes a deterministic SOL/USD arithmetic mean without IEEE-754 conversion.
///
/// The output is rounded half-up to at most eight fractional digits and trailing zeroes are removed. Arithmetic overflow degrades to `None`, which Wallet Desk
/// renders as `N.A.` without affecting the authoritative SOL balance.
#[must_use]
pub(crate) fn average_sol_usd_prices(prices: &[ksp_offchain_transport_lib::MarketPriceDecimal]) -> std::option::Option<String> {
const OUTPUT_SCALE: u8 = 8;
if prices.is_empty() {
return std::option::Option::None;
}
let max_scale = prices.iter().map(ksp_offchain_transport_lib::MarketPriceDecimal::scale).max();
let max_scale = match max_scale {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let mut sum = 0_u128;
for price in prices {
let exponent = u32::from(max_scale.saturating_sub(price.scale()));
let factor = 10_u128.checked_pow(exponent);
let factor = match factor {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let coefficient = price.coefficient().checked_mul(factor);
let coefficient = match coefficient {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
sum = match sum.checked_add(coefficient) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
}
let count = match u128::try_from(prices.len()) {
std::result::Result::Ok(value) if value > 0 => value,
_ => return std::option::Option::None,
};
let target_scale = OUTPUT_SCALE;
let (numerator, denominator) = if max_scale <= target_scale {
let factor = 10_u128.checked_pow(u32::from(target_scale - max_scale));
let factor = match factor {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let numerator = match sum.checked_mul(factor) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
(numerator, count)
} else {
let factor = 10_u128.checked_pow(u32::from(max_scale - target_scale));
let factor = match factor {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let denominator = match count.checked_mul(factor) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
(sum, denominator)
};
let quotient = numerator / denominator;
let remainder = numerator % denominator;
let round_up = remainder >= denominator.div_ceil(2);
let coefficient = if round_up {
match quotient.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
}
} else {
quotient
};
return std::option::Option::Some(format_scaled_decimal(coefficient, target_scale));
}
fn format_scaled_decimal(mut coefficient: u128, mut scale: u8) -> String {
while scale > 0 && coefficient % 10 == 0 {
coefficient /= 10;
scale -= 1;
}
let digits = coefficient.to_string();
if scale == 0 {
return digits;
}
let scale = usize::from(scale);
if digits.len() > scale {
let split = digits.len() - scale;
return format!("{}.{}", &digits[..split], &digits[split..]);
}
return format!("0.{}{}", "0".repeat(scale - digits.len()), digits);
}
#[cfg(test)]
#[path = "../unit_tests/wallet_balance.rs"]
mod tests;

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Wallet Desk",
"version": "0.2.6",
"version": "0.2.12-pre.8",
"identifier": "com.sasedev.ksp-app-wallet-desk",
"build": {
"beforeDevCommand": {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/config_composition.rs
// version: 7
// version: 8
//! Config composition contracts introduced for Wallet Desk pre.003.
@@ -18,7 +18,7 @@ fn workspace_engine() -> ksp_core_lib::Result<ksp_config_lib::ConfigDocumentEngi
}
#[test]
fn wallet_desk_composite_selects_logging_transport_and_wallet_by_file_id() {
fn wallet_desk_composite_selects_logging_offchain_transport_transport_and_wallet_by_file_id() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
@@ -31,6 +31,7 @@ fn wallet_desk_composite_selects_logging_transport_and_wallet_by_file_id() {
assert_eq!(composite.profile_id(), "devnet");
assert_component(&composite, "logging", ksp_config_lib::FILE_ID_STD_LOGGING, "supertrace");
assert_wallet_desk_supertrace_profile(&composite);
assert_component(&composite, "offchain_transport", ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT, "public_keyless");
assert_component(&composite, "transport", ksp_config_lib::FILE_ID_STD_TRANSPORT, "devnet_public");
assert_component(&composite, "wallet", ksp_config_lib::FILE_ID_STD_WALLET, "default");
}
@@ -172,3 +173,42 @@ fn wallet_desk_composite_transport_profile_builds_get_balance_pool() {
assert!(selection.is_ok(), "getBalance should route through the composite-selected Transport pool: {selection:?}");
}
}
#[test]
fn wallet_desk_composite_offchain_profile_builds_provider_neutral_service() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
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_WALLET_DESK);
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);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let component = composite.component("offchain_transport");
assert!(component.is_some(), "Wallet Desk composite should expose 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(), "public Config 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 Off-chain Transport should map");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "public_keyless");
assert_eq!(resolved.selection_source(), ksp_config_lib::ConfigProfileSelectionSource::Composite);
assert_eq!(resolved.service().registry().len(), 8);
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 24
// version: 25
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -501,3 +501,29 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
assert!(tauri_source.contains("tauri::utils::platform::resource_dir"));
assert!(tauri_source.contains("std::env::set_current_dir(layout.runtime_root())"));
}
#[test]
fn v0_2_12_pre_008_wallet_balance_refresh_adds_one_auxiliary_sol_usd_average_only() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
let composite = read_text(root.join("../../config/composite.ksp-app-wallet-desk.json").as_path());
let state = read_text(root.join("src/app_state.rs").as_path());
let balance = read_text(root.join("src/wallet_balance.rs").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
assert!(manifest.contains("ksp-offchain-transport-lib = { path = "../ksp-offchain-transport-lib" }"));
assert!(composite.contains(""component_id": "offchain_transport""));
assert!(composite.contains(""profile_id": "public_keyless""));
assert!(state.contains("initialize_offchain_transport"));
assert!(state.contains("refresh_sol_usd_average"));
assert!(state.contains("tokio::join!"));
assert!(state.contains("refresh_all().await"));
assert!(balance.contains("sol_usd_average"));
assert!(balance.contains("average_sol_usd_prices"));
assert!(html.contains("id="currentWalletSolUsdAverage""));
assert_eq!(html.matches("currentWalletSolUsdAverage").count(), 1);
assert!(main.contains("balance.solUsdAverage ? `${balance.solUsdAverage} USD` : "N.A.""));
assert!(!main.contains("refresh_market_price"));
assert!(!main.contains("refresh_all_market_prices"));
assert!(!main.contains("fetch("));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
// version: 14
// version: 15
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
@@ -122,9 +122,14 @@ fn balance_refresh_never_accepts_pubkey_or_endpoint_url_from_frontend() {
let refresh = &main[refresh_start.unwrap_or_default()..refresh_end.unwrap_or(main.len())];
assert!(refresh.contains("refresh_wallet_balance"));
assert!(!refresh.contains("pubkey"));
assert!(!refresh.contains("providerId"));
assert!(!refresh.contains("providerIds"));
assert!(!main.contains("transportUrl"));
assert!(!main.contains("endpointUrl"));
assert!(state.contains("wallet.pubkey()"));
assert!(state.contains("refresh_all().await"));
assert!(state.contains("average_sol_usd_prices"));
assert!(!state.contains("parse::<f64>"));
}
#[test]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
// version: 7
// version: 8
//! Release-wide deterministic compliance canaries for Wallet Desk.
@@ -81,6 +81,7 @@ fn backend_keeps_config_wallet_transport_and_logging_ownership_boundaries() {
assert!(source.contains("ksp_config_lib::ConfigEnvironment::load"));
assert!(source.contains("ksp_wallet_lib::"));
assert!(source.contains("ksp_onchain_transport_lib::"));
assert!(source.contains("ksp_offchain_transport_lib::"));
assert!(source.contains("ksp_logging_lib::"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_balance.rs
// version: 1
// version: 2
//! Unit tests for exact Wallet balance presentation.
@@ -11,3 +11,23 @@ fn lamports_formatting_is_exact_and_uses_nine_fractional_digits() {
assert_eq!(crate::format_lamports_as_sol(1_234_567_890), "1.234567890");
assert_eq!(crate::format_lamports_as_sol(u64::MAX), "18446744073.709551615");
}
#[test]
fn sol_usd_average_is_deterministic_rounded_and_never_uses_f64() {
let values = ["103.96", "104.16", "104.07981394241013", "104.07595797523913", "103.92807280061784", "103.9"];
let prices = values.iter().filter_map(|value| return ksp_offchain_transport_lib::MarketPriceDecimal::parse(value).ok()).collect::<std::vec::Vec<_>>();
assert_eq!(prices.len(), values.len());
assert_eq!(crate::average_sol_usd_prices(prices.as_slice()), std::option::Option::Some("104.01730745".to_owned()));
}
#[test]
fn sol_usd_average_returns_none_without_observation_and_handles_integer_fraction() {
assert_eq!(crate::average_sol_usd_prices(&[]), std::option::Option::None);
let one = ksp_offchain_transport_lib::MarketPriceDecimal::parse("1");
let two = ksp_offchain_transport_lib::MarketPriceDecimal::parse("2");
assert!(one.is_ok());
assert!(two.is_ok());
if let (std::result::Result::Ok(one), std::result::Result::Ok(two)) = (one, two) {
assert_eq!(crate::average_sol_usd_prices(&[one, two]), std::option::Option::Some("1.5".to_owned()));
}
}

View File

@@ -10,6 +10,11 @@
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "offchain_transport",
"file_id": "cfg.std.offchain_transport",
"profile_id": "public_keyless"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
@@ -30,6 +35,11 @@
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "offchain_transport",
"file_id": "cfg.std.offchain_transport",
"profile_id": "public_keyless"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
@@ -50,6 +60,11 @@
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "offchain_transport",
"file_id": "cfg.std.offchain_transport",
"profile_id": "public_keyless"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",

111
deltas/0.2.12/pre.008.md Normal file
View File

@@ -0,0 +1,111 @@
<!-- file: deltas/0.2.12/pre.008.md -->
<!-- version: 1 -->
# Delta `0.2.12-pre.008` — intégration Wallet minimale et recalibrage de clôture
## Base
```text
0.2.12-pre.007
```
Le gate opérateur de `pre.007` est intégralement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy workspace, test ciblé SOL Prices Desk, `cargo test --workspace` et `cargo tauri dev` passent. Le parcours visuel confirme la table provider-neutral et les refresh manuel/selected/all.
## Décision opérateur
Le périmètre Wallet Desk est réduit : les fonctionnalités avancées de marché restent dans `ksp-offchain-transport-lib` et `ksp-app-solprices-desk`. Wallet Desk n'a besoin que d'une information auxiliaire SOL/USD lors du refresh de balance.
Contrat retenu :
```text
Refresh balance existant
-> getBalance On-chain
-> refresh_all Off-chain des providers actuellement éligibles
-> ne garder que les observations réussies du refresh courant
-> calculer une moyenne SOL/USD backend
-> afficher une seule case SOL/USD moyen
-> N.A. si aucune observation n'est disponible
```
Aucune valorisation USD du solde, aucun provider canonique, aucun panneau provider détaillé et aucune nouvelle command prix Wallet ne sont introduits.
## Implémentation
- version workspace : `0.2.12-pre.8` ;
- `ksp-app-wallet-desk` dépend désormais directement de `ksp-offchain-transport-lib` ;
- les trois profils du composite Wallet Desk ajoutent `offchain_transport -> cfg.std.offchain_transport/public_keyless` ;
- la fixture Config et l'exemple du composite Wallet sont synchronisés ;
- ajout d'un `OffchainTransportRuntime` Wallet résolu exclusivement via Config ;
- l'initialisation prix est auxiliaire : un échec Config/Off-chain est journalisé par domain/code sûrs et dégrade la projection vers `N.A.` sans empêcher le Desk de démarrer ;
- `refresh_wallet_balance` conserve son input vide et orchestre `getBalance` et `MarketPriceService::refresh_all` dans la même opération backend ;
- la moyenne utilise uniquement les observations réellement produites par le refresh courant ;
- le calcul n'utilise aucun `f64` et arrondit half-up à huit décimales maximum avant suppression des zéros terminaux ;
- `WalletBalanceDto` gagne uniquement `sol_usd_average: Option<String>` ;
- le frontend Wallet affiche une seule case `SOL/USD moyen`, avec `N.A.` en absence de valeur ;
- aucun `provider_id`, URL, endpoint, credential, payload provider ou command prix ne traverse le frontend Wallet ;
- le lifecycle WalletSession, VIEW/OWNER, secrets, import/export et la logique `getBalance` existante restent inchangés.
## Tests/canaris
- moyenne déterministe sur valeurs réalistes ;
- moyenne entière fractionnaire (`1`, `2` -> `1.5`) sans IEEE-754 ;
- composite Wallet vérifie le composant Off-chain et sa provenance Composite ;
- service `public_keyless` construit avec huit entrées provider-neutral ;
- canari desktop exige exactement une case `currentWalletSolUsdAverage` et aucune surface prix avancée ;
- sécurité : `refresh_wallet_balance` ne reçoit toujours ni Pubkey, ni endpoint, ni provider ID depuis le frontend ;
- release compliance reconnaît explicitement `ksp-offchain-transport-lib` comme boundary KSP autorisée.
## Recalibrage du forecast
Les anciennes tranches Wallet riches et les hardenings séparés ne sont plus justifiés. Le forecast restant devient :
```text
pre.008 intégration Wallet minimale + recalibrage
pre.009 hardening transversal + gate technique/live + builds finaux
pre.010 réconciliation documentaire + prompt suivant + CHANGELOG + ROADMAP
rel.001 publication stable
```
Les responsabilités finales restent présentes ; elles sont fusionnées pour éviter des prereleases artificielles.
## Fichiers ajoutés/modifiés
```text
Cargo.toml
config/composite.ksp-app-wallet-desk.json
config/examples/composite.ksp-app-wallet-desk.example.json
crates/ksp-config-lib/unit_tests/fixtures/composite.ksp-app-wallet-desk.json
crates/ksp-app-wallet-desk/Cargo.toml
crates/ksp-app-wallet-desk/package.json
crates/ksp-app-wallet-desk/tauri.conf.json
crates/ksp-app-wallet-desk/src/app_state.rs
crates/ksp-app-wallet-desk/src/constants.rs
crates/ksp-app-wallet-desk/src/lib.rs
crates/ksp-app-wallet-desk/src/offchain_runtime.rs
crates/ksp-app-wallet-desk/src/wallet_balance.rs
crates/ksp-app-wallet-desk/frontend/main.html
crates/ksp-app-wallet-desk/frontend/ts/main.ts
crates/ksp-app-wallet-desk/unit_tests/wallet_balance.rs
crates/ksp-app-wallet-desk/tests/config_composition.rs
crates/ksp-app-wallet-desk/tests/desktop_contract.rs
crates/ksp-app-wallet-desk/tests/desktop_security.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
deltas/0.2.12/pre.008.md
```
## Gate opérateur attendu
```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-wallet-desk
cargo test --workspace
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
```
Contrôle manuel : ouvrir un Wallet VIEW/OWNER, lancer `Refresh balance`, vérifier que la balance SOL reste correcte et que `SOL/USD moyen` affiche une valeur si au moins un provider keyless répond, sinon `N.A.`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/019-V0_2_12_SOL_PRICES_DESK_PLAN.md -->
<!-- version: 13 -->
<!-- version: 14 -->
# Plan `0.2.12` — SOL Prices Desk + intégration prix Wallet Desk
@@ -29,7 +29,7 @@ Mission concrète de la release :
```text
1. créer ksp-app-solprices-desk comme HID Tauri provider-agnostic
2. valider son contrat Config + MarketPriceService + DTO/commands + UI
3. intégrer ensuite la même capacité générique dans ksp-app-wallet-desk
3. intégrer dans ksp-app-wallet-desk une projection auxiliaire minimale : moyenne SOL/USD calculée lors du refresh de balance
```
Ownership durable :
@@ -242,21 +242,27 @@ splash
Le shell reste volontairement petit : pas de dashboard trading, graphe, historique, quote routing ou éditeur Config.
### Point d'insertion Wallet Desk
### Point d'insertion Wallet Desk — décision opérateur `pre.008`
Le périmètre Wallet Desk est volontairement réduit après validation complète de SOL Prices Desk :
```text
Wallet Desk existing shell
-> Dashboard
-> panneau compact “SOL/USD prices”
-> quelques/toutes lignes provider-neutral selon espace
-> availability + fraîcheur
-> refresh row / refresh all borné
-> aucune dépendance à une Wallet sélectionnée ou déverrouillée
-> Dashboard / Wallet courant
-> Balance SOL existante
-> une seule case “SOL/USD moyen”
-> valeur moyenne dérivée du refresh courant
-> N.A. si aucune observation exploitable
-> bouton Refresh balance existant
-> getBalance On-chain
-> refresh_all Off-chain des providers éligibles
-> moyenne backend
-> projection commune Balance + SOL/USD
```
Le panneau prix est indépendant du lifecycle VIEW/OWNER. Il ne modifie ni la balance SOL ni les opérations Wallet.
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 : hors scope `0.2.12`.** Il n'existe pas de prix KSP canonique et la release ne choisit pas arbitrairement le premier provider.
**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.
## 9. DTO map retenue
@@ -299,7 +305,7 @@ Config effective secret values
Aucune command n'accepte URL, credential, header, rate-limit override ou payload provider.
Wallet Desk réutilise la même sémantique de commands (`list/refresh/refresh_all`), avec noms éventuellement préfixés pour éviter collision avec ses commandes existantes. L'input reste uniquement le `provider_id` opaque.
Wallet Desk n'expose finalement **aucune nouvelle command prix**. La command existante `refresh_wallet_balance` orchestre côté Rust le `getBalance` et le `MarketPriceService::refresh_all`; le frontend ne transmet ni `provider_id`, ni URL, ni sélection provider. `WalletBalanceDto` gagne uniquement `sol_usd_average: Option<String>`.
## 11. Refresh UX
@@ -318,35 +324,46 @@ Décisions :
**Auto-refresh périodique : hors scope.** Aucun `setInterval`, scheduler ou worker prix n'est ajouté en `0.2.12`.
## 12. Intégration Wallet Desk
## 12. Intégration Wallet Desk — périmètre simplifié
Wallet Desk ajoutera une dépendance directe à `ksp-offchain-transport-lib` parce qu'il consomme le contrat public générique retourné par Config. Cette direction est déjà admise par l'architecture; Off-chain Transport ne dépend jamais de Wallet ni de Config.
Wallet Desk ajoute une dépendance directe à `ksp-offchain-transport-lib` uniquement pour consommer les contrats publics `MarketPriceService`, `MarketPriceRefreshOutcome` et `MarketPriceDecimal` construits par Config. Aucune logique provider concrète n'est dupliquée.
Flux :
Flux retenu :
```text
Config composite Wallet Desk
-> component offchain_transport / cfg.std.offchain_transport / public_keyless
-> ConfigDocumentEngine::resolve_offchain_transport_config_profile
-> ResolvedOffchainTransportConfig
-> MarketPriceService dans AppState/MarketPriceRuntime
-> DTO provider-neutral
-> panneau Dashboard
-> MarketPriceService conservé côté Rust
refresh_wallet_balance
-> getBalance On-chain
-> 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<String> }
-> une seule case UI “SOL/USD moyen” 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`.
Non-régressions obligatoires :
```text
WalletSession inchangée
VIEW/OWNER inchangés
Wallet secrets inchangés
getBalance On-chain inchangé
aucune Pubkey requise pour obtenir un prix SOL/USD
commande refresh_wallet_balance inchangée côté input
aucun provider_id dans l'IPC Wallet
aucune command prix Wallet supplémentaire
aucun fetch TS
aucune multiplication balance x prix
aucun fallback sur une observation stale
aucun provider canonique
```
Aucun adapter, parser, limiter ou fetch TS n'est copié dans Wallet Desk.
La moyenne n'est ni un consensus de marché, ni un nouveau contrat Off-chain partagé : c'est une présentation auxiliaire propre au Wallet Desk.
## 13. Mutualisation entre applications
@@ -354,7 +371,7 @@ La mutualisation métier existe déjà dans `ksp-offchain-transport-lib` et la m
`pre.001` ne retient **aucune nouvelle crate partagée**. Les deux apps peuvent posséder de petits adapters DTO/runtime propres parce qu'ils sont de la présentation/composition et que leurs `AppState`/runtime status diffèrent déjà.
Un helper partagé ne sera envisagé que si `pre.009``pre.011` démontrent une duplication non triviale, stable et réellement réutilisable. Il ne sera pas créé pour éviter quelques conversions enum -> label.
Le périmètre Wallet simplifié ne justifie aucune nouvelle crate/helper partagé : SOL Prices Desk conserve les projections riches, Wallet Desk ne consomme que le service générique et calcule une moyenne auxiliaire locale.
## 14. Capabilities et plugins
@@ -411,7 +428,7 @@ Canari obligatoire : `ksp-app-solprices-desk` référence son propre `package.js
| CWD/resource confusion cross-app | CWD KSP normalisé + resources registry + canari paths/ports/backend dédiés |
| capabilities trop larges | core + tracing seulement pour Price Desk; aucun network/fs/dialog guest |
| logs frontend sensibles | log seulement action, provider_id, compte, code KSP; jamais DTO brut/secret/body |
| faux prix canonique | semantics visibles; aucune moyenne/fallback/first-provider |
| faux prix canonique | Price Desk sans canonique/fallback; moyenne Wallet explicitement auxiliaire |
| perte de précision JS | prix transmis comme chaîne décimale exacte |
## 17. Stratégie de tests et gates
@@ -427,7 +444,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 | panneau prix + non-régression Wallet/balance |
| Wallet Desk dev | refresh balance + une case moyenne SOL/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 |
@@ -440,7 +457,7 @@ nouveau provider ou modification des 8 adapters V1
SOL/EUR ou token arbitraire
OHLCV/historique/chart
SSE/WebSocket prix
consensus/moyenne/fallback automatique
consensus/fallback automatique
provider canonique
valorisation USD du solde Wallet
auto-refresh/polling périodique
@@ -496,13 +513,13 @@ Correction strictement locale du test `config_composition` : le message d'assert
### `pre.004` — Runtime prix, registry rows et DTOs
**Statut : implémenté ; corrigé par `pre.004-fix.001` puis `pre.004-fix.002`, gate opérateur à confirmer.**
**Statut : réalisé ; corrigé par `pre.004-fix.001` puis `pre.004-fix.002`, gate opérateur intégralement PASS.**
Création du `MarketPriceRuntime` applicatif, projection provider-neutral du registry, DTOs TS-RS et états initiaux/loading/empty. `list_market_prices` projette les lignes depuis le registry sans dispatch réseau ; l'état de présentation reste uniquement en mémoire backend. Les providers restent opaques à l'UI, le prix exact et les timestamps traversent l'IPC sous forme de chaînes exactes, et aucun refresh n'est encore disponible avant `pre.005`.
#### `pre.004-fix.001` — Synchronisation de la fixture composite Config Desk
**Statut : implémenté ; gate opérateur à confirmer.**
**Statut : réalisé ; corrigé par `pre.004-fix.002`, gate opérateur final PASS.**
Correction du gate workspace révélé après `pre.004` : le registry Config contient désormais le composite SOL Prices Desk, donc le jeu de fixtures utilisé par `ksp-app-config-desk` doit contenir le document correspondant. Le fix ajoute la fixture dédiée avec des profils valides dans l'environnement de test et renforce le canari d'inventory pour vérifier explicitement `cfg.composite.ksp-app-solprices-desk`. Aucun runtime prix, provider, DTO ou frontend n'est modifié.
@@ -532,65 +549,35 @@ Ajout de `refresh_market_prices` et `refresh_all_market_prices` sur les APIs gé
### `pre.007` — UX et instrumentation SOL Prices Desk
**Statut : implémenté ; gate opérateur à confirmer.**
**Statut : réalisé ; gate opérateur Cargo/workspace/Tauri et parcours visuel intégralement PASS.**
Finalisation de la table responsive sans changer le contrat de dispatch : résumé providers/ready/observed/attention/refreshing, header sticky, colonnes secondaires adaptatives, badges génériques d'auth/semantics/availability, prix exact conservé en String et timestamps présentés à la fois sous forme humaine et en millisecondes exactes. `provider_timestamp = None` est rendu explicitement comme `Not supplied`, sans substitution par `received_at`. La sélection gagne un contrôle global et `Clear selection`, uniquement frontend. Chaque nouveau contrôle est journalisé en `debug`; les rendus et comptes sûrs restent en `trace`. Les wrappers Tauri journalisent les échecs avec seulement `command`, `error_domain` et `error_code`. Aucun prix, timestamp, payload provider, URL, header ou credential n'entre dans les logs. La tranche reste manuelle : aucune nouvelle commande métier, aucun auto-refresh, scheduler ou persistance navigateur n'est ajouté.
### `pre.008` — Hardening SOL Prices Desk
### `pre.008` — Intégration Wallet minimale + recalibrage de clôture
**Statut : implémenté ; gate opérateur à confirmer.**
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`, `WalletBalanceDto.sol_usd_average` et une seule case UI `SOL/USD moyen`/`N.A.`. Aucun panneau provider, aucune command prix Wallet, aucun provider ID frontend et aucune valorisation USD du solde. La tranche réconcilie également le présent forecast avec cette réduction de scope.
### `pre.009` — Hardening transversal + gate technique/live final
**Statut : planifié.**
Durcissement des boundaries desktop/security/package, canaris de capabilities minimales, parcours fonctionnel complet et non-régression du bootstrap avant d'étendre Wallet Desk. Le Desk doit être stable isolément à la sortie de cette tranche.
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.
### `pre.009` — Extension composite/runtime Wallet Desk
### `pre.010` — Réconciliation documentaire + préparation de publication
**Statut : planifié.**
Extension additive du composite Wallet Desk avec Off-chain Transport et intégration du runtime prix backend, sans modifier les contrats Wallet existants, le lifecycle VIEW/OWNER ni le chemin `getBalance`.
### `pre.010` — DTOs et commands prix Wallet Desk
**Statut : planifié.**
Ajout de la surface IPC provider-neutral nécessaire au Wallet Desk pour l'inventaire et le refresh prix. Aucun secret, URL provider, wire provider-specific ou valorisation USD du solde ne traverse cette surface.
### `pre.011` — Panneau prix Wallet Desk
**Statut : planifié.**
Ajout du panneau Dashboard prix avec refresh manuel et projection des observations/availability, puis vérification des non-régressions balance et autorisations VIEW/OWNER. Le solde SOL et les prix restent deux informations distinctes ; aucune valorisation USD canonique n'est calculée.
### `pre.012` — Hardening cross-app et complétude
**Statut : planifié.**
Revue croisée des deux Desks, capabilities/resources, contracts IPC, dépendances, sécurité et canaris de complétude. Cette tranche ferme le comportement applicatif avant le gate technique/live final.
### `pre.013` — Gate technique/live final
**Statut : planifié.**
Gate workspace complet, tests applicatifs pertinents, smoke live keyless de composition application -> Config -> `MarketPriceService`, graphes de dépendances utiles et builds Tauri finaux des applications modifiées. Cette tranche ne réconcilie pas README/USAGE ni les documents finaux.
### `pre.014` — Réconciliation documentaire finale
**Statut : planifié.**
Réconciliation du plan, de la validation, des README/USAGE et autres documents durables concernés avec le runtime réellement livré après le gate technique. Cette tranche ne finalise ni `CHANGELOG.md`, ni `ROADMAP.md`, ni le prompt suivant.
### `pre.015` — Préparation minimale de publication
**Statut : planifié.**
Préparation du prompt de la release suivante, de `CHANGELOG.md` et de `ROADMAP.md`, avec la mécanique Cargo/delta imposée par le workflow. Aucun code, test, plan, validation, README ou USAGE n'est rouvert dans cette dernière prerelease.
Fusion des anciennes tranches `pre.014`/`pre.015` : réconciliation README/USAGE/plan/validation avec le runtime réellement validé, puis prompt suivant, `CHANGELOG.md` et `ROADMAP.md`. Cette tranche reste documentaire/mécanique et ne rouvre aucun code fonctionnel.
### `rel.001` — Publication stable `v0.2.12`
**Statut : planifié.**
Mécanique de publication stable uniquement, après validation de la dernière prerelease et vérification de la cohérence de release. Aucun défaut fonctionnel ou documentaire ne doit être absorbé par `rel.001`.
Mécanique de publication stable uniquement, après validation de `pre.010` et vérification de la cohérence de release. Aucun défaut fonctionnel ou documentaire ne doit être absorbé par `rel.001`.
Le packaging transversal et les refresh manuels du Price Desk sont désormais fermés par les gates opérateur jusquà `pre.006`. Après le polish `pre.007`, le prochain risque fonctionnel est borné au hardening `pre.008` avant louverture du couloir Wallet Desk `pre.009+`, sans remettre en cause le découpage souple ni les responsabilités documentaires de clôture.
Le Price Desk est fermé fonctionnellement après `pre.007`. La décision opérateur de simplifier Wallet Desk réduit le reste de la release à une seule tranche fonctionnelle `pre.008`, un gate/hardening transversal `pre.009`, une clôture documentaire/mécanique `pre.010`, puis `rel.001`.
## 20. Critères de sortie `pre.001`

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/015-V0_2_12_SOL_PRICES_DESK.md -->
<!-- version: 12 -->
<!-- version: 13 -->
# Validation `0.2.12` — SOL Prices Desk + intégration Wallet Desk
@@ -47,7 +47,7 @@ NOT RUN commande non exécutée dans le contexte indiqué
## 3. Screen/feature matrix
| Surface | Contrat | Statut |
|------------------------|-----------------------------------------|---------|
|------------------------|-----------------------------------------|--------|
| splash Price Desk | lifecycle KSP commun | PASS |
| main shell Price Desk | header/navigation KSP | PASS |
| table providers | registry provider-neutral, ordre stable | PASS |
@@ -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 price panel | Dashboard compact provider-neutral | PENDING |
| Wallet prix moyen | une case `SOL/USD moyen` / `N.A.` | PASS |
| auto-refresh | aucun scheduler/polling | OUT |
| Wallet USD valuation | aucun prix canonique | OUT |
| Wallet USD valuation | aucune multiplication balance × prix | OUT |
## 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 dIDs opaques ; borne hostile à durcir en `pre.008` | PASS |
| `MarketPriceRefreshManyRequestDto` | liste dIDs opaques ; borne hostile à durcir en `pre.009` | 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 |
@@ -115,7 +115,7 @@ NOT RUN commande non exécutée dans le contexte indiqué
| 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.008` | PENDING |
| 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 |
@@ -129,15 +129,20 @@ NOT RUN commande non exécutée dans le contexte indiqué
## 8. Wallet integration matrix
| Contrat | Attendu | Statut |
|----------------------------|-----------------------------------------------------------------|---------|
| Cargo dependency | Wallet Desk -> `ksp-offchain-transport-lib` | PENDING |
| Config component | Wallet composite -> `cfg.std.offchain_transport/public_keyless` | PENDING |
| runtime | ResolvedOffchainTransportConfig -> MarketPriceService | PENDING |
| commands/DTO | provider-neutral | PENDING |
| UI | petit panneau Dashboard | PENDING |
| balance HTTP On-chain | inchangée | PENDING |
| WalletSession VIEW/OWNER | inchangée | PENDING |
| Wallet secret projections | aucune nouvelle | PENDING |
|----------------------------|--------------------------------------------------------------------------|--------|
| Cargo dependency | Wallet Desk -> `ksp-offchain-transport-lib` | PASS |
| 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<String>` | 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 |
| 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 |
## 9. Tauri/package matrix
@@ -187,14 +192,14 @@ cargo fmt/check/clippy/test opérateur sur base stable fournie PASS
Après écriture du présent delta, les audits Rust structurel et Markdown ont été rejoués avec `deltas/0.2.12` inclus et sont propres.
## 12. Forecast de clôture
## 12. Forecast de clôture recalibré
Les responsabilités finales restent séparées :
Après décision opérateur de simplifier Wallet Desk, les responsabilités restantes sont condensées sans supprimer les gates de sortie :
```text
pre.013 gate technique/live + builds finaux
pre.014 réconciliation documentaire finale
pre.015 prompt suivant + CHANGELOG + ROADMAP uniquement
pre.008 intégration Wallet minimale + moyenne SOL/USD
pre.009 hardening transversal + gate technique/live + builds finaux
pre.010 réconciliation documentaire + prompt suivant + CHANGELOG + ROADMAP
rel.001 publication stable
```
@@ -531,7 +536,7 @@ python3 scripts/audit_markdown_tables.py README.md ... deltas PASS
cargo fmt/check/clippy/test NOT RUN — cargo indisponible dans le sandbox
```
Le gate opérateur de `pre.006` fourni le 2026-08-27 ferme ensuite cette tranche : audits, `cargo check`, Clippy, test ciblé et `cargo test --workspace` passent, `cargo tauri dev` démarre correctement et le parcours manuel confirme que le multi-refresh fonctionne. Les lignes `refresh selected/many`, `refresh all`, les deux DTOs batch, l'ordre many, le rejet des doublons et le conflit `in_flight` passent donc à `PASS`. La borne hostile d'une requête batch artificiellement surdimensionnée reste volontairement `PENDING` pour le hardening `pre.008`.
Le gate opérateur de `pre.006` fourni le 2026-08-27 ferme ensuite cette tranche : audits, `cargo check`, Clippy, test ciblé et `cargo test --workspace` passent, `cargo tauri dev` démarre correctement et le parcours manuel confirme que le multi-refresh fonctionne. Les lignes `refresh selected/many`, `refresh all`, les deux DTOs batch, l'ordre many, le rejet des doublons et le conflit `in_flight` passent donc à `PASS`. La borne hostile d'une requête batch artificiellement surdimensionnée reste volontairement `PENDING` pour le hardening `pre.009`.
## 23. Gate `0.2.12-pre.007`
@@ -565,3 +570,36 @@ python3 scripts/audit_markdown_tables.py README.md ... deltas PASS
TypeScript syntax transpile main.ts PASS
cargo fmt/check/clippy/test NOT RUN — cargo indisponible dans le sandbox
```
Le gate opérateur du `27 août 2026` ferme `pre.007` : `cargo fmt --all`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace, `cargo test -p ksp-app-solprices-desk`, `cargo test --workspace` et `cargo tauri dev` passent. Le parcours visuel confirme la table finale avec 8 providers, états Ready/Disabled/Cooling down, observations distinctes, timestamps provider/KSP et les contrôles de refresh.
## 24. Gate `0.2.12-pre.008` — intégration Wallet minimale
La décision opérateur réduit le besoin Wallet à une seule information auxiliaire liée au refresh de balance :
```text
composite Wallet + offchain_transport/public_keyless implémenté
ksp-app-wallet-desk -> ksp-offchain-transport-lib implémenté
runtime Off-chain Wallet best-effort, erreur -> N.A.
refresh_wallet_balance input inchangé
getBalance + refresh_all lancés dans la même opération backend
observations prises pour moyenne uniquement succès du refresh courant
moyenne backend, aucune conversion f64, 8 décimales max
WalletBalanceDto.sol_usd_average Option<String>
UI Wallet une case SOL/USD moyen / N.A.
provider_id / URL / credential / command prix dans frontend absent
valorisation balance SOL en USD absente
WalletSession / VIEW / OWNER / secrets inchangés
```
Le runtime prix est auxiliaire : une indisponibilité Config/Off-chain ou zéro observation ne doit pas invalider une réponse `getBalance` réussie. Aucun état provider détaillé n'est dupliqué dans Wallet Desk ; l'inspection avancée reste dans SOL Prices Desk.
Validations sandbox après matérialisation :
```text
python3 scripts/audit_rust_workspace_rules.py PASS
python3 scripts/audit_markdown_tables.py README.md ... deltas PASS
TypeScript syntax transpile Wallet main.ts PASS
cargo fmt/check/clippy/test NOT RUN — cargo indisponible dans le sandbox
```