v0.2.12-pre.004

This commit is contained in:
2026-08-27 10:02:46 +02:00
parent afc50c1e48
commit e7c3379f44
17 changed files with 914 additions and 70 deletions

View File

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

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-solprices-desk/Cargo.toml
# version: 1
# version: 2
[package]
name = "ksp-app-solprices-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" }
serde = { workspace = true, features = ["derive"] }
tauri.workspace = true
tauri-plugin-tracing.workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-solprices-desk/frontend/main.html -->
<!-- version: 4 -->
<!-- version: 5 -->
<!DOCTYPE html>
<html lang="fr">
@@ -47,22 +47,48 @@
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h1 class="h3 mb-1">Prices</h1>
<p class="text-body-secondary mb-0">Composition Config/Off-chain active. La projection provider-neutral du registry arrive dans la tranche suivante.</p>
<p class="text-body-secondary mb-0">Inventaire provider-neutral du registry Off-chain. Aucun refresh réseau n'est encore déclenché dans cette tranche.</p>
</div>
</div>
<div class="app-placeholder">
<div id="pricesLoading" class="app-placeholder">
<div>
<i class="fa-solid fa-chart-line fa-3x text-body-secondary mb-3" aria-hidden="true"></i>
<h2 class="h5">Surface prix volontairement inactive</h2>
<p class="text-body-secondary mb-0">`pre.003` initialise le service via Config, mais ne crée encore ni lignes provider, ni observation affichée, ni refresh réseau déclenché par lUI.</p>
<i class="fa-solid fa-spinner fa-spin text-body-secondary mb-3" aria-hidden="true"></i>
<h2 class="h5">Chargement du registry</h2>
<p class="text-body-secondary mb-0">Projection de l'inventaire provider-neutral depuis le runtime backend.</p>
</div>
</div>
<div id="pricesEmpty" class="app-placeholder" hidden>
<div>
<i class="fa-solid fa-table-list text-body-secondary mb-3" aria-hidden="true"></i>
<h2 class="h5">Aucun provider configuré</h2>
<p class="text-body-secondary mb-0">Le runtime Off-chain ne contient actuellement aucune ligne provider.</p>
</div>
</div>
<div id="pricesDiagnostic" class="alert alert-warning mb-3" hidden></div>
<div id="pricesTableContainer" class="table-responsive" hidden>
<table id="marketPriceTable" class="table table-hover align-middle mb-0">
<thead>
<tr>
<th scope="col">Provider</th>
<th scope="col">Pair</th>
<th scope="col">Semantics</th>
<th scope="col">Auth</th>
<th scope="col">Availability</th>
<th scope="col">Exact price</th>
<th scope="col">Provider timestamp (ms)</th>
<th scope="col">Received at (ms)</th>
<th scope="col">Retry at (ms)</th>
</tr>
</thead>
<tbody id="marketPriceRows"></tbody>
</table>
</div>
</section>
<section data-view-panel="diagnostics" hidden>
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h1 class="h3 mb-1">Diagnostics</h1>
<p class="text-body-secondary mb-0">État sûr du bootstrap Config, Logging et Off-chain Transport.</p>
<p class="text-body-secondary mb-0">État sûr du bootstrap Config, Logging et runtime prix provider-neutral.</p>
</div>
</div>
<div class="card shadow-sm">
@@ -79,6 +105,12 @@
<dd id="runtimeCompositeProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Off-chain</dt>
<dd id="runtimeOffchainProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Providers</dt>
<dd id="runtimeProviderCount" class="col-sm-7"></dd>
<dt class="col-sm-5">Providers ready</dt>
<dd id="runtimeReadyProviderCount" class="col-sm-7"></dd>
<dt class="col-sm-5">Providers unavailable</dt>
<dd id="runtimeUnavailableProviderCount" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Logging</dt>
<dd id="runtimeLoggingProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Fallback Logging</dt>

View File

@@ -1,11 +1,12 @@
// file: crates/ksp-app-solprices-desk/frontend/ts/main.ts
// version: 3
// version: 4
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { RuntimeStatusDto } from "./bindings/ksp_app_solprices_desk/dto_common/RuntimeStatusDto.ts";
import type { MarketPriceRuntimeStatusDto } from "./bindings/ksp_app_solprices_desk/dto_common/MarketPriceRuntimeStatusDto.ts";
import type { MarketPriceProviderRowDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceProviderRowDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
@@ -53,13 +54,53 @@ function bindNavigation(): void {
frontendTrace("main", "SOL Prices Desk navigation handlers installed");
}
function renderRuntimeStatus(status: RuntimeStatusDto): void {
function appendMarketPriceCell(row: HTMLTableRowElement, value: string): void {
const cell = document.createElement("td");
cell.textContent = value;
row.append(cell);
}
function renderMarketPriceRows(rows: MarketPriceProviderRowDto[]): void {
const loading = document.querySelector<HTMLElement>("#pricesLoading");
const empty = document.querySelector<HTMLElement>("#pricesEmpty");
const tableContainer = document.querySelector<HTMLElement>("#pricesTableContainer");
const tableBody = document.querySelector<HTMLTableSectionElement>("#marketPriceRows");
if (loading) {
loading.hidden = true;
}
if (!tableBody || !tableContainer || !empty) {
frontendWarn("main", "SOL Prices Desk market-price table elements are missing");
return;
}
tableBody.replaceChildren();
empty.hidden = rows.length !== 0;
tableContainer.hidden = rows.length === 0;
for (const provider of rows) {
const row = document.createElement("tr");
appendMarketPriceCell(row, provider.displayName);
appendMarketPriceCell(row, provider.pair);
appendMarketPriceCell(row, provider.semantics);
appendMarketPriceCell(row, provider.authMode);
appendMarketPriceCell(row, provider.loading ? "loading" : provider.availability);
appendMarketPriceCell(row, provider.price ?? "—");
appendMarketPriceCell(row, provider.providerTimestampUnixMillis ?? "—");
appendMarketPriceCell(row, provider.receivedAtUnixMillis ?? "—");
appendMarketPriceCell(row, provider.retryAtUnixMillis ?? "—");
tableBody.append(row);
}
frontendTrace("main", "SOL Prices Desk market-price registry rows rendered", { rowCount: rows.length });
}
function renderRuntimeStatus(status: MarketPriceRuntimeStatusDto): void {
const values: Record<string, string> = {
runtimeCompositeProfile: status.activeCompositeProfile,
runtimeOffchainProfile: status.activeOffchainProfile,
runtimeVersion: status.applicationVersion,
runtimeShellPhase: status.shellPhase,
runtimeConfigDocuments: status.configDocumentCount.toString(),
runtimeProviderCount: status.providerCount.toString(),
runtimeReadyProviderCount: status.readyProviderCount.toString(),
runtimeUnavailableProviderCount: status.unavailableProviderCount.toString(),
runtimeLoggingProfile: status.activeLoggingProfile ?? "fallback",
runtimeLoggingFallback: status.fallbackLoggingActive ? "oui" : "non",
};
@@ -74,14 +115,24 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
diagnostic.hidden = status.startupDiagnostic === null;
diagnostic.textContent = status.startupDiagnostic === null ? "" : `${status.startupDiagnostic.domain}:${status.startupDiagnostic.code}${status.startupDiagnostic.message}`;
}
frontendTrace("main", "SOL Prices Desk scaffold runtime status rendered", {
frontendTrace("main", "SOL Prices Desk market-price runtime status rendered", {
fallbackLoggingActive: status.fallbackLoggingActive,
configDocumentCount: status.configDocumentCount,
providerCount: status.providerCount,
readyProviderCount: status.readyProviderCount,
unavailableProviderCount: status.unavailableProviderCount,
});
}
async function loadMarketPrices(): Promise<void> {
frontendTrace("main", "SOL Prices Desk provider registry load started");
const rows = await invokeKsp<MarketPriceProviderRowDto[]>("main", "list_market_prices");
renderMarketPriceRows(rows);
frontendTrace("main", "SOL Prices Desk provider registry load completed", { rowCount: rows.length });
}
async function loadRuntimeStatus(): Promise<void> {
const status = await invokeKsp<RuntimeStatusDto>("main", "get_runtime_status");
const status = await invokeKsp<MarketPriceRuntimeStatusDto>("main", "get_runtime_status");
renderRuntimeStatus(status);
}
@@ -100,6 +151,20 @@ async function initializeMain(): Promise<void> {
}
frontendWarn("main", "SOL Prices Desk startup runtime status load failed");
}
try {
await loadMarketPrices();
} catch {
const loading = document.querySelector<HTMLElement>("#pricesLoading");
const diagnostic = document.querySelector<HTMLElement>("#pricesDiagnostic");
if (loading) {
loading.hidden = true;
}
if (diagnostic) {
diagnostic.hidden = false;
diagnostic.textContent = "L'inventaire provider-neutral n'a pas pu être chargé.";
}
frontendWarn("main", "SOL Prices Desk provider registry load failed");
}
}
document.addEventListener("DOMContentLoaded", () => {

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/src/app_state.rs
// version: 2
// version: 3
//! Shared backend state owned by the SOL Prices Desk Tauri application.
@@ -7,13 +7,13 @@
pub(crate) struct AppState {
config_management: ksp_config_lib::ConfigManagement,
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
offchain_transport_startup: crate::OffchainTransportStartup,
market_price_runtime: crate::MarketPriceRuntime,
splash_settings: crate::SplashSettings,
splash_sequence_started: std::sync::atomic::AtomicBool,
}
impl crate::AppState {
/// Initializes Config ownership, composite-managed Logging and the Config-selected Off-chain Transport runtime.
/// Initializes Config ownership, composite-managed Logging and the provider-neutral market-price runtime.
pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> {
let config_management = crate::config_management(arguments);
let config_management = match config_management {
@@ -35,6 +35,11 @@ impl crate::AppState {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let market_price_runtime = crate::MarketPriceRuntime::new(offchain_transport_startup);
let market_price_runtime = match market_price_runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let splash_settings = crate::SplashSettings::load();
let splash_settings = match splash_settings {
std::result::Result::Ok(value) => value,
@@ -52,14 +57,19 @@ impl crate::AppState {
fallback_active: logging_startup.fallback_active,
startup_diagnostic: logging_startup.startup_diagnostic,
}),
offchain_transport_startup,
market_price_runtime,
splash_settings,
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
});
}
/// Builds the safe runtime status exposed before provider rows and refresh commands are introduced.
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::RuntimeStatusDto> {
/// Lists the current provider-neutral market-price rows without triggering network refresh.
pub(crate) fn list_market_prices(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceProviderRowDto>> {
return self.market_price_runtime.list_rows();
}
/// Builds the safe runtime status exposed by the SOL Prices Desk shell.
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::MarketPriceRuntimeStatusDto> {
let document_count = self.config_management.engine().registry().descriptors().count();
let document_count = u32::try_from(document_count);
let document_count = match document_count {
@@ -74,6 +84,11 @@ impl crate::AppState {
);
},
};
let provider_counts = self.market_price_runtime.provider_counts();
let (provider_count, ready_provider_count, unavailable_provider_count) = match provider_counts {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime = self.logging_runtime.lock();
let runtime = match runtime {
std::result::Result::Ok(value) => value,
@@ -85,16 +100,20 @@ impl crate::AppState {
},
};
let _keep_guard_alive = &runtime.guard;
let offchain = self.offchain_transport_startup.resolved();
return std::result::Result::Ok(crate::RuntimeStatusDto {
active_composite_profile: self.offchain_transport_startup.composite_profile_id().to_owned(),
let startup = self.market_price_runtime.startup();
let offchain = startup.resolved();
return std::result::Result::Ok(crate::MarketPriceRuntimeStatusDto {
active_composite_profile: startup.composite_profile_id().to_owned(),
active_logging_profile: runtime.active_profile_id.clone(),
active_offchain_profile: offchain.profile_id().to_owned(),
application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count,
fallback_logging_active: runtime.fallback_active,
shell_phase: "pre.003-offchain-bootstrap".to_owned(),
provider_count,
ready_provider_count,
shell_phase: "pre.004-market-price-runtime".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
unavailable_provider_count,
});
}

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-app-solprices-desk/src/dto_common.rs
// version: 2
// version: 3
//! Common Tauri DTOs shared by the SOL Prices Desk bootstrap shell.
//! Common Tauri DTOs shared by the SOL Prices Desk shell.
use ts_rs::TS; // rust-rules: trait-import
@@ -30,11 +30,11 @@ impl crate::CommandErrorDto {
}
}
/// Safe Config/bootstrap snapshot exposed before market-price provider rows exist.
/// Safe provider-neutral application/runtime snapshot exposed to the SOL Prices Desk shell.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/dto_common/RuntimeStatusDto.ts")]
pub(crate) struct RuntimeStatusDto {
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/dto_common/MarketPriceRuntimeStatusDto.ts")]
pub(crate) struct MarketPriceRuntimeStatusDto {
/// SOL Prices Desk composite profile selected for this launch.
pub(crate) active_composite_profile: String,
/// Active configured Logging profile, or `None` while transient fallback Logging is active.
@@ -47,10 +47,16 @@ pub(crate) struct RuntimeStatusDto {
pub(crate) config_document_count: u32,
/// Whether SOL Prices Desk had to install its transient in-memory Logging fallback.
pub(crate) fallback_logging_active: bool,
/// Current implementation phase exposed by the bootstrap shell.
/// Total number of configured provider-neutral market-price rows.
pub(crate) provider_count: u32,
/// Number of providers currently classified as immediately ready.
pub(crate) ready_provider_count: u32,
/// Current implementation phase exposed by the shell.
pub(crate) shell_phase: String,
/// Safe startup diagnostic that caused fallback Logging, when applicable.
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
/// Number of providers currently not classified as immediately ready.
pub(crate) unavailable_provider_count: u32,
}
#[cfg(test)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/src/lib.rs
// version: 2
// version: 3
//! Tauri desktop application scaffold for provider-neutral SOL/USD price visualization.
@@ -14,6 +14,7 @@ mod dto_common;
mod errors;
mod frontend_logging;
mod logging_runtime;
mod market_price_runtime;
mod offchain_runtime;
mod splash;
mod tauri;
@@ -59,8 +60,8 @@ pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Safe command error projection exposed to Tauri commands.
pub(crate) use self::dto_common::CommandErrorDto;
/// Safe scaffold runtime snapshot exposed to the shell.
pub(crate) use self::dto_common::RuntimeStatusDto;
/// Safe provider-neutral runtime snapshot exposed to the shell.
pub(crate) use self::dto_common::MarketPriceRuntimeStatusDto;
/// Shared SOL Prices Desk application state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared SOL Prices Desk runtime state cannot be locked safely.
@@ -89,6 +90,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 SOL Prices Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Safe provider-neutral market-price row exposed to the frontend.
pub(crate) use self::market_price_runtime::MarketPriceProviderRowDto;
/// Backend application runtime owning the Config-selected service and in-memory market-price presentation state.
pub(crate) use self::market_price_runtime::MarketPriceRuntime;
/// Resolved composite and Off-chain Transport configuration retained by SOL Prices Desk.
pub(crate) use self::offchain_runtime::OffchainTransportStartup;
/// Resolves the composite-selected Off-chain Transport profile and constructs the Config-owned service.

View File

@@ -0,0 +1,209 @@
// file: crates/ksp-app-solprices-desk/src/market_price_runtime.rs
// version: 1
//! Provider-neutral market-price presentation runtime owned by SOL Prices Desk.
use ts_rs::TS; // rust-rules: trait-import
/// Safe provider-neutral row projected to the SOL Prices Desk frontend.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceProviderRowDto.ts")]
pub(crate) struct MarketPriceProviderRowDto {
/// Generic authentication capability code for the configured provider mode.
pub(crate) auth_mode: String,
/// Generic current availability code.
pub(crate) availability: String,
/// Safe provider display name supplied by Off-chain Transport.
pub(crate) display_name: String,
/// Whether one refresh is currently in flight for this provider.
pub(crate) loading: bool,
/// Stable V1 pair label.
pub(crate) pair: String,
/// Exact canonical SOL/USD decimal string, when an observation exists.
pub(crate) price: std::option::Option<String>,
/// Opaque provider identifier accepted by future refresh commands.
pub(crate) provider_id: String,
/// Provider-supplied observation timestamp in exact Unix milliseconds, when genuinely supplied.
pub(crate) provider_timestamp_unix_millis: std::option::Option<String>,
/// KSP receipt timestamp in exact Unix milliseconds, when an observation exists.
pub(crate) received_at_unix_millis: std::option::Option<String>,
/// Whether this row currently retains at least one successful observation.
pub(crate) refreshed: bool,
/// Known next retry timestamp in exact Unix milliseconds for cooldown/temporary states.
pub(crate) retry_at_unix_millis: std::option::Option<String>,
/// Generic price-semantics code retained from the provider descriptor.
pub(crate) semantics: String,
}
/// Backend application runtime that owns the Config-selected Off-chain service and current in-memory presentation state.
pub(crate) struct MarketPriceRuntime {
presentation: std::sync::Mutex<MarketPricePresentationState>,
startup: crate::OffchainTransportStartup,
}
impl crate::MarketPriceRuntime {
/// Builds the application presentation runtime from the already Config-resolved Off-chain startup.
pub(crate) fn new(startup: crate::OffchainTransportStartup) -> ksp_core_lib::Result<Self> {
let registry = startup.resolved().service().registry();
let mut entries = std::collections::BTreeMap::new();
for entry in registry.entries() {
let previous = entries
.insert(entry.descriptor().id().as_str().to_owned(), MarketPricePresentationEntry { loading: false, observation: std::option::Option::None });
if previous.is_some() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime contains duplicate provider state")
.with_context("provider_id", entry.descriptor().id().as_str()),
);
}
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
provider_count = entries.len(),
"initialized SOL Prices Desk market-price presentation runtime"
);
return std::result::Result::Ok(Self { presentation: std::sync::Mutex::new(MarketPricePresentationState { entries }), startup });
}
/// Returns the Config/bootstrap metadata retained by the price runtime.
#[must_use]
pub(crate) const fn startup(&self) -> &crate::OffchainTransportStartup {
return &self.startup;
}
/// Returns total, ready and unavailable provider counts from the detached generic registry snapshot.
pub(crate) fn provider_counts(&self) -> ksp_core_lib::Result<(u32, u32, u32)> {
let registry = self.startup.resolved().service().registry();
let total = count_to_u32(registry.len(), "provider_count");
let total = match total {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut ready_count = 0usize;
for entry in registry.entries() {
if matches!(entry.state().availability(), ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready) {
ready_count += 1;
}
}
let ready = count_to_u32(ready_count, "ready_provider_count");
let ready = match ready {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unavailable = total.saturating_sub(ready);
return std::result::Result::Ok((total, ready, unavailable));
}
/// Projects the current registry and in-memory observation state as deterministic frontend rows without network dispatch.
pub(crate) fn list_rows(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceProviderRowDto>> {
let registry = self.startup.resolved().service().registry();
let presentation = self.presentation.lock();
let presentation = match presentation {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"SOL Prices Desk market-price presentation state lock is poisoned",
));
},
};
let mut rows = std::vec::Vec::with_capacity(registry.len());
for entry in registry.entries() {
let provider_id = entry.descriptor().id().as_str();
let presentation_entry = presentation.entries.get(provider_id);
let presentation_entry = match presentation_entry {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime is missing one registry row")
.with_context("provider_id", provider_id),
);
},
};
rows.push(project_registry_entry(entry, presentation_entry));
}
return std::result::Result::Ok(rows);
}
}
struct MarketPricePresentationEntry {
loading: bool,
observation: std::option::Option<ksp_offchain_transport_lib::MarketPriceObservation>,
}
struct MarketPricePresentationState {
entries: std::collections::BTreeMap<String, MarketPricePresentationEntry>,
}
fn auth_mode_code(auth_mode: ksp_offchain_transport_lib::MarketPriceProviderAuthMode) -> &'static str {
return match auth_mode {
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None => "none",
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::OptionalApiKey => "optional_api_key",
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::RequiredApiKey => "required_api_key",
_ => "unknown",
};
}
fn availability_code(availability: ksp_offchain_transport_lib::MarketPriceProviderAvailability) -> &'static str {
return match availability {
ksp_offchain_transport_lib::MarketPriceProviderAvailability::AuthenticationUnavailable => "authentication_unavailable",
ksp_offchain_transport_lib::MarketPriceProviderAvailability::CoolingDown { .. } => "cooling_down",
ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled => "disabled",
ksp_offchain_transport_lib::MarketPriceProviderAvailability::Misconfigured => "misconfigured",
ksp_offchain_transport_lib::MarketPriceProviderAvailability::QuotaUnavailable => "quota_unavailable",
ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready => "ready",
ksp_offchain_transport_lib::MarketPriceProviderAvailability::TemporarilyUnavailable { .. } => "temporarily_unavailable",
_ => "unknown",
};
}
fn count_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
let converted = u32::try_from(value);
return match converted {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime count exceeds DTO bounds")
.with_context("field", field)
.with_source(error),
),
};
}
fn project_registry_entry(
entry: &ksp_offchain_transport_lib::MarketPriceProviderRegistryEntry,
presentation: &MarketPricePresentationEntry,
) -> crate::MarketPriceProviderRowDto {
let descriptor = entry.descriptor();
let availability = entry.state().availability();
let observation = presentation.observation.as_ref();
return crate::MarketPriceProviderRowDto {
auth_mode: auth_mode_code(descriptor.auth_mode()).to_owned(),
availability: availability_code(availability).to_owned(),
display_name: descriptor.display_name().to_owned(),
loading: presentation.loading,
pair: ksp_offchain_transport_lib::MarketPricePair::SolUsd.code().to_owned(),
price: observation.map(|value| return value.price().to_canonical_string()),
provider_id: descriptor.id().as_str().to_owned(),
provider_timestamp_unix_millis: observation.and_then(|value| return value.provider_timestamp()).map(|value| return value.unix_millis().to_string()),
received_at_unix_millis: observation.map(|value| return value.received_at().unix_millis().to_string()),
refreshed: observation.is_some(),
retry_at_unix_millis: availability.retry_at().map(|value| return value.unix_millis().to_string()),
semantics: semantics_code(descriptor.semantics()).to_owned(),
};
}
fn semantics_code(semantics: ksp_offchain_transport_lib::MarketPriceSemantics) -> &'static str {
return match semantics {
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket => "aggregated_market",
ksp_offchain_transport_lib::MarketPriceSemantics::DexPairUsd => "dex_pair_usd",
ksp_offchain_transport_lib::MarketPriceSemantics::ExchangeLastTrade => "exchange_last_trade",
ksp_offchain_transport_lib::MarketPriceSemantics::SolanaHeuristic => "solana_heuristic",
ksp_offchain_transport_lib::MarketPriceSemantics::SolanaSpot => "solana_spot",
_ => "unknown",
};
}
#[cfg(test)]
#[path = "../unit_tests/market_price_runtime.rs"]
mod tests;

View File

@@ -72,7 +72,7 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_runtime_status, splash_frontend_ready,]);
return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_runtime_status, list_market_prices, splash_frontend_ready,]);
}
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
@@ -91,7 +91,16 @@ fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri:
}
#[tauri::command]
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RuntimeStatusDto, crate::CommandErrorDto> {
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::MarketPriceRuntimeStatusDto, crate::CommandErrorDto> {
let result = state.runtime_status();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
@@ -100,10 +109,15 @@ fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::
}
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);
fn list_market_prices(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::MarketPriceProviderRowDto>, crate::CommandErrorDto> {
let result = state.list_market_prices();
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Ok(value) => {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, row_count = value.len(), "projected SOL Prices Desk market-price registry rows");
std::result::Result::Ok(value)
},
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "KSP SOL Prices Desk",
"version": "0.2.12-pre.3",
"version": "0.2.12-pre.4",
"identifier": "com.sasedev.ksp-app-solprices-desk",
"build": {
"beforeDevCommand": {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
// version: 3
// version: 4
//! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`.
@@ -37,9 +37,9 @@ fn pre_002_shell_uses_reserved_ports_windows_and_owned_backend_paths() {
let tauri = read_json(root.join("tauri.conf.json").as_path());
assert_eq!(tauri.pointer("/productName").and_then(serde_json::Value::as_str), std::option::Option::Some("KSP SOL Prices Desk"));
assert_eq!(tauri.pointer("/identifier").and_then(serde_json::Value::as_str), std::option::Option::Some("com.sasedev.ksp-app-solprices-desk"));
assert_eq!(tauri.pointer("/version").and_then(serde_json::Value::as_str), std::option::Option::Some("0.2.12-pre.3"));
assert_eq!(tauri.pointer("/version").and_then(serde_json::Value::as_str), std::option::Option::Some("0.2.12-pre.4"));
let package = read_json(root.join("package.json").as_path());
assert_eq!(package.pointer("/version").and_then(serde_json::Value::as_str), std::option::Option::Some("0.2.12-pre.3"));
assert_eq!(package.pointer("/version").and_then(serde_json::Value::as_str), std::option::Option::Some("0.2.12-pre.4"));
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("."));
@@ -82,7 +82,6 @@ fn pre_002_package_is_mixed_lib_bin_and_frontend_is_scaffold_only() {
let main_html = read_text(root.join("frontend/main.html").as_path());
assert!(main_html.contains("data-view=\"prices\""));
assert!(main_html.contains("data-view=\"diagnostics\""));
assert!(!main_html.contains("<table"));
}
#[test]
@@ -151,3 +150,32 @@ fn pre_002_fix_001_reuses_the_common_ksp_desktop_template() {
assert_eq!(sol_body, wallet_body, "SOL Prices Desk diverges from Wallet Desk template in {relative}");
}
}
#[test]
fn pre_004_market_price_runtime_projects_registry_rows_without_refresh_controls() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains(r#"ksp-offchain-transport-lib = { path = "../ksp-offchain-transport-lib" }"#));
assert!(!manifest.contains("reqwest"));
let lib_rs = read_text(root.join("src/lib.rs").as_path());
assert!(lib_rs.contains("mod market_price_runtime;"));
assert!(lib_rs.contains("MarketPriceProviderRowDto"));
assert!(lib_rs.contains("MarketPriceRuntime"));
let tauri_rs = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri_rs.contains("list_market_prices"));
for forbidden in ["refresh_market_price", "refresh_market_prices", "refresh_all_market_prices"] {
assert!(!tauri_rs.contains(forbidden));
}
let main_html = read_text(root.join("frontend/main.html").as_path());
assert!(main_html.contains(r#"id="pricesLoading""#));
assert!(main_html.contains(r#"id="pricesEmpty""#));
assert!(main_html.contains(r#"id="marketPriceTable""#));
assert!(main_html.contains(r#"id="marketPriceRows""#));
assert!(!main_html.contains("Refresh"));
let main_typescript = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(main_typescript.contains(r#""list_market_prices""#));
assert!(main_typescript.contains("MarketPriceProviderRowDto"));
assert!(main_typescript.contains(r#"provider.price ?? "—""#));
assert!(!main_typescript.contains("parseFloat("));
assert!(!main_typescript.contains("Number(provider.price"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/tests/desktop_security.rs
// version: 2
// version: 3
//! Security, ownership and frontend instrumentation canaries for SOL Prices Desk.
@@ -50,7 +50,7 @@ fn pre_002_capability_surface_is_core_plus_tracing_only() {
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("tauri-plugin-tracing.workspace = true"));
assert!(!manifest.contains("tauri-plugin-dialog"));
assert!(!manifest.contains("ksp-offchain-transport-lib"));
assert!(manifest.contains("ksp-offchain-transport-lib"));
assert!(!manifest.contains("reqwest"));
}
@@ -69,7 +69,7 @@ fn pre_002_frontend_has_no_network_persistence_or_native_dialog_surface() {
}
#[test]
fn pre_003_tauri_commands_remain_centralized_and_refresh_runtime_is_absent() {
fn pre_004_tauri_commands_remain_centralized_and_refresh_is_absent() {
let root = app_root();
let mut rust_files = std::vec::Vec::new();
collect_files(root.join("src").as_path(), "rs", &mut rust_files);
@@ -82,14 +82,13 @@ fn pre_003_tauri_commands_remain_centralized_and_refresh_runtime_is_absent() {
} else {
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
}
assert!(!source.contains("MarketPriceRuntime"), "{} advances application price-state runtime before pre.004", path.display());
assert!(!source.contains("refresh_market_price"), "{} advances refresh commands before their planned tranche", path.display());
}
assert_eq!(command_count, 3);
assert_eq!(command_count, 4);
}
#[test]
fn pre_003_frontend_control_interactions_are_logged_without_business_values() {
fn pre_004_frontend_registry_loading_is_logged_without_business_values() {
let root = app_root();
let main = read_text(root.join("frontend/ts/main.ts").as_path());
let invoke = read_text(root.join("frontend/ts/invoke.ts").as_path());
@@ -100,6 +99,8 @@ fn pre_003_frontend_control_interactions_are_logged_without_business_values() {
assert!(invoke.contains("Frontend IPC command requested"));
assert!(invoke.contains("Frontend IPC command completed"));
assert!(invoke.contains("frontendTrace"));
assert!(main.contains("SOL Prices Desk provider registry load started"));
assert!(main.contains("SOL Prices Desk market-price registry rows rendered"));
for forbidden in ["JSON.stringify(status)", "JSON.stringify(payload)", "apiKey", "authorization"] {
assert!(!main.contains(forbidden), "main frontend logging must not serialize business/secret values: {forbidden}");
}

View File

@@ -0,0 +1,140 @@
// file: crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs
// version: 1
fn descriptor() -> std::option::Option<ksp_offchain_transport_lib::MarketPriceProviderDescriptor> {
let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new("test-provider");
let provider_id = match provider_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let rate_limit = ksp_offchain_transport_lib::MarketPriceProviderRateLimit::fixed(
10,
60,
std::option::Option::None,
ksp_offchain_transport_lib::MarketPriceProviderRateLimitScope::Unspecified,
);
let rate_limit = match rate_limit {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let descriptor = ksp_offchain_transport_lib::MarketPriceProviderDescriptor::new(
provider_id,
"Test Provider",
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket,
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None,
rate_limit,
std::option::Option::None,
true,
);
return match descriptor {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(_) => std::option::Option::None,
};
}
#[test]
fn generic_codes_cover_current_provider_neutral_enums() {
assert_eq!(super::auth_mode_code(ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None), "none");
assert_eq!(super::auth_mode_code(ksp_offchain_transport_lib::MarketPriceProviderAuthMode::OptionalApiKey), "optional_api_key");
assert_eq!(super::auth_mode_code(ksp_offchain_transport_lib::MarketPriceProviderAuthMode::RequiredApiKey), "required_api_key");
assert_eq!(
super::availability_code(ksp_offchain_transport_lib::MarketPriceProviderAvailability::AuthenticationUnavailable),
"authentication_unavailable"
);
assert_eq!(
super::availability_code(ksp_offchain_transport_lib::MarketPriceProviderAvailability::CoolingDown {
retry_at: ksp_offchain_transport_lib::MarketPriceTimestamp::from_unix_millis(123),
}),
"cooling_down"
);
assert_eq!(super::availability_code(ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled), "disabled");
assert_eq!(super::availability_code(ksp_offchain_transport_lib::MarketPriceProviderAvailability::Misconfigured), "misconfigured");
assert_eq!(super::availability_code(ksp_offchain_transport_lib::MarketPriceProviderAvailability::QuotaUnavailable), "quota_unavailable");
assert_eq!(super::availability_code(ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready), "ready");
assert_eq!(
super::availability_code(ksp_offchain_transport_lib::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::None }),
"temporarily_unavailable"
);
assert_eq!(super::semantics_code(ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket), "aggregated_market");
assert_eq!(super::semantics_code(ksp_offchain_transport_lib::MarketPriceSemantics::DexPairUsd), "dex_pair_usd");
assert_eq!(super::semantics_code(ksp_offchain_transport_lib::MarketPriceSemantics::ExchangeLastTrade), "exchange_last_trade");
assert_eq!(super::semantics_code(ksp_offchain_transport_lib::MarketPriceSemantics::SolanaHeuristic), "solana_heuristic");
assert_eq!(super::semantics_code(ksp_offchain_transport_lib::MarketPriceSemantics::SolanaSpot), "solana_spot");
}
#[test]
fn initial_registry_row_contains_no_observation_or_loading_state() {
let descriptor = descriptor();
assert!(descriptor.is_some());
let descriptor = match descriptor {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let entry =
ksp_offchain_transport_lib::MarketPriceProviderRegistryEntry::new(descriptor, ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready);
let presentation = super::MarketPricePresentationEntry { loading: false, observation: std::option::Option::None };
let row = super::project_registry_entry(&entry, &presentation);
assert_eq!(row.provider_id, "test-provider");
assert_eq!(row.display_name, "Test Provider");
assert_eq!(row.pair, "SOL/USD");
assert_eq!(row.auth_mode, "none");
assert_eq!(row.availability, "ready");
assert_eq!(row.semantics, "aggregated_market");
assert!(!row.loading);
assert!(!row.refreshed);
assert_eq!(row.price, std::option::Option::None);
assert_eq!(row.provider_timestamp_unix_millis, std::option::Option::None);
assert_eq!(row.received_at_unix_millis, std::option::Option::None);
assert_eq!(row.retry_at_unix_millis, std::option::Option::None);
}
#[test]
fn observation_projection_keeps_exact_decimal_and_distinct_timestamps() {
let descriptor = descriptor();
assert!(descriptor.is_some());
let descriptor = match descriptor {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let provider_id = descriptor.id().clone();
let entry = ksp_offchain_transport_lib::MarketPriceProviderRegistryEntry::new(
descriptor,
ksp_offchain_transport_lib::MarketPriceProviderAvailability::CoolingDown {
retry_at: ksp_offchain_transport_lib::MarketPriceTimestamp::from_unix_millis(400),
},
);
let price = ksp_offchain_transport_lib::MarketPriceDecimal::parse("123.4500");
assert!(price.is_ok());
let price = match price {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let provenance = ksp_offchain_transport_lib::MarketPriceProvenance::new("unit-test");
assert!(provenance.is_ok());
let provenance = match provenance {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let observation = ksp_offchain_transport_lib::MarketPriceObservation::new(
provider_id,
price,
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket,
ksp_offchain_transport_lib::MarketPriceTimestamp::from_unix_millis(100),
ksp_offchain_transport_lib::MarketPriceTimestamp::from_unix_millis(300),
std::option::Option::Some(ksp_offchain_transport_lib::MarketPriceTimestamp::from_unix_millis(200)),
provenance,
);
assert!(observation.is_ok());
let observation = match observation {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let presentation = super::MarketPricePresentationEntry { loading: true, observation: std::option::Option::Some(observation) };
let row = super::project_registry_entry(&entry, &presentation);
assert_eq!(row.price.as_deref(), std::option::Option::Some("123.45"));
assert_eq!(row.provider_timestamp_unix_millis.as_deref(), std::option::Option::Some("200"));
assert_eq!(row.received_at_unix_millis.as_deref(), std::option::Option::Some("300"));
assert_eq!(row.retry_at_unix_millis.as_deref(), std::option::Option::Some("400"));
assert!(row.loading);
assert!(row.refreshed);
}

285
deltas/0.2.12/pre.004.md Normal file
View File

@@ -0,0 +1,285 @@
<!-- file: deltas/0.2.12/pre.004.md -->
<!-- version: 1 -->
# Delta `0.2.12-pre.004` — runtime prix, registry rows et DTOs
## 1. Base requise
Cette tranche s'applique exclusivement après :
```text
v0.2.11
+ 0.2.12-pre.001
+ 0.2.12-pre.001-fix.001
+ 0.2.12-pre.002
+ 0.2.12-pre.002-fix.001
+ 0.2.12-pre.003
+ 0.2.12-pre.003-fix.001
```
Le retour opérateur du `27 août 2026` clôt `pre.003-fix.001` pour la progression de release : `cargo fmt`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace, `cargo test -p ksp-app-solprices-desk` et `cargo tauri dev` passent. Le runtime Tauri journalise le bootstrap Logging, le profil composite `public_keyless`, le profil Off-chain `public_keyless` et `provider_count=8`. `cargo test --workspace` n'est pas présent dans ce retour précis et n'est donc pas revendiqué comme exécuté.
La version workspace passe à :
```text
0.2.12-pre.4
```
Les versions `package.json` et `tauri.conf.json` de SOL Prices Desk sont synchronisées sur la même prerelease.
## 2. Responsabilité de la tranche
`pre.004` matérialise uniquement la couche applicative provider-neutral entre le `MarketPriceService` déjà construit par Config et la future UX de refresh :
```text
MarketPriceRuntime backend
état de présentation courant en mémoire uniquement
projection du registry en lignes stables
MarketPriceRuntimeStatusDto
MarketPriceProviderRowDto
commande list_market_prices
états frontend loading / empty / table
projection initiale sans observation ni prix
```
La tranche n'appelle ni `MarketPriceService::refresh`, ni `refresh_many`, ni `refresh_all`. Aucun bouton de refresh ou scheduler n'est introduit avant les tranches prévues.
## 3. Ownership du runtime prix
`AppState` possède désormais un `MarketPriceRuntime`. Celui-ci retient :
```text
OffchainTransportStartup
-> ResolvedOffchainTransportConfig
-> MarketPriceService
presentation: Mutex<...>
-> une entrée par provider_id opaque
-> loading bool
-> dernière observation optionnelle
```
La présentation n'est persistée ni dans Config, ni dans Store, ni sur le filesystem frontend, ni dans `localStorage`/`sessionStorage`.
L'inventaire initial est dérivé du `MarketPriceService::registry()` détaché. Chaque ligne commence avec :
```text
loading = false
observation = None
price = None
refreshed = false
```
L'availability initiale reste celle possédée par Off-chain Transport (`Ready`, `Disabled`, etc.).
## 4. DTOs provider-neutral
`MarketPriceRuntimeStatusDto` remplace le nom bootstrap générique précédent et expose uniquement :
```text
application/config/composite/logging/offchain metadata sûrs
provider_count
ready_provider_count
unavailable_provider_count
startup_diagnostic sûr
```
`MarketPriceProviderRowDto` expose uniquement la projection générique décidée par le plan :
```text
provider_id opaque
display_name
pair = SOL/USD
semantics générique
auth_mode générique
availability générique
loading
refreshed
price optionnel
provider_timestamp optionnel
received_at optionnel
retry_at optionnel
```
Le prix exact traverse l'IPC sous forme de `Option<String>` issue de `MarketPriceDecimal::to_canonical_string()`. Les timestamps traversent eux aussi sous forme de chaînes de millisecondes Unix afin de ne pas introduire de perte d'entier JavaScript. Aucun `f64` n'est une vérité IPC.
Les enums Off-chain `#[non_exhaustive]` sont projetés en codes génériques bornés avec fallback `unknown`; aucune enum provider-specific n'est exposée au frontend.
## 5. Commande d'inventaire
La nouvelle commande Tauri centralisée est :
```text
list_market_prices
```
Elle n'accepte aucun argument et retourne :
```text
Vec<MarketPriceProviderRowDto>
```
Elle ne déclenche aucun réseau. L'ordre des lignes suit l'ordre stable du registry Off-chain.
La surface command reste donc à cette tranche :
```text
emit_frontend_log
get_runtime_status
list_market_prices
splash_frontend_ready
```
Les commandes `refresh_market_price`, `refresh_market_prices` et `refresh_all_market_prices` restent absentes par contrat.
## 6. Frontend initial
La vue `Prices` remplace le placeholder de `pre.003` par trois états explicites :
```text
loading
empty
table provider-neutral
```
La table initiale affiche les capacités et l'availability du registry. Comme aucun refresh n'a encore eu lieu, les prix et timestamps d'observation s'affichent comme absents.
La tranche n'ajoute aucun bouton Refresh. La finalisation responsive/interactions/DataTable reste propriétaire de `pre.007`.
Diagnostics ajoute les comptes total/ready/unavailable sans exposer endpoint, credential, header ou payload provider.
## 7. Logging frontend/backend
`KSP-APP-027` reste appliqué :
```text
navigation/clic utilisateur debug
requête IPC debug via invokeKsp
fin IPC trace
chargement registry start/end trace
rendu des lignes trace avec row_count uniquement
erreur de projection/chargement warn/error sûr
```
Aucun log ne sérialise le prix, la row DTO complète, une API key, un header ou une réponse provider.
## 8. Dépendances et boundaries
SOL Prices Desk ajoute une dépendance directe à `ksp-offchain-transport-lib` parce que la couche application consomme désormais ses types publics génériques de registry/observation.
Cette dépendance ne donne lieu à aucune connaissance provider-specific :
```text
aucun SDK provider
aucun reqwest direct app
aucun fetch frontend
aucune URL/header/API key dans IPC
aucune branche CoinGecko/Kraken/etc.
aucun limiter/retry/sleep provider dans app
```
Config reste propriétaire de la construction du service et des secrets.
## 9. Tests et canaris
Les tests ajoutés/étendus couvrent :
```text
codes génériques auth/availability/semantics
projection initiale sans observation
projection exacte d'un decimal 123.4500 -> "123.45"
provider_timestamp / received_at / retry_at distincts
MarketPriceRuntime présent à partir de pre.004
commande list_market_prices centralisée dans tauri.rs
absence des trois commandes de refresh
états frontend loading/empty/table
aucune conversion parseFloat/Number du prix
aucun reqwest direct dans la crate app
logging registry sans sérialisation métier
```
Les bindings TS-RS restent générés par les tests et ne sont pas versionnés.
## 10. Fichiers ajoutés
```text
crates/ksp-app-solprices-desk/src/market_price_runtime.rs
crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs
deltas/0.2.12/pre.004.md
```
## 11. Fichiers modifiés
```text
Cargo.toml
crates/ksp-app-solprices-desk/Cargo.toml
crates/ksp-app-solprices-desk/package.json
crates/ksp-app-solprices-desk/tauri.conf.json
crates/ksp-app-solprices-desk/frontend/main.html
crates/ksp-app-solprices-desk/frontend/ts/main.ts
crates/ksp-app-solprices-desk/src/app_state.rs
crates/ksp-app-solprices-desk/src/dto_common.rs
crates/ksp-app-solprices-desk/src/lib.rs
crates/ksp-app-solprices-desk/src/tauri.rs
crates/ksp-app-solprices-desk/tests/desktop_contract.rs
crates/ksp-app-solprices-desk/tests/desktop_security.rs
docs/plans/019-V0_2_12_SOL_PRICES_DESK_PLAN.md
docs/validation/015-V0_2_12_SOL_PRICES_DESK.md
```
## 12. Fichiers volontairement inchangés
```text
crates/ksp-offchain-transport-lib/**
crates/ksp-config-lib/**
crates/ksp-app-wallet-desk/**
crates/ksp-app-config-desk/**
README.md
ROADMAP.md
CHANGELOG.md
prompts/017-V0_2_12_START_PROMPT.md
```
Aucun provider, wire REST, rate limit ou Config document n'est modifié.
## 13. Validations exécutées dans le sandbox
Après matérialisation du code avant écriture du delta :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
```
Après écriture du présent delta :
```text
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.12
Markdown table audit: clean (143 table(s), 113 file(s))
validation JSON package.json / tauri.conf.json
JSON validation: clean
```
Le sandbox ne possède pas `cargo`; aucune commande Cargo n'est déclarée PASS localement.
## 14. 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-solprices-desk
cargo test --workspace
(cd crates/ksp-app-solprices-desk && cargo tauri dev)
```
Le parcours Tauri attendu doit afficher huit lignes provider-neutral pour le profil `public_keyless`, avec availability initiale mais sans prix ni timestamp d'observation, et sans effectuer de refresh réseau. Diagnostics doit afficher `provider_count=8` avec les comptes ready/unavailable cohérents.
## 15. Questions ouvertes
Aucune. Si le gate devient propre, `pre.005` peut brancher exclusivement le refresh individuel sur ce runtime sans modifier le format Config ni les adapters providers.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/019-V0_2_12_SOL_PRICES_DESK_PLAN.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Plan `0.2.12` — SOL Prices Desk + intégration prix Wallet Desk
@@ -484,21 +484,21 @@ Correction du scaffold pour reprendre réellement les conventions communes Confi
### `pre.003` — Composite Config et bootstrap Off-chain
**Statut : implémenté ; gate opérateur interrompu par un défaut de compilation du canari `config_composition`, corrigé par `pre.003-fix.001`.**
**Statut : réalisé ; gate opérateur de tranche PASS après `pre.003-fix.001`.**
Ajout atomique du composite `cfg.composite.ksp-app-solprices-desk`, de son descriptor registry et des resources packagées nécessaires aux trois Desks concernés, puis bootstrap Config -> Off-chain Transport du nouveau Desk. Le gate exige que le packaging Config reste cohérent ; aucune UI prix n'est ajoutée ici.
#### `pre.003-fix.001` — Correction du canari ConfigEnvironment sans élargissement de contrat
**Statut : implémenté ; gate opérateur à confirmer.**
**Statut : réalisé ; gate opérateur de tranche PASS.**
Correction strictement locale du test `config_composition` : le message d'assertion ne tente plus de formater le `Result<ConfigEnvironment, Error>` en `Debug`. `ConfigEnvironment` reste volontairement sans contrat `Debug`; aucun comportement runtime, composite, provider ou frontend n'est modifié.
### `pre.004` — Runtime prix, registry rows et DTOs
**Statut : planifié.**
**Statut : implémenté ; gate opérateur à confirmer.**
Création du `MarketPriceRuntime` applicatif, projection provider-neutral du registry, DTOs TS-RS et états initiaux/loading/empty. Les providers restent opaques à l'UI et le prix exact traverse l'IPC sous forme de chaîne décimale, jamais comme vérité `f64`.
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.005` — Refresh individuel
@@ -572,7 +572,7 @@ Préparation du prompt de la release suivante, de `CHANGELOG.md` et de `ROADMAP.
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`.
Le point le plus risqué reste `pre.003`; son scope est néanmoins borné parce qu'il ne contient aucune UI prix et traite ensemble le seul invariant de packaging transversal. Le scope global reste raisonnablement clôturable dans une session de release KSP : aucun redécoupage n'est requis avant `pre.002`.
Le point de packaging transversal de `pre.003` est désormais fermé. Le prochain risque fonctionnel est borné aux refresh `pre.005`/`pre.006`, sans remettre en cause le découpage souple ni les responsabilités documentaires de clôture.
## 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: 5 -->
<!-- version: 6 -->
# Validation `0.2.12` — SOL Prices Desk + intégration Wallet Desk
@@ -82,10 +82,10 @@ NOT RUN commande non exécutée dans le contexte indiqué
| 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` | PENDING |
| default profile | `public_keyless` | PENDING |
| optional profile | `all_free` | PENDING |
| test profile | `tests` -> offchain `public_keyless` | PENDING |
| 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 |
@@ -96,14 +96,14 @@ NOT RUN commande non exécutée dans le contexte indiqué
| Canari | Attendu | Statut |
|--------------------------------------------|------------------|---------|
| app dépend du service/registry générique | oui | PENDING |
| branche `coingecko`/`kraken`/etc. dans app | aucune | PENDING |
| SDK provider dans app | aucun | PENDING |
| reqwest/provider fetch frontend | aucun | PENDING |
| endpoint/header/API key dans DTO | aucun | PENDING |
| rate limiter/sleep/retry provider dans app | aucun | PENDING |
| app dépend du service/registry générique | oui | PASS |
| branche `coingecko`/`kraken`/etc. dans app | aucune | PASS |
| SDK provider dans app | aucun | PASS |
| reqwest/provider fetch frontend | aucun | PASS |
| endpoint/header/API key dans DTO | aucun | PASS |
| rate limiter/sleep/retry provider dans app | aucun | PASS |
| refresh many ordre service | préservé | PENDING |
| semantics provider-neutral visibles | oui, sans fusion | PENDING |
| semantics provider-neutral visibles | oui, sans fusion | PASS |
| prix canonique/fallback/moyenne | aucun | OUT |
## 7. Security matrix
@@ -121,7 +121,7 @@ NOT RUN commande non exécutée dans le contexte indiqué
| 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 | PENDING |
| frontend sensitive logging | action/ID/count/code seulement | PASS |
| Wallet VIEW/OWNER/secrets | non-régression | PENDING |
## 8. Wallet integration matrix
@@ -295,3 +295,42 @@ python3 scripts/audit_markdown_tables.py README.md ... deltas PASS
cargo fmt/check/clippy/test NOT RUN — cargo indisponible dans le sandbox
```
## 17. Gate `0.2.12-pre.004`
La tranche introduit la première projection prix applicative sans effectuer de requête provider :
```text
MarketPriceRuntime backend implémenté
état présentation mémoire backend uniquement
registry -> lignes provider-neutral implémenté
MarketPriceProviderRowDto implémenté
MarketPriceRuntimeStatusDto implémenté
prix exact Option<String>, jamais f64
timestamps observation/retry Option<String> exactes en millisecondes Unix
list_market_prices implémenté, aucun dispatch réseau
états frontend loading / empty / table implémentés
refresh row/many/all absent par contrat pre.004
frontend fetch/localStorage/sessionStorage absent
logging chargement/rendu registry trace sans prix/payload
```
Le gate opérateur précédent est désormais fermé après `pre.003-fix.001` :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS
python3 scripts/audit_markdown_tables.py ... deltas/0.2.12 PASS
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-app-solprices-desk PASS
(cd crates/ksp-app-solprices-desk && cargo tauri dev) PASS
cargo test --workspace NOT RUN dans ce retour précis
```
Validations sandbox après matérialisation de `pre.004` :
```text
python3 scripts/audit_rust_workspace_rules.py PASS
python3 scripts/audit_markdown_tables.py README.md ... deltas PASS
cargo fmt/check/clippy/test NOT RUN — cargo indisponible dans le sandbox
```