v0.2.12-pre.004
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 l’UI.</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>
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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.
|
||||
|
||||
209
crates/ksp-app-solprices-desk/src/market_price_runtime.rs
Normal file
209
crates/ksp-app-solprices-desk/src/market_price_runtime.rs
Normal 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;
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
140
crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs
Normal file
140
crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs
Normal 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
285
deltas/0.2.12/pre.004.md
Normal 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.
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user