v0.3.15-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-app-raw-transaction-ingest-desk/Cargo.toml
|
||||
# version: 1
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "ksp-app-raw-transaction-ingest-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-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tauri.workspace = true
|
||||
tauri-plugin-tracing.workspace = true
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# `ksp-app-raw-transaction-ingest-desk`
|
||||
|
||||
Application desktop Tauri KSP destinée à composer et superviser les routes live alimentant le Store commun en `RawTransaction`.
|
||||
|
||||
Le composant conserve les frontières KSP : Config possède les profils/endpoints/secrets, Transport les clients et sessions, Store la persistance et le Worker la logique d'ingestion. Le shell desktop ne doit jamais déplacer ces responsabilités vers le frontend.
|
||||
Le composant conserve les frontières KSP : Config possède les profils/endpoints/secrets, Transport les clients et sessions, Store la persistance et le Worker la logique d'ingestion. Le shell desktop ne déplace jamais ces responsabilités vers le frontend.
|
||||
|
||||
## Package
|
||||
|
||||
@@ -18,14 +18,18 @@ bin : ksp-app-raw-transaction-ingest-desk
|
||||
## Frontières
|
||||
|
||||
```text
|
||||
Config -> composite + Logging + ressources packagées
|
||||
Desk -> route IDs/states statiques + Tauri + projections sûres
|
||||
Transport / Store / Worker -> non construits dans cette tranche
|
||||
Config -> composite, profils, secrets et résolution effective
|
||||
Transport -> capabilities HTTP/WS/gRPC et ressources physiques
|
||||
Desk -> catalogue logique, inventaire Config-only et projections sûres
|
||||
Store -> persistance via la façade KSP
|
||||
Worker -> acquisition continue et lifecycle d'exécution
|
||||
```
|
||||
|
||||
Le frontend ne reçoit aucune URL, credential, URI Store, source key ou payload `RawTransaction`.
|
||||
L'inventaire de routes est calculé côté Rust à partir du composite Config validé et des settings Transport/Store résolus. Il ne lance aucun probe réseau et n'ouvre ni Store ni Worker.
|
||||
|
||||
## Routes V1 réservées
|
||||
Le frontend ne reçoit aucune URL, credential, URI Store, metadata secrète, source key, handle runtime ou payload `RawTransaction`.
|
||||
|
||||
## Routes V1
|
||||
|
||||
```text
|
||||
yellowstone-hydrated
|
||||
@@ -35,7 +39,7 @@ helius-transaction-hydrated
|
||||
http-block-polling
|
||||
```
|
||||
|
||||
Leur présence dans l'interface n'est pas une preuve de composabilité : seule la projection backend issue de Config et des préconditions runtime peut déclarer une route disponible.
|
||||
Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle des connexions, du Store et du Worker est revalidée lors du Start avant qu'une route puisse devenir `Running`.
|
||||
|
||||
## Ports de développement
|
||||
|
||||
@@ -50,4 +54,6 @@ Lancement :
|
||||
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Le cycle frontend est déclenché par Tauri via ses hooks configurés ; les scripts npm applicatifs ne sont pas lancés directement par l'opérateur.
|
||||
|
||||
Voir [`USAGE.md`](USAGE.md) et [`../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md`](../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# Utilisation de `ksp-app-raw-transaction-ingest-desk`
|
||||
|
||||
@@ -11,22 +11,56 @@ Depuis la racine du workspace :
|
||||
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Tauri déclenche lui-même les hooks frontend configurés. Les scripts npm applicatifs de développement, contrôle ou build ne sont pas lancés directement par l'opérateur.
|
||||
|
||||
Le runtime prépare les ressources Config nécessaires, charge le composite dédié, initialise Logging puis ouvre le shell desktop.
|
||||
|
||||
## 2. Vue Routes
|
||||
|
||||
La vue Routes présente les routes logiques connues par l'application et leur état applicatif. La présence d'un identifiant de route dans l'interface ne constitue pas, à elle seule, une preuve de composabilité ou de disponibilité runtime ; l'état projeté par le backend reste l'autorité.
|
||||
La vue Routes charge un inventaire déterministe construit côté Rust depuis le composite Config validé. Le sélecteur présente les profils logiques du composite et la vue projette, pour chaque route V1 :
|
||||
|
||||
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker et payloads RAW restent backend-only.
|
||||
```text
|
||||
route_id
|
||||
famille et label sûrs
|
||||
réseau logique lorsque résolu
|
||||
état Config
|
||||
selectable oui/non
|
||||
raison sûre lorsque la route est indisponible
|
||||
génération d'inventaire
|
||||
```
|
||||
|
||||
## 3. Diagnostics
|
||||
`Configured` signifie uniquement que toutes les requirements Config nécessaires à la route sont cohérentes. Aucun probe réseau, aucune ouverture de Store et aucun Worker ne sont déclenchés par l'inventaire.
|
||||
|
||||
La vue Diagnostics expose uniquement des informations techniques sûres nécessaires à l'exploitation du Desk, notamment l'identité de version, l'état du shell, la composition Config active, l'état Logging et les diagnostics projetés par codes/messages sûrs.
|
||||
Un refresh reconstruit l'inventaire depuis Config et publie une nouvelle génération. La sélection visuelle d'un profil ne choisit aucune URL ou ressource physique et ne démarre aucune acquisition.
|
||||
|
||||
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker, URI Store, source keys et payloads RAW restent backend-only.
|
||||
|
||||
## 3. Raisons d'indisponibilité
|
||||
|
||||
Les reasons exposées sont des codes applicatifs bornés, par exemple :
|
||||
|
||||
```text
|
||||
profile_unresolved
|
||||
network_mismatch
|
||||
missing_http_get_transaction
|
||||
missing_http_block_scan
|
||||
missing_ws_logs_capability
|
||||
missing_ws_block_capability
|
||||
missing_helius_transaction_capability
|
||||
missing_yellowstone_grpc
|
||||
missing_required_secret
|
||||
```
|
||||
|
||||
Le texte arbitraire d'une erreur provider ou d'un secret Config n'est jamais projeté dans l'inventaire.
|
||||
|
||||
## 4. Diagnostics
|
||||
|
||||
La vue Diagnostics expose uniquement des informations techniques sûres nécessaires à l'exploitation du Desk, notamment l'identité de version, l'état du shell, la composition Config bootstrap, l'état Logging et la génération d'inventaire.
|
||||
|
||||
Les diagnostics ne doivent pas exposer de credentials, d'URI Store, d'URL d'endpoint, de source key, de handle runtime ou de payload `RawTransaction`.
|
||||
|
||||
## 4. Runtime packagé
|
||||
## 5. Runtime packagé
|
||||
|
||||
Le build Tauri embarque les documents Config et schemas enregistrés. `ksp-config-lib` prépare ensuite la racine KSP user-writable sans embarquer `.env`.
|
||||
|
||||
Les fichiers Config et les secrets restent administrés par les mécanismes Config KSP ; l'interface ne doit pas contourner cette ownership.
|
||||
Les fichiers Config et les secrets restent administrés par les mécanismes Config KSP ; l'interface ne contourne pas cette ownership.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
<span class="mx-2 fs-5 text-body-secondary" aria-hidden="true">—</span>
|
||||
<span id="headerViewTitle" class="fs-5 text-body text-nowrap">Routes</span>
|
||||
</div>
|
||||
<span id="headerPhase" class="badge text-bg-secondary ms-auto">scaffold</span>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<span id="headerProfile" class="badge text-bg-light border text-dark">—</span>
|
||||
<span id="headerPhase" class="badge text-bg-secondary">inventory</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -48,29 +51,38 @@
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Routes live</h1>
|
||||
<p class="text-body-secondary mb-0">Fondation statique V1 ; la composabilité Config sera matérialisée en pre.006.</p>
|
||||
<p class="text-body-secondary mb-0">Inventaire déterministe depuis Config, sans probe réseau ni démarrage de Worker.</p>
|
||||
</div>
|
||||
<button id="refreshRouteFoundation" class="btn btn-outline-primary btn-sm" type="button">
|
||||
<button id="refreshRouteInventory" class="btn btn-outline-primary btn-sm" type="button">
|
||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert alert-info" role="status">
|
||||
Aucun Worker n'est démarré par ce scaffold. Les cinq IDs ci-dessous ne constituent aucune preuve de disponibilité runtime.
|
||||
</div>
|
||||
<div id="routeFoundation" class="row g-3 app-route-grid" aria-live="polite"></div>
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-header fw-semibold">États réservés</div>
|
||||
<div class="card-body">
|
||||
<div id="routeStates" class="d-flex flex-wrap gap-2"></div>
|
||||
<div class="row g-3 align-items-end mb-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="routeProfile">Profil applicatif</label>
|
||||
<select id="routeProfile" class="form-select" aria-label="Profil Raw Transaction Ingest Desk"></select>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="small text-body-secondary">Réseau</div>
|
||||
<div id="routeNetwork" class="fw-semibold app-route-code">—</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="small text-body-secondary">Génération</div>
|
||||
<div id="routeInventoryGeneration" class="fw-semibold app-route-code">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info" role="status">
|
||||
<strong>Configured</strong> signifie uniquement que Config prouve la composabilité. Les connexions, le Store et le Worker seront revalidés au Start dans les tranches suivantes.
|
||||
</div>
|
||||
<div class="small text-body-secondary mb-3">Profil affiché : <span id="routeSelectedProfile" class="app-route-code">—</span></div>
|
||||
<div id="routeFoundation" class="row g-3 app-route-grid" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<section data-view-panel="diagnostics" hidden>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Diagnostics</h1>
|
||||
<p class="text-body-secondary mb-0">État sûr du scaffold Config, Logging et desktop.</p>
|
||||
<p class="text-body-secondary mb-0">État sûr du shell Config, Logging et de la génération d'inventaire.</p>
|
||||
</div>
|
||||
<button id="refreshRuntimeStatus" class="btn btn-outline-primary btn-sm" type="button">
|
||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
|
||||
@@ -83,9 +95,11 @@
|
||||
<dt class="col-sm-5">Version</dt><dd id="runtimeVersion" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Phase</dt><dd id="runtimeShellPhase" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Documents Config</dt><dd id="runtimeConfigDocuments" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Profil composite</dt><dd id="runtimeCompositeProfile" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Profil composite bootstrap</dt><dd id="runtimeCompositeProfile" 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><dd id="runtimeLoggingFallback" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Inventaire default</dt><dd id="runtimeInventoryDefaultProfile" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Génération inventaire</dt><dd id="runtimeInventoryGeneration" class="col-sm-7">—</dd>
|
||||
</dl>
|
||||
<div id="runtimeDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
import type { ShellStatusDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts";
|
||||
import type { RawIngestRouteFoundationDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFoundationDto.ts";
|
||||
import type { RawIngestRouteInventoryDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts";
|
||||
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
@@ -19,6 +19,9 @@ const viewTitles: Record<ViewId, string> = {
|
||||
diagnostics: "Diagnostics",
|
||||
};
|
||||
|
||||
let routeInventory: RawIngestRouteInventoryDto | null = null;
|
||||
let selectedProfileId: string | null = null;
|
||||
|
||||
function isViewId(value: string | undefined): value is ViewId {
|
||||
return value === "routes" || value === "diagnostics";
|
||||
}
|
||||
@@ -75,22 +78,40 @@ function bindNavigation(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function routeLabel(routeId: string): string {
|
||||
function reasonLabel(reason: string | null): string {
|
||||
const labels: Record<string, string> = {
|
||||
"yellowstone-hydrated": "Yellowstone + HTTP hydration",
|
||||
"standard-logs-hydrated": "Standard Logs + HTTP hydration",
|
||||
"standard-block-direct": "Standard Block direct",
|
||||
"helius-transaction-hydrated": "Helius Transaction + HTTP hydration",
|
||||
"http-block-polling": "HTTP Block Polling",
|
||||
profile_unresolved: "profil Config non résolu",
|
||||
network_mismatch: "réseau Transport/Store incohérent",
|
||||
missing_http_get_transaction: "HTTP getTransaction indisponible",
|
||||
missing_http_block_scan: "HTTP block scan incomplet",
|
||||
missing_ws_logs_capability: "capability WS Logs absente",
|
||||
missing_ws_block_capability: "capability WS Block absente",
|
||||
missing_helius_transaction_capability: "capability Helius Transaction absente",
|
||||
missing_yellowstone_grpc: "Yellowstone gRPC absent",
|
||||
missing_required_secret: "secret Config requis non résolu",
|
||||
};
|
||||
return labels[routeId] ?? routeId;
|
||||
if (reason === null) {
|
||||
return "composable depuis Config";
|
||||
}
|
||||
return labels[reason] ?? reason;
|
||||
}
|
||||
|
||||
function renderRouteFoundation(foundation: RawIngestRouteFoundationDto): void {
|
||||
function renderSelectedProfile(): void {
|
||||
if (routeInventory === null) {
|
||||
return;
|
||||
}
|
||||
const profile = routeInventory.profiles.find(candidate => candidate.profileId === selectedProfileId) ?? routeInventory.profiles[0];
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
selectedProfileId = profile.profileId;
|
||||
text("#routeNetwork", profile.network ?? "non résolu");
|
||||
text("#routeSelectedProfile", profile.profileId);
|
||||
text("#headerProfile", profile.profileId);
|
||||
const routeRoot = document.querySelector<HTMLElement>("#routeFoundation");
|
||||
if (routeRoot) {
|
||||
routeRoot.replaceChildren();
|
||||
for (const routeId of foundation.routeIds) {
|
||||
for (const route of profile.routes) {
|
||||
const column = document.createElement("div");
|
||||
column.className = "col-12 col-xl-6 app-route-column";
|
||||
const card = document.createElement("div");
|
||||
@@ -99,32 +120,58 @@ function renderRouteFoundation(foundation: RawIngestRouteFoundationDto): void {
|
||||
body.className = "card-body";
|
||||
const title = document.createElement("h2");
|
||||
title.className = "h6 mb-2";
|
||||
title.textContent = routeLabel(routeId);
|
||||
title.textContent = route.label;
|
||||
const code = document.createElement("div");
|
||||
code.className = "app-route-code text-body-secondary small";
|
||||
code.textContent = routeId;
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "badge text-bg-secondary mt-3";
|
||||
badge.textContent = "foundation only";
|
||||
body.append(title, code, badge);
|
||||
code.textContent = route.routeId;
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta";
|
||||
const state = document.createElement("span");
|
||||
state.className = route.selectable ? "badge text-bg-success" : "badge text-bg-secondary";
|
||||
state.textContent = route.state;
|
||||
const network = document.createElement("span");
|
||||
network.className = "badge text-bg-light border text-dark";
|
||||
network.textContent = route.network ?? "network unresolved";
|
||||
const reason = document.createElement("span");
|
||||
reason.className = route.selectable ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
|
||||
reason.textContent = reasonLabel(route.reason);
|
||||
meta.append(state, network, reason);
|
||||
body.append(title, code, meta);
|
||||
card.append(body);
|
||||
column.append(card);
|
||||
routeRoot.append(column);
|
||||
}
|
||||
}
|
||||
const statesRoot = document.querySelector<HTMLElement>("#routeStates");
|
||||
if (statesRoot) {
|
||||
statesRoot.replaceChildren();
|
||||
for (const state of foundation.states) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "badge text-bg-light border text-dark";
|
||||
badge.textContent = state;
|
||||
statesRoot.append(badge);
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory profile rendered", {
|
||||
profileId: profile.profileId,
|
||||
routeCount: profile.routes.length,
|
||||
selectableCount: profile.routes.filter(route => route.selectable).length,
|
||||
});
|
||||
}
|
||||
|
||||
function renderRouteInventory(inventory: RawIngestRouteInventoryDto): void {
|
||||
routeInventory = inventory;
|
||||
const requestedProfile = selectedProfileId;
|
||||
const selectedStillExists = requestedProfile !== null && inventory.profiles.some(profile => profile.profileId === requestedProfile);
|
||||
selectedProfileId = selectedStillExists ? requestedProfile : inventory.defaultProfile;
|
||||
const selector = document.querySelector<HTMLSelectElement>("#routeProfile");
|
||||
if (selector) {
|
||||
selector.replaceChildren();
|
||||
for (const profile of inventory.profiles) {
|
||||
const option = document.createElement("option");
|
||||
option.value = profile.profileId;
|
||||
option.textContent = profile.isDefault ? `${profile.profileId} (default)` : profile.profileId;
|
||||
option.selected = profile.profileId === selectedProfileId;
|
||||
selector.append(option);
|
||||
}
|
||||
}
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk route foundation rendered", {
|
||||
routeCount: foundation.routeIds.length,
|
||||
stateCount: foundation.states.length,
|
||||
text("#routeInventoryGeneration", inventory.generation.toString());
|
||||
text("#runtimeInventoryGeneration", inventory.generation.toString());
|
||||
text("#runtimeInventoryDefaultProfile", inventory.defaultProfile);
|
||||
renderSelectedProfile();
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory rendered", {
|
||||
generation: inventory.generation,
|
||||
profileCount: inventory.profiles.length,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -154,10 +201,10 @@ function renderRuntimeStatus(status: ShellStatusDto): void {
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshRouteFoundation(): Promise<void> {
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk route foundation refresh requested");
|
||||
const foundation = await invokeKsp<RawIngestRouteFoundationDto>("main", "get_route_foundation");
|
||||
renderRouteFoundation(foundation);
|
||||
async function refreshRouteInventory(): Promise<void> {
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk Config route inventory refresh requested");
|
||||
const inventory = await invokeKsp<RawIngestRouteInventoryDto>("main", "get_route_inventory");
|
||||
renderRouteInventory(inventory);
|
||||
}
|
||||
|
||||
async function refreshRuntimeStatus(): Promise<void> {
|
||||
@@ -167,12 +214,21 @@ async function refreshRuntimeStatus(): Promise<void> {
|
||||
}
|
||||
|
||||
function bindRefreshControls(): void {
|
||||
document.querySelector<HTMLButtonElement>("#refreshRouteFoundation")?.addEventListener("click", () => {
|
||||
void refreshRouteFoundation().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk route foundation refresh failed"));
|
||||
document.querySelector<HTMLButtonElement>("#refreshRouteInventory")?.addEventListener("click", () => {
|
||||
void refreshRouteInventory().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk Config route inventory refresh failed"));
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#refreshRuntimeStatus")?.addEventListener("click", () => {
|
||||
void refreshRuntimeStatus().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk runtime status refresh failed"));
|
||||
});
|
||||
document.querySelector<HTMLSelectElement>("#routeProfile")?.addEventListener("change", event => {
|
||||
const selector = event.currentTarget;
|
||||
if (!(selector instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
selectedProfileId = selector.value;
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk logical profile selected", { profileId: selectedProfileId });
|
||||
renderSelectedProfile();
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeMain(): Promise<void> {
|
||||
@@ -181,9 +237,9 @@ async function initializeMain(): Promise<void> {
|
||||
bindNavigation();
|
||||
bindRefreshControls();
|
||||
activateView("routes", "startup");
|
||||
await refreshRouteFoundation();
|
||||
await refreshRouteInventory();
|
||||
await refreshRuntimeStatus();
|
||||
frontendInfo("main", "Raw Transaction Ingest Desk scaffold frontend ready");
|
||||
frontendInfo("main", "Raw Transaction Ingest Desk Config inventory frontend ready");
|
||||
}
|
||||
|
||||
void initializeMain().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk frontend initialization failed"));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
pub(crate) struct AppState {
|
||||
active_composite_profile: std::option::Option<String>,
|
||||
config_management: ksp_config_lib::ConfigManagement,
|
||||
inventory_generation: std::sync::Mutex<u32>,
|
||||
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
|
||||
splash_settings: crate::SplashSettings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool,
|
||||
@@ -47,6 +48,7 @@ impl crate::AppState {
|
||||
return std::result::Result::Ok(Self {
|
||||
active_composite_profile,
|
||||
config_management,
|
||||
inventory_generation: std::sync::Mutex::new(0),
|
||||
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
|
||||
guard: logging_startup.guard,
|
||||
active_profile_id: logging_startup.active_profile_id,
|
||||
@@ -65,6 +67,37 @@ impl crate::AppState {
|
||||
return crate::RawIngestRouteFoundationDto::scaffold();
|
||||
}
|
||||
|
||||
/// Rebuilds the complete Config-only route inventory and publishes a fresh monotonic generation.
|
||||
pub(crate) fn route_inventory(&self) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
|
||||
let generation = self.inventory_generation.lock();
|
||||
let mut generation = match generation {
|
||||
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,
|
||||
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
let next_generation = generation.checked_add(1);
|
||||
let next_generation = match next_generation {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_INVENTORY_INVALID,
|
||||
"Raw Transaction Ingest Desk route inventory generation is exhausted",
|
||||
));
|
||||
},
|
||||
};
|
||||
let inventory = crate::build_route_inventory(&self.config_management, next_generation);
|
||||
let inventory = match inventory {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
*generation = next_generation;
|
||||
return std::result::Result::Ok(inventory);
|
||||
}
|
||||
|
||||
/// Builds the safe scaffold status exposed by the shell.
|
||||
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
|
||||
let document_count = self.config_management.engine().registry().descriptors().count();
|
||||
@@ -98,7 +131,7 @@ impl crate::AppState {
|
||||
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
config_document_count: document_count,
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
shell_phase: "pre.005-scaffold".to_owned(),
|
||||
shell_phase: "pre.006-route-inventory".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/bootstrap.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Config composite and Logging bootstrap for Raw Transaction Ingest Desk.
|
||||
|
||||
@@ -46,13 +46,21 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li
|
||||
/// Loads the concrete Raw Transaction Ingest Desk Config composite using its registered logical `file_id` and autonomous default profile.
|
||||
pub(crate) fn load_raw_transaction_ingest_desk_composite(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
|
||||
return crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::None);
|
||||
}
|
||||
|
||||
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile by registered logical file identifier.
|
||||
pub(crate) fn load_raw_transaction_ingest_desk_composite_profile(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
|
||||
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return management.engine().load_resolved_composite(&file_id, std::option::Option::None);
|
||||
return management.engine().load_resolved_composite(&file_id, requested_profile);
|
||||
}
|
||||
|
||||
/// Returns one required standard profile from the Raw Transaction Ingest Desk composite after validating the referenced logical `file_id`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Application-owned tracing targets and Config component identifiers.
|
||||
|
||||
@@ -13,6 +13,8 @@ pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
|
||||
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "raw_transaction_ingest.bootstrap";
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain used while rebuilding Config-only route inventory.
|
||||
pub(crate) const TRACING_DOMAIN_INVENTORY: &str = "raw_transaction_ingest.inventory";
|
||||
/// Structured domain used by the Raw Transaction Ingest Desk shell.
|
||||
pub(crate) const TRACING_DOMAIN_SHELL: &str = "raw_transaction_ingest.shell";
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Safe route-identity and lifecycle-state foundation for Raw Transaction Ingest Desk.
|
||||
//! Safe route-identity, Config-composability and lifecycle DTOs for Raw Transaction Ingest Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
@@ -28,6 +28,72 @@ impl crate::RawIngestRouteId {
|
||||
pub(crate) fn all() -> std::vec::Vec<Self> {
|
||||
return vec![Self::YellowstoneHydrated, Self::StandardLogsHydrated, Self::StandardBlockDirect, Self::HeliusTransactionHydrated, Self::HttpBlockPolling];
|
||||
}
|
||||
|
||||
/// Returns the application-owned route family used by safe UI projections.
|
||||
#[must_use]
|
||||
pub(crate) const fn family(self) -> crate::RawIngestRouteFamily {
|
||||
return match self {
|
||||
Self::YellowstoneHydrated => crate::RawIngestRouteFamily::Yellowstone,
|
||||
Self::StandardLogsHydrated => crate::RawIngestRouteFamily::StandardLogs,
|
||||
Self::StandardBlockDirect => crate::RawIngestRouteFamily::StandardBlock,
|
||||
Self::HeliusTransactionHydrated => crate::RawIngestRouteFamily::HeliusTransaction,
|
||||
Self::HttpBlockPolling => crate::RawIngestRouteFamily::HttpBlockPolling,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the stable operator-facing route label without exposing physical endpoint metadata.
|
||||
#[must_use]
|
||||
pub(crate) const fn label(self) -> &'static str {
|
||||
return match self {
|
||||
Self::YellowstoneHydrated => "Yellowstone + HTTP hydration",
|
||||
Self::StandardLogsHydrated => "Standard Logs + HTTP hydration",
|
||||
Self::StandardBlockDirect => "Standard Block direct",
|
||||
Self::HeliusTransactionHydrated => "Helius Transaction + HTTP hydration",
|
||||
Self::HttpBlockPolling => "HTTP Block Polling",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable application-owned route families used only for safe presentation and orchestration.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFamily.ts")]
|
||||
pub(crate) enum RawIngestRouteFamily {
|
||||
/// Yellowstone gRPC subscription route.
|
||||
Yellowstone,
|
||||
/// Standard Solana logs WebSocket route.
|
||||
StandardLogs,
|
||||
/// Standard Solana block WebSocket route.
|
||||
StandardBlock,
|
||||
/// Helius transaction WebSocket route.
|
||||
HeliusTransaction,
|
||||
/// HTTP block polling route.
|
||||
HttpBlockPolling,
|
||||
}
|
||||
|
||||
/// Safe reasons explaining why one logical route cannot currently be composed from Config.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteUnavailableReason.ts")]
|
||||
pub(crate) enum RawIngestRouteUnavailableReason {
|
||||
/// The selected composite or one of its typed Config components could not be resolved safely.
|
||||
ProfileUnresolved,
|
||||
/// Transport and Store do not prove one identical logical network.
|
||||
NetworkMismatch,
|
||||
/// No HTTP role can satisfy `getTransaction` for same-network hydration.
|
||||
MissingHttpGetTransaction,
|
||||
/// No HTTP role can satisfy the complete block-polling method set.
|
||||
MissingHttpBlockScan,
|
||||
/// No enabled standard WebSocket endpoint explicitly declares `Logs`.
|
||||
MissingWsLogsCapability,
|
||||
/// No enabled standard WebSocket endpoint explicitly declares `Block`.
|
||||
MissingWsBlockCapability,
|
||||
/// No enabled Helius WebSocket endpoint explicitly declares `HeliusTransaction`.
|
||||
MissingHeliusTransactionCapability,
|
||||
/// No enabled same-network Yellowstone gRPC endpoint is configured.
|
||||
MissingYellowstoneGrpc,
|
||||
/// A required Config secret is not resolved for the selected composite profile.
|
||||
MissingRequiredSecret,
|
||||
}
|
||||
|
||||
/// Stable Raw Transaction Ingest Desk route lifecycle states.
|
||||
@@ -59,25 +125,104 @@ impl crate::RawIngestRouteState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Static route/state foundation exposed by the scaffold before Config route inventory exists.
|
||||
/// Static route/state foundation retained as the bounded vocabulary behind the live Config inventory.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFoundationDto.ts")]
|
||||
pub(crate) struct RawIngestRouteFoundationDto {
|
||||
/// Five stable V1 logical route identifiers; this is not a composability claim.
|
||||
pub(crate) route_ids: std::vec::Vec<crate::RawIngestRouteId>,
|
||||
/// Stable lifecycle state vocabulary reserved for later runtime projections.
|
||||
/// Stable lifecycle state vocabulary reserved for runtime projections.
|
||||
pub(crate) states: std::vec::Vec<crate::RawIngestRouteState>,
|
||||
}
|
||||
|
||||
impl crate::RawIngestRouteFoundationDto {
|
||||
/// Builds the static scaffold contract without resolving endpoints, secrets or Worker resources.
|
||||
/// Builds the bounded route/state vocabulary without resolving endpoints, secrets or Worker resources.
|
||||
#[must_use]
|
||||
pub(crate) fn scaffold() -> Self {
|
||||
return Self { route_ids: crate::RawIngestRouteId::all(), states: crate::RawIngestRouteState::all() };
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe Config-only composability projection for one logical route.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteDto.ts")]
|
||||
pub(crate) struct RawIngestRouteDto {
|
||||
/// Stable route family used for presentation and bounded orchestration.
|
||||
pub(crate) family: crate::RawIngestRouteFamily,
|
||||
/// Stable operator-facing label with no physical resource identity.
|
||||
pub(crate) label: String,
|
||||
/// Logical network proven by Config when available.
|
||||
pub(crate) network: std::option::Option<String>,
|
||||
/// Safe reason explaining an unavailable route.
|
||||
pub(crate) reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
|
||||
/// Stable logical V1 route identifier.
|
||||
pub(crate) route_id: crate::RawIngestRouteId,
|
||||
/// Whether all Config requirements for the route are currently composable.
|
||||
pub(crate) selectable: bool,
|
||||
/// Config-level lifecycle projection; `Configured` is not a runtime health claim.
|
||||
pub(crate) state: crate::RawIngestRouteState,
|
||||
}
|
||||
|
||||
impl crate::RawIngestRouteDto {
|
||||
/// Builds one route that is composable from Config without claiming runtime operability.
|
||||
#[must_use]
|
||||
pub(crate) fn configured(route_id: crate::RawIngestRouteId, network: &str) -> Self {
|
||||
return Self {
|
||||
family: route_id.family(),
|
||||
label: route_id.label().to_owned(),
|
||||
network: std::option::Option::Some(network.to_owned()),
|
||||
reason: std::option::Option::None,
|
||||
route_id,
|
||||
selectable: true,
|
||||
state: crate::RawIngestRouteState::Configured,
|
||||
};
|
||||
}
|
||||
|
||||
/// Builds one route that is not composable from the current Config profile.
|
||||
#[must_use]
|
||||
pub(crate) fn unavailable(route_id: crate::RawIngestRouteId, network: std::option::Option<&str>, reason: crate::RawIngestRouteUnavailableReason) -> Self {
|
||||
return Self {
|
||||
family: route_id.family(),
|
||||
label: route_id.label().to_owned(),
|
||||
network: network.map(str::to_owned),
|
||||
reason: std::option::Option::Some(reason),
|
||||
route_id,
|
||||
selectable: false,
|
||||
state: crate::RawIngestRouteState::Unavailable,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe route inventory for one logical Raw Transaction Ingest Desk composite profile.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestProfileInventoryDto.ts")]
|
||||
pub(crate) struct RawIngestProfileInventoryDto {
|
||||
/// Whether this profile is the composite's autonomous default.
|
||||
pub(crate) is_default: bool,
|
||||
/// Logical network proven from the resolved Config profile when available.
|
||||
pub(crate) network: std::option::Option<String>,
|
||||
/// Stable composite profile identifier.
|
||||
pub(crate) profile_id: String,
|
||||
/// Five logical routes in stable V1 presentation order.
|
||||
pub(crate) routes: std::vec::Vec<crate::RawIngestRouteDto>,
|
||||
}
|
||||
|
||||
/// Deterministic Config-only route inventory returned to the frontend.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts")]
|
||||
pub(crate) struct RawIngestRouteInventoryDto {
|
||||
/// Composite default profile identifier.
|
||||
pub(crate) default_profile: String,
|
||||
/// Monotonic application-owned generation incremented after each successful inventory rebuild.
|
||||
pub(crate) generation: u32,
|
||||
/// Safe inventories for every validated composite profile in source order.
|
||||
pub(crate) profiles: std::vec::Vec<crate::RawIngestProfileInventoryDto>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_route.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Stable Raw Transaction Ingest Desk application error codes.
|
||||
|
||||
@@ -20,6 +20,9 @@ pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode
|
||||
/// Raw Transaction Ingest Desk could not install managed Logging or its safe fallback.
|
||||
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "logging_bootstrap_failed");
|
||||
/// Config-only route inventory cannot be projected safely from the validated composite.
|
||||
pub(crate) const ERROR_CODE_ROUTE_INVENTORY_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_inventory_invalid");
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "splash_origin_invalid");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes.
|
||||
|
||||
@@ -15,6 +15,7 @@ mod dto_route;
|
||||
mod errors;
|
||||
mod frontend_logging;
|
||||
mod logging_runtime;
|
||||
mod route_inventory;
|
||||
mod splash;
|
||||
mod tauri;
|
||||
mod tw_main;
|
||||
@@ -33,6 +34,8 @@ pub(crate) use self::bootstrap::config_management;
|
||||
pub(crate) use self::bootstrap::initialize_logging;
|
||||
/// Loads the Raw Transaction Ingest Desk composite selected by its registered logical file identifier.
|
||||
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite;
|
||||
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile.
|
||||
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite_profile;
|
||||
/// Resolves one required standard Config profile from the Raw Transaction Ingest Desk composite.
|
||||
pub(crate) use self::bootstrap::required_composite_component_profile;
|
||||
/// Composite-local identifier for the standard Logging component.
|
||||
@@ -45,6 +48,8 @@ pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
/// Structured domain used while rebuilding Config-only route inventory.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_INVENTORY;
|
||||
/// Structured domain used by the Raw Transaction Ingest Desk shell.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
@@ -61,12 +66,22 @@ pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
|
||||
pub(crate) use self::dto_common::CommandErrorDto;
|
||||
/// Safe scaffold/runtime snapshot exposed to the shell.
|
||||
pub(crate) use self::dto_common::ShellStatusDto;
|
||||
/// Static route/state foundation exposed before route inventory is implemented.
|
||||
/// Safe route inventory for one logical composite profile.
|
||||
pub(crate) use self::dto_route::RawIngestProfileInventoryDto;
|
||||
/// Safe Config-only composability projection for one logical route.
|
||||
pub(crate) use self::dto_route::RawIngestRouteDto;
|
||||
/// Stable application-owned route family.
|
||||
pub(crate) use self::dto_route::RawIngestRouteFamily;
|
||||
/// Static route/state foundation retained as the bounded vocabulary behind the Config inventory.
|
||||
pub(crate) use self::dto_route::RawIngestRouteFoundationDto;
|
||||
/// Stable logical V1 route identifiers owned by the Desk.
|
||||
pub(crate) use self::dto_route::RawIngestRouteId;
|
||||
/// Stable route lifecycle states reserved for later runtime projections.
|
||||
/// Complete deterministic Config-only route inventory.
|
||||
pub(crate) use self::dto_route::RawIngestRouteInventoryDto;
|
||||
/// Stable route lifecycle states reserved for runtime projections.
|
||||
pub(crate) use self::dto_route::RawIngestRouteState;
|
||||
/// Safe reason explaining why one logical route is not composable from Config.
|
||||
pub(crate) use self::dto_route::RawIngestRouteUnavailableReason;
|
||||
/// Shared Raw Transaction Ingest Desk application state is internally inconsistent.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||
/// Shared Raw Transaction Ingest Desk runtime state cannot be locked safely.
|
||||
@@ -79,6 +94,8 @@ pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||
/// Managed and fallback Logging initialization failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
/// Config-only route inventory cannot be projected safely.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_INVENTORY_INVALID;
|
||||
/// Splash readiness originated from an invalid window.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
/// Splash timing configuration is invalid.
|
||||
@@ -95,6 +112,8 @@ pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
|
||||
pub(crate) use self::frontend_logging::emit_frontend_log_event;
|
||||
/// Creates the stable runtime identity for this Raw Transaction Ingest Desk process launch.
|
||||
pub(crate) use self::logging_runtime::launch_identity;
|
||||
/// Rebuilds the complete safe route inventory from validated Config without runtime I/O.
|
||||
pub(crate) use self::route_inventory::build_route_inventory;
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
pub(crate) use self::splash::SplashOrderDto;
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs
|
||||
// version: 1
|
||||
|
||||
//! Config-only logical route inventory for Raw Transaction Ingest Desk.
|
||||
|
||||
struct CompositeProfileCatalog {
|
||||
default_profile: String,
|
||||
profile_ids: std::vec::Vec<String>,
|
||||
}
|
||||
|
||||
/// Rebuilds the complete safe route inventory from validated Config without opening any network, Store or Worker resource.
|
||||
pub(crate) fn build_route_inventory(management: &ksp_config_lib::ConfigManagement, generation: u32) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
|
||||
let catalog = composite_profile_catalog(management);
|
||||
let catalog = match catalog {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut profiles = std::vec::Vec::with_capacity(catalog.profile_ids.len());
|
||||
for profile_id in &catalog.profile_ids {
|
||||
profiles.push(build_profile_inventory(management, &environment, profile_id.as_str(), profile_id == &catalog.default_profile));
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_INVENTORY,
|
||||
generation,
|
||||
profile_count = profiles.len(),
|
||||
"rebuilt Raw Transaction Ingest Desk Config-only route inventory"
|
||||
);
|
||||
return std::result::Result::Ok(crate::RawIngestRouteInventoryDto { default_profile: catalog.default_profile, generation, profiles });
|
||||
}
|
||||
|
||||
fn composite_profile_catalog(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<CompositeProfileCatalog> {
|
||||
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let document = management.engine().load_validated_document(&file_id);
|
||||
let document = match document {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let root = document.value().as_object();
|
||||
let root = match root {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return inventory_invalid("validated Raw Transaction Ingest Desk composite root is not an object"),
|
||||
};
|
||||
let default_profile = root.get("default_profile").and_then(|value| return value.as_str());
|
||||
let default_profile = match default_profile {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value.to_owned(),
|
||||
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no readable default profile"),
|
||||
};
|
||||
let profile_values = root.get("profiles").and_then(|value| return value.as_array());
|
||||
let profile_values = match profile_values {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no profile inventory"),
|
||||
};
|
||||
let mut profile_ids = std::vec::Vec::with_capacity(profile_values.len());
|
||||
let mut seen = std::collections::BTreeSet::<String>::new();
|
||||
for profile in profile_values {
|
||||
let profile_id = profile.get("profile_id").and_then(|value| return value.as_str());
|
||||
let profile_id = match profile_id {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite contains an unreadable profile identifier"),
|
||||
};
|
||||
if !seen.insert(profile_id.to_owned()) {
|
||||
return inventory_invalid("validated Raw Transaction Ingest Desk composite contains duplicate profile identifiers");
|
||||
}
|
||||
profile_ids.push(profile_id.to_owned());
|
||||
}
|
||||
if !seen.contains(default_profile.as_str()) {
|
||||
return inventory_invalid("validated Raw Transaction Ingest Desk composite default profile is absent from the profile inventory");
|
||||
}
|
||||
return std::result::Result::Ok(CompositeProfileCatalog { default_profile, profile_ids });
|
||||
}
|
||||
|
||||
fn build_profile_inventory(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
environment: &ksp_config_lib::ConfigEnvironment,
|
||||
profile_id: &str,
|
||||
is_default: bool,
|
||||
) -> crate::RawIngestProfileInventoryDto {
|
||||
let composite = crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::Some(profile_id));
|
||||
let composite = match composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let transport_profile =
|
||||
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
let transport_profile = match transport_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
||||
let store_profile = match store_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let transport = management.engine().resolve_transport_config_profile(&transport_profile, environment);
|
||||
let store = management.engine().resolve_store_config_profile(&store_profile, environment);
|
||||
let network_hint = resolved_network_hint(&transport, &store);
|
||||
if transport.is_err() || store.is_err() {
|
||||
let reason = resolution_failure_reason(&transport, &store);
|
||||
return unavailable_profile(profile_id, is_default, network_hint.as_deref(), reason);
|
||||
}
|
||||
let transport = match transport {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, network_hint.as_deref(), crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, network_hint.as_deref(), crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let store_network = store.settings().network().as_str().to_owned();
|
||||
if !transport_matches_network(&transport, store_network.as_str()) {
|
||||
return unavailable_profile(
|
||||
profile_id,
|
||||
is_default,
|
||||
std::option::Option::Some(store_network.as_str()),
|
||||
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
|
||||
);
|
||||
}
|
||||
let routes = build_configured_profile_routes(&transport, store_network.as_str());
|
||||
return crate::RawIngestProfileInventoryDto {
|
||||
is_default,
|
||||
network: std::option::Option::Some(store_network),
|
||||
profile_id: profile_id.to_owned(),
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
fn build_configured_profile_routes(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> std::vec::Vec<crate::RawIngestRouteDto> {
|
||||
let has_get_transaction = http_role_supports_methods(transport.http_settings(), &["getTransaction"]);
|
||||
let has_http_block_scan = http_role_supports_methods(transport.http_settings(), &["getSlot", "getBlocksWithLimit", "getBlock"]);
|
||||
let has_yellowstone = has_yellowstone_grpc(transport, network);
|
||||
let has_standard_logs =
|
||||
has_ws_capability(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Logs);
|
||||
let has_standard_block =
|
||||
has_ws_capability(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Block);
|
||||
let has_helius_transaction = has_ws_capability(
|
||||
transport,
|
||||
network,
|
||||
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
|
||||
);
|
||||
return vec![
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::YellowstoneHydrated,
|
||||
network,
|
||||
&[
|
||||
(has_yellowstone, crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc),
|
||||
(has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::StandardLogsHydrated,
|
||||
network,
|
||||
&[
|
||||
(has_standard_logs, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
|
||||
(has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::StandardBlockDirect,
|
||||
network,
|
||||
&[(has_standard_block, crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability)],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::HeliusTransactionHydrated,
|
||||
network,
|
||||
&[
|
||||
(has_helius_transaction, crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability),
|
||||
(has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::HttpBlockPolling,
|
||||
network,
|
||||
&[(has_http_block_scan, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
fn route_with_requirements(
|
||||
route_id: crate::RawIngestRouteId,
|
||||
network: &str,
|
||||
requirements: &[(bool, crate::RawIngestRouteUnavailableReason)],
|
||||
) -> crate::RawIngestRouteDto {
|
||||
for (available, reason) in requirements.iter().copied() {
|
||||
if !available {
|
||||
return crate::RawIngestRouteDto::unavailable(route_id, std::option::Option::Some(network), reason);
|
||||
}
|
||||
}
|
||||
return crate::RawIngestRouteDto::configured(route_id, network);
|
||||
}
|
||||
|
||||
fn unavailable_profile(
|
||||
profile_id: &str,
|
||||
is_default: bool,
|
||||
network: std::option::Option<&str>,
|
||||
reason: crate::RawIngestRouteUnavailableReason,
|
||||
) -> crate::RawIngestProfileInventoryDto {
|
||||
let routes = crate::RawIngestRouteId::all()
|
||||
.into_iter()
|
||||
.map(|route_id| return crate::RawIngestRouteDto::unavailable(route_id, network, reason))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
return crate::RawIngestProfileInventoryDto { is_default, network: network.map(str::to_owned), profile_id: profile_id.to_owned(), routes };
|
||||
}
|
||||
|
||||
fn resolution_failure_reason(
|
||||
transport: &ksp_core_lib::Result<ksp_config_lib::ResolvedTransportConfig>,
|
||||
store: &ksp_core_lib::Result<ksp_config_lib::ResolvedStoreConfig>,
|
||||
) -> crate::RawIngestRouteUnavailableReason {
|
||||
let transport_missing_secret = match transport {
|
||||
std::result::Result::Err(error) => error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING,
|
||||
std::result::Result::Ok(_) => false,
|
||||
};
|
||||
let store_missing_secret = match store {
|
||||
std::result::Result::Err(error) => error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING,
|
||||
std::result::Result::Ok(_) => false,
|
||||
};
|
||||
if transport_missing_secret || store_missing_secret {
|
||||
return crate::RawIngestRouteUnavailableReason::MissingRequiredSecret;
|
||||
}
|
||||
return crate::RawIngestRouteUnavailableReason::ProfileUnresolved;
|
||||
}
|
||||
|
||||
fn resolved_network_hint(
|
||||
transport: &ksp_core_lib::Result<ksp_config_lib::ResolvedTransportConfig>,
|
||||
store: &ksp_core_lib::Result<ksp_config_lib::ResolvedStoreConfig>,
|
||||
) -> std::option::Option<String> {
|
||||
if let std::result::Result::Ok(store) = store {
|
||||
return std::option::Option::Some(store.settings().network().as_str().to_owned());
|
||||
}
|
||||
if let std::result::Result::Ok(transport) = transport {
|
||||
let networks = enabled_transport_networks(transport);
|
||||
if networks.len() == 1 {
|
||||
return networks.into_iter().next();
|
||||
}
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
|
||||
let networks = enabled_transport_networks(transport);
|
||||
return networks.len() == 1 && networks.contains(network);
|
||||
}
|
||||
|
||||
fn enabled_transport_networks(transport: &ksp_config_lib::ResolvedTransportConfig) -> std::collections::BTreeSet<String> {
|
||||
let mut networks = std::collections::BTreeSet::<String>::new();
|
||||
for endpoint in transport.http_settings().endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(settings) = transport.ws_settings() {
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(settings) = transport.grpc_settings() {
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
return networks;
|
||||
}
|
||||
|
||||
fn has_ws_capability(
|
||||
transport: &ksp_config_lib::ResolvedTransportConfig,
|
||||
network: &str,
|
||||
protocol: ksp_onchain_transport_lib::WsProtocolKind,
|
||||
capability: ksp_onchain_transport_lib::WsSubscriptionKind,
|
||||
) -> bool {
|
||||
let settings = match transport.ws_settings() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() && endpoint.cluster().as_str() == network && endpoint.protocol() == protocol && endpoint.supports_subscription(capability) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn has_yellowstone_grpc(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
|
||||
let settings = match transport.grpc_settings() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
return settings.endpoints().iter().any(|endpoint| return endpoint.enabled() && endpoint.cluster().as_str() == network);
|
||||
}
|
||||
|
||||
fn http_role_supports_methods(settings: &ksp_onchain_transport_lib::HttpTransportSettings, methods: &[&'static str]) -> bool {
|
||||
let mut request_kinds = std::vec::Vec::<&'static str>::with_capacity(methods.len());
|
||||
for method_name in methods {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name);
|
||||
let descriptor = match descriptor {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
request_kinds.push(descriptor.request_kind());
|
||||
}
|
||||
let mut role_names = std::collections::BTreeSet::<String>::new();
|
||||
for endpoint in settings.endpoints() {
|
||||
if !endpoint.enabled() {
|
||||
continue;
|
||||
}
|
||||
for role in endpoint.roles() {
|
||||
if role.enabled() {
|
||||
role_names.insert(role.role().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
for role_name in role_names {
|
||||
let mut complete = true;
|
||||
for request_kind in &request_kinds {
|
||||
if !http_role_supports_request_kind(settings, role_name.as_str(), request_kind) {
|
||||
complete = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if complete {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn http_role_supports_request_kind(settings: &ksp_onchain_transport_lib::HttpTransportSettings, role_name: &str, request_kind: &str) -> bool {
|
||||
for endpoint in settings.endpoints() {
|
||||
if !endpoint.enabled() {
|
||||
continue;
|
||||
}
|
||||
for role in endpoint.roles() {
|
||||
if !role.enabled() || role.role().as_str() != role_name {
|
||||
continue;
|
||||
}
|
||||
for capability in role.request_kinds() {
|
||||
if capability.is_wildcard() || capability.as_str() == request_kind {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn inventory_invalid(message: &'static str) -> ksp_core_lib::Result<CompositeProfileCatalog> {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, message));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/route_inventory.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
|
||||
|
||||
@@ -74,7 +74,13 @@ fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_
|
||||
|
||||
#[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_route_foundation, get_runtime_status, splash_frontend_ready]);
|
||||
return builder.invoke_handler(tauri::generate_handler![
|
||||
emit_frontend_log,
|
||||
get_route_foundation,
|
||||
get_route_inventory,
|
||||
get_runtime_status,
|
||||
splash_frontend_ready
|
||||
]);
|
||||
}
|
||||
|
||||
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
|
||||
@@ -103,6 +109,15 @@ fn get_route_foundation(state: tauri::State<'_, crate::AppState>) -> crate::RawI
|
||||
return state.route_foundation();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_route_inventory(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RawIngestRouteInventoryDto, crate::CommandErrorDto> {
|
||||
let result = state.route_inventory();
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_route_inventory", crate::TRACING_DOMAIN_INVENTORY, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> {
|
||||
let result = state.shell_status();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/dependency_boundary.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Dependency-boundary canaries for the Raw Transaction Ingest Desk scaffold.
|
||||
//! Dependency-boundary canaries for Raw Transaction Ingest Desk Config-only route inventory.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -21,13 +21,12 @@ fn read_text(path: &std::path::Path) -> String {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_manifest_is_scaffold_only_and_backend_neutral() {
|
||||
fn pre_006_manifest_opens_only_transport_for_typed_config_composability() {
|
||||
let manifest = read_text(app_root().join("Cargo.toml").as_path());
|
||||
for required in ["ksp-config-lib", "ksp-core-lib", "ksp-logging-lib", "tauri-plugin-tracing"] {
|
||||
assert!(manifest.contains(required), "missing scaffold dependency {required}");
|
||||
for required in ["ksp-config-lib", "ksp-core-lib", "ksp-logging-lib", "ksp-onchain-transport-lib", "tauri-plugin-tracing"] {
|
||||
assert!(manifest.contains(required), "missing pre.006 dependency {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
"ksp-onchain-transport-lib",
|
||||
"ksp-store-lib",
|
||||
"ksp-store-api",
|
||||
"ksp-store-postgres-lib",
|
||||
@@ -39,12 +38,12 @@ fn pre_005_manifest_is_scaffold_only_and_backend_neutral() {
|
||||
"tonic",
|
||||
"yellowstone-grpc",
|
||||
] {
|
||||
assert!(!manifest.contains(forbidden), "scaffold opens forbidden dependency {forbidden}");
|
||||
assert!(!manifest.contains(forbidden), "pre.006 opens forbidden dependency {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_production_sources_do_not_construct_transport_store_or_worker_runtime() {
|
||||
fn pre_006_production_sources_use_transport_settings_only_without_runtime_io_or_store_open() {
|
||||
let root = app_root().join("src");
|
||||
let entries = std::fs::read_dir(root.as_path());
|
||||
assert!(entries.is_ok());
|
||||
@@ -57,11 +56,13 @@ fn pre_005_production_sources_do_not_construct_transport_store_or_worker_runtime
|
||||
let source = read_text(path.as_path());
|
||||
for forbidden in [
|
||||
"HttpTransportPool",
|
||||
"WsTransportSettings",
|
||||
"WsProtocolSession",
|
||||
"YellowstoneGrpcChannel",
|
||||
"ksp_store_lib::Store",
|
||||
"Store::open",
|
||||
"RawTransactionIngestWorker",
|
||||
"RawTransactionIngestRuntimeResources",
|
||||
"start_with_runtime_resources",
|
||||
"request_stop",
|
||||
] {
|
||||
assert!(!source.contains(forbidden), "{} contains premature runtime marker {forbidden}", path.display());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
|
||||
|
||||
@@ -108,7 +108,7 @@ fn pre_005_splash_assets_match_existing_ksp_desk_template_byte_exact() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_route_foundation_is_backend_owned_and_has_exact_five_ids_and_seven_states() {
|
||||
fn pre_006_route_inventory_is_backend_owned_and_keeps_exact_five_ids_and_seven_states() {
|
||||
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||
for route in ["YellowstoneHydrated", "StandardLogsHydrated", "StandardBlockDirect", "HeliusTransactionHydrated", "HttpBlockPolling"] {
|
||||
assert!(dto.contains(route));
|
||||
@@ -118,7 +118,8 @@ fn pre_005_route_foundation_is_backend_owned_and_has_exact_five_ids_and_seven_st
|
||||
}
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
assert!(tauri.contains("get_route_foundation"));
|
||||
for forbidden in ["start_worker", "stop_worker", "RawTransactionIngestWorker", "Store::open", "HttpTransportPool"] {
|
||||
assert!(tauri.contains("get_route_inventory"));
|
||||
for forbidden in ["start_worker", "stop_worker", "RawTransactionIngestWorker", "Store::open"] {
|
||||
assert!(!tauri.contains(forbidden), "scaffold Tauri surface contains premature runtime marker {forbidden}");
|
||||
}
|
||||
}
|
||||
@@ -153,3 +154,18 @@ fn pre_005_fix_002_shell_reuses_common_light_template_and_bounds_route_foundatio
|
||||
assert!(styles.contains(required), "missing bounded layout marker: {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_frontend_projects_profile_network_generation_and_safe_route_reasons() {
|
||||
let html = read_text(app_root().join("frontend/main.html").as_path());
|
||||
for required in ["id=\"routeProfile\"", "id=\"routeNetwork\"", "id=\"routeInventoryGeneration\"", "id=\"routeFoundation\""] {
|
||||
assert!(html.contains(required), "missing route inventory UI marker: {required}");
|
||||
}
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
for required in ["get_route_inventory", "missing_ws_block_capability", "missing_required_secret", "Config route inventory"] {
|
||||
assert!(main.contains(required), "missing route inventory frontend marker: {required}");
|
||||
}
|
||||
for forbidden in ["endpointUrl", "connectionUri", "apiKey", "sourceKey", "x-token"] {
|
||||
assert!(!main.contains(forbidden), "frontend exposes forbidden physical marker: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Desktop security canaries for the Raw Transaction Ingest Desk scaffold.
|
||||
|
||||
@@ -58,15 +58,27 @@ fn pre_005_frontend_has_no_network_persistence_or_native_dialog_surface() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_frontend_tracing_is_control_and_command_oriented_only() {
|
||||
fn pre_006_frontend_tracing_is_control_inventory_and_command_oriented_only() {
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
assert!(main.contains("frontend control clicked"));
|
||||
assert!(main.contains("navigation tab clicked"));
|
||||
assert!(main.contains("runtime status refresh requested"));
|
||||
assert!(main.contains("route foundation refresh requested"));
|
||||
assert!(main.contains("Config route inventory refresh requested"));
|
||||
assert!(main.contains("logical profile selected"));
|
||||
let invoke = read_text(app_root().join("frontend/ts/invoke.ts").as_path());
|
||||
assert!(invoke.contains("Frontend IPC command requested"));
|
||||
for forbidden in ["apiKey", "authorization", "connectionUri", "endpointUrl", "sourceKey", "rawTransaction", "transactionPayload"] {
|
||||
for forbidden in ["apiKey", "authorization", "connectionUri", "endpointUrl", "sourceKey", "rawTransaction", "transactionPayload", "x-token"] {
|
||||
assert!(!main.contains(forbidden), "frontend contains forbidden sensitive marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_route_inventory_dto_surface_excludes_endpoint_secret_and_physical_store_fields() {
|
||||
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||
for required in ["RawIngestRouteInventoryDto", "RawIngestProfileInventoryDto", "RawIngestRouteUnavailableReason"] {
|
||||
assert!(dto.contains(required));
|
||||
}
|
||||
for forbidden in ["endpoint_name", "endpoint_url", "api_key", "connection_uri", "source_key", "secret_metadata", "worker_handle"] {
|
||||
assert!(!dto.contains(forbidden), "route DTO contains forbidden field marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Release-completeness canaries for the Raw Transaction Ingest Desk scaffold tranche.
|
||||
//! Release-completeness canaries for Raw Transaction Ingest Desk Config-only route inventory.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -21,7 +21,7 @@ fn read_text(path: &std::path::Path) -> String {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_production_module_inventory_is_exact_and_scaffold_only() {
|
||||
fn pre_006_production_module_inventory_adds_only_route_inventory_to_the_scaffold() {
|
||||
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||
let expected = [
|
||||
"mod app_state;",
|
||||
@@ -32,6 +32,7 @@ fn pre_005_production_module_inventory_is_exact_and_scaffold_only() {
|
||||
"mod errors;",
|
||||
"mod frontend_logging;",
|
||||
"mod logging_runtime;",
|
||||
"mod route_inventory;",
|
||||
"mod splash;",
|
||||
"mod tauri;",
|
||||
"mod tw_main;",
|
||||
@@ -44,14 +45,14 @@ fn pre_005_production_module_inventory_is_exact_and_scaffold_only() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_public_surface_exposes_only_application_run_entry_point() {
|
||||
fn pre_006_public_surface_still_exposes_only_application_run_entry_point() {
|
||||
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||
let public_reexports = lib.lines().filter(|line| return line.starts_with("pub use ")).collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(public_reexports, vec!["pub use self::tauri::run;"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_scaffold_keeps_start_stop_inventory_and_runtime_for_later_tranches() {
|
||||
fn pre_006_config_inventory_keeps_start_stop_and_runtime_resources_for_later_tranches() {
|
||||
let app = read_text(app_root().join("src/app_state.rs").as_path());
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
for forbidden in [
|
||||
@@ -68,3 +69,22 @@ fn pre_005_scaffold_keeps_start_stop_inventory_and_runtime_for_later_tranches()
|
||||
assert!(!tauri.contains(forbidden));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_inventory_source_is_config_only_and_contains_all_five_capability_requirements() {
|
||||
let inventory = read_text(app_root().join("src/route_inventory.rs").as_path());
|
||||
for required in [
|
||||
"WsSubscriptionKind::Logs",
|
||||
"WsSubscriptionKind::Block",
|
||||
"WsSubscriptionKind::HeliusTransaction",
|
||||
"getTransaction",
|
||||
"getBlocksWithLimit",
|
||||
"MissingYellowstoneGrpc",
|
||||
"MissingRequiredSecret",
|
||||
] {
|
||||
assert!(inventory.contains(required), "missing pre.006 inventory requirement {required}");
|
||||
}
|
||||
for forbidden in ["connect(", "Store::open", "start_with_runtime_resources", "request_stop"] {
|
||||
assert!(!inventory.contains(forbidden), "pre.006 inventory performs premature runtime operation {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn pre_005_route_ids_are_exact_and_stably_ordered() {
|
||||
@@ -25,3 +25,36 @@ fn pre_005_route_states_are_exact_without_runtime_claims() {
|
||||
assert_eq!(value, serde_json::json!(["unavailable", "configured", "starting", "running", "stopping", "stopped", "faulted"]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_unavailable_reason_codes_are_exact_and_safe() {
|
||||
let values = [
|
||||
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
|
||||
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
|
||||
crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction,
|
||||
crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan,
|
||||
crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability,
|
||||
crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability,
|
||||
crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability,
|
||||
crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc,
|
||||
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret,
|
||||
];
|
||||
let serialized = serde_json::to_value(values);
|
||||
assert!(serialized.is_ok());
|
||||
if let std::result::Result::Ok(serialized) = serialized {
|
||||
assert_eq!(
|
||||
serialized,
|
||||
serde_json::json!([
|
||||
"profile_unresolved",
|
||||
"network_mismatch",
|
||||
"missing_http_get_transaction",
|
||||
"missing_http_block_scan",
|
||||
"missing_ws_logs_capability",
|
||||
"missing_ws_block_capability",
|
||||
"missing_helius_transaction_capability",
|
||||
"missing_yellowstone_grpc",
|
||||
"missing_required_secret"
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn pre_006_route_requirement_projection_is_fail_closed_and_ordered() {
|
||||
let route = super::route_with_requirements(
|
||||
crate::RawIngestRouteId::StandardLogsHydrated,
|
||||
"devnet",
|
||||
&[
|
||||
(false, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
|
||||
(false, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
);
|
||||
assert!(!route.selectable);
|
||||
assert_eq!(route.state, crate::RawIngestRouteState::Unavailable);
|
||||
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability));
|
||||
assert_eq!(route.network.as_deref(), std::option::Option::Some("devnet"));
|
||||
let configured = super::route_with_requirements(
|
||||
crate::RawIngestRouteId::HttpBlockPolling,
|
||||
"mainnet",
|
||||
&[(true, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
|
||||
);
|
||||
assert!(configured.selectable);
|
||||
assert_eq!(configured.state, crate::RawIngestRouteState::Configured);
|
||||
assert_eq!(configured.reason, std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_unavailable_profile_projects_all_five_routes_without_physical_material() {
|
||||
let profile = super::unavailable_profile(
|
||||
"provider_profile",
|
||||
false,
|
||||
std::option::Option::Some("testnet"),
|
||||
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret,
|
||||
);
|
||||
assert_eq!(profile.routes.len(), 5);
|
||||
assert_eq!(profile.network.as_deref(), std::option::Option::Some("testnet"));
|
||||
for route in profile.routes {
|
||||
assert!(!route.selectable);
|
||||
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_committed_devnet_public_inventory_proves_only_config_composability() {
|
||||
let arguments = std::vec::Vec::<std::ffi::OsString>::new();
|
||||
let management = crate::config_management(arguments.as_slice());
|
||||
assert!(management.is_ok());
|
||||
let management = match management {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let inventory = crate::build_route_inventory(&management, 7);
|
||||
assert!(inventory.is_ok());
|
||||
let inventory = match inventory {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(inventory.generation, 7);
|
||||
assert_eq!(inventory.default_profile, "devnet_public");
|
||||
let profile = inventory.profiles.iter().find(|profile| return profile.profile_id == "devnet_public");
|
||||
assert!(profile.is_some());
|
||||
let profile = match profile {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(profile.is_default);
|
||||
assert_eq!(profile.network.as_deref(), std::option::Option::Some("devnet"));
|
||||
assert_eq!(profile.routes.len(), 5);
|
||||
assert_route(
|
||||
profile,
|
||||
crate::RawIngestRouteId::YellowstoneHydrated,
|
||||
false,
|
||||
std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc),
|
||||
);
|
||||
assert_route(profile, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
|
||||
assert_route(
|
||||
profile,
|
||||
crate::RawIngestRouteId::StandardBlockDirect,
|
||||
false,
|
||||
std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability),
|
||||
);
|
||||
assert_route(
|
||||
profile,
|
||||
crate::RawIngestRouteId::HeliusTransactionHydrated,
|
||||
false,
|
||||
std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability),
|
||||
);
|
||||
assert_route(profile, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
|
||||
let serialized = serde_json::to_string(&inventory);
|
||||
assert!(serialized.is_ok());
|
||||
if let std::result::Result::Ok(serialized) = serialized {
|
||||
for forbidden in ["http://", "https://", "ws://", "wss://", "api-key", "x-token", "connection_uri", "source_key", "endpoint_name"] {
|
||||
assert!(!serialized.contains(forbidden), "safe inventory leaked forbidden marker {forbidden}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_route(
|
||||
profile: &crate::RawIngestProfileInventoryDto,
|
||||
route_id: crate::RawIngestRouteId,
|
||||
selectable: bool,
|
||||
reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
|
||||
) {
|
||||
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
|
||||
assert!(route.is_some());
|
||||
if let std::option::Option::Some(route) = route {
|
||||
assert_eq!(route.selectable, selectable);
|
||||
assert_eq!(route.reason, reason);
|
||||
assert_eq!(route.state, if selectable { crate::RawIngestRouteState::Configured } else { crate::RawIngestRouteState::Unavailable });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user