Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85937b8cbb | |||
| 107986bf85 | |||
| a8cabbebd5 | |||
| 636a0c5f43 | |||
| bf46782db3 | |||
| 02919e959e | |||
| d231eba8be | |||
| 637382f364 | |||
| bf6ef5b426 | |||
| 822e8fcd86 | |||
| 63e1a866a3 | |||
| 22c88c449d | |||
| ed6c0ac10c | |||
| 355c5d1e9a | |||
| 61d8b1dee2 | |||
| 0db32a865c | |||
| a4061ee1e1 | |||
| ec64be340e | |||
| f1c383c392 | |||
| 984c327162 | |||
| e2689d1b04 | |||
| b5c84f3df8 | |||
| 36c683df9e | |||
| 510adcb49b | |||
| ab87fd23cd | |||
| 1a352aa8d0 | |||
| 919b4ed46d | |||
| f7c57f21c5 | |||
| d4f4237723 | |||
| ba574f3052 | |||
| 0683b70abe | |||
| eaf6fae94c | |||
| e19a202565 | |||
| 8309d829cc | |||
| aabe8dedb2 | |||
| d0da907c00 | |||
| 6b434750c5 | |||
| c9047af626 | |||
| 52d692e8ec | |||
| 5c3c8b3fef | |||
| b0079fc3ee | |||
| ecfa123a79 | |||
| 9da372e4a2 | |||
| 9a998e7b52 | |||
| c3c9a80310 | |||
| 116eabba7d | |||
| e6c2649410 | |||
| e85d6f6963 | |||
| eb39aaa904 | |||
| bb7ee3cc3a | |||
| 84647c12ea | |||
| 156d237460 | |||
| 7d0710a1e6 | |||
| 5c797827f7 | |||
| b87b7f4505 | |||
| 92f08ca5a8 | |||
| 85f292a36d | |||
| 877411c6e5 | |||
| 081a5352c0 | |||
| c0b3190256 | |||
| eaefec2259 | |||
| cfbf849e2a | |||
| e01fe88594 | |||
| 80fe2fd9e2 | |||
| 516c52ece9 | |||
| 2b79818848 | |||
| e2e603ca6d | |||
| 01502678bf | |||
| 32c4b67541 | |||
| 8c23625fee | |||
| 489d686023 | |||
| 10515d1562 | |||
| c9e52d1926 | |||
| 32577ea48d | |||
| d5b4aef194 | |||
| 8130e01b70 | |||
| 3a38914be7 | |||
| 62d6cf9dac | |||
| 4e2647cce1 | |||
| 9b92900dd7 | |||
| bf73481180 | |||
| 3bb73e81e4 | |||
| c4198ac656 | |||
| 009b957523 | |||
| 9300dccbe2 | |||
| 74890a6268 | |||
| 6a1892792c | |||
| 3df8fc74c1 |
10
.env.example
10
.env.example
@@ -1,5 +1,5 @@
|
||||
# file: .env.example
|
||||
# version: 13
|
||||
# version: 14
|
||||
|
||||
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
||||
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
|
||||
@@ -28,16 +28,16 @@ KSP_WALLETS_DIRECTORY=wallets
|
||||
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
|
||||
|
||||
# Optional public Solana Mainnet HTTP endpoint override used by config/std.transport.json and its example.
|
||||
# The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
|
||||
# The committed Transport document falls back to https://api.mainnet.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet.solana.com
|
||||
|
||||
# Optional public Solana Devnet WebSocket endpoint override used by config/std.transport.json.
|
||||
# The committed Transport document falls back to wss://api.devnet.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_DEVNET_WS_URL=wss://api.devnet.solana.com
|
||||
|
||||
# Optional public Solana Mainnet WebSocket endpoint override used by config/std.transport.json and its example.
|
||||
# The committed Transport document falls back to wss://api.mainnet-beta.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet-beta.solana.com
|
||||
# The committed Transport document falls back to wss://api.mainnet.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet.solana.com
|
||||
|
||||
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
|
||||
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
||||
|
||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,8 +1,66 @@
|
||||
<!-- file: CHANGELOG.md -->
|
||||
<!-- version: 27 -->
|
||||
<!-- version: 31 -->
|
||||
|
||||
# Changelog KSP
|
||||
|
||||
## 0.3.11 — Fondation runtime source-neutral du Worker RawTransaction ingest — 2026-09-08
|
||||
|
||||
`0.3.11` introduit `ksp-worker-raw-transaction-ingest-lib` comme premier Worker concret KSP pour l’acquisition continue de `RawTransaction`, tout en fermant volontairement la release **sans source réseau productive**. La crate consomme `ksp-worker-api`, `ksp-raw-transaction-lib` et la seule façade `ksp-store-lib`; elle ne dépend ni de Config, ni d’un Job, ni d’un backend Store physique, ni encore de `ksp-onchain-transport-lib`. Son identité stable est `raw_transaction_ingest`, ses settings bornent la capacité d’admission, la concurrence de persistence et la deadline de drain, et son `start` utilise exclusivement le runtime Tokio actif fourni par le caller.
|
||||
|
||||
Le runtime possède un supervisor privé, des tâches source/persistence détenues, une admission centrale `mpsc` bornée avec backpressure, la canonicalisation/assembly Common RAW, une observation key déterministe et la persistence atomique en mode `Normal`. Les outcomes distinguent insert, idempotence et tombstone purgée ; un contenu divergent devient `worker_raw_transaction_ingest.content_conflict`, les erreurs Store restent classifiées sans texte distant, et aucun provider gagnant n’est choisi implicitement. Les snapshots concrets latest-value se projettent directement sur `WorkerSnapshotSource`, utilisent séquences/compteurs checked et conservent le terminal pour les lecteurs tardifs.
|
||||
|
||||
Le hardening final ferme les races stop/fault, les source failures, la saturation observable, le Store lent/fautif et le shutdown borné. À l’expiration de `shutdown_drain_timeout`, le Worker ferme les admissions, abort les tâches restantes, les rejoint toutes puis publie `drain_timeout`; aucune tâche privée ne survit au terminal. Les tests externes de hardening/release verrouillent les sept codes d’erreur publics, les huit modules de production, les vingt-quatre exports crate-root, la redaction, le dependency firewall et l’absence de surface historique/Backfill/Transport/Config/backend direct.
|
||||
|
||||
Le gate technique final `pre.011` passe rustfmt check, audits Rust/Markdown, `cargo check --workspace`, Clippy strict, 56 tests Worker, `cargo test --workspace --all-targets --all-features`, les arbres Cargo Worker normal/features et `cargo tree --duplicates`. La réconciliation `pre.012` ajoute ensuite les README/USAGE durables du Worker et réaligne l’architecture sur l’état matériel : la fondation `0.3.11` n’a aucun adapter HTTP/WS/Yellowstone productif, aucune API publique d’enqueue/source registration et aucun replay/gap-repair live.
|
||||
|
||||
La trajectoire live reste découpée : `0.3.12` ouvre Yellowstone transactions/blocks/status + hydration HTTP + continuité de run, `0.3.13` ajoute WS standard/Helius/HTTP live et la convergence multi-source, puis `0.3.14` ferme le gap repair/hardening multi-source. `prompts/031-V0_3_12_START_PROMPT.md` ouvre `0.3.12` uniquement depuis le tag stable `v0.3.11`; son `pre.001` doit réauditer les surfaces Yellowstone/Solana/provider actuelles, l’injection Transport, l’hydration, le frontier, `from_slot`/ReplayInfo, les smokes accessibles et le sizing avant toute implémentation lourde.
|
||||
|
||||
## 0.3.10 — Lower-layer RAW Transaction commune + qualification cross-source — 2026-09-08
|
||||
|
||||
`0.3.10` introduit `ksp-raw-transaction-lib` comme lower-layer source-neutral commune aux producteurs `RawTransaction`. La canonicalisation RAW v1 auparavant locale à `ksp-job-backfill-lib` est extraite sans modifier les golden bytes/hash existants ; le Backfill consomme désormais cette common crate sans créer de dépendance vers un Worker. La bibliothèque construit les modèles `RawTransaction`/`RawTransactionObservation` de `ksp-store-api`, conserve l'identité canonique `(network, signature)`, borne et redacted les entrées hostiles, et reste volontairement sans Transport, Config, runtime async, Job, Worker ou backend Store physique.
|
||||
|
||||
La common matérialise également un wire Solana source-neutral Legacy/V0/V1 avec sérialisation Base64 et extraction de signature depuis le matériau transactionnel complet. La représentation V1 couvre header, compiled instructions, configuration V1 et signatures terminales ; les gardes structurelles rejettent les états non supportés plutôt que d'inventer une compatibilité. `ksp-onchain-transport-lib` ajoute `get_block_observed`, qui conserve l'identité sûre de la route réellement gagnante tout en réutilisant la validation `getBlock` existante.
|
||||
|
||||
Les preuves cross-source ferment explicitement ce qui est qualifié et ce qui exige encore hydration. Un `blockSubscribe` standard `full/base64` peut produire le même RAW v1 qu'un `getBlock` HTTP sur le sous-ensemble déterministe prouvé. Helius `transactionSubscribe` reste un signal nécessitant hydration HTTP lorsque `blockTime`/version manquent. Yellowstone qualifie le wire Transaction V1 et la conservation du `block_time` dans Block, mais `Transaction` nécessite hydration pour le temps de bloc et le chemin Block complet reste conservateur tant que l'identité byte-for-byte entre meta protobuf et meta JSON HTTP n'est pas prouvée. Aucun RAW-direct Yellowstone complet ni feed EARLY n'est revendiqué sans preuve.
|
||||
|
||||
Le gate technique final `pre.007` passe les audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features`, les suites ciblées common/Transport/Backfill et les graphes Cargo demandés ; les smokes live opt-in restent ignorés conformément à leur contrat. La réconciliation `pre.008` aligne ensuite la terminologie durable des couches Store sur `D1 RAW -> D2 STRUCTURAL -> D3 DECODED -> D4 DOMAIN`, réserve `N1–N4` aux niveaux architecturaux et complète `README.md`/`USAGE.md` de `ksp-raw-transaction-lib`. Son gate opérateur repasse format, audits, `cargo check --workspace` et Clippy strict sur `0.3.10-pre.8`.
|
||||
|
||||
Le runtime concret a été volontairement retiré de `0.3.10` après recalibrage du sizing. La trajectoire devient `0.3.11` fondation runtime/persistence déterministe, `0.3.12` Yellowstone + hydration/continuité, `0.3.13` WS standard/Helius/HTTP live + convergence multi-source, `0.3.14` gap repair/hardening/smokes, `0.3.15` Desk d'ingestion et `0.3.16` extension Backfill multi-source. Le Job historique paramétré et le Worker continu restent des producteurs indépendants du même Store.
|
||||
|
||||
`prompts/030-V0_3_11_START_PROMPT.md` ouvre `0.3.11` uniquement depuis le tag stable `v0.3.10`. Son `pre.001` doit auditer la base réelle, le graphe de dépendances, le runtime/supervision, les bornes de channels, la persistence/déduplication déterministe, les snapshots et le dimensionnement avant toute implémentation lourde ; aucune source live complexe n'est requise pour fermer cette release.
|
||||
|
||||
## 0.3.9 — Worker API générique + audit exhaustif d'acquisition RawTransaction — 2026-09-05
|
||||
|
||||
`0.3.9` introduit `ksp-worker-api` comme façade générique, courte et runtime-neutral pour services continus. La surface stable reste Core-only et expose `WorkerId`, `WorkerKindCode`, `WorkerState`, `WorkerHealth`, `WorkerActivity`, `WorkerLifecycle`, `WorkerStopToken`, `WorkerSnapshotSequence`, `WorkerSnapshot`, `WorkerSnapshotFuture` et `WorkerSnapshotSource`. Les identités sont bornées et redacted, le lifecycle possède des transitions explicites avec terminaux immuables, le stop token est partagé/idempotent, la séquence ne wrappe pas et le snapshot source latest-value reste object-safe, `Send + Sync` et implémentable depuis l'extérieur. L'API ne possède aucun runtime `start/stop` universel, aucune sémantique Job/checkpoint, aucun domaine Solana et aucune dépendance Transport/Store/Config/Tauri.
|
||||
|
||||
Après freeze de cette API, la seconde moitié de la release réalise puis consolide l'audit exhaustif des voies d'acquisition `RawTransaction`. Le document durable `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md` est réécrit comme une synthèse unique Store-centrique : `RawTransaction` est le cœur RAW durable, tandis que `RawTransactionObservation` conserve les provenances multiples. L'audit distingue systématiquement possibilité protocolaire/provider, support architectural KSP, support implémenté et preuve live ; il couvre HTTP, WebSocket standard, Helius `transactionSubscribe`, Yellowstone gRPC, acquisition par blocs/slots, discovery + hydration, replay/gap repair, archives et sources EARLY/pre-execution. Les matrices recensent aussi les principaux providers/réseaux/tier/coûts connus sans réduire l'architecture aux seuls comptes gratuits actuellement disponibles.
|
||||
|
||||
La séparation des producteurs RAW est figée. `ksp-job-backfill-lib` reste un producteur historique **paramétré, borné et terminable** ; le futur `ksp-worker-raw-transaction-ingest-lib` sera un producteur live **continu start/stop sans requête métier historique**. Ils ne s'appellent pas, ne se supervisent pas et ne collaborent pas : chacun alimente indépendamment le même Store. HTTP, WS, gRPC, replay et archive sont des capabilities orthogonales à ces rôles ; un Worker peut utiliser HTTP pour hydration ou réparation de sa propre continuité live, tandis qu'un Job pourra utiliser gRPC/replay si une campagne historique bornée le justifie. Le handoff `0.3.10` retient `ksp-raw-transaction-lib` comme lower-layer source-neutral commune afin d'extraire la canonicalisation RAW v1 aujourd'hui locale au Backfill sans duplication ni edge Job ↔ Worker ; les golden bytes/hash RAW v1 doivent rester inchangés.
|
||||
|
||||
La release normalise aussi l'identité réseau KSP : `mainnet` devient l'identité canonique dans Config/Store/Transport/tests et `mainnet-beta` reste seulement un alias legacy/externe lorsque la frontière provider l'exige. Les données Mainnet N1 encore utilisées comme données de test ne dictent aucune compatibilité durable et aucune migration SQL n'est introduite pour préserver l'ancien libellé. Pendant le gate final, les baselines workspace sont également relevées à `jsonschema ^0.53` et `yellowstone-grpc-proto ^12.7`; Yellowstone 12.7 ajoute le RPC serveur `SubscribeGossip`, ce qui nécessite uniquement l'adaptation des fixtures `Geyser` avec une réponse `UNIMPLEMENTED` et n'ouvre aucune capability Gossip de production.
|
||||
|
||||
Le gate technique final passe les audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, 385/385 tests unitaires `ksp-onchain-transport-lib`, 43/43 canaries `release_completeness`, `cargo test --workspace --all-targets --all-features`, le bundle complet `ksp-worker-api` et les graphes Cargo jusqu'à `cargo tree --duplicates`; seuls les smokes/probes explicitement opt-in restent ignorés. La réconciliation documentaire `pre.008` réaligne ensuite README/USAGE/indexes/architecture/plan/validation et son gate opérateur repasse audits Rust/Markdown plus `cargo check --workspace` en `0.3.9-pre.8`.
|
||||
|
||||
`prompts/029-V0_3_10_START_PROMPT.md` ouvre `0.3.10` exclusivement depuis le tag stable `v0.3.9`. La prochaine release doit d'abord matérialiser `ksp-raw-transaction-lib`, migrer le Backfill vers cette canonicalisation commune sans changement fonctionnel, puis construire `ksp-worker-raw-transaction-ingest-lib` multi-source dès V1. Son `pre.001` est obligatoirement un gate d'audit/sizing : inventaire exact des gaps Transport/Config, runtime Worker, capabilities live, continuité, preuves provider et dépendances avant toute implémentation lourde.
|
||||
|
||||
## 0.3.8 — Store Desk V1 RAW, inspection backend-neutral et trajectoire Worker multi-source — 2026-09-04
|
||||
|
||||
`0.3.8` introduit `ksp-app-store-desk`, application Tauri KSP read-only dédiée à l'inspection du Store. Le package reprend le gabarit Desk commun (splash/shell/assets/styles/tracing), compose `ksp-config-lib`, `ksp-logging-lib` et la seule façade `ksp-store-lib`, et interdit à l'application tout accès direct à `ksp-store-api`, `ksp-store-postgres-lib`, SQL, driver/pool PostgreSQL, Transport on-chain ou ressource physique. Le bootstrap ouvre un Store lié au profil/réseau composite, expose health/runtime backend-neutral et effectue un shutdown borné à la fermeture de la fenêtre principale.
|
||||
|
||||
Pour éviter de détourner la pagination machine existante, Store API ajoute une primitive d'inspection backend-neutral distincte : `RawInspectionPageRequest`/`RawInspectionPage<T>`, summaries `RawTransaction`/`RawAccountState` sans gros bytes et quatre capabilities d'inspection couvrant entités et observations. Le backend PostgreSQL implémente les vues d'inspection par `COUNT` exact + `OFFSET/LIMIT` contrôlé, tandis que les queries historiques `RawPage` restent keyset/cursor, sans `OFFSET`, pour workers, backfills et replays. La façade `ksp-store-lib` conserve ainsi le même ensemble de 14 capabilities RAW que le backend sans exposer de détail physique.
|
||||
|
||||
La Desk projette `RawTransaction` et `RawAccountState` via DataTables `serverSide` avec **un seul pager visible**, filtres Store explicites, tailles de page bornées et counts vérifiés avant conversion JavaScript. Les détails sont chargés à la demande avec preview RAW bornée à 512 octets ; les rows ne transportent pas le payload transactionnel ni les bytes account. Les observations Transaction/Account sont elles aussi paginées côté serveur dans les détails et n'exposent que la provenance sûre déjà normalisée par Store API : provider/protocol/method/origin, timestamps, endpoint/session/filter/commitment optionnels, hash/taille de payload source et metadata Yellowstone account lorsqu'elle existe, jamais les bytes source. Les identifiants longs utilisent affichage tronqué, tooltip et copie explicite ; le tracing frontend reste metadata-only et ne journalise aucune valeur métier copiée.
|
||||
|
||||
Le hardening final fige les inventaires Cargo/npm/modules/commands/capabilities Tauri, la composition Config, l'absence de réseau/browser-storage/SQL/backend physique dans le frontend, les DTO IPC sans curseur machine et la coexistence exacte entre navigation keyset et inspection random-access. Le correctif `pre.012-fix.001` réaligne aussi `tsconfig.json` Store Desk byte-for-byte sur le gabarit TypeScript commun des autres Desk, ce qui rétablit le build `tsc && vite build` des imports CSS DataTables et ajoute un canari de non-divergence.
|
||||
|
||||
Le gate technique autoritaire passe `cargo fmt --all -- --check`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features`, `ksp-store-lib --no-default-features`, graphes Cargo et build Tauri Store Desk ; il comptabilise 1 592 tests passés, 0 échec et 15 tests opt-in/operator-only ignorés. Après la réconciliation documentaire, un rerun propre depuis `cargo clean` confirme encore les audits/check/Clippy/tests et reconstruit avec succès les cinq Desk KSP Config, SOL Prices, Wallet, Backfill et Store en bundles Linux `.deb` + `.rpm`, sans rouvrir le runtime.
|
||||
|
||||
La trajectoire suivante est explicitement redécoupée pour éviter de répéter l'hypothèse HTTP du premier backfill. `0.3.9` introduira d'abord `ksp-worker-api` comme abstraction générique de service continu, puis seulement **après fermeture fonctionnelle de cette API** produira un audit exhaustif des sources/méthodes `RawTransaction`. Cet audit couvrira HTTP, WS standard, extensions provider, Yellowstone gRPC, block/slot, discovery + hydration, replay/gap-repair et multi-provider, et classera les sources comme alternatives, complémentaires, redondantes ou spécialisées. Il servira d'entrée architecturale à `0.3.10`, qui introduira `ksp-worker-raw-transaction-ingest-lib` multi-source dès V1 et ne modifiera Transport/Config que pour les adaptations réellement justifiées par l'audit. Les futures sources Helius HTTP/WS Mainnet/Devnet devront réutiliser `KSP_SECRET_HELIUS_API_KEY` via Config ; aucune URL Helius n'est ajoutée par `0.3.8`. La terminologie `mainnet`/`mainnet-beta` sera auditée avant tout changement afin de préserver une identité KSP unique pour le même cluster.
|
||||
|
||||
`0.3.11` ouvrira ensuite `ksp-app-raw-transaction-ingest-desk`, capable de choisir/superviser une ou plusieurs sources/méthodes sans dupliquer la logique du worker. `0.3.12` reviendra sur `ksp-job-backfill-lib` et `ksp-app-backfill-desk` pour ajouter les autres stratégies historiques/catch-up pertinentes issues du même audit ; la verticale `getSignaturesForAddress + getTransaction` de `0.3.6`/`0.3.7` reste une première stratégie HTTP valide, pas la définition générale du backfill KSP. kbot3 reste uniquement une référence fonctionnelle pour ces audits, jamais une source de code, DTO, Config, URL ou dépendance.
|
||||
|
||||
`prompts/028-V0_3_9_START_PROMPT.md` ouvre `0.3.9` sur cette base : Worker API doit rester Core-only/générique et être stabilisée avant l'audit RAW Transaction, lequel prépare `0.3.10` sans ajouter de worker concret ni d'endpoints provider pendant `0.3.9`.
|
||||
|
||||
## 0.3.7 — Backfill Desk : contrôle, monitoring, Cancel/Resume et autocomplete — 2026-09-03
|
||||
|
||||
`0.3.7` introduit `ksp-app-backfill-desk`, première application Tauri KSP spécialisée dans le contrôle d'un `ksp-job-backfill-lib` historique `RawTransaction`. L'application reste une couche de composition : `ksp-config-lib` possède les profils/composites/secrets, `ksp-onchain-transport-lib` le pool HTTP et les politiques provider/retry/rate-limit, `ksp-store-lib` la persistence backend-neutral, `ksp-job-backfill-lib` la découverte/hydratation/frontier/checkpoint/runtime et `ksp-job-api` le lifecycle commun. La Desk ne dépend ni d'un backend Store physique, ni de SQL, ni d'un SDK provider.
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 444
|
||||
# version: 518
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-backfill-desk", "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-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.7"
|
||||
version = "0.3.11"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
@@ -25,7 +25,7 @@ fs2 = { version = "^0.4" }
|
||||
futures-util = { version = "^0.3", default-features = false }
|
||||
getrandom = { version = "^0.4", default-features = false }
|
||||
http = { version = "^1.5", default-features = false }
|
||||
jsonschema = { version = "^0.52", default-features = false }
|
||||
jsonschema = { version = "^0.53", default-features = false }
|
||||
reqwest = { version = "^0.13", default-features = false }
|
||||
rustls = { version = "^0.23", default-features = false }
|
||||
rustls-native-certs = { version = "^0.8", default-features = false }
|
||||
@@ -49,7 +49,7 @@ tokio-tungstenite = { version = "^0.30", default-features = false }
|
||||
tonic = { version = "^0.14", default-features = false }
|
||||
tonic-prost = { version = "^0.14", default-features = false }
|
||||
ts-rs = { version = "^12.0" }
|
||||
yellowstone-grpc-proto = { version = "^12.6", default-features = false }
|
||||
yellowstone-grpc-proto = { version = "^12.7", default-features = false }
|
||||
zeroize = { version = "^1.9" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: README.md -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# Khadhroony Solana Project
|
||||
|
||||
@@ -53,7 +53,11 @@ La couche RAW dispose d'une façade Store backend-neutral et d'un premier job hi
|
||||
|
||||
`ksp-app-backfill-desk` fournit la surface desktop spécialisée de composition de ce runtime : sélection d'un scope historique et d'un rôle HTTP logique, validation backend des bornes, Start single-run, monitoring latest-value, Cancel ciblé et Resume in-session sur checkpoint Rust-only. L'application ouvre Transport et Store via leurs façades KSP, ne dépend d'aucun backend Store concret et n'expose ni URL/credential, ni payload RAW, ni checkpoint opaque au frontend.
|
||||
|
||||
Ces contrats restent distincts des futurs workers continus : un job borné n'est ni un service worker ni un pipeline générique imposé aux autres couches.
|
||||
`ksp-app-store-desk` fournit l'inspection desktop read-only du Store RAW via `ksp-store-lib` : health/runtime backend-neutral, tables server-side `RawTransaction` et `RawAccountState`, observations associées, détails avec previews bornées et lecture des états de rétention/tombstones. DataTables possède l'unique pagination visible de l'inspection random-access ; la pagination cursor/keyset des consumers machine reste distincte et intacte.
|
||||
|
||||
`ksp-worker-api` fournit désormais la fondation générique des services continus : identité Worker, lifecycle borné, health/activity, intention de stop coopératif et observation latest-value. Cette API reste Core-only, runtime-neutral et sans connaissance Solana, Transport, Store ou Job. Elle ne démarre ni n'arrête elle-même un runtime concret.
|
||||
|
||||
`ksp-worker-raw-transaction-ingest-lib` est désormais le premier Worker concret distinct du Job Backfill. Sa fondation source-neutral possède le lifecycle Start/Stop, l'admission bornée, la canonicalisation Common RAW, la persistance Store backend-neutral, le shutdown borné et les snapshots latest-value, mais aucune source réseau productive ni dépendance Transport. Les adapters live/catch-up seront ajoutés dans les tranches suivantes sans transformer le Worker en campagne historique. `ksp-job-backfill-lib` conserve de son côté son rôle historique paramétré et borné ; les deux producteurs restent indépendants et convergent uniquement vers les mêmes contrats RAW/Store.
|
||||
|
||||
## Points d'entrée
|
||||
|
||||
|
||||
34
ROADMAP.md
34
ROADMAP.md
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 102 -->
|
||||
<!-- version: 109 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -40,7 +40,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
||||
|
||||
### Cadrage
|
||||
|
||||
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés.
|
||||
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/STRUCTURAL/DECODED/DOMAIN stabilisés.
|
||||
- [X] `0.2.1` — HTTP foundation stable : matrice 52+14, runtime/routing/résilience/exécution HTTP, 4 canaris typés, `std.transport`, adapter Config -> Transport, canaries de clôture, smoke Devnet opt-in et documentation durable validés ; les 48 wrappers typés restants sont reportés à `0.2.2`–`0.2.4`.
|
||||
|
||||
### Releases fonctionnelles décidées/pressenties
|
||||
@@ -87,32 +87,40 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||
```
|
||||
|
||||
- **RAW** et **STRUCTURAL** ne nécessitent aucun décodage Program.
|
||||
- La progression n'est pas une chaîne obligatoire pour chaque famille : une donnée event-only peut s'arrêter en N1, et un état de compte pourra aller directement vers un futur decoder si aucune décomposition STRUCTURAL utile n'existe.
|
||||
- La progression n'est pas une chaîne obligatoire pour chaque famille : une donnée event-only peut s'arrêter en D1, et un état de compte pourra aller directement vers un futur decoder si aucune décomposition STRUCTURAL utile n'existe.
|
||||
- À la fin des couches horizontales RAW/STRUCTURAL réellement persistées, ajouter les jobs/workers/apps nécessaires avant d'ouvrir la couche suivante.
|
||||
- À partir de **DECODED**, avancer verticalement groupe par groupe ; **DOMAIN** désigne les projections métier et la matérialisation est le processus qui les produit.
|
||||
|
||||
## 0.3.x — RAW / acquisition persistée
|
||||
|
||||
- [X] `0.3.1` — `ksp-store-api` stable : modèles N1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
|
||||
- [X] `0.3.1` — `ksp-store-api` stable : modèles D1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
|
||||
- [X] `0.3.2` — `ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
|
||||
- [X] `0.3.3` — Vertical slice PostgreSQL `RawTransaction` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : six capabilities transaction/observation/rétention, V001 physique liée à un réseau, acquisition canonical+observation atomique, idempotence/conflit, get/list keyset cursorisé, archive/purge/tombstone/ForceRehydrate, hardening des erreurs et du schéma, concurrence et rollback validés sur PostgreSQL 17.
|
||||
- [X] `0.3.4` — Vertical slice PostgreSQL `RawAccountState` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : quatre capabilities account ajoutées aux six transaction pour une conformance RAW 10/10, V002 additive de 32 ressources au-dessus de V000/V001 immuables, state+observation atomiques, idempotence/conflit exacts, metadata Yellowstone observation-only, get/list keyset `(slot,pubkey,state_hash)` avec cursor KSPA anti-replay, hardening cross-family et live validé sur PostgreSQL 17 sans rétention destructive account.
|
||||
- [X] `0.3.5` — `ksp-interface-lib` étendu avec deux familles passives réellement partagées : `SlotLifecycleEvent` (`Processed`, `FirstShredReceived`, `Completed`, `CreatedBank`, `Dead`, `OptimisticallyConfirmed`, `Rooted`) et `TransactionExecutionEvent` (`slot + TransactionSignature[64] + Succeeded/Failed`). Interface reste Core-only, provider-neutral, sans serde/codec/runtime/event bus et sans duplication de `RawTransaction`/`RawAccountState`; les DTOs riches restent Transport-owned et les candidats non convergents restent différés.
|
||||
- [X] `0.3.6` — `ksp-job-api` + `ksp-job-backfill-lib` stables pour la première verticale historique `RawTransaction` : lifecycle/cancellation/latest-value runtime-neutral, scopes `Latest`/`Before`/`After`/signatures explicites, découverte/hydratation Transport observée, RAW v1 canonique, persistance Store atomique/idempotente, concurrence bornée, frontier contiguë, checkpoint/reprise caller-owned, snapshots sûrs et hardening externe sans dépendance backend/provider directe.
|
||||
- [X] `0.3.7` — `ksp-app-backfill-desk` livrée comme Desk Tauri KSP spécialisée : composition Config Mainnet/Devnet/Testnet, quatre scopes HTTP, validation/start single-run, monitoring latest-value `ksp-backfill-status`, Cancel ciblé/idempotent, Resume in-session par checkpoint Rust-only réémis pour un nouveau JobId, autocomplete libre depuis `ksp-core-lib`, hardening IPC/dependency boundaries et build Linux `.deb`/`.rpm`/`.AppImage`. Aucun SQL/backend/provider physique ni checkpoint durable n’est exposé.
|
||||
- [ ] `0.3.8` — Introduire `ksp-app-store-desk` V1 sur le **gabarit KSP courant** (shell/splash/styles/assets/logging et dépendances npm de base des Desk KSP), avec DataTables selon le pattern déjà utilisé par Config/Wallet Desk. La V1 reste backend-agnostique et consulte le Store uniquement via `ksp-store-lib` + Config : health/runtime sûr, tableaux `RawTransaction` et `RawAccountState`, observations lorsque la surface de listing backend-neutral est disponible/ajoutée proprement, rétention/tombstones en lecture et pagination Store opaque distincte du filtrage/paging local DataTables. kbot3 sert uniquement de référence fonctionnelle/UX des anciens tableaux RAW, jamais de gabarit, source de code, SQL, DTO, commandes ou versions npm. L’application évoluera ensuite avec STRUCTURAL, DECODED, processing/materialization et DOMAIN réellement persistés.
|
||||
- [ ] `0.3.9` — Introduire `ksp-worker-api` comme API générique de lifecycle/health/progression pour services continus, en reprenant le pattern latest-value stabilisé par Job tout en gardant les sémantiques Worker distinctes des jobs terminables et des wake-ups Store post-commit.
|
||||
- [ ] `0.3.10` — Introduire `ksp-worker-raw-transaction-ingest-lib` pour l'acquisition continue de `RawTransaction` via les surfaces live de `ksp-onchain-transport-lib`, persistance atomique par `ksp-store-lib`, reprise/backpressure/idempotence et notifications `ksp-worker-api`, sans decode Program ni dépendance backend/provider directe.
|
||||
- [ ] `0.3.11` — Introduire `ksp-app-raw-transaction-ingest-desk`, application Tauri spécialisée de contrôle et monitoring du worker live : lifecycle, health, rates, backpressure, reconnect/recovery et compteurs sûrs ; la consultation détaillée des données persistées reste la responsabilité de `ksp-app-store-desk`.
|
||||
- [X] `0.3.8` — `ksp-app-store-desk` V1 RAW livrée sur le gabarit KSP courant comme application Tauri read-only backend-neutral. La Desk compose Config + Logging + `ksp-store-lib`, expose health/runtime sûrs, DataTables `serverSide` pour `RawTransaction`, `RawAccountState` et leurs observations, détails bornés, provenance sûre et états de rétention/tombstone, sans SQL/backend physique/Transport dans l'application. La nouvelle inspection random-access `offset + limit + counts exacts` reste distincte de la pagination machine cursor/keyset conservée pour workers/backfills/replays. Le gate final et les builds Linux `.deb`/`.rpm` sont verts.
|
||||
- [X] `0.3.9` — `ksp-worker-api` stable comme API générique et volontairement courte pour services continus : identité/kind bornés, lifecycle explicite, health/activity sûrs, stop token partagé, séquence/snapshot latest-value et `WorkerSnapshotSource` object-safe, avec dépendance Core-only et sans runtime/start-stop universel, Solana, Transport, Store, Config ou Tauri. Après freeze Worker API, l'audit `RawTransaction` a été consolidé dans une synthèse Store-centrique exhaustive séparant possibilités/support/preuve et classant HTTP, WS standard/provider, Yellowstone gRPC, blocs/slots, discovery + hydration, replay de continuité, archives et sources EARLY. Le Job Backfill historique paramétré et le Worker Ingest live start/stop sont deux producteurs indépendants du même Store ; `mainnet` est désormais l'identité KSP canonique et `mainnet-beta` un alias legacy/externe. Le handoff retient `ksp-raw-transaction-lib` comme lower-layer commune de canonicalisation RAW v1 pour `0.3.10`, sans edge Job ↔ Worker.
|
||||
- [X] `0.3.10` — Lower-layer RAW Transaction commune stabilisée : `ksp-raw-transaction-lib` possède la canonicalisation RAW v1 source-neutral partagée, le Backfill est migré sans changement des golden bytes/hash, Transport expose `get_block_observed`, le wire Solana Legacy/V0/V1 est matérialisé et les canaris HTTP/WS standard/Helius/Yellowstone qualifient précisément les voies directes ou nécessitant hydration. La release se ferme sans runtime Worker concret ; celui-ci commence en `0.3.11` selon le redécoupage `0.3.11`–`0.3.14`.
|
||||
- [X] `0.3.11` — Fondation runtime de `ksp-worker-raw-transaction-ingest-lib` livrée : crate/dependency firewall, settings source-neutral, handle/start-stop, lifecycle, snapshots, supervisor, admission bornée, canonicalisation Common RAW, persistence/déduplication Store déterministe, backpressure et shutdown/fault hardening. La release se ferme volontairement sans source réseau productive ; Yellowstone + hydration HTTP commencent en `0.3.12`.
|
||||
- [ ] `0.3.12` — Ajouter au Worker la voie **Yellowstone + hydration HTTP** : transactions/blocks/status, projection vers la common RAW, provenance sûre, `from_slot`, replay info, frontier de run et continuité propre au run, avec fixtures déterministes et smokes accessibles sans prétendre à un RAW-direct lorsque les métadonnées restent incomplètes.
|
||||
- [ ] `0.3.13` — Ajouter les voies **WS standard / Helius / HTTP live** et leur convergence multi-source : `logsSubscribe + getTransaction`, `blockSubscribe`, `transactionSubscribe + hydration`, polling blocs HTTP, observations multiples, content conflict explicite, coalescence et backpressure sans second actor Transport.
|
||||
- [ ] `0.3.14` — Fermer le Worker par le **gap repair/hardening multi-source** : replay natif, source redondante, scan HTTP blocs, hydration de réparation, unresolved gaps visibles, politiques degraded/unhealthy/faulted, shutdown pendant repair, smokes provider accessibles et adapter EARLY seulement si réellement prouvé.
|
||||
- [ ] `0.3.15` — Introduire `ksp-app-raw-transaction-ingest-desk`, Desk Tauri KSP spécialisée qui choisit et supervise une ou plusieurs sources/méthodes réellement admises par le Worker finalisé : lifecycle, health, rates, backpressure, reconnect/recovery, gap state et compteurs sûrs. La Desk ne réimplémente ni discovery, hydration, déduplication, reprise ni persistence.
|
||||
- [ ] `0.3.16` — Étendre `ksp-job-backfill-lib` et `ksp-app-backfill-desk` au **backfill multi-source/multi-stratégie** à partir de la même matrice d'acquisition auditée en `0.3.9`. Conserver la stratégie actuelle `getSignaturesForAddress + getTransaction` comme première voie HTTP valide, puis ajouter seulement les voies historiques/catch-up/gap-repair réellement pertinentes et sûres, sans supposer qu'une source live WS constitue un historique universel.
|
||||
|
||||
### TODO/IDEAS — applications spécialisées et control plane
|
||||
|
||||
- [ ] **TODO** — faire évoluer `ksp-app-store-desk` avec chaque nouvelle couche réellement persistée : RAW d'abord, puis STRUCTURAL, DECODED, journal de processing/materialization et projections DOMAIN selon les contrats effectivement disponibles. Les vues avancées restent backend-agnostiques et passent uniquement par `ksp-store-lib`.
|
||||
- [ ] **TODO** — différer une future `ksp-app-control-desk` jusqu'à ce que KSP dispose au minimum d'un niveau N3/D3 de processing/materialization exploitable et de plusieurs decoders réels. Cette application sera une surface **end-user simplifiée** : démarrer/arrêter les flux autorisés, lancer une recherche courante, voir l'état global et les erreurs importantes. Elle ne recopiera pas le diagnostic détaillé, les réglages avancés, les tables complètes ni toutes les fonctions des `ksp-app-*-desk` spécialisées, qui resteront les outils d'administration, développement et investigation approfondie.
|
||||
- [ ] **TODO** — différer une future `ksp-app-control-desk` jusqu'à ce que KSP dispose au minimum d'une couche D3 DECODED de processing/materialization exploitable et de plusieurs decoders réels. Cette application sera une surface **end-user simplifiée** : démarrer/arrêter les flux autorisés, lancer une recherche courante, voir l'état global et les erreurs importantes. Elle ne recopiera pas le diagnostic détaillé, les réglages avancés, les tables complètes ni toutes les fonctions des `ksp-app-*-desk` spécialisées, qui resteront les outils d'administration, développement et investigation approfondie.
|
||||
|
||||
### TODO/IDEAS — taxonomie N1, processing et rétention
|
||||
### TODO/IDEAS — taxonomie D1, processing et rétention
|
||||
|
||||
- [ ] **TODO** — maintenir la matrice d’admission HTTP/WS/gRPC/provider lors de toute nouvelle famille N1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
|
||||
- [ ] **TODO** — maintenir la matrice d’admission HTTP/WS/gRPC/provider lors de toute nouvelle famille D1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
|
||||
- [X] **TODO `0.3.9`** — matrice RAW Transaction unique produite et consolidée dans `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`, avec séparation possibilités/support/preuve, capabilities orthogonales aux producteurs, provenance/déduplication/continuité et handoffs désormais scindés `0.3.11`–`0.3.14` Worker live / `0.3.16` Backfill historique.
|
||||
- [ ] **TODO Helius `0.3.13`** — ajouter uniquement les profils/capabilities Helius réellement nécessaires au Worker live après revalidation des surfaces/tier courants ; réutiliser exclusivement `KSP_SECRET_HELIUS_API_KEY` via Config et la surface `transactionSubscribe` déjà Transport-owned, sans SDK provider ni second client parallèle.
|
||||
- [X] **TODO réseau** — identité KSP canonique fixée à `mainnet` dans Config/Store/Transport/tests ; `mainnet-beta` reste uniquement un alias legacy/externe lorsque la frontière provider l'exige. Les données D1 RAW antérieures restent non autoritaires pendant cette phase et aucune compatibilité de base de test n'impose l'ancien libellé.
|
||||
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique est complétée en `0.3.4` avec les quatre capabilities account et la conformance RAW 10/10.
|
||||
- [ ] **TODO** — statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif d’exécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsqu’un consumer de commitment/snapshot réel apparaît, sans fusionner snapshot, transition et execution update dans un modèle Option-soup.
|
||||
- [ ] **IDEA** — logs realtime enrichis : `logsSubscribe` alimente déjà la projection minimale `TransactionExecutionEvent`, mais un éventuel `TransactionLogEvent` portant les lignes de log reste différé dans `docs/IDEAS.md` jusqu’à démonstration d’un consumer et de bornes explicites. `logMessages` reste dans `RawTransaction` jusqu’à STRUCTURAL ; le wake-up post-commit reste distinct et Store API-owned conformément à `KSP-NOTIFY-*`.
|
||||
@@ -125,13 +133,13 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||
- [X] rétention physique `RawTransaction` PostgreSQL — `0.3.3` matérialise `Full -> Archived -> Purged`, tombstone et ForceRehydrate atomiques ; `Compacted` reste explicitement unsupported tant qu’aucune représentation compactée réelle n’existe.
|
||||
- [ ] **TODO** — policy de rétention/compaction : définir les critères d’éligibilité fondés sur les preuves de processing et la maintenance worker/job ; Store applique une transition demandée mais ne décide pas seul qu’un RAW peut être archivé/purgé, et la compaction physique ne sera ajoutée qu’avec un besoin réel.
|
||||
- [X] frontière `ksp-interface-lib` / `ksp-store-api` — ownership documenté et canaris de non-duplication stabilisés en `0.3.1`; les events passifs non persistés restent Interface, les modèles persistants/replayables restent Store API.
|
||||
- [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de l’ouverture de N2/N3 ; conserver l’isolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.
|
||||
- [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de l’ouverture de D2/D3 ; conserver l’isolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.
|
||||
|
||||
## Série STRUCTURAL suivante
|
||||
|
||||
- [ ] Définir la persistence STRUCTURAL canonique Solana générique pour les familles réellement décomposables.
|
||||
- [ ] Implémenter en priorité `RawTransaction -> STRUCTURAL` sans decoder Program : transaction/message, comptes/références, instructions top-level, CPI/inner instructions, logs/meta/balances/return data et relations structurelles.
|
||||
- [ ] Vérifier avant extension si d'autres familles N1 possèdent une vraie décomposition STRUCTURAL utile ; ne pas créer de niveau vide par convention.
|
||||
- [ ] Vérifier avant extension si d'autres familles D1 possèdent une vraie décomposition STRUCTURAL utile ; ne pas créer de niveau vide par convention.
|
||||
- [ ] Ajouter replay/backfill RAW -> STRUCTURAL avec processing versionné.
|
||||
- [ ] Ajouter le worker/service STRUCTURAL utile sans le coupler à un worker RAW concret.
|
||||
- [ ] Faire évoluer `ksp-app-store-desk` avec des vues/requêtes STRUCTURAL lorsque cette couche est réellement persistée ; ne pas créer une seconde application de browsing des mêmes données.
|
||||
|
||||
51
config/composite.ksp-app-store-desk.json
Normal file
51
config/composite.ksp-app-store-desk.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "mainnet",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "devnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "supertrace"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "devnet"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "supertrace"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "mainnet"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "testnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "supertrace"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "testnet"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"network": "mainnet-beta",
|
||||
"network": "mainnet",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
"name": "mainnet_public",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
||||
"cluster": "mainnet",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 15000,
|
||||
"max_idle_connections_per_host": 8,
|
||||
@@ -57,7 +57,7 @@
|
||||
"name": "mainnet_private",
|
||||
"enabled": true,
|
||||
"provider": "private-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 10000,
|
||||
@@ -85,15 +85,15 @@
|
||||
"name": "mainnet_public_ws",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||
},
|
||||
{
|
||||
"name": "mainnet_helius_ws",
|
||||
"enabled": true,
|
||||
"provider": "helius",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"kind": "helius_laserstream",
|
||||
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}",
|
||||
"session": {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"network": "mainnet-beta",
|
||||
"network": "mainnet",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||
|
||||
@@ -144,8 +144,8 @@
|
||||
"name": "solana_mainnet_public",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
||||
"cluster": "mainnet",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 15000,
|
||||
"max_idle_connections_per_host": 8,
|
||||
@@ -172,9 +172,9 @@
|
||||
"name": "solana_mainnet_public_ws",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -185,8 +185,8 @@
|
||||
"name": "solana_mainnet_public",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
||||
"cluster": "mainnet",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 15000,
|
||||
"max_idle_connections_per_host": 8,
|
||||
@@ -225,7 +225,7 @@
|
||||
"name": "publicnode_solana_mainnet_http",
|
||||
"enabled": true,
|
||||
"provider": "publicnode",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"url": "https://solana-rpc.publicnode.com",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 15000,
|
||||
@@ -267,9 +267,9 @@
|
||||
"name": "solana_mainnet_public_ws",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -280,8 +280,8 @@
|
||||
"name": "solana_mainnet_public",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
||||
"cluster": "mainnet",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 15000,
|
||||
"max_idle_connections_per_host": 8,
|
||||
@@ -308,9 +308,9 @@
|
||||
"name": "solana_mainnet_public_ws",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||
}
|
||||
],
|
||||
"grpc_endpoints": [
|
||||
@@ -318,7 +318,7 @@
|
||||
"name": "publicnode_solana_mainnet_yellowstone",
|
||||
"enabled": true,
|
||||
"provider": "publicnode",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"protocol": "solana_yellowstone",
|
||||
"url": "https://solana-yellowstone-grpc.publicnode.com:443",
|
||||
"secret_metadata": [
|
||||
|
||||
@@ -54,18 +54,19 @@
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||
// version: 16
|
||||
// version: 17
|
||||
|
||||
//! Structural desktop contract checks for the Backfill Desk scaffold.
|
||||
|
||||
@@ -145,10 +145,11 @@ fn pre_003_composite_packaging_follows_atomic_config_registry_contract() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(resources.len(), 14);
|
||||
assert_eq!(resources.len(), 15);
|
||||
for required in [
|
||||
"../../config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
fn ready_options() -> crate::BackfillDeskOptionsDto {
|
||||
let limits = crate::backfill_request_limits();
|
||||
@@ -22,7 +22,7 @@ fn ready_options() -> crate::BackfillDeskOptionsDto {
|
||||
return crate::BackfillDeskOptionsDto {
|
||||
commitments: crate::backfill_commitment_codes(),
|
||||
composition_ready: true,
|
||||
configured_networks: vec!["mainnet-beta".to_owned()],
|
||||
configured_networks: vec!["mainnet".to_owned()],
|
||||
http_routes: vec![
|
||||
crate::BackfillHttpRouteOptionDto {
|
||||
pooled: true,
|
||||
@@ -36,7 +36,7 @@ fn ready_options() -> crate::BackfillDeskOptionsDto {
|
||||
program_id_options: crate::program_id_autocomplete_options(),
|
||||
scope_kinds: crate::backfill_scope_kind_codes(),
|
||||
store_diagnostic: std::option::Option::None,
|
||||
store_network: std::option::Option::Some("mainnet-beta".to_owned()),
|
||||
store_network: std::option::Option::Some("mainnet".to_owned()),
|
||||
store_ready: true,
|
||||
transport_diagnostic: std::option::Option::None,
|
||||
transport_ready: true,
|
||||
@@ -81,7 +81,7 @@ fn latest_address_maps_to_backend_network_role_commitment_and_bounds() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(mapped.network().as_str(), "mainnet-beta");
|
||||
assert_eq!(mapped.network().as_str(), "mainnet");
|
||||
assert_eq!(mapped.role().as_str(), "backfill_pool");
|
||||
assert_eq!(mapped.commitment(), ksp_job_backfill_lib::BackfillCommitment::Finalized);
|
||||
assert_eq!(mapped.scope().kind(), ksp_job_backfill_lib::BackfillScopeKind::LatestAddress);
|
||||
@@ -193,7 +193,7 @@ fn request_preview_never_contains_address_or_signature_values() {
|
||||
if let std::result::Result::Ok(serialized) = serialized {
|
||||
assert!(!serialized.contains("11111111111111111111111111111111"));
|
||||
assert!(!serialized.contains(&"1".repeat(64)));
|
||||
assert!(serialized.contains("mainnet-beta"));
|
||||
assert!(serialized.contains("mainnet"));
|
||||
assert!(serialized.contains("backfill_pool"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#[test]
|
||||
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
|
||||
@@ -30,7 +30,7 @@ fn cancellable_runtime(
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let network = match ksp_store_lib::RawNetworkId::new("mainnet-beta") {
|
||||
let network = match ksp_store_lib::RawNetworkId::new("mainnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>> {
|
||||
let signature = match ksp_job_backfill_lib::BackfillSignature::new("1".repeat(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
|
||||
@@ -14,7 +14,7 @@ fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ks
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let network = match ksp_store_lib::RawNetworkId::new("mainnet-beta") {
|
||||
let network = match ksp_store_lib::RawNetworkId::new("mainnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/unit_tests/store_runtime.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn store_health_mapping_is_explicit_and_conservative() {
|
||||
@@ -10,7 +10,7 @@ fn store_health_mapping_is_explicit_and_conservative() {
|
||||
#[test]
|
||||
fn store_transport_network_coherence_requires_exact_logical_identity() {
|
||||
assert!(super::validate_network_coherence("devnet", "devnet").is_ok());
|
||||
let mismatch = super::validate_network_coherence("mainnet-beta", "devnet");
|
||||
let mismatch = super::validate_network_coherence("mainnet", "devnet");
|
||||
assert!(mismatch.is_err());
|
||||
if let std::result::Result::Err(error) = mismatch {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_STORE_NETWORK_MISMATCH);
|
||||
|
||||
@@ -54,18 +54,19 @@
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
//! Desktop build/shell contract audits for Config Desk.
|
||||
|
||||
@@ -110,7 +110,7 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writab
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 14);
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
|
||||
@@ -119,6 +119,10 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writab
|
||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-store-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-store-desk.json"),
|
||||
);
|
||||
assert_eq!(resources.get("../../config/std.logging.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.logging.json"));
|
||||
assert_eq!(
|
||||
resources.get("../../config/std.offchain_transport.json").and_then(serde_json::Value::as_str),
|
||||
|
||||
@@ -54,18 +54,19 @@
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
//! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`.
|
||||
|
||||
@@ -85,13 +85,13 @@ fn pre_002_package_is_mixed_lib_bin_and_frontend_is_scaffold_only() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_packaging_contains_current_fourteen_config_resources() {
|
||||
fn v0_3_8_pre_006_packaging_contains_current_fifteen_config_resources() {
|
||||
let root = app_root();
|
||||
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some());
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 14);
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
|
||||
@@ -100,6 +100,10 @@ fn pre_004_packaging_contains_current_fourteen_config_resources() {
|
||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-store-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-store-desk.json"),
|
||||
);
|
||||
assert!(resources.contains_key("../../config/std.offchain_transport.json"));
|
||||
assert!(resources.contains_key("../../config/schemas/std.offchain_transport.schema.json"));
|
||||
assert!(resources.contains_key("../../config/std.store.json"));
|
||||
|
||||
41
crates/ksp-app-store-desk/Cargo.toml
Normal file
41
crates/ksp-app-store-desk/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
||||
# file: crates/ksp-app-store-desk/Cargo.toml
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "ksp-app-store-desk"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "ksp_app_store_desk_lib"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "ksp-app-store-desk"
|
||||
path = "src/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["std", "now"] }
|
||||
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-store-lib = { path = "../ksp-store-lib" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tauri.workspace = true
|
||||
tauri-plugin-tracing.workspace = true
|
||||
tokio = { workspace = true, features = ["sync", "time"] }
|
||||
ts-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
106
crates/ksp-app-store-desk/README.md
Normal file
106
crates/ksp-app-store-desk/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
<!-- file: crates/ksp-app-store-desk/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# `ksp-app-store-desk`
|
||||
|
||||
`ksp-app-store-desk` est l’application desktop spécialisée d’inspection read-only du Store KSP.
|
||||
|
||||
Elle reste une couche de composition Tauri : Config sélectionne le profil Store et Logging, `ksp-store-lib` possède la façade backend-neutral et les contrats d’inspection, et le backend physique reste entièrement masqué à l’application comme au frontend.
|
||||
|
||||
## Package
|
||||
|
||||
```text
|
||||
package : ksp-app-store-desk
|
||||
lib : ksp_app_store_desk_lib
|
||||
bin : ksp-app-store-desk
|
||||
```
|
||||
|
||||
Le binaire est un launcher mince. La bibliothèque applicative possède le bootstrap, le lifecycle Store, les DTOs Tauri, les commands, les projections de détail bornées et le bridge de tracing frontend.
|
||||
|
||||
## Composition
|
||||
|
||||
```text
|
||||
ksp-config-lib -> composite, profils, secrets et runtime packagé
|
||||
ksp-core-lib -> erreurs et primitives communes
|
||||
ksp-store-lib -> façade Store backend-neutral et contrats RAW/inspection
|
||||
ksp-logging-lib -> logging/tracing applicatif
|
||||
ksp-app-store-desk -> orchestration Tauri + projections sûres read-only
|
||||
```
|
||||
|
||||
L’application ne dépend pas directement de `ksp-store-api`, `ksp-store-postgres-lib`, `tokio-postgres` ou d’un autre backend physique. Elle ne possède aucun SQL.
|
||||
|
||||
## Vues
|
||||
|
||||
La surface utilisateur comprend :
|
||||
|
||||
- **Overview** : état du runtime Store, health portable, réseau, backend logique, migration et compteurs de pool sûrs ;
|
||||
- **RAW Transactions** : inspection DataTables server-side, filtres de slot et direction, état de rétention et détail explicite ;
|
||||
- **RAW Accounts** : inspection DataTables server-side, filtre pubkey exact, plage de slots, direction et détail explicite ;
|
||||
- **Diagnostics** : projection sûre du shell, de Config et du runtime Store.
|
||||
|
||||
Les détails Transaction et Account contiennent également leurs observations dans des DataTables server-side dédiées lorsque ces observations existent.
|
||||
|
||||
## Pagination et inspection
|
||||
|
||||
Store Desk utilise la surface d’inspection random-access de `ksp-store-lib` : `offset + limit` avec counts exacts. DataTables est l’unique propriétaire de la pagination visible et les tailles admises sont `25`, `50` et `100`.
|
||||
|
||||
Cette voie d’inspection humaine ne remplace pas la pagination cursor/keyset du Store. Les workers, jobs et autres consumers machine peuvent continuer à utiliser `RawPageCursor` / `RawPageRequest`; Store Desk ne consomme pas ces cursors.
|
||||
|
||||
Le frontend conserve `draw` localement et ne transmet ni search globale DataTables, ni ordre de colonnes arbitraire, ni cursor, ni réseau physique au Store.
|
||||
|
||||
## Rows, détails et observations
|
||||
|
||||
Les rows de table ne transportent jamais les bytes RAW complets :
|
||||
|
||||
- Transaction : identité, slot, format, hash, taille éventuelle et état de rétention ;
|
||||
- Account : pubkey, slot, state hash, owner, lamports, executable, rent epoch et longueur de data ;
|
||||
- Observation : provenance d’acquisition sûre et metadata bornées, sans source payload bytes.
|
||||
|
||||
Un détail est chargé explicitement pour une seule entité. Les previews payload Transaction et data Account sont limitées à **512 bytes**, rendues en hexadécimal et accompagnées de la taille exacte et d’un indicateur de troncature.
|
||||
|
||||
Pour une Transaction, les états `Full`, `Archived` et `Purged` restent distincts. Un état `Purged` est représenté via son tombstone ; Store Desk n’offre aucune action d’archive, purge ou force-rehydrate.
|
||||
|
||||
## Valeurs longues et copie
|
||||
|
||||
Les signatures, hashes, pubkeys, owners et autres identifiants longs sont tronqués visuellement au centre tout en restant disponibles via tooltip et bouton de copie. La copie n’ajoute aucune capability Tauri : elle utilise le WebView et le tracing ne transporte que l’identifiant logique du champ, jamais la valeur copiée.
|
||||
|
||||
## Config et réseaux
|
||||
|
||||
Le composite dédié est :
|
||||
|
||||
```text
|
||||
cfg.composite.ksp-app-store-desk
|
||||
```
|
||||
|
||||
Les profils committed couvrent `mainnet`, `devnet` et `testnet`. Mainnet est le profil par défaut. Le composite contient uniquement Logging + Store ; aucun Transport n’est ouvert par l’application.
|
||||
|
||||
Le frontend ne reçoit jamais URI Store, credential, secret Config, SQL, handle pool ou type backend physique.
|
||||
|
||||
## Sécurité et ownership
|
||||
|
||||
Store Desk est read-only par contrat applicatif : aucun command Tauri d’écriture Store n’est exposé. Les capabilities guest restent limitées à `core:default` et `tracing:default`.
|
||||
|
||||
Le frontend ne possède ni accès réseau navigateur direct, ni persistence navigateur des données RAW, ni accès filesystem, ni dialogue natif pour contourner les façades KSP.
|
||||
|
||||
Les comptes et counts `u64` susceptibles d’excéder le domaine entier sûr JavaScript traversent IPC sous forme décimale et sont vérifiés avant conversion côté TypeScript.
|
||||
|
||||
## Ports et développement
|
||||
|
||||
```text
|
||||
Vite HTTP : 1438
|
||||
Vite WS : 1439
|
||||
```
|
||||
|
||||
Lancement :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-store-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Build production :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-store-desk && cargo tauri build)
|
||||
```
|
||||
|
||||
Voir également [`USAGE.md`](USAGE.md), [`../ksp-store-lib/README.md`](../ksp-store-lib/README.md), [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) et [`../../docs/validation/025-V0_3_8_STORE_DESK.md`](../../docs/validation/025-V0_3_8_STORE_DESK.md).
|
||||
149
crates/ksp-app-store-desk/USAGE.md
Normal file
149
crates/ksp-app-store-desk/USAGE.md
Normal file
@@ -0,0 +1,149 @@
|
||||
<!-- file: crates/ksp-app-store-desk/USAGE.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Utilisation de `ksp-app-store-desk`
|
||||
|
||||
## 1. Lancement
|
||||
|
||||
Depuis la racine du workspace :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-store-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Le runtime charge le composite Store Desk via `ksp-config-lib`, initialise Logging puis ouvre le Store sélectionné via `ksp-store-lib`.
|
||||
|
||||
## 2. Profil et readiness
|
||||
|
||||
Le composite `cfg.composite.ksp-app-store-desk` sélectionne un profil Logging et un profil Store. Mainnet est le profil par défaut ; Devnet et Testnet sont également définis dans la configuration committed.
|
||||
|
||||
La vue **Overview** expose notamment :
|
||||
|
||||
```text
|
||||
Store open
|
||||
health state
|
||||
profile
|
||||
network
|
||||
backend kind
|
||||
migration version / pending migrations
|
||||
pool size / capacity / available / waiting
|
||||
```
|
||||
|
||||
Ces informations sont des projections portables. Elles ne contiennent jamais URI PostgreSQL, credential, secret ou handle physique.
|
||||
|
||||
## 3. RAW Transactions
|
||||
|
||||
La vue **RAW Transactions** charge les données côté serveur via la façade d’inspection Store.
|
||||
|
||||
Filtres disponibles :
|
||||
|
||||
```text
|
||||
slot minimum
|
||||
slot maximum
|
||||
direction ascending / descending
|
||||
```
|
||||
|
||||
La table affiche des summaries sans payload RAW complet. Les signatures et hashes longs restent copiables sans que leur valeur soit journalisée.
|
||||
|
||||
Le bouton de détail recharge explicitement une seule Transaction. Le modal affiche les metadata canoniques, l’état de rétention et, lorsqu’un payload est encore retenu, un preview hexadécimal limité à 512 bytes avec taille exacte et indicateur de troncature.
|
||||
|
||||
Un état `Purged` est présenté à partir du tombstone disponible. Aucune opération d’archive, purge ou force-rehydrate n’est proposée par l’application.
|
||||
|
||||
## 4. RAW Accounts
|
||||
|
||||
La vue **RAW Accounts** utilise le même modèle d’inspection server-side.
|
||||
|
||||
Filtres disponibles :
|
||||
|
||||
```text
|
||||
pubkey exacte
|
||||
slot minimum
|
||||
slot maximum
|
||||
direction ascending / descending
|
||||
```
|
||||
|
||||
La table ne transporte pas les bytes Account. Elle expose seulement les metadata utiles : pubkey, slot, state hash, owner, lamports, executable, rent epoch et longueur de data.
|
||||
|
||||
Le détail recharge exactement l’identité `(pubkey, slot, state_hash)` puis affiche un preview hexadécimal de data limité à 512 bytes, la longueur exacte et l’indicateur de troncature.
|
||||
|
||||
## 5. Observations
|
||||
|
||||
Les modals Transaction et Account contiennent une table **Observations** associée à l’entité actuellement ouverte.
|
||||
|
||||
La table est server-side et expose uniquement de la provenance sûre : provider logique, protocole, méthode/origine d’acquisition, timestamps, endpoint/session/filter/commitment lorsqu’ils existent, hash/taille de source et metadata Account optionnelles admises.
|
||||
|
||||
Les bytes du payload source ne traversent jamais IPC.
|
||||
|
||||
## 6. Pagination DataTables
|
||||
|
||||
Les tables Transactions, Accounts et Observations utilisent DataTables comme unique pager visible.
|
||||
|
||||
Tailles disponibles :
|
||||
|
||||
```text
|
||||
25
|
||||
50
|
||||
100
|
||||
```
|
||||
|
||||
Le frontend mappe `start` vers l’offset Store et `length` vers la limite. Les counts exacts restent des chaînes décimales jusqu’à vérification du domaine `Number.MAX_SAFE_INTEGER`.
|
||||
|
||||
La pagination d’inspection est distincte de la pagination cursor/keyset utilisée par les consumers machine du Store. Store Desk n’accepte ni cursor, ni search globale DataTables, ni tri arbitraire de colonnes.
|
||||
|
||||
## 7. Rafraîchissement et filtres
|
||||
|
||||
**Refresh** redessine la table concernée avec ses filtres Store explicites. Un changement de filtre repart de la première page.
|
||||
|
||||
Les valeurs de filtre sont validées côté Rust : slots décimaux, pubkey canonique et direction autorisée. Elles ne sont pas injectées dans les logs frontend.
|
||||
|
||||
## 8. Valeurs longues et copie
|
||||
|
||||
Les valeurs longues sont affichées sous forme tronquée au centre. Le survol permet de consulter la valeur complète et le bouton de copie copie la valeur actuellement affichée par le frontend.
|
||||
|
||||
Les événements de tracing de copie transportent uniquement un `fieldId` logique. Ils ne contiennent jamais signature, pubkey, owner, hash ou preview hex.
|
||||
|
||||
## 9. Read-only et frontières
|
||||
|
||||
Store Desk est un outil d’inspection. Il ne fournit aucune commande pour :
|
||||
|
||||
```text
|
||||
écrire une entité RAW
|
||||
ajouter une observation
|
||||
archiver
|
||||
purger
|
||||
force-rehydrate
|
||||
démarrer un backfill
|
||||
démarrer un worker
|
||||
modifier la configuration Store
|
||||
```
|
||||
|
||||
Ces responsabilités appartiennent aux composants KSP propriétaires.
|
||||
|
||||
Le frontend ne reçoit jamais :
|
||||
|
||||
```text
|
||||
URI Store
|
||||
credentials/secrets
|
||||
SQL
|
||||
handles backend/pool
|
||||
payload RAW complet
|
||||
data Account complète
|
||||
source payload bytes
|
||||
contexte d’erreur arbitraire
|
||||
```
|
||||
|
||||
## 10. Fermeture
|
||||
|
||||
La fermeture de la fenêtre principale bloque les nouvelles admissions puis ferme le Store de manière bornée avant de terminer le process.
|
||||
|
||||
Le shutdown applicatif n’effectue aucune mutation métier de rétention ou de données.
|
||||
|
||||
## 11. Build production
|
||||
|
||||
Depuis la racine du workspace :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-store-desk && cargo tauri build)
|
||||
```
|
||||
|
||||
Le build frontend passe par TypeScript puis Vite avant le build Tauri. Les resources Config enregistrées sont packagées selon le contrat commun des applications Desk KSP.
|
||||
12
crates/ksp-app-store-desk/build.rs
Normal file
12
crates/ksp-app-store-desk/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
// file: crates/ksp-app-store-desk/build.rs
|
||||
// version: 1
|
||||
|
||||
//! Build script for the KSP Store desktop application.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
13
crates/ksp-app-store-desk/capabilities/default.json
Normal file
13
crates/ksp-app-store-desk/capabilities/default.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Minimal Store Desk desktop capability set.",
|
||||
"windows": [
|
||||
"splash",
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"tracing:default"
|
||||
]
|
||||
}
|
||||
BIN
crates/ksp-app-store-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
BIN
crates/ksp-app-store-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
Binary file not shown.
25
crates/ksp-app-store-desk/frontend/fonts/README.md
Normal file
25
crates/ksp-app-store-desk/frontend/fonts/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
<!-- file: crates/ksp-app-backfill-desk/frontend/fonts/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Fonts du splash Backfill Desk
|
||||
|
||||
Backfill Desk réutilise le gabarit de splash KSP déjà stabilisé dans Config Desk et Wallet Desk et conserve la police locale :
|
||||
|
||||
```text
|
||||
DOS_Amazigh.ttf
|
||||
font-family: Dos Amazigh
|
||||
```
|
||||
|
||||
Destination :
|
||||
|
||||
```text
|
||||
crates/ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf
|
||||
```
|
||||
|
||||
Empreinte SHA-256 de l'asset repris du gabarit :
|
||||
|
||||
```text
|
||||
a01ff4b5a0d699db7cefcf5336bf0e379ff6fbf0d096087571a13c8782f9aa02
|
||||
```
|
||||
|
||||
`splash.scss` déclare `Dos Amazigh` avec `@font-face` et l'applique au titre `#app-name`.
|
||||
BIN
crates/ksp-app-store-desk/frontend/imgs/logo.png
Normal file
BIN
crates/ksp-app-store-desk/frontend/imgs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
crates/ksp-app-store-desk/frontend/imgs/splash.png
Normal file
BIN
crates/ksp-app-store-desk/frontend/imgs/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
338
crates/ksp-app-store-desk/frontend/main.html
Normal file
338
crates/ksp-app-store-desk/frontend/main.html
Normal file
@@ -0,0 +1,338 @@
|
||||
<!-- file: crates/ksp-app-store-desk/frontend/main.html -->
|
||||
<!-- version: 6 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<link rel="icon" href="imgs/logo.png">
|
||||
<link rel="stylesheet" href="sass/main.scss">
|
||||
<title>Store Desk — Overview</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<nav class="navbar h-100 py-0 bg-light text-dark">
|
||||
<div class="container-fluid px-4 flex-nowrap">
|
||||
<div class="navbar-brand d-flex align-items-center me-4 flex-nowrap">
|
||||
<img alt="KSP" src="imgs/logo.png" class="app-logo">
|
||||
<span class="ps-2 fs-4 fw-semibold text-primary text-nowrap">Store Desk</span>
|
||||
<span class="mx-2 fs-5 text-body-secondary" aria-hidden="true">—</span>
|
||||
<span id="headerViewTitle" class="fs-5 text-body text-nowrap">Overview</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main container-fluid p-0">
|
||||
<div class="row g-0 h-100 flex-nowrap">
|
||||
<aside class="col-auto app-sidebar border-end bg-body-tertiary">
|
||||
<div class="app-sidebar-scroll h-100" data-simplebar>
|
||||
<nav class="nav nav-pills flex-column gap-1 p-3" aria-label="Navigation Store Desk">
|
||||
<button class="nav-link text-start active" type="button" data-view="overview">
|
||||
<i class="fa-solid fa-gauge-high me-2" aria-hidden="true"></i>Overview
|
||||
</button>
|
||||
<button class="nav-link text-start" type="button" data-view="transactions">
|
||||
<i class="fa-solid fa-receipt me-2" aria-hidden="true"></i>RAW Transactions
|
||||
</button>
|
||||
<button class="nav-link text-start" type="button" data-view="accounts">
|
||||
<i class="fa-solid fa-box-archive me-2" aria-hidden="true"></i>RAW Accounts
|
||||
</button>
|
||||
<button class="nav-link text-start" type="button" data-view="diagnostics">
|
||||
<i class="fa-solid fa-stethoscope me-2" aria-hidden="true"></i>Diagnostics
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="col app-content app-scrollable h-100" data-simplebar>
|
||||
<div class="container-fluid px-4 py-4">
|
||||
<div class="card app-shell-card mx-auto shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<section data-view-panel="overview">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Overview</h1>
|
||||
<p class="text-body-secondary mb-0">État backend-neutral du Store sélectionné par le composite Config. Aucune donnée RAW complète n'est chargée ici.</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span id="appVersionBadge" class="badge text-bg-secondary">—</span>
|
||||
<button id="refreshOverview" class="btn btn-outline-primary" type="button">
|
||||
<i class="fa-solid fa-arrows-rotate me-1" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Store profile</span><div id="overviewStoreProfile" class="fw-semibold mt-2">—</div></div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Target</span><div id="overviewStoreTarget" class="fw-semibold mt-2">—</div></div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Health</span><div id="overviewStoreHealth" class="fw-semibold mt-2">—</div></div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Pool</span><div id="overviewStorePool" class="fw-semibold mt-2">—</div></div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Migration</span><div id="overviewStoreMigration" class="fw-semibold mt-2">—</div></div></div></div>
|
||||
</div>
|
||||
<div id="overviewStoreDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
|
||||
</section>
|
||||
|
||||
<section data-view-panel="transactions" hidden>
|
||||
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">RAW Transactions</h1>
|
||||
<p class="text-body-secondary mb-0">Inspection Store random-access ; DataTables reste l'unique propriétaire de la pagination visible.</p>
|
||||
</div>
|
||||
<button id="refreshTransactions" class="btn btn-outline-primary" type="button">
|
||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="transactionSlotMin">Slot minimum</label>
|
||||
<input id="transactionSlotMin" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="transactionSlotMax">Slot maximum</label>
|
||||
<input id="transactionSlotMax" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="transactionDirection">Direction canonique</label>
|
||||
<select id="transactionDirection" class="form-select">
|
||||
<option value="descending" selected>Descending</option>
|
||||
<option value="ascending">Ascending</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transactionsQueryError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<div class="table-responsive app-store-table-container">
|
||||
<table id="rawTransactionsTable" class="table table-hover align-middle mb-0 app-store-table">
|
||||
<caption class="visually-hidden">Inspection server-side des transactions RAW.</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Signature</th>
|
||||
<th>Slot</th>
|
||||
<th>Block time</th>
|
||||
<th>Format ID</th>
|
||||
<th>Version</th>
|
||||
<th>Payload size</th>
|
||||
<th>Content hash</th>
|
||||
<th>Retention</th>
|
||||
<th>Détail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section data-view-panel="accounts" hidden>
|
||||
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">RAW Accounts</h1>
|
||||
<p class="text-body-secondary mb-0">Inspection Store random-access ; DataTables reste l'unique propriétaire de la pagination visible.</p>
|
||||
</div>
|
||||
<button id="refreshAccounts" class="btn btn-outline-primary" type="button">
|
||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-5">
|
||||
<label class="form-label" for="accountPubkey">Pubkey exacte</label>
|
||||
<input id="accountPubkey" class="form-control font-monospace" type="text" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-2">
|
||||
<label class="form-label" for="accountSlotMin">Slot minimum</label>
|
||||
<input id="accountSlotMin" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-2">
|
||||
<label class="form-label" for="accountSlotMax">Slot maximum</label>
|
||||
<input id="accountSlotMax" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<label class="form-label" for="accountDirection">Direction canonique</label>
|
||||
<select id="accountDirection" class="form-select">
|
||||
<option value="descending" selected>Descending</option>
|
||||
<option value="ascending">Ascending</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accountsQueryError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<div class="table-responsive app-store-table-container">
|
||||
<table id="rawAccountsTable" class="table table-hover align-middle mb-0 app-store-table">
|
||||
<caption class="visually-hidden">Inspection server-side des états RAW Account.</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pubkey</th>
|
||||
<th>Slot</th>
|
||||
<th>State hash</th>
|
||||
<th>Owner</th>
|
||||
<th>Lamports</th>
|
||||
<th>Executable</th>
|
||||
<th>Rent epoch</th>
|
||||
<th>Data length</th>
|
||||
<th>Détail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</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 Config / Logging / Store. Les URI et détails physiques sensibles restent hors IPC.</p>
|
||||
</div>
|
||||
<button id="refreshDiagnostics" class="btn btn-outline-primary" type="button">
|
||||
<i class="fa-solid fa-arrows-rotate me-1" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header fw-semibold">Runtime</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0 app-runtime-list">
|
||||
<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 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">Store profile</dt><dd id="runtimeStoreProfile" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Store target</dt><dd id="runtimeStoreTarget" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Store health</dt><dd id="runtimeStoreHealth" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Migration</dt><dd id="runtimeStoreMigration" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Pool</dt><dd id="runtimeStorePool" 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 id="runtimeStoreDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="transactionDetailModal" class="modal fade" tabindex="-1" aria-labelledby="transactionDetailTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="transactionDetailTitle" class="modal-title h5">RAW Transaction detail</h2>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="transactionDetailError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<dl class="row app-runtime-list mb-4">
|
||||
<dt class="col-sm-3">Signature</dt><dd id="transactionDetailSignature" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Slot</dt><dd id="transactionDetailSlot" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Block time</dt><dd id="transactionDetailBlockTime" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Format</dt><dd id="transactionDetailFormat" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Content hash</dt><dd id="transactionDetailContentHash" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Retention</dt><dd id="transactionDetailRetention" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Payload size</dt><dd id="transactionDetailPayloadSize" class="col-sm-9">—</dd>
|
||||
</dl>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-2">
|
||||
<h3 class="h6 mb-0">Payload preview hex</h3>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button id="transactionDetailPayloadPreviewCopy" class="btn btn-sm btn-outline-secondary" type="button" title="Copier le preview hex affiché" aria-label="Copier le preview hex affiché" hidden>
|
||||
<i class="fa-regular fa-copy" aria-hidden="true"></i>
|
||||
<span>Copier</span>
|
||||
</button>
|
||||
<span id="transactionDetailPreviewTruncated" class="badge text-bg-warning" hidden>Preview tronquée à 512 bytes</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="transactionDetailPayloadPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0">—</pre>
|
||||
<section class="border-top mt-4 pt-4" aria-labelledby="transactionObservationsTitle">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h3 id="transactionObservationsTitle" class="h6 mb-1">Observations d’acquisition</h3>
|
||||
<p class="small text-body-secondary mb-0">Provenance sûre uniquement ; aucun payload source n’est chargé.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transactionObservationsError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<div class="table-responsive">
|
||||
<table id="transactionObservationsTable" class="table table-hover align-middle mb-0 app-store-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Received</th><th>Observed</th><th>Provider</th><th>Protocol</th><th>Method</th><th>Origin</th><th>Endpoint</th><th>Commitment</th><th>Session</th><th>Filter</th><th>Source hash</th><th>Source bytes</th><th>Observation key</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="accountDetailModal" class="modal fade" tabindex="-1" aria-labelledby="accountDetailTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="accountDetailTitle" class="modal-title h5">RAW Account detail</h2>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="accountDetailError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<dl class="row app-runtime-list mb-4">
|
||||
<dt class="col-sm-3">Pubkey</dt><dd id="accountDetailPubkey" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Slot</dt><dd id="accountDetailSlot" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">State hash</dt><dd id="accountDetailStateHash" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Owner</dt><dd id="accountDetailOwner" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Lamports</dt><dd id="accountDetailLamports" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Executable</dt><dd id="accountDetailExecutable" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Rent epoch</dt><dd id="accountDetailRentEpoch" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Data length</dt><dd id="accountDetailDataLength" class="col-sm-9">—</dd>
|
||||
</dl>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-2">
|
||||
<h3 class="h6 mb-0">Account data preview hex</h3>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button id="accountDetailDataPreviewCopy" class="btn btn-sm btn-outline-secondary" type="button" title="Copier le preview hex affiché" aria-label="Copier le preview hex affiché" hidden>
|
||||
<i class="fa-regular fa-copy" aria-hidden="true"></i>
|
||||
<span>Copier</span>
|
||||
</button>
|
||||
<span id="accountDetailPreviewTruncated" class="badge text-bg-warning" hidden>Preview tronquée à 512 bytes</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="accountDetailDataPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0">—</pre>
|
||||
<section class="border-top mt-4 pt-4" aria-labelledby="accountObservationsTitle">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h3 id="accountObservationsTitle" class="h6 mb-1">Observations d’acquisition</h3>
|
||||
<p class="small text-body-secondary mb-0">Provenance sûre et métadonnées Yellowstone optionnelles ; aucun byte Account n’est chargé.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accountObservationsError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<div class="table-responsive">
|
||||
<table id="accountObservationsTable" class="table table-hover align-middle mb-0 app-store-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Received</th><th>Observed</th><th>Provider</th><th>Protocol</th><th>Method</th><th>Origin</th><th>Endpoint</th><th>Commitment</th><th>Session</th><th>Filter</th><th>Source hash</th><th>Source bytes</th><th>Observation key</th><th>Startup</th><th>Write version</th><th>Tx signature</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center justify-content-center">
|
||||
<small>© 2026 SASEDEV</small>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="ts/main.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
117
crates/ksp-app-store-desk/frontend/sass/_app.scss
Normal file
117
crates/ksp-app-store-desk/frontend/sass/_app.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/sass/_app.scss
|
||||
// version: 2
|
||||
|
||||
$app-header-height: 72px;
|
||||
$app-footer-height: 42px;
|
||||
$app-sidebar-width: 250px;
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: $gray-100;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
position: fixed;
|
||||
inset: 0 0 auto;
|
||||
height: $app-header-height;
|
||||
z-index: 1020;
|
||||
border-bottom: 2px solid rgba($primary, 0.3);
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
position: fixed;
|
||||
inset: auto 0 0;
|
||||
height: $app-footer-height;
|
||||
z-index: 1020;
|
||||
border-top: 2px solid rgba($primary, 0.3);
|
||||
}
|
||||
|
||||
.app-main {
|
||||
position: relative;
|
||||
height: calc(100vh - $app-header-height - $app-footer-height);
|
||||
margin-top: $app-header-height;
|
||||
margin-bottom: $app-footer-height;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main > .row {
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-scrollable {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.app-shell-card {
|
||||
max-width: 1320px;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
width: $app-sidebar-width;
|
||||
min-width: $app-sidebar-width;
|
||||
max-width: $app-sidebar-width;
|
||||
}
|
||||
|
||||
.app-sidebar-scroll,
|
||||
.app-content {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-sidebar .nav-link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-runtime-list dd,
|
||||
.app-store-table td:first-child,
|
||||
.app-store-table td:nth-child(2) {
|
||||
font-family: var(--bs-font-monospace);
|
||||
}
|
||||
|
||||
.app-store-table-container {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
|
||||
.app-copyable-long-text {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: .375rem;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-copyable-long-text__value {
|
||||
cursor: help;
|
||||
display: inline-block;
|
||||
max-width: 24ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-copyable-long-text__copy {
|
||||
flex: 0 0 auto;
|
||||
line-height: 1;
|
||||
padding: .25rem .375rem;
|
||||
}
|
||||
160
crates/ksp-app-store-desk/frontend/sass/_bootswatch.scss
Normal file
160
crates/ksp-app-store-desk/frontend/sass/_bootswatch.scss
Normal file
@@ -0,0 +1,160 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/sass/_bootswatch.scss
|
||||
// version: 1
|
||||
|
||||
// Pulse 5.3.8
|
||||
// Bootswatch
|
||||
|
||||
|
||||
// Variables
|
||||
|
||||
// Buttons
|
||||
|
||||
.btn {
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:active:focus,
|
||||
&.active:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&-secondary {
|
||||
color: $gray-900;
|
||||
background-color: $white;
|
||||
border-color: #ccc;
|
||||
|
||||
&:hover {
|
||||
color: $gray-900;
|
||||
background-color: $gray-300;
|
||||
border-color: $gray-500;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: tint-color($gray-900, 5%);
|
||||
background-color: $white;
|
||||
border-color: tint-color(#ccc, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&-warning {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&-primary:focus {
|
||||
box-shadow: 0 0 5px tint-color($primary, 10%);
|
||||
}
|
||||
|
||||
&-secondary:focus {
|
||||
box-shadow: 0 0 5px $gray-400;
|
||||
}
|
||||
|
||||
&-success:focus {
|
||||
box-shadow: 0 0 5px tint-color($success, 10%);
|
||||
}
|
||||
|
||||
&-info:focus {
|
||||
box-shadow: 0 0 5px tint-color($info, 10%);
|
||||
}
|
||||
|
||||
&-warning:focus {
|
||||
box-shadow: 0 0 5px tint-color($warning, 10%);
|
||||
}
|
||||
|
||||
&-danger:focus {
|
||||
box-shadow: 0 0 5px tint-color($danger, 10%);
|
||||
}
|
||||
|
||||
&.disabled:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Tables
|
||||
|
||||
.table .thead-dark th {
|
||||
background-color: $secondary;
|
||||
border-color: $table-border-color;
|
||||
}
|
||||
|
||||
.table-primary,
|
||||
.table-secondary,
|
||||
.table-success,
|
||||
.table-warning,
|
||||
.table-danger,
|
||||
.table-info,
|
||||
.table-light {
|
||||
--#{$prefix}table-color: #{$body-color};
|
||||
}
|
||||
|
||||
// Forms
|
||||
|
||||
.form-control:focus {
|
||||
box-shadow: 0 0 5px rgba(100, 65, 164, .4);
|
||||
}
|
||||
|
||||
// Navs
|
||||
|
||||
.nav-tabs {
|
||||
|
||||
.nav-link,
|
||||
.nav-link.active {
|
||||
border-width: 0 0 1px;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active,
|
||||
.nav-link.active:hover,
|
||||
.nav-link.active:focus {
|
||||
border-bottom: 1px solid $primary;
|
||||
}
|
||||
|
||||
.nav-item+.nav-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
&-item.active {
|
||||
color: $gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
// Indicators
|
||||
|
||||
.badge {
|
||||
&.bg-light {
|
||||
color: $dark;
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bars
|
||||
|
||||
.progress {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// Containers
|
||||
|
||||
.list-group {
|
||||
&-item {
|
||||
color: rgba(255, 255, 255, .8);
|
||||
|
||||
&.active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: 700;
|
||||
|
||||
&:hover {
|
||||
background-color: $list-group-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled:hover {
|
||||
color: $list-group-disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
crates/ksp-app-store-desk/frontend/sass/_fontawesome.scss
Normal file
19
crates/ksp-app-store-desk/frontend/sass/_fontawesome.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/sass/_fontawesome.scss
|
||||
// version: 1
|
||||
|
||||
//@use '@fortawesome/fontawesome-free/scss/variables' with (
|
||||
// // customizing $font-path - make sure it points to where your webfonts are stored in your project
|
||||
// $font-path: '../webfonts',
|
||||
//);
|
||||
@use '@fortawesome/fontawesome-free/scss/variables' with (
|
||||
// use fonts from @fortawesome/fontawesome-free
|
||||
$font-path: '@fortawesome/fontawesome-free/webfonts',
|
||||
);
|
||||
// load Font Awesome core
|
||||
@use '@fortawesome/fontawesome-free/scss/fontawesome';
|
||||
|
||||
// load and make available Font Awesome helpers (mixins, functions, and variables)
|
||||
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
|
||||
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
|
||||
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
|
||||
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;
|
||||
248
crates/ksp-app-store-desk/frontend/sass/_simplebar.scss
Normal file
248
crates/ksp-app-store-desk/frontend/sass/_simplebar.scss
Normal file
@@ -0,0 +1,248 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/sass/_simplebar.scss
|
||||
// version: 1
|
||||
|
||||
/* Rtl support */
|
||||
[data-simplebar] {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.simplebar-wrapper {
|
||||
overflow: hidden;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
max-width: inherit;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.simplebar-mask {
|
||||
direction: inherit;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
// z-index: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.simplebar-offset {
|
||||
direction: inherit !important;
|
||||
box-sizing: inherit !important;
|
||||
resize: none !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.simplebar-content-wrapper {
|
||||
direction: inherit;
|
||||
box-sizing: border-box !important;
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-hide-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.simplebar-content {
|
||||
&:before {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-placeholder {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer-wrapper {
|
||||
box-sizing: inherit !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 1px;
|
||||
position: relative;
|
||||
float: left;
|
||||
max-height: 1px;
|
||||
overflow: hidden;
|
||||
// z-index: -1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
flex-grow: inherit;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer {
|
||||
box-sizing: inherit;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1000%;
|
||||
width: 1000%;
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
// z-index: -1;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
// z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-simplebar].simplebar-dragging {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
.simplebar-content {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-scrollbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 10px;
|
||||
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background: black;
|
||||
border-radius: 7px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s 0.5s linear;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-scrollbar.simplebar-visible {
|
||||
&:before {
|
||||
opacity: 0.5;
|
||||
transition-delay: 0s;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-vertical {
|
||||
top: 0;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-horizontal {
|
||||
left: 0;
|
||||
height: 11px;
|
||||
|
||||
.simplebar-scrollbar {
|
||||
right: auto;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
min-height: 0;
|
||||
min-width: 10px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
[data-simplebar-direction='rtl'] {
|
||||
.simplebar-track.simplebar-vertical {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-dummy-scrollbar-size {
|
||||
direction: rtl;
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: 500px;
|
||||
width: 500px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: scrollbar !important;
|
||||
|
||||
>div {
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
95
crates/ksp-app-store-desk/frontend/sass/_variables.scss
Normal file
95
crates/ksp-app-store-desk/frontend/sass/_variables.scss
Normal file
@@ -0,0 +1,95 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/sass/_variables.scss
|
||||
// version: 1
|
||||
|
||||
// Pulse 5.3.8
|
||||
// Bootswatch
|
||||
|
||||
$theme: "pulse" !default;
|
||||
|
||||
//
|
||||
// Color system
|
||||
//
|
||||
|
||||
$white: #fff !default;
|
||||
$gray-100: #fafafa !default;
|
||||
$gray-200: #f9f8fc !default;
|
||||
$gray-300: #ededed !default;
|
||||
$gray-400: #cbc8d0 !default;
|
||||
$gray-500: #adb5bd !default;
|
||||
$gray-600: #868e96 !default;
|
||||
$gray-700: #444 !default;
|
||||
$gray-800: #343a40 !default;
|
||||
$gray-900: #17141f !default;
|
||||
$black: #000 !default;
|
||||
|
||||
$blue: #007bff !default;
|
||||
$indigo: #6610f2 !default;
|
||||
$purple: #593196 !default;
|
||||
$pink: #e83e8c !default;
|
||||
$red: #fc3939 !default;
|
||||
$orange: #fd7e14 !default;
|
||||
$yellow: #efa31d !default;
|
||||
$green: #13b955 !default;
|
||||
$teal: #20c997 !default;
|
||||
$cyan: #009cdc !default;
|
||||
|
||||
$primary: $purple !default;
|
||||
$secondary: #a991d4 !default;
|
||||
$success: $green !default;
|
||||
$info: $cyan !default;
|
||||
$warning: $yellow !default;
|
||||
$danger: $red !default;
|
||||
$light: $gray-200 !default;
|
||||
$dark: $gray-900 !default;
|
||||
|
||||
$min-contrast-ratio: 2.1 !default;
|
||||
|
||||
// Options
|
||||
|
||||
$enable-rounded: false !default;
|
||||
|
||||
// Body
|
||||
|
||||
$body-color: $gray-700 !default;
|
||||
|
||||
// Links
|
||||
|
||||
$link-hover-color: $primary !default;
|
||||
|
||||
// Tables
|
||||
|
||||
$table-color: initial !default;
|
||||
|
||||
$table-border-color: rgba(0, 0, 0, .05) !default;
|
||||
|
||||
// Forms
|
||||
|
||||
$input-focus-border-color: $primary !default;
|
||||
|
||||
// Dropdowns
|
||||
|
||||
$dropdown-link-hover-color: $white !default;
|
||||
$dropdown-link-hover-bg: $primary !default;
|
||||
|
||||
// Navs
|
||||
|
||||
$nav-tabs-border-color: $gray-300 !default;
|
||||
$nav-tabs-link-hover-border-color: $primary !default;
|
||||
|
||||
// Navbar
|
||||
|
||||
$navbar-padding-y: 1.2rem !default;
|
||||
|
||||
// Progress bars
|
||||
|
||||
$progress-bg: $gray-300 !default;
|
||||
$progress-bar-bg: $primary !default;
|
||||
|
||||
// List group
|
||||
|
||||
$list-group-bg: $gray-900 !default;
|
||||
$list-group-border-color: transparent !default;
|
||||
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
|
||||
$list-group-active-color: $white !default;
|
||||
$list-group-active-bg: $list-group-bg !default;
|
||||
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;
|
||||
10
crates/ksp-app-store-desk/frontend/sass/main.scss
Normal file
10
crates/ksp-app-store-desk/frontend/sass/main.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/sass/main.scss
|
||||
// version: 1
|
||||
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "variables";
|
||||
@import "fontawesome";
|
||||
@import "simplebar";
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
@import "bootswatch";
|
||||
@import "app";
|
||||
137
crates/ksp-app-store-desk/frontend/sass/splash.scss
Normal file
137
crates/ksp-app-store-desk/frontend/sass/splash.scss
Normal file
@@ -0,0 +1,137 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/sass/splash.scss
|
||||
// version: 1
|
||||
|
||||
@font-face {
|
||||
font-family: "Dos Amazigh";
|
||||
src: url("../fonts/DOS_Amazigh.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
position: relative;
|
||||
width: 960px;
|
||||
max-width: 100%;
|
||||
height: 637px;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
#splash-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#app-name {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 2;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
font-family: "Dos Amazigh", sans-serif;
|
||||
font-size: 76px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#debug-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
max-height: 30%;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#messages-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
max-height: 30%;
|
||||
overflow-y: auto;
|
||||
padding: 10px 10px 36px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#debug-info::-webkit-scrollbar,
|
||||
#messages-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.splash-message {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
animation: splashMessageFadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.splash-message.info { background: rgba(33, 150, 243, 0.22); }
|
||||
.splash-message.warning { background: rgba(255, 152, 0, 0.22); }
|
||||
.splash-message.error { background: rgba(244, 67, 54, 0.22); }
|
||||
.splash-message.success { background: rgba(76, 175, 80, 0.22); }
|
||||
|
||||
@keyframes splashMessageFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
24
crates/ksp-app-store-desk/frontend/splash.html
Normal file
24
crates/ksp-app-store-desk/frontend/splash.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!-- file: crates/ksp-app-store-desk/frontend/splash.html -->
|
||||
<!-- version: 1 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<link rel="icon" href="imgs/logo.png">
|
||||
<link rel="stylesheet" href="sass/splash.scss">
|
||||
<title>KSP Store Desk — Chargement</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="splash-container" style="opacity: 0;">
|
||||
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Store Desk">
|
||||
<div id="app-name">Store</div>
|
||||
<div id="debug-info" aria-live="off" hidden></div>
|
||||
<div id="messages-container" aria-live="polite"></div>
|
||||
</div>
|
||||
<script type="module" src="ts/splash.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
110
crates/ksp-app-store-desk/frontend/ts/frontend_log.ts
Normal file
110
crates/ksp-app-store-desk/frontend/ts/frontend_log.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/ts/frontend_log.ts
|
||||
// version: 1
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_store_desk/frontend_logging/FrontendLogPayloadDto.ts";
|
||||
|
||||
export type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error";
|
||||
export type FrontendLogTargetId = "frontend" | "main" | "splash";
|
||||
|
||||
type ConsoleMethod = (...items: unknown[]) => void;
|
||||
|
||||
const originalConsole = {
|
||||
trace: console.trace.bind(console),
|
||||
debug: console.debug.bind(console),
|
||||
log: console.log.bind(console),
|
||||
info: console.info.bind(console),
|
||||
warn: console.warn.bind(console),
|
||||
error: console.error.bind(console),
|
||||
};
|
||||
|
||||
function stringifyItem(item: unknown): string {
|
||||
if (item instanceof Error) {
|
||||
return item.stack ?? item.message;
|
||||
}
|
||||
if (typeof item === "string") {
|
||||
return item;
|
||||
}
|
||||
try {
|
||||
const serialized = JSON.stringify(item);
|
||||
return serialized ?? String(item);
|
||||
} catch {
|
||||
return String(item);
|
||||
}
|
||||
}
|
||||
|
||||
function formatMessage(items: unknown[]): string {
|
||||
return items.map(item => stringifyItem(item)).join(" ");
|
||||
}
|
||||
|
||||
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
|
||||
const payload: FrontendLogPayloadDto = {
|
||||
level,
|
||||
targetId,
|
||||
message,
|
||||
};
|
||||
await invoke("emit_frontend_log", { payload });
|
||||
}
|
||||
|
||||
function writeOriginalConsole(level: FrontendLogLevel, message: string): void {
|
||||
return level === "trace"
|
||||
? originalConsole.trace(message)
|
||||
: level === "debug"
|
||||
? originalConsole.debug(message)
|
||||
: level === "info"
|
||||
? originalConsole.info(message)
|
||||
: level === "warn"
|
||||
? originalConsole.warn(message)
|
||||
: originalConsole.error(message);
|
||||
}
|
||||
|
||||
export async function emitFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
|
||||
writeOriginalConsole(level, message);
|
||||
await sendFrontendLog(level, targetId, message);
|
||||
}
|
||||
|
||||
export function frontendTrace(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.trace(message);
|
||||
void sendFrontendLog("trace", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
export function frontendDebug(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.debug(message);
|
||||
void sendFrontendLog("debug", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
export function frontendInfo(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.info(message);
|
||||
void sendFrontendLog("info", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
export function frontendWarn(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.warn(message);
|
||||
void sendFrontendLog("warn", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
export function frontendError(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.error(message);
|
||||
void sendFrontendLog("error", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
function buildConsoleBridge(level: FrontendLogLevel, targetId: FrontendLogTargetId, original: ConsoleMethod): ConsoleMethod {
|
||||
return (...items: unknown[]) => {
|
||||
original(...items);
|
||||
void sendFrontendLog(level, targetId, formatMessage(items)).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
};
|
||||
}
|
||||
|
||||
export function installFrontendConsoleBridge(targetId: FrontendLogTargetId): void {
|
||||
console.trace = buildConsoleBridge("trace", targetId, originalConsole.trace);
|
||||
console.debug = buildConsoleBridge("debug", targetId, originalConsole.debug);
|
||||
console.log = buildConsoleBridge("info", targetId, originalConsole.log);
|
||||
console.info = buildConsoleBridge("info", targetId, originalConsole.info);
|
||||
console.warn = buildConsoleBridge("warn", targetId, originalConsole.warn);
|
||||
console.error = buildConsoleBridge("error", targetId, originalConsole.error);
|
||||
}
|
||||
17
crates/ksp-app-store-desk/frontend/ts/invoke.ts
Normal file
17
crates/ksp-app-store-desk/frontend/ts/invoke.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/ts/invoke.ts
|
||||
// version: 1
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { frontendDebug, frontendError, frontendTrace, type FrontendLogTargetId } from "./frontend_log";
|
||||
|
||||
export async function invokeKsp<T>(targetId: FrontendLogTargetId, command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
frontendDebug(targetId, "Frontend IPC command requested", { command });
|
||||
try {
|
||||
const result = await invoke<T>(command, args);
|
||||
frontendTrace(targetId, "Frontend IPC command completed", { command });
|
||||
return result;
|
||||
} catch (caughtError) {
|
||||
frontendError(targetId, "Frontend IPC command failed", { command });
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
1030
crates/ksp-app-store-desk/frontend/ts/main.ts
Normal file
1030
crates/ksp-app-store-desk/frontend/ts/main.ts
Normal file
File diff suppressed because it is too large
Load Diff
131
crates/ksp-app-store-desk/frontend/ts/splash.ts
Normal file
131
crates/ksp-app-store-desk/frontend/ts/splash.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/ts/splash.ts
|
||||
// version: 1
|
||||
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { SplashOrderDto } from "./bindings/ksp_app_store_desk/splash/SplashOrderDto.ts";
|
||||
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
installFrontendConsoleBridge("splash");
|
||||
|
||||
let activeOpacityFrame: number | null = null;
|
||||
|
||||
function easeInOut(value: number): number {
|
||||
return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 2) / 2;
|
||||
}
|
||||
|
||||
function normalizeDurationMs(value: number | null): number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
||||
}
|
||||
|
||||
async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise<void> {
|
||||
frontendTrace("splash", "Splash opacity animation started", { fromOpacity, toOpacity, durationMs });
|
||||
if (activeOpacityFrame !== null) {
|
||||
cancelAnimationFrame(activeOpacityFrame);
|
||||
activeOpacityFrame = null;
|
||||
}
|
||||
element.style.opacity = fromOpacity.toString();
|
||||
element.style.willChange = "opacity";
|
||||
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
|
||||
await new Promise<void>(resolve => {
|
||||
const startedAt = performance.now();
|
||||
const opacityDelta = toOpacity - fromOpacity;
|
||||
const updateOpacity = (currentTime: number): void => {
|
||||
const elapsedMs = currentTime - startedAt;
|
||||
const rawProgress = durationMs === 0 ? 1 : Math.min(elapsedMs / durationMs, 1);
|
||||
element.style.opacity = (fromOpacity + opacityDelta * easeInOut(rawProgress)).toString();
|
||||
if (rawProgress >= 1) {
|
||||
element.style.opacity = toOpacity.toString();
|
||||
element.style.willChange = "auto";
|
||||
activeOpacityFrame = null;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
activeOpacityFrame = requestAnimationFrame(updateOpacity);
|
||||
};
|
||||
activeOpacityFrame = requestAnimationFrame(updateOpacity);
|
||||
});
|
||||
frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs });
|
||||
}
|
||||
|
||||
function scrollToLatest(element: HTMLElement): void {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
|
||||
function addMessage(message: string, status: string | null): void {
|
||||
const container = document.querySelector<HTMLElement>("#messages-container");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const line = document.createElement("div");
|
||||
const safeStatus = status === "warning" || status === "error" || status === "success" ? status : "info";
|
||||
line.className = `splash-message ${safeStatus}`;
|
||||
line.textContent = message;
|
||||
container.appendChild(line);
|
||||
scrollToLatest(container);
|
||||
}
|
||||
|
||||
function addDebugMessage(message: string): void {
|
||||
const container = document.querySelector<HTMLElement>("#debug-info");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.hidden = false;
|
||||
const line = document.createElement("div");
|
||||
line.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
|
||||
container.appendChild(line);
|
||||
scrollToLatest(container);
|
||||
}
|
||||
|
||||
async function handleSplashOrder(order: SplashOrderDto): Promise<void> {
|
||||
frontendTrace("splash", "Splash order received", { action: order.action });
|
||||
const container = document.querySelector<HTMLElement>("#splash-container");
|
||||
if (order.action === "add_message" && order.message) {
|
||||
addMessage(order.message, order.status);
|
||||
return;
|
||||
}
|
||||
if (order.action === "add_debug" && order.message) {
|
||||
addDebugMessage(order.message);
|
||||
return;
|
||||
}
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
if (order.action === "fade_in") {
|
||||
await animateOpacity(container, 0, 1, normalizeDurationMs(order.durationMs));
|
||||
return;
|
||||
}
|
||||
if (order.action === "fade_out") {
|
||||
await animateOpacity(container, 1, 0, normalizeDurationMs(order.durationMs));
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeSplash(): Promise<void> {
|
||||
const windowLabel = getCurrentWindow().label;
|
||||
frontendInfo("splash", "Store Desk splash frontend loaded", { windowLabel });
|
||||
const container = document.querySelector<HTMLElement>("#splash-container");
|
||||
if (container) {
|
||||
container.style.opacity = "0";
|
||||
container.style.willChange = "opacity";
|
||||
frontendTrace("splash", "Splash container prepared for managed fade-in");
|
||||
}
|
||||
await listen<SplashOrderDto>("ksp-splash-order", event => {
|
||||
void handleSplashOrder(event.payload);
|
||||
});
|
||||
frontendDebug("splash", "Splash lifecycle listener installed");
|
||||
try {
|
||||
await invokeKsp<void>("splash", "splash_frontend_ready");
|
||||
} catch {
|
||||
addMessage("Le lifecycle du splash n'a pas pu démarrer.", "error");
|
||||
if (container) {
|
||||
container.style.opacity = "1";
|
||||
container.style.willChange = "auto";
|
||||
}
|
||||
frontendError("splash", "Splash readiness command failed");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
void initializeSplash();
|
||||
});
|
||||
BIN
crates/ksp-app-store-desk/icons/favicon.ico
Normal file
BIN
crates/ksp-app-store-desk/icons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
BIN
crates/ksp-app-store-desk/icons/favicon.png
Normal file
BIN
crates/ksp-app-store-desk/icons/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
27
crates/ksp-app-store-desk/package.json
Normal file
27
crates/ksp-app-store-desk/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "ksp-app-store-desk",
|
||||
"private": true,
|
||||
"version": "0.3.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fltsci/tauri-plugin-tracing": "^0.3",
|
||||
"@fortawesome/fontawesome-free": "^7.3",
|
||||
"@tauri-apps/api": "^2.11",
|
||||
"bootstrap": "^5.3",
|
||||
"datatables.net-bs5": "^3.0",
|
||||
"resize-observer-polyfill": "^1.5",
|
||||
"simplebar": "^6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.11",
|
||||
"@types/bootstrap": "^5.2",
|
||||
"@types/node": "^26.1",
|
||||
"sass-embedded": "^1.102",
|
||||
"typescript": "^7.0",
|
||||
"vite": "^8.2"
|
||||
}
|
||||
}
|
||||
187
crates/ksp-app-store-desk/src/app_state.rs
Normal file
187
crates/ksp-app-store-desk/src/app_state.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
// file: crates/ksp-app-store-desk/src/app_state.rs
|
||||
// version: 5
|
||||
|
||||
//! Shared backend state owned by the Store Desk Tauri application.
|
||||
|
||||
/// Shared Store Desk application state managed by Tauri.
|
||||
pub(crate) struct AppState {
|
||||
config_management: ksp_config_lib::ConfigManagement,
|
||||
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
|
||||
shutdown_started: std::sync::atomic::AtomicBool,
|
||||
splash_settings: crate::SplashSettings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool,
|
||||
store_startup: crate::StoreStartup,
|
||||
}
|
||||
|
||||
impl crate::AppState {
|
||||
/// Initializes Config ownership, composite-managed Logging, Store readiness and common splash settings.
|
||||
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 {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let runtime_identity = crate::launch_identity();
|
||||
let runtime_identity = match runtime_identity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let logging_startup = crate::initialize_logging(&config_management, &runtime_identity);
|
||||
let logging_startup = match logging_startup {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store_startup = tauri::async_runtime::block_on(crate::initialize_store(&config_management));
|
||||
let splash_settings = crate::SplashSettings::load();
|
||||
let splash_settings = match splash_settings {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"managed splash timings are invalid; using transient in-memory defaults"
|
||||
);
|
||||
crate::SplashSettings::fallback()
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
minimum_ms = splash_settings.minimum_ms(),
|
||||
minimum_source = splash_settings.minimum_source(),
|
||||
fade_in_ms = splash_settings.fade_in_ms(),
|
||||
fade_in_source = splash_settings.fade_in_source(),
|
||||
fade_out_ms = splash_settings.fade_out_ms(),
|
||||
fade_out_source = splash_settings.fade_out_source(),
|
||||
expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(),
|
||||
"resolved Store Desk splash timings"
|
||||
);
|
||||
return std::result::Result::Ok(Self {
|
||||
config_management,
|
||||
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
|
||||
guard: logging_startup.guard,
|
||||
active_profile_id: logging_startup.active_profile_id,
|
||||
fallback_active: logging_startup.fallback_active,
|
||||
startup_diagnostic: logging_startup.startup_diagnostic,
|
||||
}),
|
||||
shutdown_started: std::sync::atomic::AtomicBool::new(false),
|
||||
splash_settings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
|
||||
store_startup,
|
||||
});
|
||||
}
|
||||
|
||||
/// Builds the safe shell/Config/Logging status exposed by Store Desk diagnostics.
|
||||
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
|
||||
let document_count = self.config_management.engine().registry().descriptors().count();
|
||||
let document_count = u32::try_from(document_count);
|
||||
let document_count = match document_count {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Config registry contains too many descriptors for the Store Desk runtime DTO",
|
||||
)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let runtime = self.logging_runtime.lock();
|
||||
let runtime = match runtime {
|
||||
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,
|
||||
"Store Desk Logging runtime state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
let _keep_guard_alive = &runtime.guard;
|
||||
return std::result::Result::Ok(crate::ShellStatusDto {
|
||||
active_logging_profile: runtime.active_profile_id.clone(),
|
||||
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
config_document_count: document_count,
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
shell_phase: "pre.010-observations-ui".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Builds one fresh safe Store runtime/health projection for the Overview and Diagnostics screens.
|
||||
pub(crate) async fn store_runtime_status(&self) -> crate::StoreRuntimeStatusDto {
|
||||
return self.store_startup.status().await;
|
||||
}
|
||||
|
||||
/// Executes one bounded RAW account-state inspection query through the retained Store facade.
|
||||
pub(crate) async fn query_accounts(&self, request: crate::StoreAccountQueryRequestDto) -> ksp_core_lib::Result<crate::StoreAccountQueryResponseDto> {
|
||||
return self.store_startup.query_accounts(request).await;
|
||||
}
|
||||
|
||||
/// Loads one explicit RAW account-state detail through the retained Store facade.
|
||||
pub(crate) async fn account_detail(&self, request: crate::StoreAccountDetailRequestDto) -> ksp_core_lib::Result<crate::StoreAccountDetailDto> {
|
||||
return self.store_startup.account_detail(request).await;
|
||||
}
|
||||
|
||||
/// Executes one bounded metadata-only observation inspection for one explicit RAW account state.
|
||||
pub(crate) async fn query_account_observations(
|
||||
&self,
|
||||
request: crate::StoreAccountObservationQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreAccountObservationQueryResponseDto> {
|
||||
return self.store_startup.query_account_observations(request).await;
|
||||
}
|
||||
|
||||
/// Executes one bounded RAW transaction inspection query through the retained Store facade.
|
||||
pub(crate) async fn query_transactions(
|
||||
&self,
|
||||
request: crate::StoreTransactionQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionQueryResponseDto> {
|
||||
return self.store_startup.query_transactions(request).await;
|
||||
}
|
||||
|
||||
/// Loads one explicit RAW transaction detail through the retained Store facade.
|
||||
pub(crate) async fn transaction_detail(&self, request: crate::StoreTransactionDetailRequestDto) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
return self.store_startup.transaction_detail(request).await;
|
||||
}
|
||||
|
||||
/// Executes one bounded metadata-only observation inspection for one explicit RAW transaction.
|
||||
pub(crate) async fn query_transaction_observations(
|
||||
&self,
|
||||
request: crate::StoreTransactionObservationQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionObservationQueryResponseDto> {
|
||||
return self.store_startup.query_transaction_observations(request).await;
|
||||
}
|
||||
|
||||
/// Returns the resolved splash timings captured during application bootstrap.
|
||||
#[must_use]
|
||||
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
|
||||
return self.splash_settings;
|
||||
}
|
||||
|
||||
/// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition.
|
||||
pub(crate) fn begin_splash_sequence(&self) -> bool {
|
||||
return self
|
||||
.splash_sequence_started
|
||||
.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire)
|
||||
.is_ok();
|
||||
}
|
||||
|
||||
/// Marks graceful application shutdown as started and reports whether this caller won the one-shot transition.
|
||||
pub(crate) fn begin_shutdown(&self) -> bool {
|
||||
return self.shutdown_started.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire).is_ok();
|
||||
}
|
||||
|
||||
/// Explicitly closes the retained Store runtime when application shutdown begins.
|
||||
pub(crate) async fn close_store(&self) -> ksp_core_lib::Result<()> {
|
||||
return self.store_startup.close().await;
|
||||
}
|
||||
}
|
||||
|
||||
struct LoggingRuntimeState {
|
||||
guard: ksp_logging_lib::LoggingGuard,
|
||||
active_profile_id: std::option::Option<String>,
|
||||
fallback_active: bool,
|
||||
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
171
crates/ksp-app-store-desk/src/bootstrap.rs
Normal file
171
crates/ksp-app-store-desk/src/bootstrap.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
// file: crates/ksp-app-store-desk/src/bootstrap.rs
|
||||
// version: 3
|
||||
|
||||
//! Composite Config and verbose development Logging bootstrap for Store Desk.
|
||||
|
||||
/// Crate-internal Logging startup state shared by the application state.
|
||||
pub(crate) struct LoggingStartup {
|
||||
/// Guard that keeps the installed Logging runtime alive.
|
||||
pub(crate) guard: ksp_logging_lib::LoggingGuard,
|
||||
/// Active managed profile when Config resolved one successfully.
|
||||
pub(crate) active_profile_id: std::option::Option<String>,
|
||||
/// Whether a transient in-memory fallback was installed.
|
||||
pub(crate) fallback_active: bool,
|
||||
/// Safe startup diagnostic retained for the shell.
|
||||
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
|
||||
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
|
||||
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
|
||||
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = ksp_config_lib::ConfigFileRegistry::from_args(arguments);
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
|
||||
}
|
||||
|
||||
/// Loads the concrete Store Desk Config composite using its registered logical `file_id` and autonomous default profile.
|
||||
pub(crate) fn load_store_desk_composite(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
|
||||
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_STORE_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);
|
||||
}
|
||||
|
||||
/// Returns one required standard profile from the Store Desk composite after validating the referenced logical `file_id`.
|
||||
pub(crate) fn required_composite_component_profile(
|
||||
composite: &ksp_config_lib::ResolvedConfigComposite,
|
||||
component_id: &str,
|
||||
expected_file_id: &str,
|
||||
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigProfile> {
|
||||
let component = composite.component(component_id);
|
||||
let component = match component {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_CONFIG_COMPOSITE_INVALID, "Store Desk composite is missing a required component")
|
||||
.with_context("composite_file_id", composite.file_id().as_str())
|
||||
.with_context("composite_profile_id", composite.profile_id())
|
||||
.with_context("component_id", component_id),
|
||||
);
|
||||
},
|
||||
};
|
||||
if component.resolved().file_id().as_str() != expected_file_id {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_CONFIG_COMPOSITE_INVALID, "Store Desk composite component references an unexpected Config document")
|
||||
.with_context("composite_file_id", composite.file_id().as_str())
|
||||
.with_context("composite_profile_id", composite.profile_id())
|
||||
.with_context("component_id", component_id)
|
||||
.with_context("expected_file_id", expected_file_id)
|
||||
.with_context("actual_file_id", component.resolved().file_id().as_str()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(component.resolved().clone());
|
||||
}
|
||||
|
||||
/// Initializes development Logging from the Store Desk composite, with a trace-level in-memory fallback.
|
||||
pub(crate) fn initialize_logging(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
|
||||
};
|
||||
let composite = crate::load_store_desk_composite(management);
|
||||
let composite = match composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
|
||||
};
|
||||
let logging_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_LOGGING, ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
let logging_profile = match logging_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
|
||||
};
|
||||
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
||||
if let std::result::Result::Err(error) = store_profile {
|
||||
return initialize_fallback_logging(error, runtime_identity);
|
||||
}
|
||||
if logging_profile.profile_id() != crate::DEVELOPMENT_LOGGING_PROFILE {
|
||||
let error =
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_CONFIG_COMPOSITE_INVALID, "Store Desk development composite must select the supertrace Logging profile")
|
||||
.with_context("component_id", crate::COMPOSITE_COMPONENT_ID_LOGGING)
|
||||
.with_context("expected_profile_id", crate::DEVELOPMENT_LOGGING_PROFILE)
|
||||
.with_context("actual_profile_id", logging_profile.profile_id());
|
||||
return initialize_fallback_logging(error, runtime_identity);
|
||||
}
|
||||
let resolved = management.engine().resolve_logging_config_profile(&logging_profile, &environment);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
|
||||
};
|
||||
let active_profile_id = resolved.profile_id().to_owned();
|
||||
let settings = resolved.into_settings();
|
||||
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
|
||||
return match guard {
|
||||
std::result::Result::Ok(guard) => {
|
||||
ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
|
||||
active_profile = active_profile_id.as_str(),
|
||||
composite_profile = composite.profile_id(),
|
||||
"initialized Store Desk development logging from composite-managed supertrace configuration"
|
||||
);
|
||||
std::result::Result::Ok(crate::LoggingStartup {
|
||||
guard,
|
||||
active_profile_id: std::option::Option::Some(active_profile_id),
|
||||
fallback_active: false,
|
||||
startup_diagnostic: std::option::Option::None,
|
||||
})
|
||||
},
|
||||
std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
|
||||
};
|
||||
}
|
||||
|
||||
fn initialize_fallback_logging(
|
||||
initial_error: ksp_core_lib::Error,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
|
||||
let settings = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Trace,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::vec::Vec::new(),
|
||||
);
|
||||
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
|
||||
let guard = match guard {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(fallback_error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED, "Cannot initialize Store Desk fallback Logging runtime")
|
||||
.with_context("initial_error_domain", initial_error.code().domain())
|
||||
.with_context("initial_error_code", initial_error.code().code())
|
||||
.with_source(fallback_error),
|
||||
);
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
|
||||
error_domain = diagnostic.domain.as_str(),
|
||||
error_code = diagnostic.code.as_str(),
|
||||
"Store Desk uses transient trace-level fallback Logging"
|
||||
);
|
||||
return std::result::Result::Ok(crate::LoggingStartup {
|
||||
guard,
|
||||
active_profile_id: std::option::Option::None,
|
||||
fallback_active: true,
|
||||
startup_diagnostic: std::option::Option::Some(diagnostic),
|
||||
});
|
||||
}
|
||||
29
crates/ksp-app-store-desk/src/constants.rs
Normal file
29
crates/ksp-app-store-desk/src/constants.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
// file: crates/ksp-app-store-desk/src/constants.rs
|
||||
// version: 3
|
||||
|
||||
//! Application-owned tracing targets, domains and composite component identifiers.
|
||||
|
||||
/// Composite component identifier for Logging.
|
||||
pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
|
||||
/// Composite component identifier for Store.
|
||||
pub(crate) const COMPOSITE_COMPONENT_ID_STORE: &str = "store";
|
||||
/// Logging profile used while Store Desk is under active development.
|
||||
pub(crate) const DEVELOPMENT_LOGGING_PROFILE: &str = "supertrace";
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "store.bootstrap";
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain used by the Store Desk shell.
|
||||
pub(crate) const TRACING_DOMAIN_SHELL: &str = "store.shell";
|
||||
/// Structured domain used by Store readiness, inspection admission and shutdown operations.
|
||||
pub(crate) const TRACING_DOMAIN_STORE: &str = "store.runtime";
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||
/// Owning target for backend events emitted by Store Desk.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-app-store-desk";
|
||||
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-store-desk.frontend";
|
||||
/// Owning target for main-window frontend events.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-store-desk.frontend.main";
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-store-desk.frontend.splash";
|
||||
105
crates/ksp-app-store-desk/src/dto_account.rs
Normal file
105
crates/ksp-app-store-desk/src/dto_account.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
// file: crates/ksp-app-store-desk/src/dto_account.rs
|
||||
// version: 1
|
||||
|
||||
//! Application-owned DTOs for RAW account-state inspection and detail commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Bounded Store-owned query request projected from one DataTables page request.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountQueryRequestDto.ts")]
|
||||
pub(crate) struct StoreAccountQueryRequestDto {
|
||||
/// Stable Store sort direction code (`ascending` or `descending`).
|
||||
pub(crate) direction: String,
|
||||
/// Whitelisted page size (`25`, `50`, or `100`).
|
||||
pub(crate) limit: u32,
|
||||
/// Absolute zero-based inspection offset encoded as exact decimal text.
|
||||
pub(crate) offset: String,
|
||||
/// Optional exact account public key in canonical base58 text.
|
||||
pub(crate) pubkey: std::option::Option<String>,
|
||||
/// Optional inclusive maximum slot encoded as decimal text.
|
||||
pub(crate) slot_max: std::option::Option<String>,
|
||||
/// Optional inclusive minimum slot encoded as decimal text.
|
||||
pub(crate) slot_min: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// Data-free RAW account-state row returned to the server-side DataTables adapter.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountRowDto.ts")]
|
||||
pub(crate) struct StoreAccountRowDto {
|
||||
/// Exact complete account-data length encoded as decimal text.
|
||||
pub(crate) data_length_decimal: String,
|
||||
/// Whether the account is executable.
|
||||
pub(crate) executable: bool,
|
||||
/// Exact lamport balance encoded as decimal text.
|
||||
pub(crate) lamports_decimal: String,
|
||||
/// Account owner program public key in canonical base58 text.
|
||||
pub(crate) owner: String,
|
||||
/// Account public key in canonical base58 text.
|
||||
pub(crate) pubkey: String,
|
||||
/// Exact rent epoch encoded as decimal text.
|
||||
pub(crate) rent_epoch_decimal: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot_decimal: String,
|
||||
/// Canonical account-state hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) state_hash: String,
|
||||
}
|
||||
|
||||
/// Server-side account-state table result using exact decimal counts.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountQueryResponseDto.ts")]
|
||||
pub(crate) struct StoreAccountQueryResponseDto {
|
||||
/// Exact count after optional Store filters.
|
||||
pub(crate) records_filtered_decimal: String,
|
||||
/// Exact count in the mandatory Store network scope before optional filters.
|
||||
pub(crate) records_total_decimal: String,
|
||||
/// Data-free inspection rows for the requested random-access page.
|
||||
pub(crate) rows: std::vec::Vec<crate::StoreAccountRowDto>,
|
||||
}
|
||||
|
||||
/// App-owned identity request for one explicit RAW account-state detail load.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountDetailRequestDto.ts")]
|
||||
pub(crate) struct StoreAccountDetailRequestDto {
|
||||
/// Account public key in canonical base58 text.
|
||||
pub(crate) pubkey: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot: String,
|
||||
/// Canonical account-state hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) state_hash: String,
|
||||
}
|
||||
|
||||
/// Bounded detail projection for one complete RAW account state.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountDetailDto.ts")]
|
||||
pub(crate) struct StoreAccountDetailDto {
|
||||
/// Exact complete account-data length encoded as decimal text.
|
||||
pub(crate) data_length_decimal: String,
|
||||
/// Bounded lower-case hexadecimal preview of complete canonical account data.
|
||||
pub(crate) data_preview_hex: String,
|
||||
/// Whether complete account data is larger than the returned preview.
|
||||
pub(crate) data_preview_truncated: bool,
|
||||
/// Whether the account is executable.
|
||||
pub(crate) executable: bool,
|
||||
/// Exact lamport balance encoded as decimal text.
|
||||
pub(crate) lamports_decimal: String,
|
||||
/// Account owner program public key in canonical base58 text.
|
||||
pub(crate) owner: String,
|
||||
/// Account public key in canonical base58 text.
|
||||
pub(crate) pubkey: String,
|
||||
/// Exact rent epoch encoded as decimal text.
|
||||
pub(crate) rent_epoch_decimal: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot_decimal: String,
|
||||
/// Canonical account-state hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) state_hash: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_account.rs"]
|
||||
mod tests;
|
||||
85
crates/ksp-app-store-desk/src/dto_common.rs
Normal file
85
crates/ksp-app-store-desk/src/dto_common.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
// file: crates/ksp-app-store-desk/src/dto_common.rs
|
||||
// version: 3
|
||||
|
||||
//! Common Tauri DTOs shared by the Store Desk shell and Store runtime Overview.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_common/CommandErrorDto.ts")]
|
||||
pub(crate) struct CommandErrorDto {
|
||||
/// Stable KSP error domain.
|
||||
pub(crate) domain: String,
|
||||
/// Stable KSP error code within the domain.
|
||||
pub(crate) code: String,
|
||||
/// Human-readable error message without arbitrary context fields.
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
impl crate::CommandErrorDto {
|
||||
/// Builds a bounded safe projection from a KSP error.
|
||||
#[must_use]
|
||||
pub(crate) fn from_error(error: &ksp_core_lib::Error) -> Self {
|
||||
return Self {
|
||||
domain: error.code().domain().to_owned(),
|
||||
code: error.code().code().to_owned(),
|
||||
message: error.message().to_owned(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe shell/Config/Logging snapshot exposed to Store Desk diagnostics.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_common/ShellStatusDto.ts")]
|
||||
pub(crate) struct ShellStatusDto {
|
||||
/// Active configured Logging profile, or `None` while transient fallback Logging is active.
|
||||
pub(crate) active_logging_profile: std::option::Option<String>,
|
||||
/// Cargo application version.
|
||||
pub(crate) application_version: String,
|
||||
/// Number of logical Config resources registered by the current runtime.
|
||||
pub(crate) config_document_count: u32,
|
||||
/// Whether Store Desk had to install its transient in-memory Logging fallback.
|
||||
pub(crate) fallback_logging_active: bool,
|
||||
/// 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<crate::CommandErrorDto>,
|
||||
}
|
||||
|
||||
/// Backend-neutral Store runtime and health projection used by the Overview screen.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_common/StoreRuntimeStatusDto.ts")]
|
||||
pub(crate) struct StoreRuntimeStatusDto {
|
||||
/// Stable selected Store backend kind, when configuration resolution reached that phase.
|
||||
pub(crate) backend_kind: std::option::Option<String>,
|
||||
/// Safe Store startup/health diagnostic, when present.
|
||||
pub(crate) diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
/// Portable Store health code (`ready`, `not_ready`, `unavailable`, `closed`, or `unknown`).
|
||||
pub(crate) health_state: String,
|
||||
/// Applied migration version rendered as an exact decimal string.
|
||||
pub(crate) migration_version_decimal: std::option::Option<String>,
|
||||
/// Logical Store network, when configuration resolution reached that phase.
|
||||
pub(crate) network: std::option::Option<String>,
|
||||
/// Number of embedded migrations newer than the applied migration version.
|
||||
pub(crate) pending_migration_count: u32,
|
||||
/// Number of currently available pooled backend objects.
|
||||
pub(crate) pool_available: u32,
|
||||
/// Configured pool capacity.
|
||||
pub(crate) pool_capacity: u32,
|
||||
/// Current pool size.
|
||||
pub(crate) pool_size: u32,
|
||||
/// Number of tasks currently waiting for pool capacity.
|
||||
pub(crate) pool_waiting: u32,
|
||||
/// Store profile selected by the Store Desk composite.
|
||||
pub(crate) profile_id: std::option::Option<String>,
|
||||
/// Whether the application currently retains an opened Store facade.
|
||||
pub(crate) store_open: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_common.rs"]
|
||||
mod tests;
|
||||
125
crates/ksp-app-store-desk/src/dto_observation.rs
Normal file
125
crates/ksp-app-store-desk/src/dto_observation.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
// file: crates/ksp-app-store-desk/src/dto_observation.rs
|
||||
// version: 1
|
||||
|
||||
//! Application-owned DTOs for safe RAW observation inspection inside entity detail modals.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Safe source-independent acquisition provenance projected for Store Desk observation tables.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreObservationProvenanceDto.ts")]
|
||||
pub(crate) struct StoreObservationProvenanceDto {
|
||||
/// Logical acquisition-method code.
|
||||
pub(crate) acquisition_method: String,
|
||||
/// Optional bounded capture/session identifier.
|
||||
pub(crate) capture_session_id: std::option::Option<String>,
|
||||
/// Optional commitment code captured at acquisition.
|
||||
pub(crate) commitment: std::option::Option<String>,
|
||||
/// Optional Config-owned logical endpoint identifier.
|
||||
pub(crate) endpoint_id: std::option::Option<String>,
|
||||
/// Optional bounded logical filter identifier.
|
||||
pub(crate) filter_id: std::option::Option<String>,
|
||||
/// Optional source-observation timestamp encoded as exact Unix-millisecond decimal text.
|
||||
pub(crate) observed_at_unix_millis_decimal: std::option::Option<String>,
|
||||
/// Stable acquisition-origin code.
|
||||
pub(crate) origin: String,
|
||||
/// Logical transport/protocol code.
|
||||
pub(crate) protocol: String,
|
||||
/// Safe provider code.
|
||||
pub(crate) provider: String,
|
||||
/// Local receipt timestamp encoded as exact Unix-millisecond decimal text.
|
||||
pub(crate) received_at_unix_millis_decimal: String,
|
||||
/// Optional source-wire payload digest encoded as lower-case hexadecimal text, never source bytes.
|
||||
pub(crate) source_payload_hash: std::option::Option<String>,
|
||||
/// Optional source-wire payload size encoded as exact decimal text.
|
||||
pub(crate) source_payload_size_decimal: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// DataTables random-access request for observations of one explicit RAW transaction.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationQueryRequestDto.ts")]
|
||||
pub(crate) struct StoreTransactionObservationQueryRequestDto {
|
||||
/// Whitelisted page size (`25`, `50`, or `100`).
|
||||
pub(crate) limit: u32,
|
||||
/// Absolute zero-based observation offset encoded as exact decimal text.
|
||||
pub(crate) offset: String,
|
||||
/// Canonical 64-byte transaction signature encoded as lower-case hexadecimal text.
|
||||
pub(crate) signature: String,
|
||||
}
|
||||
|
||||
/// Safe metadata-only row for one RAW transaction observation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationRowDto.ts")]
|
||||
pub(crate) struct StoreTransactionObservationRowDto {
|
||||
/// Deterministic observation key encoded as lower-case hexadecimal text.
|
||||
pub(crate) observation_key: String,
|
||||
/// Safe source-independent acquisition provenance.
|
||||
pub(crate) provenance: crate::StoreObservationProvenanceDto,
|
||||
}
|
||||
|
||||
/// Server-side transaction-observation table result using exact decimal counts.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreTransactionObservationQueryResponseDto.ts")]
|
||||
pub(crate) struct StoreTransactionObservationQueryResponseDto {
|
||||
/// Exact count after the mandatory transaction filter.
|
||||
pub(crate) records_filtered_decimal: String,
|
||||
/// Exact count in the mandatory Store network scope before the entity filter.
|
||||
pub(crate) records_total_decimal: String,
|
||||
/// Metadata-only observation rows for the requested random-access page.
|
||||
pub(crate) rows: std::vec::Vec<crate::StoreTransactionObservationRowDto>,
|
||||
}
|
||||
|
||||
/// DataTables random-access request for observations of one explicit RAW account state.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationQueryRequestDto.ts")]
|
||||
pub(crate) struct StoreAccountObservationQueryRequestDto {
|
||||
/// Whitelisted page size (`25`, `50`, or `100`).
|
||||
pub(crate) limit: u32,
|
||||
/// Absolute zero-based observation offset encoded as exact decimal text.
|
||||
pub(crate) offset: String,
|
||||
/// Account public key in canonical base58 text.
|
||||
pub(crate) pubkey: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot: String,
|
||||
/// Canonical account-state hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) state_hash: String,
|
||||
}
|
||||
|
||||
/// Safe metadata-only row for one RAW account observation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationRowDto.ts")]
|
||||
pub(crate) struct StoreAccountObservationRowDto {
|
||||
/// Optional source-reported startup/replay marker.
|
||||
pub(crate) is_startup: std::option::Option<bool>,
|
||||
/// Deterministic observation key encoded as lower-case hexadecimal text.
|
||||
pub(crate) observation_key: String,
|
||||
/// Safe source-independent acquisition provenance.
|
||||
pub(crate) provenance: crate::StoreObservationProvenanceDto,
|
||||
/// Optional transaction signature associated with the account write, encoded as lower-case hexadecimal text.
|
||||
pub(crate) transaction_signature: std::option::Option<String>,
|
||||
/// Optional source-specific account write version encoded as exact decimal text.
|
||||
pub(crate) write_version_decimal: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// Server-side account-observation table result using exact decimal counts.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_observation/StoreAccountObservationQueryResponseDto.ts")]
|
||||
pub(crate) struct StoreAccountObservationQueryResponseDto {
|
||||
/// Exact count after the mandatory account-state filter.
|
||||
pub(crate) records_filtered_decimal: String,
|
||||
/// Exact count in the mandatory Store network scope before the entity filter.
|
||||
pub(crate) records_total_decimal: String,
|
||||
/// Metadata-only observation rows for the requested random-access page.
|
||||
pub(crate) rows: std::vec::Vec<crate::StoreAccountObservationRowDto>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_observation.rs"]
|
||||
mod tests;
|
||||
99
crates/ksp-app-store-desk/src/dto_transaction.rs
Normal file
99
crates/ksp-app-store-desk/src/dto_transaction.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
// file: crates/ksp-app-store-desk/src/dto_transaction.rs
|
||||
// version: 1
|
||||
|
||||
//! Application-owned DTOs for RAW transaction inspection and detail commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Bounded Store-owned query request projected from one DataTables page request.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionQueryRequestDto.ts")]
|
||||
pub(crate) struct StoreTransactionQueryRequestDto {
|
||||
/// Stable Store sort direction code (`ascending` or `descending`).
|
||||
pub(crate) direction: String,
|
||||
/// Absolute zero-based inspection offset encoded as exact decimal text.
|
||||
pub(crate) offset: String,
|
||||
/// Whitelisted page size (`25`, `50`, or `100`).
|
||||
pub(crate) limit: u32,
|
||||
/// Optional inclusive maximum slot encoded as decimal text.
|
||||
pub(crate) slot_max: std::option::Option<String>,
|
||||
/// Optional inclusive minimum slot encoded as decimal text.
|
||||
pub(crate) slot_min: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// Payload-free RAW transaction row returned to the server-side DataTables adapter.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionRowDto.ts")]
|
||||
pub(crate) struct StoreTransactionRowDto {
|
||||
/// Optional canonical block timestamp encoded as exact Unix-millisecond decimal text.
|
||||
pub(crate) block_time_unix_millis_decimal: std::option::Option<String>,
|
||||
/// Canonical transaction content hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) content_hash: String,
|
||||
/// KSP-owned RAW format identifier.
|
||||
pub(crate) format_id: String,
|
||||
/// KSP-owned RAW format version, safely representable in JavaScript.
|
||||
pub(crate) format_version: u32,
|
||||
/// Logical payload size encoded as exact decimal text while payload remains retained.
|
||||
pub(crate) payload_size_decimal: std::option::Option<String>,
|
||||
/// Stable logical retention-state code.
|
||||
pub(crate) retention_state: String,
|
||||
/// Canonical 64-byte transaction signature encoded as lower-case hexadecimal text.
|
||||
pub(crate) signature: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot_decimal: String,
|
||||
}
|
||||
|
||||
/// Server-side transaction table result using exact decimal counts.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionQueryResponseDto.ts")]
|
||||
pub(crate) struct StoreTransactionQueryResponseDto {
|
||||
/// Exact count after optional Store filters.
|
||||
pub(crate) records_filtered_decimal: String,
|
||||
/// Exact count in the mandatory Store network scope before optional filters.
|
||||
pub(crate) records_total_decimal: String,
|
||||
/// Payload-free inspection rows for the requested random-access page.
|
||||
pub(crate) rows: std::vec::Vec<crate::StoreTransactionRowDto>,
|
||||
}
|
||||
|
||||
/// App-owned identity request for one explicit RAW transaction detail load.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailRequestDto.ts")]
|
||||
pub(crate) struct StoreTransactionDetailRequestDto {
|
||||
/// Canonical 64-byte transaction signature encoded as lower-case hexadecimal text.
|
||||
pub(crate) signature: String,
|
||||
}
|
||||
|
||||
/// Bounded detail projection for one RAW transaction.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailDto.ts")]
|
||||
pub(crate) struct StoreTransactionDetailDto {
|
||||
/// Optional canonical block timestamp encoded as exact Unix-millisecond decimal text.
|
||||
pub(crate) block_time_unix_millis_decimal: std::option::Option<String>,
|
||||
/// Canonical content hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) content_hash: String,
|
||||
/// KSP-owned RAW format identifier.
|
||||
pub(crate) format_id: String,
|
||||
/// KSP-owned RAW format version.
|
||||
pub(crate) format_version: u32,
|
||||
/// Bounded lower-case hexadecimal preview of the canonical RAW payload, when retained.
|
||||
pub(crate) payload_preview_hex: std::option::Option<String>,
|
||||
/// Whether the canonical payload is larger than the returned preview.
|
||||
pub(crate) payload_preview_truncated: bool,
|
||||
/// Exact canonical payload size while retained.
|
||||
pub(crate) payload_size_decimal: std::option::Option<String>,
|
||||
/// Stable logical retention-state code.
|
||||
pub(crate) retention_state: String,
|
||||
/// Canonical transaction signature encoded as lower-case hexadecimal text.
|
||||
pub(crate) signature: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot_decimal: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_transaction.rs"]
|
||||
mod tests;
|
||||
38
crates/ksp-app-store-desk/src/errors.rs
Normal file
38
crates/ksp-app-store-desk/src/errors.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
// file: crates/ksp-app-store-desk/src/errors.rs
|
||||
// version: 4
|
||||
|
||||
//! Application-local error codes for Store Desk desktop runtime surfaces.
|
||||
|
||||
/// Shared Store Desk runtime state is internally inconsistent.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "app_state_invalid");
|
||||
/// Shared Store Desk runtime state cannot be locked safely.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "app_state_lock_failed");
|
||||
/// Store Desk composite configuration is missing or references an unexpected document/profile.
|
||||
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "config_composite_invalid");
|
||||
/// Frontend logging requested an unsupported level.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "frontend_log_level_invalid");
|
||||
/// Frontend logging requested a target outside the application whitelist.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "frontend_log_target_invalid");
|
||||
/// Store Desk could not install the managed Logging runtime or its safe fallback.
|
||||
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "logging_bootstrap_failed");
|
||||
/// 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("store_desk", "splash_origin_invalid");
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "splash_setting_invalid");
|
||||
/// Store Desk could not resolve the requested RAW account-state identity to a coherent Store entity.
|
||||
pub(crate) const ERROR_CODE_STORE_ACCOUNT_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_account_not_found");
|
||||
/// Store Desk received an invalid RAW inspection/detail request.
|
||||
pub(crate) const ERROR_CODE_STORE_QUERY_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_query_invalid");
|
||||
/// Store Desk cannot serve a Store command because the runtime is unavailable or already closed.
|
||||
pub(crate) const ERROR_CODE_STORE_RUNTIME_UNAVAILABLE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_runtime_unavailable");
|
||||
/// Store Desk cannot complete the bounded Store shutdown lifecycle.
|
||||
pub(crate) const ERROR_CODE_STORE_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_shutdown_failed");
|
||||
/// Store Desk could not resolve the requested transaction identity to a coherent Store entity.
|
||||
pub(crate) const ERROR_CODE_STORE_TRANSACTION_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_transaction_not_found");
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "tauri_runtime_failed");
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "tauri_window_missing");
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("store_desk", "tauri_window_operation_failed");
|
||||
205
crates/ksp-app-store-desk/src/frontend_logging.rs
Normal file
205
crates/ksp-app-store-desk/src/frontend_logging.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
// file: crates/ksp-app-store-desk/src/frontend_logging.rs
|
||||
// version: 2
|
||||
|
||||
//! KSP-owned bridge for technical log events emitted by Store Desk frontend scripts.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Log payload sent by Store Desk frontend scripts.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/frontend_logging/FrontendLogPayloadDto.ts")]
|
||||
pub(crate) struct FrontendLogPayloadDto {
|
||||
/// Lowercase KSP log level.
|
||||
pub(crate) level: String,
|
||||
/// Whitelisted logical frontend target identifier.
|
||||
pub(crate) target_id: String,
|
||||
/// Rendered technical frontend message.
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum FrontendLogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum FrontendLogTarget {
|
||||
Frontend,
|
||||
Main,
|
||||
Splash,
|
||||
}
|
||||
|
||||
/// Emits one validated frontend event through the KSP Logging facade.
|
||||
pub(crate) fn emit_frontend_log_event(payload: crate::FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
|
||||
let level = parse_level(payload.level.as_str());
|
||||
let level = match level {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let target = parse_target(payload.target_id.as_str());
|
||||
let target = match target {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
emit_validated_frontend_log(level, target, payload.message.as_str());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
|
||||
return match level.trim().to_ascii_lowercase().as_str() {
|
||||
"trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
|
||||
"debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
|
||||
"info" => std::result::Result::Ok(FrontendLogLevel::Info),
|
||||
"warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
|
||||
"error" => std::result::Result::Ok(FrontendLogLevel::Error),
|
||||
_ => std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported")),
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_target(target_id: &str) -> ksp_core_lib::Result<FrontendLogTarget> {
|
||||
return match target_id.trim().to_ascii_lowercase().as_str() {
|
||||
"frontend" => std::result::Result::Ok(FrontendLogTarget::Frontend),
|
||||
"main" => std::result::Result::Ok(FrontendLogTarget::Main),
|
||||
"splash" => std::result::Result::Ok(FrontendLogTarget::Splash),
|
||||
_ => {
|
||||
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID, "Frontend log target identifier is not supported"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) {
|
||||
return match target {
|
||||
FrontendLogTarget::Frontend => emit_frontend_target(level, message),
|
||||
FrontendLogTarget::Main => emit_main_target(level, message),
|
||||
FrontendLogTarget::Splash => emit_splash_target(level, message),
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_frontend_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Info => ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Error => ksp_logging_lib::error!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_main_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Info => ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Error => ksp_logging_lib::error!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_splash_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Info => ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Error => ksp_logging_lib::error!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/frontend_logging.rs"]
|
||||
mod tests;
|
||||
160
crates/ksp-app-store-desk/src/lib.rs
Normal file
160
crates/ksp-app-store-desk/src/lib.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
// file: crates/ksp-app-store-desk/src/lib.rs
|
||||
// version: 6
|
||||
|
||||
//! Tauri desktop scaffold for backend-neutral KSP Store inspection.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod app_state;
|
||||
mod bootstrap;
|
||||
mod constants;
|
||||
mod dto_account;
|
||||
mod dto_common;
|
||||
mod dto_observation;
|
||||
mod dto_transaction;
|
||||
mod errors;
|
||||
mod frontend_logging;
|
||||
mod logging_runtime;
|
||||
mod splash;
|
||||
mod store_runtime;
|
||||
mod tauri;
|
||||
mod tw_main;
|
||||
mod tw_splash;
|
||||
|
||||
/// Runs the KSP Store desktop application.
|
||||
pub use self::tauri::run;
|
||||
|
||||
/// Shared Store Desk application state managed by Tauri.
|
||||
pub(crate) use self::app_state::AppState;
|
||||
/// Crate-internal Logging startup state shared by the application state.
|
||||
pub(crate) use self::bootstrap::LoggingStartup;
|
||||
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
|
||||
pub(crate) use self::bootstrap::config_management;
|
||||
/// Initializes verbose development Logging from the Store Desk composite.
|
||||
pub(crate) use self::bootstrap::initialize_logging;
|
||||
/// Loads the concrete Store Desk Config composite.
|
||||
pub(crate) use self::bootstrap::load_store_desk_composite;
|
||||
/// Returns one required standard component profile from the Store Desk composite.
|
||||
pub(crate) use self::bootstrap::required_composite_component_profile;
|
||||
/// Composite component identifier for Logging.
|
||||
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
|
||||
/// Composite component identifier for Store.
|
||||
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_STORE;
|
||||
/// Logging profile used while Store Desk is under active development.
|
||||
pub(crate) use self::constants::DEVELOPMENT_LOGGING_PROFILE;
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
/// Structured domain used by the Store Desk shell.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
|
||||
/// Structured domain used by Store readiness and shutdown operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_STORE;
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||
/// Owning target for backend events emitted by Store Desk.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||
/// Owning target for main-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
|
||||
/// Bounded detail projection for one complete RAW account state.
|
||||
pub(crate) use self::dto_account::StoreAccountDetailDto;
|
||||
/// App-owned identity request for one explicit RAW account-state detail load.
|
||||
pub(crate) use self::dto_account::StoreAccountDetailRequestDto;
|
||||
/// Bounded Store-owned account query request projected from one DataTables page request.
|
||||
pub(crate) use self::dto_account::StoreAccountQueryRequestDto;
|
||||
/// Server-side account-state table result using exact decimal counts.
|
||||
pub(crate) use self::dto_account::StoreAccountQueryResponseDto;
|
||||
/// Data-free RAW account-state row returned to the server-side DataTables adapter.
|
||||
pub(crate) use self::dto_account::StoreAccountRowDto;
|
||||
/// Safe command error projection exposed to Tauri commands.
|
||||
pub(crate) use self::dto_common::CommandErrorDto;
|
||||
/// Safe shell/Config/Logging snapshot exposed to the shell.
|
||||
pub(crate) use self::dto_common::ShellStatusDto;
|
||||
/// Backend-neutral Store runtime and health projection exposed to Overview.
|
||||
pub(crate) use self::dto_common::StoreRuntimeStatusDto;
|
||||
/// DataTables random-access request for observations of one explicit RAW account state.
|
||||
pub(crate) use self::dto_observation::StoreAccountObservationQueryRequestDto;
|
||||
/// Server-side account-observation table result using exact decimal counts.
|
||||
pub(crate) use self::dto_observation::StoreAccountObservationQueryResponseDto;
|
||||
/// Safe metadata-only row for one RAW account observation.
|
||||
pub(crate) use self::dto_observation::StoreAccountObservationRowDto;
|
||||
/// Safe source-independent acquisition provenance projected for observation tables.
|
||||
pub(crate) use self::dto_observation::StoreObservationProvenanceDto;
|
||||
/// DataTables random-access request for observations of one explicit RAW transaction.
|
||||
pub(crate) use self::dto_observation::StoreTransactionObservationQueryRequestDto;
|
||||
/// Server-side transaction-observation table result using exact decimal counts.
|
||||
pub(crate) use self::dto_observation::StoreTransactionObservationQueryResponseDto;
|
||||
/// Safe metadata-only row for one RAW transaction observation.
|
||||
pub(crate) use self::dto_observation::StoreTransactionObservationRowDto;
|
||||
/// Bounded detail projection for one RAW transaction.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionDetailDto;
|
||||
/// App-owned identity request for one explicit RAW transaction detail load.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionDetailRequestDto;
|
||||
/// Bounded Store-owned query request projected from one DataTables page request.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionQueryRequestDto;
|
||||
/// Server-side transaction table result using exact decimal counts.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionQueryResponseDto;
|
||||
/// Payload-free RAW transaction row returned to the server-side DataTables adapter.
|
||||
pub(crate) use self::dto_transaction::StoreTransactionRowDto;
|
||||
/// Shared Store Desk runtime state is internally inconsistent.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||
/// Shared Store Desk runtime state cannot be locked safely.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
||||
/// Store Desk composite configuration is missing or references an unexpected document/profile.
|
||||
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
|
||||
/// Frontend logging requested an unsupported level.
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
||||
/// Frontend logging requested a target outside the application whitelist.
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||
/// Store Desk could not install the managed Logging runtime or its safe fallback.
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||
/// Store Desk could not resolve the requested RAW account-state identity to a coherent Store entity.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_ACCOUNT_NOT_FOUND;
|
||||
/// Store Desk received an invalid transaction inspection/detail request.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_QUERY_INVALID;
|
||||
/// Store Desk cannot serve a Store command because the runtime is unavailable or already closed.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_RUNTIME_UNAVAILABLE;
|
||||
/// Store Desk cannot complete the bounded Store shutdown lifecycle.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_SHUTDOWN_FAILED;
|
||||
/// Store Desk could not resolve the requested transaction identity to a coherent Store entity.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_TRANSACTION_NOT_FOUND;
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
|
||||
/// Log payload sent by Store Desk frontend scripts.
|
||||
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
|
||||
/// Emits one validated frontend event through the KSP Logging facade.
|
||||
pub(crate) use self::frontend_logging::emit_frontend_log_event;
|
||||
/// Creates the stable runtime identity for this Store Desk process launch.
|
||||
pub(crate) use self::logging_runtime::launch_identity;
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
pub(crate) use self::splash::SplashOrderDto;
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
pub(crate) use self::splash::SplashSettings;
|
||||
/// Store runtime retained behind the Store Desk lifecycle boundary.
|
||||
pub(crate) use self::store_runtime::StoreRuntime;
|
||||
/// Store startup state retained by the application shell.
|
||||
pub(crate) use self::store_runtime::StoreStartup;
|
||||
/// Initializes Store from the Store Desk composite.
|
||||
pub(crate) use self::store_runtime::initialize_store;
|
||||
/// Resolves the required main window or returns a typed error.
|
||||
pub(crate) use self::tw_main::require_main_window;
|
||||
/// Shows and focuses the main Store Desk window.
|
||||
pub(crate) use self::tw_main::show_main_window;
|
||||
/// Resolves the required splash window or returns a typed error.
|
||||
pub(crate) use self::tw_splash::require_splash_window;
|
||||
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
|
||||
pub(crate) use self::tw_splash::splash_frontend_ready_service;
|
||||
10
crates/ksp-app-store-desk/src/logging_runtime.rs
Normal file
10
crates/ksp-app-store-desk/src/logging_runtime.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// file: crates/ksp-app-store-desk/src/logging_runtime.rs
|
||||
// version: 1
|
||||
|
||||
//! Store Desk Logging runtime identity helpers.
|
||||
|
||||
/// Creates the stable runtime identity for this Store Desk process launch.
|
||||
pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::LoggingRuntimeIdentity> {
|
||||
let timestamp = format!("{}-p{}", chrono::Utc::now().format("%Y%m%d-%H%M%S%.3fZ"), std::process::id());
|
||||
return ksp_logging_lib::LoggingRuntimeIdentity::new(crate::TRACING_TARGET, timestamp);
|
||||
}
|
||||
58
crates/ksp-app-store-desk/src/main.rs
Normal file
58
crates/ksp-app-store-desk/src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
// file: crates/ksp-app-store-desk/src/main.rs
|
||||
// version: 1
|
||||
|
||||
//! Binary entry point for the KSP Store desktop application.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use fs2::FileExt; // rust-rules: trait-import
|
||||
|
||||
fn main() -> std::process::ExitCode {
|
||||
let working_directory = configure_runtime_working_directory();
|
||||
if let std::result::Result::Err(error) = working_directory {
|
||||
eprintln!("cannot configure Store Desk runtime working directory: {error}");
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
let mut lock_path = std::env::temp_dir();
|
||||
lock_path.push("com_sasedev_ksp_app_store_desk.lock");
|
||||
let lock_file = match std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&lock_path) {
|
||||
std::result::Result::Ok(file) => file,
|
||||
std::result::Result::Err(error) => {
|
||||
eprintln!("cannot create application lock '{}': {error}", lock_path.display());
|
||||
return std::process::ExitCode::FAILURE;
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = lock_file.try_lock_exclusive() {
|
||||
if error.kind() == std::io::ErrorKind::WouldBlock {
|
||||
eprintln!("another ksp-app-store-desk instance is already running");
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
eprintln!("cannot acquire application lock '{}': {error}", lock_path.display());
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
let _lock_file = lock_file;
|
||||
let arguments = std::env::args_os().collect::<std::vec::Vec<std::ffi::OsString>>();
|
||||
let run_result = ksp_app_store_desk_lib::run(arguments.as_slice());
|
||||
return match run_result {
|
||||
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
|
||||
std::result::Result::Err(error) => {
|
||||
eprintln!("application error: {error}");
|
||||
std::process::ExitCode::FAILURE
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_runtime_working_directory() -> std::io::Result<()> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
return std::env::set_current_dir(workspace_root);
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
214
crates/ksp-app-store-desk/src/splash.rs
Normal file
214
crates/ksp-app-store-desk/src/splash.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
// file: crates/ksp-app-store-desk/src/splash.rs
|
||||
// version: 2
|
||||
|
||||
//! Common splash settings and frontend event contracts for Store Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300;
|
||||
const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300;
|
||||
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
|
||||
const ENV_SPLASH_FADE_IN_MS: &str = "KSP_DESK_SPLASH_FADE_IN_MS";
|
||||
const ENV_SPLASH_FADE_OUT_MS: &str = "KSP_DESK_SPLASH_FADE_OUT_MS";
|
||||
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
|
||||
const MAX_SPLASH_FADE_MS: u32 = 10_000;
|
||||
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
|
||||
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct SplashSettings {
|
||||
fade_in_ms: u32,
|
||||
minimum_ms: u64,
|
||||
fade_out_ms: u32,
|
||||
fade_in_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
minimum_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
fade_out_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
}
|
||||
|
||||
impl crate::SplashSettings {
|
||||
/// Resolves splash timings through the Config-owned environment snapshot.
|
||||
pub(crate) fn load() -> ksp_core_lib::Result<Self> {
|
||||
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 fade_in = environment.resolve_variable(ENV_SPLASH_FADE_IN_MS, std::option::Option::Some("300"));
|
||||
let fade_in = match fade_in {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let minimum = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200"));
|
||||
let minimum = match minimum {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_out = environment.resolve_variable(ENV_SPLASH_FADE_OUT_MS, std::option::Option::Some("300"));
|
||||
let fade_out = match fade_out {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_in_ms = parse_u32_setting(ENV_SPLASH_FADE_IN_MS, fade_in.value(), MAX_SPLASH_FADE_MS);
|
||||
let fade_in_ms = match fade_in_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let minimum_ms = parse_u64_setting(ENV_SPLASH_MINIMUM_MS, minimum.value(), MAX_SPLASH_MINIMUM_MS);
|
||||
let minimum_ms = match minimum_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_out_ms = parse_u32_setting(ENV_SPLASH_FADE_OUT_MS, fade_out.value(), MAX_SPLASH_FADE_MS);
|
||||
let fade_out_ms = match fade_out_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self {
|
||||
fade_in_ms,
|
||||
minimum_ms,
|
||||
fade_out_ms,
|
||||
fade_in_source: fade_in.source(),
|
||||
minimum_source: minimum.source(),
|
||||
fade_out_source: fade_out.source(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns safe in-memory timings used when the managed environment cannot be resolved.
|
||||
#[must_use]
|
||||
pub(crate) const fn fallback() -> Self {
|
||||
return Self {
|
||||
fade_in_ms: DEFAULT_SPLASH_FADE_IN_MS,
|
||||
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
|
||||
fade_out_ms: DEFAULT_SPLASH_FADE_OUT_MS,
|
||||
fade_in_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
fade_out_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the configured fade-in duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_in_ms(self) -> u32 {
|
||||
return self.fade_in_ms;
|
||||
}
|
||||
|
||||
/// Returns the minimum fully-visible splash duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn minimum_ms(self) -> u64 {
|
||||
return self.minimum_ms;
|
||||
}
|
||||
|
||||
/// Returns the configured fade-out duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_out_ms(self) -> u32 {
|
||||
return self.fade_out_ms;
|
||||
}
|
||||
|
||||
/// Returns the safe provenance code for the fade-in duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_in_source(self) -> &'static str {
|
||||
return environment_source_code(self.fade_in_source);
|
||||
}
|
||||
|
||||
/// Returns the safe provenance code for the minimum duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn minimum_source(self) -> &'static str {
|
||||
return environment_source_code(self.minimum_source);
|
||||
}
|
||||
|
||||
/// Returns the safe provenance code for the fade-out duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_out_source(self) -> &'static str {
|
||||
return environment_source_code(self.fade_out_source);
|
||||
}
|
||||
|
||||
/// Returns the expected additive backend lifecycle from readiness through fade-out.
|
||||
#[must_use]
|
||||
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
|
||||
return u64::from(self.fade_in_ms) + self.minimum_ms + u64::from(self.fade_out_ms);
|
||||
}
|
||||
}
|
||||
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/splash/SplashOrderDto.ts")]
|
||||
pub(crate) struct SplashOrderDto {
|
||||
/// Splash action name.
|
||||
pub(crate) action: String,
|
||||
/// Optional status text.
|
||||
pub(crate) message: std::option::Option<String>,
|
||||
/// Optional message severity used by the bottom message feed.
|
||||
pub(crate) status: std::option::Option<String>,
|
||||
/// Optional animation duration in milliseconds.
|
||||
pub(crate) duration_ms: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
impl crate::SplashOrderDto {
|
||||
/// Creates a new `SplashOrderDto` value.
|
||||
#[must_use]
|
||||
pub(crate) fn new(action: &str, message: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
|
||||
return Self {
|
||||
action: action.to_owned(),
|
||||
message: message.map(std::string::ToString::to_string),
|
||||
status: status.map(std::string::ToString::to_string),
|
||||
duration_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
|
||||
return match source {
|
||||
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
|
||||
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
|
||||
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
|
||||
let parsed = value.parse::<u64>();
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if parsed > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_context("maximum_ms", maximum.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
fn parse_u32_setting(variable_name: &str, value: &str, maximum: u32) -> ksp_core_lib::Result<u32> {
|
||||
let parsed = value.parse::<u32>();
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if parsed > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_context("maximum_ms", maximum.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/splash.rs"]
|
||||
mod tests;
|
||||
921
crates/ksp-app-store-desk/src/store_runtime.rs
Normal file
921
crates/ksp-app-store-desk/src/store_runtime.rs
Normal file
@@ -0,0 +1,921 @@
|
||||
// file: crates/ksp-app-store-desk/src/store_runtime.rs
|
||||
// version: 5
|
||||
|
||||
//! Composite-selected Store readiness, status and bounded shutdown lifecycle owned by Store Desk.
|
||||
|
||||
/// Startup result for Store, including safe diagnostics when the desktop shell remains available without an opened runtime.
|
||||
pub(crate) struct StoreStartup {
|
||||
backend_kind: std::option::Option<String>,
|
||||
diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
network: std::option::Option<String>,
|
||||
profile_id: std::option::Option<String>,
|
||||
runtime: std::option::Option<crate::StoreRuntime>,
|
||||
}
|
||||
|
||||
impl crate::StoreStartup {
|
||||
/// Builds a fresh backend-neutral runtime/health projection for the Overview screen.
|
||||
pub(crate) async fn status(&self) -> crate::StoreRuntimeStatusDto {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.status().await,
|
||||
std::option::Option::None => crate::StoreRuntimeStatusDto {
|
||||
backend_kind: self.backend_kind.clone(),
|
||||
diagnostic: self.diagnostic.clone(),
|
||||
health_state: "unavailable".to_owned(),
|
||||
migration_version_decimal: std::option::Option::None,
|
||||
network: self.network.clone(),
|
||||
pending_migration_count: 0,
|
||||
pool_available: 0,
|
||||
pool_capacity: 0,
|
||||
pool_size: 0,
|
||||
pool_waiting: 0,
|
||||
profile_id: self.profile_id.clone(),
|
||||
store_open: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one bounded server-side RAW account-state inspection query.
|
||||
pub(crate) async fn query_accounts(&self, request: crate::StoreAccountQueryRequestDto) -> ksp_core_lib::Result<crate::StoreAccountQueryResponseDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.query_accounts(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Loads one explicit bounded RAW account-state detail projection.
|
||||
pub(crate) async fn account_detail(&self, request: crate::StoreAccountDetailRequestDto) -> ksp_core_lib::Result<crate::StoreAccountDetailDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.account_detail(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one bounded metadata-only observation inspection for an explicit RAW account state.
|
||||
pub(crate) async fn query_account_observations(
|
||||
&self,
|
||||
request: crate::StoreAccountObservationQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreAccountObservationQueryResponseDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.query_account_observations(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one bounded server-side RAW transaction inspection query.
|
||||
pub(crate) async fn query_transactions(
|
||||
&self,
|
||||
request: crate::StoreTransactionQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionQueryResponseDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.query_transactions(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Loads one explicit bounded RAW transaction detail projection.
|
||||
pub(crate) async fn transaction_detail(&self, request: crate::StoreTransactionDetailRequestDto) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.transaction_detail(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one bounded metadata-only observation inspection for an explicit RAW transaction.
|
||||
pub(crate) async fn query_transaction_observations(
|
||||
&self,
|
||||
request: crate::StoreTransactionObservationQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionObservationQueryResponseDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.query_transaction_observations(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Closes the retained Store runtime if startup reached the physical Store-open phase.
|
||||
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.close().await,
|
||||
std::option::Option::None => std::result::Result::Ok(()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Executable Store runtime retained behind an async read/write lock so read-only Desk queries may coexist while shutdown remains exclusive.
|
||||
pub(crate) struct StoreRuntime {
|
||||
profile_id: String,
|
||||
store: tokio::sync::RwLock<std::option::Option<ksp_store_lib::Store>>,
|
||||
}
|
||||
|
||||
impl crate::StoreRuntime {
|
||||
/// Builds one fresh safe Store status from the retained facade without exposing backend connection details.
|
||||
pub(crate) async fn status(&self) -> crate::StoreRuntimeStatusDto {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::StoreRuntimeStatusDto {
|
||||
backend_kind: std::option::Option::None,
|
||||
diagnostic: std::option::Option::None,
|
||||
health_state: "closed".to_owned(),
|
||||
migration_version_decimal: std::option::Option::None,
|
||||
network: std::option::Option::None,
|
||||
pending_migration_count: 0,
|
||||
pool_available: 0,
|
||||
pool_capacity: 0,
|
||||
pool_size: 0,
|
||||
pool_waiting: 0,
|
||||
profile_id: std::option::Option::Some(self.profile_id.clone()),
|
||||
store_open: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
let runtime = store.runtime_snapshot();
|
||||
let health = store.health().await;
|
||||
let health_state = store_health_state_code(health.state()).to_owned();
|
||||
let diagnostic = non_ready_health_diagnostic(&health);
|
||||
return crate::StoreRuntimeStatusDto {
|
||||
backend_kind: std::option::Option::Some(runtime.backend_kind().code().to_owned()),
|
||||
diagnostic,
|
||||
health_state,
|
||||
migration_version_decimal: health.migration_version().map(|value| return value.to_string()),
|
||||
network: std::option::Option::Some(runtime.network().as_str().to_owned()),
|
||||
pending_migration_count: health.pending_migration_count(),
|
||||
pool_available: runtime.pool_available(),
|
||||
pool_capacity: runtime.pool_capacity(),
|
||||
pool_size: runtime.pool_size(),
|
||||
pool_waiting: runtime.pool_waiting(),
|
||||
profile_id: std::option::Option::Some(self.profile_id.clone()),
|
||||
store_open: true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one validated random-access account-state inspection query while retaining the Store read guard.
|
||||
pub(crate) async fn query_accounts(&self, request: crate::StoreAccountQueryRequestDto) -> ksp_core_lib::Result<crate::StoreAccountQueryResponseDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let query = account_query_from_request(store, &request);
|
||||
let query = match query {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let page = ksp_store_lib::RawAccountStateInspectionRead::inspect_raw_account_states(store, &query).await;
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let records_total_decimal = page.total_items().to_string();
|
||||
let records_filtered_decimal = page.filtered_items().to_string();
|
||||
let items = page.into_items();
|
||||
let mut rows = std::vec::Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
rows.push(account_row_dto(&item));
|
||||
}
|
||||
return std::result::Result::Ok(crate::StoreAccountQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
||||
}
|
||||
|
||||
/// Loads one account-state detail by application-owned row identity without exposing Store network selection to the frontend.
|
||||
pub(crate) async fn account_detail(&self, request: crate::StoreAccountDetailRequestDto) -> ksp_core_lib::Result<crate::StoreAccountDetailDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let reference = account_reference_from_request(store, &request);
|
||||
let reference = match reference {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let state = ksp_store_lib::RawAccountStateRead::get_raw_account_state(store, &reference).await;
|
||||
let state = match state {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(store_account_not_found()),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(account_detail_dto(&state));
|
||||
}
|
||||
|
||||
/// Executes one validated metadata-only observation query for one exact account-state identity.
|
||||
pub(crate) async fn query_account_observations(
|
||||
&self,
|
||||
request: crate::StoreAccountObservationQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreAccountObservationQueryResponseDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let query = account_observation_query_from_request(store, &request);
|
||||
let query = match query {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let page = ksp_store_lib::RawAccountObservationInspectionRead::inspect_raw_account_observations(store, &query).await;
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let records_total_decimal = page.total_items().to_string();
|
||||
let records_filtered_decimal = page.filtered_items().to_string();
|
||||
let items = page.into_items();
|
||||
let mut rows = std::vec::Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
rows.push(account_observation_row_dto(&item));
|
||||
}
|
||||
return std::result::Result::Ok(crate::StoreAccountObservationQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
||||
}
|
||||
|
||||
/// Executes one validated random-access transaction inspection query while retaining the Store read guard.
|
||||
pub(crate) async fn query_transactions(
|
||||
&self,
|
||||
request: crate::StoreTransactionQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionQueryResponseDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let query = transaction_query_from_request(store, &request);
|
||||
let query = match query {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let page = ksp_store_lib::RawTransactionInspectionRead::inspect_raw_transactions(store, &query).await;
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let records_total_decimal = page.total_items().to_string();
|
||||
let records_filtered_decimal = page.filtered_items().to_string();
|
||||
let items = page.into_items();
|
||||
let mut rows = std::vec::Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
rows.push(transaction_row_dto(&item));
|
||||
}
|
||||
return std::result::Result::Ok(crate::StoreTransactionQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
||||
}
|
||||
|
||||
/// Loads one transaction detail by application-owned row identity without exposing Store network selection to the frontend.
|
||||
pub(crate) async fn transaction_detail(&self, request: crate::StoreTransactionDetailRequestDto) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let reference = transaction_reference_from_request(store, request.signature.as_str());
|
||||
let reference = match reference {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let retention = ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_retention_state(store, &reference).await;
|
||||
let retention = match retention {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(store_transaction_not_found()),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match retention {
|
||||
ksp_store_lib::RawRetentionState::Full | ksp_store_lib::RawRetentionState::Archived => {
|
||||
transaction_detail_with_payload(store, &reference, retention).await
|
||||
},
|
||||
ksp_store_lib::RawRetentionState::Purged => transaction_detail_from_tombstone(store, &reference).await,
|
||||
_ => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Store Desk cannot project an unsupported transaction retention state",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one validated metadata-only observation query for one exact transaction identity.
|
||||
pub(crate) async fn query_transaction_observations(
|
||||
&self,
|
||||
request: crate::StoreTransactionObservationQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionObservationQueryResponseDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let query = transaction_observation_query_from_request(store, &request);
|
||||
let query = match query {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let page = ksp_store_lib::RawTransactionObservationInspectionRead::inspect_raw_transaction_observations(store, &query).await;
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let records_total_decimal = page.total_items().to_string();
|
||||
let records_filtered_decimal = page.filtered_items().to_string();
|
||||
let items = page.into_items();
|
||||
let mut rows = std::vec::Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
rows.push(transaction_observation_row_dto(&item));
|
||||
}
|
||||
return std::result::Result::Ok(crate::StoreTransactionObservationQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
||||
}
|
||||
|
||||
/// Explicitly closes the Store exactly once through its backend-neutral facade after all read guards have drained.
|
||||
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||
let mut locked = self.store.write().await;
|
||||
let store = locked.take();
|
||||
drop(locked);
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Ok(()),
|
||||
};
|
||||
let snapshot = store.runtime_snapshot();
|
||||
let backend_kind = snapshot.backend_kind().code().to_owned();
|
||||
let network = snapshot.network().as_str().to_owned();
|
||||
let closed = store.close().await;
|
||||
return match closed {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_STORE,
|
||||
store_profile = self.profile_id.as_str(),
|
||||
backend = backend_kind.as_str(),
|
||||
network = network.as_str(),
|
||||
"closed Store Desk Store runtime"
|
||||
);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_SHUTDOWN_FAILED, "Store Desk Store shutdown failed").with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512;
|
||||
const TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512;
|
||||
|
||||
fn inspection_page_from_request(offset_text: &str, limit_value: u32) -> ksp_core_lib::Result<ksp_store_lib::RawInspectionPageRequest> {
|
||||
let limit = match limit_value {
|
||||
25 | 50 | 100 => ksp_store_lib::RawPageLimit::new(u64::from(limit_value)),
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let limit = match limit {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let offset = offset_text.parse::<u64>();
|
||||
let offset = match offset {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_store_lib::RawInspectionPageRequest::new(offset, limit));
|
||||
}
|
||||
|
||||
fn account_observation_query_from_request(
|
||||
store: &ksp_store_lib::Store,
|
||||
request: &crate::StoreAccountObservationQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountObservationInspectionQuery> {
|
||||
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let detail_request =
|
||||
crate::StoreAccountDetailRequestDto { pubkey: request.pubkey.clone(), slot: request.slot.clone(), state_hash: request.state_hash.clone() };
|
||||
let reference = account_reference_from_request(store, &detail_request);
|
||||
let reference = match reference {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
let query = ksp_store_lib::RawAccountObservationInspectionQuery::try_new(
|
||||
network,
|
||||
std::option::Option::Some(reference),
|
||||
ksp_store_lib::RawSortDirection::Descending,
|
||||
page,
|
||||
);
|
||||
return match query {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
}
|
||||
|
||||
fn transaction_observation_query_from_request(
|
||||
store: &ksp_store_lib::Store,
|
||||
request: &crate::StoreTransactionObservationQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionObservationInspectionQuery> {
|
||||
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let reference = transaction_reference_from_request(store, request.signature.as_str());
|
||||
let reference = match reference {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
let query = ksp_store_lib::RawTransactionObservationInspectionQuery::try_new(
|
||||
network,
|
||||
std::option::Option::Some(reference),
|
||||
ksp_store_lib::RawSortDirection::Descending,
|
||||
page,
|
||||
);
|
||||
return match query {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
}
|
||||
|
||||
fn account_query_from_request(
|
||||
store: &ksp_store_lib::Store,
|
||||
request: &crate::StoreAccountQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountStateInspectionQuery> {
|
||||
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pubkey = parse_optional_pubkey(request.pubkey.as_deref());
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot_min = parse_optional_decimal_u64(request.slot_min.as_deref());
|
||||
let slot_min = match slot_min {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot_max = parse_optional_decimal_u64(request.slot_max.as_deref());
|
||||
let slot_max = match slot_max {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slots = ksp_store_lib::RawSlotRange::new(slot_min, slot_max);
|
||||
let slots = match slots {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let direction = match request.direction.as_str() {
|
||||
"ascending" => ksp_store_lib::RawSortDirection::Ascending,
|
||||
"descending" => ksp_store_lib::RawSortDirection::Descending,
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
return std::result::Result::Ok(ksp_store_lib::RawAccountStateInspectionQuery::new(network, pubkey, slots, direction, page));
|
||||
}
|
||||
|
||||
fn account_reference_from_request(
|
||||
store: &ksp_store_lib::Store,
|
||||
request: &crate::StoreAccountDetailRequestDto,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountStateReference> {
|
||||
let pubkey = request.pubkey.parse::<ksp_store_lib::Pubkey>();
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let slot = request.slot.parse::<u64>();
|
||||
let slot = match slot {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let state_hash = decode_hex_32(request.state_hash.as_str());
|
||||
let state_hash = match state_hash {
|
||||
std::result::Result::Ok(value) => ksp_store_lib::RawContentHash::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
return std::result::Result::Ok(ksp_store_lib::RawAccountStateReference::new(network, pubkey, slot, state_hash));
|
||||
}
|
||||
|
||||
fn account_detail_dto(state: &ksp_store_lib::RawAccountState) -> crate::StoreAccountDetailDto {
|
||||
let data = state.data();
|
||||
let preview_len = std::cmp::min(data.len(), ACCOUNT_DETAIL_PREVIEW_BYTES);
|
||||
let state_hash = state.reference().state_hash();
|
||||
return crate::StoreAccountDetailDto {
|
||||
data_length_decimal: state.data_len().to_string(),
|
||||
data_preview_hex: encode_hex(&data[..preview_len]),
|
||||
data_preview_truncated: data.len() > preview_len,
|
||||
executable: state.executable(),
|
||||
lamports_decimal: state.lamports().to_string(),
|
||||
owner: state.owner().to_string(),
|
||||
pubkey: state.reference().pubkey().to_string(),
|
||||
rent_epoch_decimal: state.rent_epoch().to_string(),
|
||||
slot_decimal: state.reference().slot().to_string(),
|
||||
state_hash: encode_hex(state_hash.as_bytes()),
|
||||
};
|
||||
}
|
||||
|
||||
fn account_row_dto(summary: &ksp_store_lib::RawAccountStateSummary) -> crate::StoreAccountRowDto {
|
||||
let state_hash = summary.reference().state_hash();
|
||||
return crate::StoreAccountRowDto {
|
||||
data_length_decimal: summary.data_length_bytes().to_string(),
|
||||
executable: summary.executable(),
|
||||
lamports_decimal: summary.lamports().to_string(),
|
||||
owner: summary.owner().to_string(),
|
||||
pubkey: summary.reference().pubkey().to_string(),
|
||||
rent_epoch_decimal: summary.rent_epoch().to_string(),
|
||||
slot_decimal: summary.reference().slot().to_string(),
|
||||
state_hash: encode_hex(state_hash.as_bytes()),
|
||||
};
|
||||
}
|
||||
|
||||
fn observation_provenance_dto(provenance: &ksp_store_lib::RawAcquisitionProvenance) -> crate::StoreObservationProvenanceDto {
|
||||
return crate::StoreObservationProvenanceDto {
|
||||
acquisition_method: provenance.acquisition_method().as_str().to_owned(),
|
||||
capture_session_id: provenance.capture_session_id().map(|value| return value.as_str().to_owned()),
|
||||
commitment: provenance.commitment().map(|value| return value.as_str().to_owned()),
|
||||
endpoint_id: provenance.endpoint_id().map(|value| return value.as_str().to_owned()),
|
||||
filter_id: provenance.filter_id().map(|value| return value.as_str().to_owned()),
|
||||
observed_at_unix_millis_decimal: provenance.observed_at().map(|value| return value.unix_millis().to_string()),
|
||||
origin: acquisition_origin_code(provenance.origin()).to_owned(),
|
||||
protocol: provenance.protocol().as_str().to_owned(),
|
||||
provider: provenance.provider().as_str().to_owned(),
|
||||
received_at_unix_millis_decimal: provenance.received_at().unix_millis().to_string(),
|
||||
source_payload_hash: provenance.source_payload_hash().map(|value| return encode_hex(value.as_bytes())),
|
||||
source_payload_size_decimal: provenance.source_payload_size_bytes().map(|value| return value.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
fn account_observation_row_dto(summary: &ksp_store_lib::RawAccountObservationSummary) -> crate::StoreAccountObservationRowDto {
|
||||
return crate::StoreAccountObservationRowDto {
|
||||
is_startup: summary.is_startup(),
|
||||
observation_key: encode_hex(summary.observation_key().as_bytes()),
|
||||
provenance: observation_provenance_dto(summary.provenance()),
|
||||
transaction_signature: summary.transaction_signature().map(|value| return encode_hex(value.as_bytes())),
|
||||
write_version_decimal: summary.write_version().map(|value| return value.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
fn transaction_observation_row_dto(summary: &ksp_store_lib::RawTransactionObservationSummary) -> crate::StoreTransactionObservationRowDto {
|
||||
return crate::StoreTransactionObservationRowDto {
|
||||
observation_key: encode_hex(summary.observation_key().as_bytes()),
|
||||
provenance: observation_provenance_dto(summary.provenance()),
|
||||
};
|
||||
}
|
||||
|
||||
fn acquisition_origin_code(origin: ksp_store_lib::RawAcquisitionOrigin) -> &'static str {
|
||||
return match origin {
|
||||
ksp_store_lib::RawAcquisitionOrigin::Backfill => "backfill",
|
||||
ksp_store_lib::RawAcquisitionOrigin::Import => "import",
|
||||
ksp_store_lib::RawAcquisitionOrigin::Live => "live",
|
||||
ksp_store_lib::RawAcquisitionOrigin::Repair => "repair",
|
||||
ksp_store_lib::RawAcquisitionOrigin::Replay => "replay",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
fn transaction_query_from_request(
|
||||
store: &ksp_store_lib::Store,
|
||||
request: &crate::StoreTransactionQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionInspectionQuery> {
|
||||
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot_min = parse_optional_decimal_u64(request.slot_min.as_deref());
|
||||
let slot_min = match slot_min {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot_max = parse_optional_decimal_u64(request.slot_max.as_deref());
|
||||
let slot_max = match slot_max {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slots = ksp_store_lib::RawSlotRange::new(slot_min, slot_max);
|
||||
let slots = match slots {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let direction = match request.direction.as_str() {
|
||||
"ascending" => ksp_store_lib::RawSortDirection::Ascending,
|
||||
"descending" => ksp_store_lib::RawSortDirection::Descending,
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
return std::result::Result::Ok(ksp_store_lib::RawTransactionInspectionQuery::new(network, slots, direction, page));
|
||||
}
|
||||
|
||||
fn transaction_reference_from_request(store: &ksp_store_lib::Store, signature_text: &str) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionReference> {
|
||||
let signature = decode_hex_64(signature_text);
|
||||
let signature = match signature {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
return std::result::Result::Ok(ksp_store_lib::RawTransactionReference::new(network, ksp_store_lib::RawTransactionSignature::new(signature)));
|
||||
}
|
||||
|
||||
async fn transaction_detail_with_payload(
|
||||
store: &ksp_store_lib::Store,
|
||||
reference: &ksp_store_lib::RawTransactionReference,
|
||||
retention: ksp_store_lib::RawRetentionState,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
let transaction = ksp_store_lib::RawTransactionRead::get_raw_transaction(store, reference).await;
|
||||
let transaction = match transaction {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Store Desk transaction retention state and payload availability disagree",
|
||||
));
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let payload = transaction.payload();
|
||||
let payload_bytes = payload.bytes();
|
||||
let preview_len = std::cmp::min(payload_bytes.len(), TRANSACTION_DETAIL_PREVIEW_BYTES);
|
||||
let preview = encode_hex(&payload_bytes[..preview_len]);
|
||||
return std::result::Result::Ok(crate::StoreTransactionDetailDto {
|
||||
block_time_unix_millis_decimal: transaction.block_time().map(|value| return value.unix_millis().to_string()),
|
||||
content_hash: encode_hex(payload.content_hash().as_bytes()),
|
||||
format_id: payload.format_id().as_str().to_owned(),
|
||||
format_version: payload.format_version(),
|
||||
payload_preview_hex: std::option::Option::Some(preview),
|
||||
payload_preview_truncated: payload_bytes.len() > preview_len,
|
||||
payload_size_decimal: std::option::Option::Some(payload.byte_len().to_string()),
|
||||
retention_state: retention_state_code(retention).to_owned(),
|
||||
signature: encode_hex(reference.signature().as_bytes()),
|
||||
slot_decimal: transaction.slot().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
async fn transaction_detail_from_tombstone(
|
||||
store: &ksp_store_lib::Store,
|
||||
reference: &ksp_store_lib::RawTransactionReference,
|
||||
) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
||||
let tombstone = ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_tombstone(store, reference).await;
|
||||
let tombstone = match tombstone {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Store Desk purged transaction is missing its durable tombstone",
|
||||
));
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::StoreTransactionDetailDto {
|
||||
block_time_unix_millis_decimal: std::option::Option::None,
|
||||
content_hash: encode_hex(tombstone.content_hash().as_bytes()),
|
||||
format_id: tombstone.format_id().as_str().to_owned(),
|
||||
format_version: tombstone.format_version(),
|
||||
payload_preview_hex: std::option::Option::None,
|
||||
payload_preview_truncated: false,
|
||||
payload_size_decimal: std::option::Option::None,
|
||||
retention_state: retention_state_code(ksp_store_lib::RawRetentionState::Purged).to_owned(),
|
||||
signature: encode_hex(reference.signature().as_bytes()),
|
||||
slot_decimal: tombstone.slot().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
fn transaction_row_dto(summary: &ksp_store_lib::RawTransactionSummary) -> crate::StoreTransactionRowDto {
|
||||
return crate::StoreTransactionRowDto {
|
||||
block_time_unix_millis_decimal: summary.block_time().map(|value| return value.unix_millis().to_string()),
|
||||
content_hash: encode_hex(summary.content_hash().as_bytes()),
|
||||
format_id: summary.format_id().as_str().to_owned(),
|
||||
format_version: summary.format_version(),
|
||||
payload_size_decimal: summary.payload_size_bytes().map(|value| return value.to_string()),
|
||||
retention_state: retention_state_code(summary.retention_state()).to_owned(),
|
||||
signature: encode_hex(summary.reference().signature().as_bytes()),
|
||||
slot_decimal: summary.slot().to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn retention_state_code(state: ksp_store_lib::RawRetentionState) -> &'static str {
|
||||
return match state {
|
||||
ksp_store_lib::RawRetentionState::Full => "full",
|
||||
ksp_store_lib::RawRetentionState::Compacted => "compacted",
|
||||
ksp_store_lib::RawRetentionState::Archived => "archived",
|
||||
ksp_store_lib::RawRetentionState::Purged => "purged",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_optional_pubkey(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<ksp_store_lib::Pubkey>> {
|
||||
return match value {
|
||||
std::option::Option::Some(text) => match text.parse::<ksp_store_lib::Pubkey>() {
|
||||
std::result::Result::Ok(parsed) => std::result::Result::Ok(std::option::Option::Some(parsed)),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
||||
},
|
||||
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_optional_decimal_u64(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<u64>> {
|
||||
return match value {
|
||||
std::option::Option::Some(text) => match text.parse::<u64>() {
|
||||
std::result::Result::Ok(parsed) => std::result::Result::Ok(std::option::Option::Some(parsed)),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
||||
},
|
||||
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_hex_32(value: &str) -> ksp_core_lib::Result<[u8; 32]> {
|
||||
if value.len() != 64 || !value.is_ascii() {
|
||||
return std::result::Result::Err(store_query_invalid());
|
||||
}
|
||||
let source = value.as_bytes();
|
||||
let mut decoded = [0_u8; 32];
|
||||
for index in 0..32 {
|
||||
let high = decode_hex_nibble(source[index * 2]);
|
||||
let low = decode_hex_nibble(source[index * 2 + 1]);
|
||||
let (high, low) = match (high, low) {
|
||||
(std::option::Option::Some(high), std::option::Option::Some(low)) => (high, low),
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
decoded[index] = (high << 4) | low;
|
||||
}
|
||||
return std::result::Result::Ok(decoded);
|
||||
}
|
||||
|
||||
fn decode_hex_64(value: &str) -> ksp_core_lib::Result<[u8; 64]> {
|
||||
if value.len() != 128 || !value.is_ascii() {
|
||||
return std::result::Result::Err(store_query_invalid());
|
||||
}
|
||||
let source = value.as_bytes();
|
||||
let mut decoded = [0_u8; 64];
|
||||
for index in 0..64 {
|
||||
let high = decode_hex_nibble(source[index * 2]);
|
||||
let low = decode_hex_nibble(source[index * 2 + 1]);
|
||||
let (high, low) = match (high, low) {
|
||||
(std::option::Option::Some(high), std::option::Option::Some(low)) => (high, low),
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
decoded[index] = (high << 4) | low;
|
||||
}
|
||||
return std::result::Result::Ok(decoded);
|
||||
}
|
||||
|
||||
fn decode_hex_nibble(value: u8) -> std::option::Option<u8> {
|
||||
return match value {
|
||||
b'0'..=b'9' => std::option::Option::Some(value - b'0'),
|
||||
b'a'..=b'f' => std::option::Option::Some(value - b'a' + 10),
|
||||
b'A'..=b'F' => std::option::Option::Some(value - b'A' + 10),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn encode_hex(bytes: &[u8]) -> String {
|
||||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut encoded = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
let value = *byte;
|
||||
encoded.push(char::from(HEX[usize::from(value >> 4)]));
|
||||
encoded.push(char::from(HEX[usize::from(value & 0x0f)]));
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
fn store_account_not_found() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_ACCOUNT_NOT_FOUND, "Store Desk account state was not found");
|
||||
}
|
||||
|
||||
fn store_query_invalid() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_QUERY_INVALID, "Store Desk RAW inspection/detail request is invalid");
|
||||
}
|
||||
|
||||
fn store_runtime_unavailable() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_RUNTIME_UNAVAILABLE, "Store Desk Store runtime is unavailable");
|
||||
}
|
||||
|
||||
fn store_transaction_not_found() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_TRANSACTION_NOT_FOUND, "Store Desk transaction was not found");
|
||||
}
|
||||
|
||||
/// Resolves the composite Store target, opens Store through the facade and captures one initial readiness probe.
|
||||
pub(crate) async fn initialize_store(management: &ksp_config_lib::ConfigManagement) -> crate::StoreStartup {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, std::option::Option::None, std::option::Option::None, error),
|
||||
};
|
||||
let composite = crate::load_store_desk_composite(management);
|
||||
let composite = match composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, std::option::Option::None, std::option::Option::None, error),
|
||||
};
|
||||
let profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
||||
let profile = match profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return unavailable_startup(
|
||||
std::option::Option::Some(composite.profile_id().to_owned()),
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
error,
|
||||
);
|
||||
},
|
||||
};
|
||||
let resolved = management.engine().resolve_store_config_profile(&profile, &environment);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return unavailable_startup(
|
||||
std::option::Option::Some(profile.profile_id().to_owned()),
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
error,
|
||||
);
|
||||
},
|
||||
};
|
||||
let profile_id = resolved.profile_id().to_owned();
|
||||
let backend_kind = resolved.settings().backend_kind().code().to_owned();
|
||||
let network = resolved.settings().network().as_str().to_owned();
|
||||
let store = ksp_store_lib::Store::open(resolved.into_settings()).await;
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return unavailable_startup(
|
||||
std::option::Option::Some(profile_id),
|
||||
std::option::Option::Some(backend_kind),
|
||||
std::option::Option::Some(network),
|
||||
error,
|
||||
);
|
||||
},
|
||||
};
|
||||
let health = store.health().await;
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_STORE,
|
||||
store_profile = profile_id.as_str(),
|
||||
backend = backend_kind.as_str(),
|
||||
network = network.as_str(),
|
||||
health_state = store_health_state_code(health.state()),
|
||||
pending_migration_count = health.pending_migration_count(),
|
||||
"initialized Store Desk Store from composite-managed configuration"
|
||||
);
|
||||
return crate::StoreStartup {
|
||||
backend_kind: std::option::Option::Some(backend_kind),
|
||||
diagnostic: non_ready_health_diagnostic(&health),
|
||||
network: std::option::Option::Some(network),
|
||||
profile_id: std::option::Option::Some(profile_id.clone()),
|
||||
runtime: std::option::Option::Some(crate::StoreRuntime { profile_id, store: tokio::sync::RwLock::new(std::option::Option::Some(store)) }),
|
||||
};
|
||||
}
|
||||
|
||||
fn non_ready_health_diagnostic(health: &ksp_store_lib::StoreHealthSnapshot) -> std::option::Option<crate::CommandErrorDto> {
|
||||
if matches!(health.state(), ksp_store_lib::StoreHealthState::Ready) {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let code = health.last_error_code();
|
||||
return match code {
|
||||
std::option::Option::Some(value) => std::option::Option::Some(crate::CommandErrorDto {
|
||||
code: value.code().to_owned(),
|
||||
domain: value.domain().to_owned(),
|
||||
message: "Store health probe did not prove readiness".to_owned(),
|
||||
}),
|
||||
std::option::Option::None => std::option::Option::Some(crate::CommandErrorDto {
|
||||
code: "store_not_ready".to_owned(),
|
||||
domain: "store_desk".to_owned(),
|
||||
message: "Store health probe did not prove readiness".to_owned(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
fn store_health_state_code(state: ksp_store_lib::StoreHealthState) -> &'static str {
|
||||
return match state {
|
||||
ksp_store_lib::StoreHealthState::Ready => "ready",
|
||||
ksp_store_lib::StoreHealthState::NotReady => "not_ready",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
fn unavailable_startup(
|
||||
profile_id: std::option::Option<String>,
|
||||
backend_kind: std::option::Option<String>,
|
||||
network: std::option::Option<String>,
|
||||
error: ksp_core_lib::Error,
|
||||
) -> crate::StoreStartup {
|
||||
let diagnostic = crate::CommandErrorDto::from_error(&error);
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_STORE,
|
||||
error_domain = diagnostic.domain.as_str(),
|
||||
error_code = diagnostic.code.as_str(),
|
||||
"Store Desk Store readiness is unavailable; keeping desktop shell available"
|
||||
);
|
||||
return crate::StoreStartup {
|
||||
backend_kind,
|
||||
diagnostic: std::option::Option::Some(diagnostic),
|
||||
network,
|
||||
profile_id,
|
||||
runtime: std::option::Option::None,
|
||||
};
|
||||
}
|
||||
273
crates/ksp-app-store-desk/src/tauri.rs
Normal file
273
crates/ksp-app-store-desk/src/tauri.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
// file: crates/ksp-app-store-desk/src/tauri.rs
|
||||
// version: 5
|
||||
|
||||
//! Tauri runtime assembly for the KSP Store desktop application.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Runs the Store desktop application.
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
let context = tauri::generate_context!();
|
||||
let runtime_layout = configure_packaged_runtime(&context);
|
||||
if let std::result::Result::Err(error) = runtime_layout {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let app_state = crate::AppState::initialize(arguments);
|
||||
let app_state = match app_state {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut builder = tauri::Builder::default();
|
||||
builder = configure_state(builder, app_state);
|
||||
builder = configure_plugins(builder);
|
||||
builder = configure_commands(builder);
|
||||
builder = configure_setup(builder);
|
||||
builder = configure_window_events(builder);
|
||||
let run_result = builder.run(context);
|
||||
return match run_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot run KSP Store Desk Tauri runtime")
|
||||
.with_context("tauri_error", error.to_string()),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_lib::Result<()> {
|
||||
if cfg!(debug_assertions) {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let resource_root = tauri::utils::platform::resource_dir(context.package_info(), &tauri::Env::default());
|
||||
let resource_root = match resource_root {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot resolve packaged Tauri resource directory")
|
||||
.with_context("tauri_error", error.to_string()),
|
||||
);
|
||||
},
|
||||
};
|
||||
let layout = ksp_config_lib::prepare_packaged_runtime(resource_root.as_path());
|
||||
let layout = match layout {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let working_directory = std::env::set_current_dir(layout.runtime_root());
|
||||
return match working_directory {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot activate packaged KSP runtime directory")
|
||||
.with_context("runtime_root", layout.runtime_root().to_string_lossy().into_owned())
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.manage(app_state);
|
||||
}
|
||||
|
||||
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
|
||||
return builder.plugin(tracing_plugin);
|
||||
}
|
||||
|
||||
#[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_shell_status,
|
||||
splash_frontend_ready,
|
||||
store_get_account_detail,
|
||||
store_get_transaction_detail,
|
||||
store_query_account_observations,
|
||||
store_query_accounts,
|
||||
store_query_transaction_observations,
|
||||
store_query_transactions,
|
||||
store_runtime_status,
|
||||
]);
|
||||
}
|
||||
|
||||
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.setup(|app| {
|
||||
let splash = crate::require_splash_window(app);
|
||||
if let std::result::Result::Err(error) = splash {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
let main = crate::require_main_window(app);
|
||||
if let std::result::Result::Err(error) = main {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_SHELL, "Store Desk Tauri shell setup completed with composite Store lifecycle");
|
||||
return std::result::Result::Ok(());
|
||||
});
|
||||
}
|
||||
|
||||
fn configure_window_events(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.on_window_event(|window, event| {
|
||||
if window.label() != "main" {
|
||||
return;
|
||||
}
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let app_handle = window.app_handle().clone();
|
||||
let state = app_handle.state::<crate::AppState>();
|
||||
if !state.begin_shutdown() {
|
||||
return;
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
"Store Desk main-window close requested; starting bounded Store shutdown"
|
||||
);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = app_handle.state::<crate::AppState>();
|
||||
let closed = state.close_store().await;
|
||||
match closed {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
"Store Desk Store shutdown completed; exiting application"
|
||||
);
|
||||
},
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Store Desk Store shutdown failed; exiting application after bounded close attempt"
|
||||
);
|
||||
},
|
||||
}
|
||||
app_handle.exit(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = domain,
|
||||
command = command,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Store Desk Tauri command failed"
|
||||
);
|
||||
return crate::CommandErrorDto::from_error(error);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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(project_command_error("emit_frontend_log", crate::TRACING_DOMAIN_FRONTEND, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_shell_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> {
|
||||
let result = state.shell_status();
|
||||
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_shell_status", crate::TRACING_DOMAIN_SHELL, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_query_account_observations(
|
||||
request: crate::StoreAccountObservationQueryRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreAccountObservationQueryResponseDto, crate::CommandErrorDto> {
|
||||
let result = state.query_account_observations(request).await;
|
||||
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("store_query_account_observations", crate::TRACING_DOMAIN_STORE, &error))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_query_accounts(
|
||||
request: crate::StoreAccountQueryRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreAccountQueryResponseDto, crate::CommandErrorDto> {
|
||||
let result = state.query_accounts(request).await;
|
||||
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("store_query_accounts", crate::TRACING_DOMAIN_STORE, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_get_account_detail(
|
||||
request: crate::StoreAccountDetailRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreAccountDetailDto, crate::CommandErrorDto> {
|
||||
let result = state.account_detail(request).await;
|
||||
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("store_get_account_detail", crate::TRACING_DOMAIN_STORE, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_query_transaction_observations(
|
||||
request: crate::StoreTransactionObservationQueryRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreTransactionObservationQueryResponseDto, crate::CommandErrorDto> {
|
||||
let result = state.query_transaction_observations(request).await;
|
||||
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("store_query_transaction_observations", crate::TRACING_DOMAIN_STORE, &error))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_query_transactions(
|
||||
request: crate::StoreTransactionQueryRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreTransactionQueryResponseDto, crate::CommandErrorDto> {
|
||||
let result = state.query_transactions(request).await;
|
||||
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("store_query_transactions", crate::TRACING_DOMAIN_STORE, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_get_transaction_detail(
|
||||
request: crate::StoreTransactionDetailRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreTransactionDetailDto, crate::CommandErrorDto> {
|
||||
let result = state.transaction_detail(request).await;
|
||||
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("store_get_transaction_detail", crate::TRACING_DOMAIN_STORE, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::StoreRuntimeStatusDto, crate::CommandErrorDto> {
|
||||
return std::result::Result::Ok(state.store_runtime_status().await);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn splash_frontend_ready(
|
||||
app: tauri::AppHandle,
|
||||
webview_window: tauri::WebviewWindow,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::splash_frontend_ready_service(app, webview_window, &state).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("splash_frontend_ready", crate::TRACING_DOMAIN_WINDOWS, &error)),
|
||||
};
|
||||
}
|
||||
53
crates/ksp-app-store-desk/src/tw_main.rs
Normal file
53
crates/ksp-app-store-desk/src/tw_main.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// file: crates/ksp-app-store-desk/src/tw_main.rs
|
||||
// version: 2
|
||||
|
||||
//! Tauri-window helpers for the Store Desk main window.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Private main-window label owned by this module.
|
||||
const WINDOW_LABEL_MAIN: &str = "main";
|
||||
|
||||
/// Resolves the required main window or returns a typed error.
|
||||
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Store Desk main window is missing")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Shows main window.
|
||||
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
|
||||
let window = crate::require_main_window(app);
|
||||
let window = match window {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let shown = window.show();
|
||||
if let std::result::Result::Err(error) = shown {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot show Store Desk main window")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
let focused = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focused {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot focus Store Desk main window")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_MAIN,
|
||||
"main window shown and focused"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
172
crates/ksp-app-store-desk/src/tw_splash.rs
Normal file
172
crates/ksp-app-store-desk/src/tw_splash.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
// file: crates/ksp-app-store-desk/src/tw_splash.rs
|
||||
// version: 2
|
||||
|
||||
//! Tauri-window lifecycle for the Store Desk splash window.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
|
||||
/// Private splash-window label owned by this module.
|
||||
const WINDOW_LABEL_SPLASH: &str = "splash";
|
||||
|
||||
/// Resolves the required splash window or returns a typed error.
|
||||
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Store Desk splash window is missing")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
|
||||
pub(crate) async fn splash_frontend_ready_service(
|
||||
app: tauri::AppHandle,
|
||||
invoking_window: tauri::WebviewWindow,
|
||||
state: &crate::AppState,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if invoking_window.label() != WINDOW_LABEL_SPLASH {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
|
||||
.with_context("window_label", invoking_window.label()),
|
||||
);
|
||||
}
|
||||
if !state.begin_splash_sequence() {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored");
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let settings = state.splash_settings();
|
||||
let lifecycle_started = std::time::Instant::now();
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_SPLASH,
|
||||
fade_in_ms = settings.fade_in_ms(),
|
||||
fade_in_source = settings.fade_in_source(),
|
||||
minimum_ms = settings.minimum_ms(),
|
||||
minimum_source = settings.minimum_source(),
|
||||
fade_out_ms = settings.fade_out_ms(),
|
||||
fade_out_source = settings.fade_out_source(),
|
||||
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
|
||||
"splash frontend readiness accepted; starting splash to main lifecycle"
|
||||
);
|
||||
let emitted = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_in", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_in_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_message(&invoking_window, "Initialisation de Store Desk...", "info");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust.");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
sleep_and_log(&invoking_window, "fade-in", u64::from(settings.fade_in_ms()), lifecycle_started).await;
|
||||
let emitted = emit_message(&invoking_window, "Configuration KSP chargée.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_debug(&invoking_window, "Configuration et provenance des timings résolues via ksp-config-lib.");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_message(&invoking_window, "Runtime Logging initialisé.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_message(&invoking_window, "Scaffold desktop prêt.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
sleep_and_log(&invoking_window, "minimum", settings.minimum_ms(), lifecycle_started).await;
|
||||
let emitted = emit_message(&invoking_window, "Initialisation terminée.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_out", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_out_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
sleep_and_log(&invoking_window, "fade-out", u64::from(settings.fade_out_ms()), lifecycle_started).await;
|
||||
let show_main = crate::show_main_window(&app);
|
||||
if let std::result::Result::Err(error) = show_main {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let destroyed = invoking_window.destroy();
|
||||
if let std::result::Result::Err(error) = destroyed {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Store Desk splash window")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_SPLASH,
|
||||
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
|
||||
actual_backend_lifecycle_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
|
||||
"splash window destroyed after main activation"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn emit_message(window: &tauri::WebviewWindow, message: &str, status: &str) -> ksp_core_lib::Result<()> {
|
||||
return emit_order(
|
||||
window,
|
||||
crate::SplashOrderDto::new("add_message", std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None),
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_debug(window: &tauri::WebviewWindow, message: &str) -> ksp_core_lib::Result<()> {
|
||||
if !cfg!(debug_assertions) {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
return emit_order(
|
||||
window,
|
||||
crate::SplashOrderDto::new("add_debug", std::option::Option::Some(message), std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
}
|
||||
|
||||
async fn sleep_and_log(window: &tauri::WebviewWindow, phase: &str, duration_ms: u64, lifecycle_started: std::time::Instant) {
|
||||
let wait_started = std::time::Instant::now();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = window.label(),
|
||||
phase,
|
||||
configured_wait_ms = duration_ms,
|
||||
actual_wait_ms = wait_started.elapsed().as_secs_f64() * 1000.0,
|
||||
lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
|
||||
"splash lifecycle wait completed"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
|
||||
let action = order.action.clone();
|
||||
let emitted = window.emit(SPLASH_EVENT_NAME, order);
|
||||
return match emitted {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, action = action.as_str(), "splash order emitted");
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Store Desk splash order")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||
.with_context("action", action)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
72
crates/ksp-app-store-desk/tauri.conf.json
Normal file
72
crates/ksp-app-store-desk/tauri.conf.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP Store Desk",
|
||||
"identifier": "com.sasedev.ksp-app-store-desk",
|
||||
"build": {
|
||||
"beforeDevCommand": {
|
||||
"script": "npm run dev",
|
||||
"cwd": "."
|
||||
},
|
||||
"devUrl": "http://localhost:1438",
|
||||
"beforeBuildCommand": {
|
||||
"script": "npm run build",
|
||||
"cwd": "."
|
||||
},
|
||||
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-store-desk/dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "splash",
|
||||
"url": "splash.html",
|
||||
"title": "Chargement — KSP Store Desk",
|
||||
"width": 960,
|
||||
"height": 637,
|
||||
"resizable": false,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"center": true,
|
||||
"alwaysOnTop": true
|
||||
},
|
||||
{
|
||||
"label": "main",
|
||||
"url": "main.html",
|
||||
"title": "KSP Store Desk",
|
||||
"width": 1500,
|
||||
"height": 960,
|
||||
"minWidth": 1024,
|
||||
"minHeight": 768,
|
||||
"center": true,
|
||||
"visible": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/favicon.png",
|
||||
"icons/favicon.ico"
|
||||
],
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
164
crates/ksp-app-store-desk/tests/dependency_boundary.rs
Normal file
164
crates/ksp-app-store-desk/tests/dependency_boundary.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
// file: crates/ksp-app-store-desk/tests/dependency_boundary.rs
|
||||
// version: 7
|
||||
|
||||
//! Dependency-boundary tests for the Store Desk application.
|
||||
|
||||
#[test]
|
||||
fn pre_006_manifest_uses_store_facade_without_physical_or_transport_dependencies() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
for forbidden in [
|
||||
"ksp-store-api",
|
||||
"ksp-store-postgres-lib",
|
||||
"ksp-onchain-transport-lib",
|
||||
"ksp-offchain-transport-lib",
|
||||
"ksp-job-api",
|
||||
"ksp-job-backfill-lib",
|
||||
"reqwest",
|
||||
"tokio-postgres",
|
||||
"deadpool-postgres",
|
||||
] {
|
||||
assert!(!manifest.contains(forbidden), "forbidden Store Desk dependency present: {forbidden}");
|
||||
}
|
||||
for required in ["ksp-config-lib", "ksp-core-lib", "ksp-logging-lib", "ksp-store-lib", "tauri-plugin-tracing"] {
|
||||
assert!(manifest.contains(required), "required Store Desk dependency missing: {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_store_open_is_owned_by_store_runtime_and_never_escapes_to_physical_sql() {
|
||||
let bootstrap = include_str!("../src/bootstrap.rs");
|
||||
let store_runtime = include_str!("../src/store_runtime.rs");
|
||||
let app_state = include_str!("../src/app_state.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
assert!(!bootstrap.contains("Store::open"));
|
||||
assert!(store_runtime.contains("ksp_store_lib::Store::open"));
|
||||
assert!(store_runtime.contains("store.runtime_snapshot()"));
|
||||
assert!(store_runtime.contains("store.health().await"));
|
||||
assert!(store_runtime.contains("store.close().await"));
|
||||
let sources = [bootstrap, store_runtime, app_state, tauri].join("\n");
|
||||
for forbidden in ["tokio_postgres", "deadpool_postgres", "SELECT ", "INSERT ", "UPDATE ", "DELETE ", "ksp_store_api", "ksp_store_postgres"] {
|
||||
assert!(!sources.contains(forbidden), "Store Desk leaked forbidden physical Store surface: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_fix_001_shared_visible_items_use_the_crate_root_facade_inside_owner_modules() {
|
||||
let lib = include_str!("../src/lib.rs");
|
||||
let dto_common = include_str!("../src/dto_common.rs");
|
||||
let frontend_logging = include_str!("../src/frontend_logging.rs");
|
||||
let splash = include_str!("../src/splash.rs");
|
||||
let store_runtime = include_str!("../src/store_runtime.rs");
|
||||
let tw_main = include_str!("../src/tw_main.rs");
|
||||
let tw_splash = include_str!("../src/tw_splash.rs");
|
||||
for expected in [
|
||||
"pub(crate) use self::dto_common::CommandErrorDto;",
|
||||
"pub(crate) use self::frontend_logging::FrontendLogPayloadDto;",
|
||||
"pub(crate) use self::splash::SplashOrderDto;",
|
||||
"pub(crate) use self::splash::SplashSettings;",
|
||||
"pub(crate) use self::store_runtime::StoreRuntime;",
|
||||
"pub(crate) use self::store_runtime::StoreStartup;",
|
||||
] {
|
||||
assert!(lib.contains(expected), "missing Store Desk crate-root re-export: {expected}");
|
||||
}
|
||||
assert!(dto_common.contains("std::option::Option<crate::CommandErrorDto>"));
|
||||
assert!(frontend_logging.contains("payload: crate::FrontendLogPayloadDto"));
|
||||
assert!(splash.contains("impl crate::SplashSettings"));
|
||||
assert!(splash.contains("impl crate::SplashOrderDto"));
|
||||
assert!(store_runtime.contains("std::option::Option<crate::StoreRuntime>"));
|
||||
assert!(store_runtime.contains("impl crate::StoreRuntime"));
|
||||
assert!(store_runtime.contains("impl crate::StoreStartup"));
|
||||
assert!(store_runtime.contains("-> crate::StoreStartup"));
|
||||
assert!(store_runtime.contains("crate::StoreStartup {"));
|
||||
assert!(store_runtime.contains("std::option::Option::Some(crate::StoreRuntime {"));
|
||||
assert!(tw_main.contains("const WINDOW_LABEL_MAIN: &str = \"main\";"));
|
||||
assert!(!tw_main.contains("pub(crate) const WINDOW_LABEL_MAIN"));
|
||||
assert!(tw_splash.contains("const WINDOW_LABEL_SPLASH: &str = \"splash\";"));
|
||||
assert!(!tw_splash.contains("pub(crate) const WINDOW_LABEL_SPLASH"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_inspection_uses_store_facade_and_new_visible_items_use_crate_root_exports() {
|
||||
let lib = include_str!("../src/lib.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let app_state = include_str!("../src/app_state.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
for expected in [
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionDetailRequestDto;",
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionDetailDto;",
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionQueryRequestDto;",
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionQueryResponseDto;",
|
||||
"pub(crate) use self::dto_transaction::StoreTransactionRowDto;",
|
||||
] {
|
||||
assert!(lib.contains(expected), "missing pre.007 crate-root DTO re-export: {expected}");
|
||||
}
|
||||
assert!(runtime.contains("ksp_store_lib::RawTransactionInspectionRead::inspect_raw_transactions"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawTransactionRead::get_raw_transaction"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_retention_state"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_tombstone"));
|
||||
assert!(app_state.contains("request: crate::StoreTransactionQueryRequestDto"));
|
||||
assert!(app_state.contains("request: crate::StoreTransactionDetailRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreTransactionQueryRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreTransactionDetailRequestDto"));
|
||||
let sources = [runtime, app_state, tauri].join("\n");
|
||||
for forbidden in ["ksp_store_api", "ksp_store_postgres", "tokio_postgres", "deadpool_postgres", "SELECT ", "OFFSET "] {
|
||||
assert!(!sources.contains(forbidden), "Store Desk transaction inspection crossed physical/backend-neutral boundary: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_inspection_uses_store_facade_and_new_visible_items_use_crate_root_exports() {
|
||||
let lib = include_str!("../src/lib.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let app_state = include_str!("../src/app_state.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
for expected in [
|
||||
"pub(crate) use self::dto_account::StoreAccountDetailRequestDto;",
|
||||
"pub(crate) use self::dto_account::StoreAccountDetailDto;",
|
||||
"pub(crate) use self::dto_account::StoreAccountQueryRequestDto;",
|
||||
"pub(crate) use self::dto_account::StoreAccountQueryResponseDto;",
|
||||
"pub(crate) use self::dto_account::StoreAccountRowDto;",
|
||||
] {
|
||||
assert!(lib.contains(expected), "missing pre.008 crate-root DTO re-export: {expected}");
|
||||
}
|
||||
assert!(runtime.contains("ksp_store_lib::RawAccountStateInspectionRead::inspect_raw_account_states"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawAccountStateRead::get_raw_account_state"));
|
||||
assert!(app_state.contains("request: crate::StoreAccountQueryRequestDto"));
|
||||
assert!(app_state.contains("request: crate::StoreAccountDetailRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreAccountQueryRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreAccountDetailRequestDto"));
|
||||
let sources = [runtime, app_state, tauri].join("\n");
|
||||
for forbidden in ["ksp_store_api", "ksp_store_postgres", "tokio_postgres", "deadpool_postgres", "SELECT ", "OFFSET "] {
|
||||
assert!(!sources.contains(forbidden), "Store Desk account inspection crossed physical/backend-neutral boundary: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_observation_ui_uses_store_facade_and_new_visible_items_use_crate_root_exports() {
|
||||
let lib = include_str!("../src/lib.rs");
|
||||
let dto = include_str!("../src/dto_observation.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let app_state = include_str!("../src/app_state.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
for expected in [
|
||||
"pub(crate) use self::dto_observation::StoreAccountObservationQueryRequestDto;",
|
||||
"pub(crate) use self::dto_observation::StoreAccountObservationQueryResponseDto;",
|
||||
"pub(crate) use self::dto_observation::StoreAccountObservationRowDto;",
|
||||
"pub(crate) use self::dto_observation::StoreObservationProvenanceDto;",
|
||||
"pub(crate) use self::dto_observation::StoreTransactionObservationQueryRequestDto;",
|
||||
"pub(crate) use self::dto_observation::StoreTransactionObservationQueryResponseDto;",
|
||||
"pub(crate) use self::dto_observation::StoreTransactionObservationRowDto;",
|
||||
] {
|
||||
assert!(lib.contains(expected), "missing pre.010 crate-root observation DTO re-export: {expected}");
|
||||
}
|
||||
assert!(dto.contains("crate::StoreObservationProvenanceDto"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawTransactionObservationInspectionRead::inspect_raw_transaction_observations"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawAccountObservationInspectionRead::inspect_raw_account_observations"));
|
||||
assert!(app_state.contains("request: crate::StoreTransactionObservationQueryRequestDto"));
|
||||
assert!(app_state.contains("request: crate::StoreAccountObservationQueryRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreTransactionObservationQueryRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreAccountObservationQueryRequestDto"));
|
||||
let sources = [runtime, app_state, tauri].join("\n");
|
||||
for forbidden in ["ksp_store_api", "ksp_store_postgres", "tokio_postgres", "deadpool_postgres", "SELECT ", "OFFSET "] {
|
||||
assert!(!sources.contains(forbidden), "Store Desk observation UI crossed physical/backend-neutral boundary: {forbidden}");
|
||||
}
|
||||
}
|
||||
336
crates/ksp-app-store-desk/tests/desktop_contract.rs
Normal file
336
crates/ksp-app-store-desk/tests/desktop_contract.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs
|
||||
// version: 10
|
||||
|
||||
//! Desktop template and frontend contract tests for the Store Desk scaffold.
|
||||
|
||||
fn parse_json(source: &str) -> serde_json::Result<serde_json::Value> {
|
||||
return serde_json::from_str::<serde_json::Value>(source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_tauri_package_and_ports_match_store_desk_identity() {
|
||||
let parsed = parse_json(include_str!("../tauri.conf.json"));
|
||||
assert!(parsed.is_ok(), "committed Tauri JSON must be valid");
|
||||
let config = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(config["productName"], "KSP Store Desk");
|
||||
assert_eq!(config["identifier"], "com.sasedev.ksp-app-store-desk");
|
||||
assert_eq!(config["build"]["devUrl"], "http://localhost:1438");
|
||||
assert!(config.get("version").is_none());
|
||||
let vite = include_str!("../vite.config.ts");
|
||||
assert!(vite.contains("port: 1438"));
|
||||
assert!(vite.contains("strictPort: true"));
|
||||
assert!(vite.contains("port: 1439"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_fix_001_vite_config_matches_ksp_desk_template_except_identity_ports_and_header() {
|
||||
let store = include_str!("../vite.config.ts")
|
||||
.replace("crates/ksp-app-store-desk/vite.config.ts", "crates/ksp-app-backfill-desk/vite.config.ts")
|
||||
.replace("// version: 2", "// version: 1")
|
||||
.replace("ksp-app-store-desk", "ksp-app-backfill-desk")
|
||||
.replace("1438", "1436")
|
||||
.replace("1439", "1437");
|
||||
let reference = include_str!("../../ksp-app-backfill-desk/vite.config.ts");
|
||||
assert_eq!(store, reference);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_fix_001_tsconfig_matches_common_ksp_desk_template_byte_exact() {
|
||||
let store = include_str!("../tsconfig.json");
|
||||
let reference = include_str!("../../ksp-app-backfill-desk/tsconfig.json");
|
||||
assert_eq!(store, reference);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_frontend_dependency_baseline_is_ksp_owned_and_select_free() {
|
||||
let parsed = parse_json(include_str!("../package.json"));
|
||||
assert!(parsed.is_ok(), "committed package JSON must be valid");
|
||||
let package = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let dependencies = &package["dependencies"];
|
||||
for required in [
|
||||
"@fltsci/tauri-plugin-tracing",
|
||||
"@fortawesome/fontawesome-free",
|
||||
"@tauri-apps/api",
|
||||
"bootstrap",
|
||||
"datatables.net-bs5",
|
||||
"resize-observer-polyfill",
|
||||
"simplebar",
|
||||
] {
|
||||
assert!(dependencies.get(required).is_some(), "required frontend dependency missing: {required}");
|
||||
}
|
||||
assert!(dependencies.get("datatables.net-select-bs5").is_none());
|
||||
let dev_dependencies = &package["devDependencies"];
|
||||
let expected = ["@tauri-apps/cli", "@types/bootstrap", "@types/node", "sass-embedded", "typescript", "vite"];
|
||||
let count = match dev_dependencies.as_object() {
|
||||
std::option::Option::Some(value) => value.len(),
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
assert_eq!(count, expected.len());
|
||||
for dependency in expected {
|
||||
assert!(dev_dependencies.get(dependency).is_some(), "required dev dependency missing: {dependency}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_binary_assets_reuse_the_ksp_desktop_template_byte_exact() {
|
||||
assert_eq!(include_bytes!("../frontend/imgs/logo.png"), include_bytes!("../../ksp-app-backfill-desk/frontend/imgs/logo.png"));
|
||||
assert_eq!(include_bytes!("../frontend/imgs/splash.png"), include_bytes!("../../ksp-app-backfill-desk/frontend/imgs/splash.png"));
|
||||
assert_eq!(include_bytes!("../frontend/fonts/DOS_Amazigh.ttf"), include_bytes!("../../ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf"));
|
||||
assert_eq!(include_bytes!("../icons/favicon.png"), include_bytes!("../../ksp-app-backfill-desk/icons/favicon.png"));
|
||||
assert_eq!(include_bytes!("../icons/favicon.ico"), include_bytes!("../../ksp-app-backfill-desk/icons/favicon.ico"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_shell_exposes_transaction_filters_detail_and_planned_account_view() {
|
||||
let html = include_str!("../frontend/main.html");
|
||||
for view in ["overview", "transactions", "accounts", "diagnostics"] {
|
||||
assert!(html.contains(&format!("data-view=\"{view}\"")));
|
||||
assert!(html.contains(&format!("data-view-panel=\"{view}\"")));
|
||||
}
|
||||
assert!(html.contains("id=\"rawTransactionsTable\""));
|
||||
assert!(html.contains("id=\"rawAccountsTable\""));
|
||||
for overview_id in ["refreshOverview", "overviewStoreProfile", "overviewStoreTarget", "overviewStoreHealth", "overviewStorePool", "overviewStoreMigration"]
|
||||
{
|
||||
assert!(html.contains(&format!("id=\"{overview_id}\"")), "missing Store Overview field: {overview_id}");
|
||||
}
|
||||
for transaction_id in ["refreshTransactions", "transactionSlotMin", "transactionSlotMax", "transactionDirection", "transactionDetailModal"] {
|
||||
assert!(html.contains(&format!("id=\"{transaction_id}\"")), "missing pre.007 transaction control: {transaction_id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_datatable_is_server_side_and_no_second_pager_exists() {
|
||||
let source = include_str!("../frontend/ts/main.ts");
|
||||
assert!(source.contains("datatables.net-bs5"));
|
||||
assert!(source.contains("paging: true"));
|
||||
assert!(source.contains("pageLength: 25"));
|
||||
assert!(source.contains("lengthMenu: [25, 50, 100]"));
|
||||
assert!(source.contains("store_query_transactions"));
|
||||
assert!(source.contains("serverSide: true"));
|
||||
assert!(source.contains("Store Desk transaction DataTable initialized"));
|
||||
assert!(source.contains("searching: false"));
|
||||
assert!(source.contains("ordering: false"));
|
||||
let html = include_str!("../frontend/main.html");
|
||||
for forbidden in ["Premier", "Précédent", "Suivant", "storePager", "store-pagination"] {
|
||||
assert!(!html.contains(forbidden), "second Store pager marker present: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_composite_and_packaged_resources_cover_store_desk_and_the_complete_config_registry() {
|
||||
let composite = parse_json(include_str!("../../../config/composite.ksp-app-store-desk.json"));
|
||||
assert!(composite.is_ok(), "Store Desk composite JSON must be valid");
|
||||
if let std::result::Result::Ok(composite) = composite {
|
||||
assert_eq!(composite["default_profile"], "mainnet");
|
||||
let profiles = composite["profiles"].as_array();
|
||||
assert!(profiles.is_some());
|
||||
if let std::option::Option::Some(profiles) = profiles {
|
||||
assert_eq!(profiles.len(), 3);
|
||||
for profile in profiles {
|
||||
let documents = profile["documents"].as_array();
|
||||
assert!(documents.is_some());
|
||||
if let std::option::Option::Some(documents) = documents {
|
||||
assert_eq!(documents.len(), 2);
|
||||
assert_eq!(documents[0]["component_id"], "logging");
|
||||
assert_eq!(documents[0]["file_id"], "cfg.std.logging");
|
||||
assert_eq!(documents[0]["profile_id"], "supertrace");
|
||||
assert_eq!(documents[1]["component_id"], "store");
|
||||
assert_eq!(documents[1]["file_id"], "cfg.std.store");
|
||||
assert_eq!(documents[1]["profile_id"], profile["profile_id"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let tauri = parse_json(include_str!("../tauri.conf.json"));
|
||||
assert!(tauri.is_ok(), "Store Desk Tauri JSON must be valid");
|
||||
if let std::result::Result::Ok(tauri) = tauri {
|
||||
let resources = tauri["bundle"]["resources"].as_object();
|
||||
assert!(resources.is_some());
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert!(resources.contains_key("../../config/composite.ksp-app-store-desk.json"));
|
||||
assert!(resources.contains_key("../../config/std.store.json"));
|
||||
assert!(resources.contains_key("../../config/schemas/std.store.schema.json"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_store_lifecycle_is_composite_managed_health_visible_and_bounded_on_window_close() {
|
||||
let bootstrap = include_str!("../src/bootstrap.rs");
|
||||
let store = include_str!("../src/store_runtime.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
assert!(bootstrap.contains("FILE_ID_COMPOSITE_KSP_APP_STORE_DESK"));
|
||||
assert!(bootstrap.contains("COMPOSITE_COMPONENT_ID_STORE"));
|
||||
assert!(store.contains("ksp_store_lib::Store::open"));
|
||||
assert!(store.contains("store.runtime_snapshot()"));
|
||||
assert!(store.contains("store.health().await"));
|
||||
assert!(store.contains("store.close().await"));
|
||||
assert!(tauri.contains("CloseRequested"));
|
||||
assert!(tauri.contains("api.prevent_close()"));
|
||||
assert!(tauri.contains("begin_shutdown"));
|
||||
assert!(tauri.contains("close_store().await"));
|
||||
assert!(tauri.contains("app_handle.exit(0)"));
|
||||
assert!(tauri.contains("store_runtime_status"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_ipc_maps_datatables_offset_limit_counts_and_detail_without_cursor_contract() {
|
||||
let dto = include_str!("../src/dto_transaction.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
assert!(dto.contains("records_total_decimal"));
|
||||
assert!(dto.contains("records_filtered_decimal"));
|
||||
assert!(runtime.contains("RawInspectionPageRequest::new(offset, limit)"));
|
||||
assert!(runtime.contains("RawTransactionInspectionQuery::new"));
|
||||
assert!(tauri.contains("store_query_transactions"));
|
||||
assert!(tauri.contains("store_get_transaction_detail"));
|
||||
assert!(frontend.contains("offset: page.start.toString()"));
|
||||
assert!(frontend.contains("limit: page.length"));
|
||||
assert!(frontend.contains("decimalCountToSafeNumber"));
|
||||
assert!(frontend.contains("recordsTotalDecimal"));
|
||||
assert!(frontend.contains("recordsFilteredDecimal"));
|
||||
assert!(frontend.contains("data-transaction-detail"));
|
||||
assert!(!dto.contains("RawPageCursor"));
|
||||
assert!(!frontend.contains("storePager"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_fix_001_long_transaction_identifiers_are_truncated_tooltipped_and_copyable() {
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let sass = include_str!("../frontend/sass/_app.scss");
|
||||
assert!(frontend.contains("truncateLongText"));
|
||||
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "transaction-signature")"#));
|
||||
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "transaction-content-hash")"#));
|
||||
assert!(frontend.contains(r#"setCopyableLongText("transactionDetailSignature""#));
|
||||
assert!(frontend.contains(r#"setCopyableLongText("transactionDetailContentHash""#));
|
||||
assert!(frontend.contains(r#"title="${escapedValue}""#));
|
||||
assert!(frontend.contains("data-copy-long-text"));
|
||||
assert!(frontend.contains("navigator.clipboard.writeText"));
|
||||
assert!(frontend.contains(r#"document.execCommand("copy")"#));
|
||||
assert!(sass.contains(".app-copyable-long-text__value"));
|
||||
assert!(sass.contains("text-overflow: ellipsis"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_fix_002_payload_preview_remains_bounded_and_is_explicitly_copyable() {
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let html = include_str!("../frontend/main.html");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
assert!(runtime.contains("const TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512;"));
|
||||
assert!(html.contains("transactionDetailPayloadPreviewCopy"));
|
||||
assert!(html.contains("Copier le preview hex affiché"));
|
||||
assert!(html.contains("Preview tronquée à 512 bytes"));
|
||||
assert!(frontend.contains(r#"payloadPreviewCopy.dataset.copyField = "transaction-payload-preview""#));
|
||||
assert!(frontend.contains("payloadPreviewCopy.dataset.copyLongText = payloadPreview"));
|
||||
assert!(frontend.contains("payloadPreviewCopy.hidden = false"));
|
||||
assert!(frontend.contains("payloadPreviewCopy.hidden = true"));
|
||||
assert!(frontend.contains("const previousTitle = button.title"));
|
||||
assert!(frontend.contains("button.title = previousTitle"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_ipc_maps_datatables_filters_counts_and_bounded_detail_without_cursor_contract() {
|
||||
let dto = include_str!("../src/dto_account.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
assert!(dto.contains("records_total_decimal"));
|
||||
assert!(dto.contains("records_filtered_decimal"));
|
||||
assert!(runtime.contains("RawAccountStateInspectionQuery::new"));
|
||||
assert!(runtime.contains("RawInspectionPageRequest::new(offset, limit)"));
|
||||
assert!(runtime.contains("RawAccountStateRead::get_raw_account_state"));
|
||||
assert!(runtime.contains("const ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512;"));
|
||||
assert!(tauri.contains("store_query_accounts"));
|
||||
assert!(tauri.contains("store_get_account_detail"));
|
||||
assert!(frontend.contains("store_query_accounts"));
|
||||
assert!(frontend.contains("store_get_account_detail"));
|
||||
assert!(frontend.contains(r#"pubkey: pubkey === "" ? null : pubkey"#));
|
||||
assert!(frontend.contains("recordsTotalDecimal"));
|
||||
assert!(frontend.contains("recordsFilteredDecimal"));
|
||||
assert!(frontend.contains("data-account-detail"));
|
||||
assert!(!dto.contains("RawPageCursor"));
|
||||
assert!(!frontend.contains("storePager"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_datatable_is_server_side_copyable_and_uses_datatables_as_only_pager() {
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let html = include_str!("../frontend/main.html");
|
||||
assert!(frontend.contains("Store Desk account DataTable initialized"));
|
||||
assert!(frontend.contains("Store Desk account DataTables server-side query started"));
|
||||
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "account-pubkey")"#));
|
||||
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "account-state-hash")"#));
|
||||
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "account-owner")"#));
|
||||
assert!(frontend.contains(r#"dataPreviewCopy.dataset.copyField = "account-data-preview""#));
|
||||
for control in ["refreshAccounts", "accountPubkey", "accountSlotMin", "accountSlotMax", "accountDirection", "accountDetailModal"] {
|
||||
assert!(html.contains(&format!("id=\"{control}\"")), "missing pre.008 account control: {control}");
|
||||
}
|
||||
assert!(html.contains("Account data preview hex"));
|
||||
assert!(html.contains("Preview tronquée à 512 bytes"));
|
||||
for forbidden in ["Premier", "Précédent", "Suivant", "storePager", "store-pagination"] {
|
||||
assert!(!html.contains(forbidden), "second Store pager marker present: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_observations_are_server_side_inside_entity_details_with_datatables_as_only_pager() {
|
||||
let html = include_str!("../frontend/main.html");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
for expected in [
|
||||
"id=\"transactionObservationsTable\"",
|
||||
"id=\"accountObservationsTable\"",
|
||||
"id=\"transactionObservationsError\"",
|
||||
"id=\"accountObservationsError\"",
|
||||
] {
|
||||
assert!(html.contains(expected), "missing pre.010 observation UI contract: {expected}");
|
||||
}
|
||||
assert!(frontend.contains("store_query_transaction_observations"));
|
||||
assert!(frontend.contains("store_query_account_observations"));
|
||||
assert!(frontend.contains("initializeTransactionObservationTable"));
|
||||
assert!(frontend.contains("initializeAccountObservationTable"));
|
||||
assert!(frontend.contains("currentTransactionObservationSignature"));
|
||||
assert!(frontend.contains("currentAccountObservationIdentity"));
|
||||
assert!(frontend.contains("stale transaction observation response ignored"));
|
||||
assert!(frontend.contains("stale account observation response ignored"));
|
||||
assert!(tauri.contains("store_query_transaction_observations"));
|
||||
assert!(tauri.contains("store_query_account_observations"));
|
||||
assert!(runtime.contains("RawSortDirection::Descending"));
|
||||
assert!(!html.contains("store-observation-pager"));
|
||||
assert!(!frontend.contains("cursor:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_observation_rows_project_safe_provenance_and_reuse_long_text_copy_without_source_bytes() {
|
||||
let dto = include_str!("../src/dto_observation.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
for expected in [
|
||||
"source_payload_hash",
|
||||
"source_payload_size_decimal",
|
||||
"received_at_unix_millis_decimal",
|
||||
"observed_at_unix_millis_decimal",
|
||||
"capture_session_id",
|
||||
"endpoint_id",
|
||||
"filter_id",
|
||||
"transaction_signature",
|
||||
"write_version_decimal",
|
||||
] {
|
||||
assert!(dto.contains(expected), "missing safe observation metadata field: {expected}");
|
||||
}
|
||||
for forbidden in ["source_payload_bytes", "payload_preview", "data_preview", "raw_bytes"] {
|
||||
assert!(!dto.contains(forbidden), "observation DTO leaked forbidden bytes field: {forbidden}");
|
||||
}
|
||||
assert!(frontend.contains("transaction-observation-key"));
|
||||
assert!(frontend.contains("account-observation-key"));
|
||||
assert!(frontend.contains("account-observation-transaction-signature"));
|
||||
assert!(frontend.contains("renderCopyableLongText"));
|
||||
}
|
||||
280
crates/ksp-app-store-desk/tests/desktop_security.rs
Normal file
280
crates/ksp-app-store-desk/tests/desktop_security.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
// file: crates/ksp-app-store-desk/tests/desktop_security.rs
|
||||
// version: 12
|
||||
|
||||
//! Security boundary tests for the Store Desk scaffold.
|
||||
|
||||
fn parse_json(source: &str) -> serde_json::Result<serde_json::Value> {
|
||||
return serde_json::from_str::<serde_json::Value>(source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_capabilities_are_core_and_tracing_only() {
|
||||
let parsed = parse_json(include_str!("../capabilities/default.json"));
|
||||
assert!(parsed.is_ok(), "committed capability JSON must be valid");
|
||||
let capability = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let permissions = capability["permissions"].as_array();
|
||||
assert!(permissions.is_some(), "capability permissions must be an array");
|
||||
let permissions = match permissions {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(permissions.len(), 2);
|
||||
assert_eq!(permissions[0], "core:default");
|
||||
assert_eq!(permissions[1], "tracing:default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_frontend_avoids_network_browser_storage_and_blocking_dialogs() {
|
||||
let frontend = [
|
||||
include_str!("../frontend/ts/frontend_log.ts"),
|
||||
include_str!("../frontend/ts/invoke.ts"),
|
||||
include_str!("../frontend/ts/main.ts"),
|
||||
include_str!("../frontend/ts/splash.ts"),
|
||||
]
|
||||
.join("\n");
|
||||
for forbidden in ["fetch(", "XMLHttpRequest", "localStorage", "sessionStorage", "window.alert", "window.confirm", "window.prompt"] {
|
||||
assert!(!frontend.contains(forbidden), "forbidden frontend primitive present: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_frontend_tracing_covers_navigation_buttons_tabs_controls_refresh_and_ipc() {
|
||||
let main = include_str!("../frontend/ts/main.ts");
|
||||
let invoke = include_str!("../frontend/ts/invoke.ts");
|
||||
assert!(main.contains("navigation button clicked"));
|
||||
assert!(main.contains("diagnostics refresh button clicked"));
|
||||
assert!(main.contains("generic button clicked"));
|
||||
assert!(main.contains("tab control clicked"));
|
||||
assert!(main.contains("tab activated"));
|
||||
assert!(main.contains("interactive control changed"));
|
||||
assert!(main.contains("view activated"));
|
||||
assert!(invoke.contains("IPC command requested"));
|
||||
assert!(invoke.contains("IPC command completed"));
|
||||
assert!(invoke.contains("IPC command failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_store_desk_development_logging_explicitly_selects_supertrace() {
|
||||
let constants = include_str!("../src/constants.rs");
|
||||
let bootstrap = include_str!("../src/bootstrap.rs");
|
||||
assert!(constants.contains("DEVELOPMENT_LOGGING_PROFILE: &str = \"supertrace\""));
|
||||
assert!(bootstrap.contains("FILE_ID_COMPOSITE_KSP_APP_STORE_DESK"));
|
||||
assert!(bootstrap.contains("logging_profile.profile_id() != crate::DEVELOPMENT_LOGGING_PROFILE"));
|
||||
assert!(bootstrap.contains("LogFilterLevel::Trace"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_store_status_ipc_is_backend_neutral_and_secret_free() {
|
||||
let dto = include_str!("../src/dto_common.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
assert!(dto.contains("StoreRuntimeStatusDto"));
|
||||
assert!(runtime.contains("runtime_snapshot"));
|
||||
assert!(runtime.contains("health().await"));
|
||||
assert!(tauri.contains("store_runtime_status"));
|
||||
for forbidden in ["connection_uri", "connectionUri", "postgresql://", "password", "RawPayload", "RawAccountState", "tokio_postgres"] {
|
||||
assert!(!dto.contains(forbidden), "Store status DTO source contains forbidden field/material: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_rows_are_payload_free_detail_is_bounded_and_filter_values_are_not_logged() {
|
||||
let dto = include_str!("../src/dto_transaction.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let row_start = dto.find("pub(crate) struct StoreTransactionRowDto");
|
||||
let response_start = dto.find("pub(crate) struct StoreTransactionQueryResponseDto");
|
||||
assert!(row_start.is_some());
|
||||
assert!(response_start.is_some());
|
||||
if let (std::option::Option::Some(row_start), std::option::Option::Some(response_start)) = (row_start, response_start) {
|
||||
let row_source = &dto[row_start..response_start];
|
||||
assert!(!row_source.contains("payload_preview_hex"));
|
||||
assert!(!row_source.contains("payload: "));
|
||||
}
|
||||
assert!(dto.contains("payload_preview_hex"));
|
||||
assert!(dto.contains("payload_preview_truncated"));
|
||||
assert!(runtime.contains("TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512"));
|
||||
assert!(runtime.contains("std::cmp::min(payload_bytes.len(), TRANSACTION_DETAIL_PREVIEW_BYTES)"));
|
||||
assert!(frontend.contains("transaction DataTables server-side query started"));
|
||||
assert!(frontend.contains("transaction DataTables page changed"));
|
||||
assert!(frontend.contains("transaction DataTables page length changed"));
|
||||
assert!(frontend.contains("transaction DataTables redraw completed"));
|
||||
assert!(frontend.contains("transaction detail opened"));
|
||||
assert!(frontend.contains("transaction detail closed"));
|
||||
for forbidden in [
|
||||
"frontendDebug(\"main\", \"Store Desk transaction Store filter changed\", { slotMin",
|
||||
"frontendDebug(\"main\", \"Store Desk transaction Store filter changed\", { slotMax",
|
||||
"frontendDebug(\"main\", \"Store Desk transaction detail opened\", { signature",
|
||||
"payloadPreviewHex:",
|
||||
] {
|
||||
assert!(!frontend.contains(forbidden), "transaction business/filter value leaked into frontend tracing: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_request_rejects_datatables_search_order_draw_and_physical_material() {
|
||||
let dto = include_str!("../src/dto_transaction.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
for forbidden in ["draw:", "columns:", "search:", "order:", "cursor:", "connection_uri", "postgresql://", "endpoint"] {
|
||||
assert!(!dto.contains(forbidden), "forbidden DataTables/physical request material present: {forbidden}");
|
||||
}
|
||||
assert!(runtime.contains("25 | 50 | 100"));
|
||||
assert!(runtime.contains("inspection_page_from_request(request.offset.as_str(), request.limit)"));
|
||||
assert!(runtime.contains("offset_text.parse::<u64>()"));
|
||||
assert!(runtime.contains("RawSlotRange::new"));
|
||||
assert!(runtime.contains("\"ascending\" =>"));
|
||||
assert!(runtime.contains("\"descending\" =>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_fix_003_copy_tracing_is_field_id_only_without_value_material() {
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
for allowed in [
|
||||
r#"frontendDebug("main", "Store Desk long text copy requested", { fieldId })"#,
|
||||
r#"frontendError("main", "Store Desk long text copy failed", { fieldId })"#,
|
||||
r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId })"#,
|
||||
] {
|
||||
assert!(frontend.contains(allowed), "expected field-id-only copy tracing call is missing: {allowed}");
|
||||
}
|
||||
for forbidden in [
|
||||
r#"frontendDebug("main", "Store Desk long text copy requested", { fieldId, value })"#,
|
||||
r#"frontendDebug("main", "Store Desk long text copy requested", { value, fieldId })"#,
|
||||
r#"frontendDebug("main", "Store Desk long text copy requested", { value })"#,
|
||||
r#"frontendError("main", "Store Desk long text copy failed", { fieldId, value })"#,
|
||||
r#"frontendError("main", "Store Desk long text copy failed", { value, fieldId })"#,
|
||||
r#"frontendError("main", "Store Desk long text copy failed", { value })"#,
|
||||
r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId, value })"#,
|
||||
r#"frontendDebug("main", "Store Desk long text copy completed", { value, fieldId })"#,
|
||||
r#"frontendDebug("main", "Store Desk long text copy completed", { value })"#,
|
||||
] {
|
||||
assert!(!frontend.contains(forbidden), "copy tracing leaked copied value material: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_fix_002_payload_preview_copy_never_logs_preview_material() {
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
assert!(frontend.contains(r#"payloadPreviewCopy.dataset.copyField = "transaction-payload-preview""#));
|
||||
assert!(!frontend.contains(r#"frontendDebug("main", "Store Desk long text copy requested", { fieldId, value })"#));
|
||||
assert!(!frontend.contains(r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId, value })"#));
|
||||
assert!(!frontend.contains(r#"frontendError("main", "Store Desk long text copy failed", { fieldId, value })"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_rows_are_data_free_detail_is_bounded_and_filter_values_are_not_logged() {
|
||||
let dto = include_str!("../src/dto_account.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let row_start = dto.find("pub(crate) struct StoreAccountRowDto");
|
||||
let response_start = dto.find("pub(crate) struct StoreAccountQueryResponseDto");
|
||||
assert!(row_start.is_some());
|
||||
assert!(response_start.is_some());
|
||||
if let (std::option::Option::Some(row_start), std::option::Option::Some(response_start)) = (row_start, response_start) {
|
||||
let row_source = &dto[row_start..response_start];
|
||||
assert!(!row_source.contains("data_preview_hex"));
|
||||
assert!(!row_source.contains("pub(crate) data:"));
|
||||
}
|
||||
assert!(dto.contains("data_preview_hex"));
|
||||
assert!(dto.contains("data_preview_truncated"));
|
||||
assert!(runtime.contains("ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512"));
|
||||
assert!(runtime.contains("std::cmp::min(data.len(), ACCOUNT_DETAIL_PREVIEW_BYTES)"));
|
||||
for required in [
|
||||
"account DataTables page changed",
|
||||
"account DataTables page length changed",
|
||||
"account DataTables redraw completed",
|
||||
"account detail opened",
|
||||
"account detail closed",
|
||||
] {
|
||||
assert!(frontend.contains(required), "missing account tracing event: {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
r#"frontendDebug("main", "Store Desk account Store filter changed", { pubkey"#,
|
||||
r#"frontendDebug("main", "Store Desk account Store filter changed", { slotMin"#,
|
||||
r#"frontendDebug("main", "Store Desk account Store filter changed", { slotMax"#,
|
||||
r#"frontendDebug("main", "Store Desk account detail opened", { pubkey"#,
|
||||
"dataPreviewHex:",
|
||||
] {
|
||||
assert!(!frontend.contains(forbidden), "account business/filter value leaked into frontend tracing: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_request_rejects_datatables_search_order_draw_network_and_physical_material() {
|
||||
let dto = include_str!("../src/dto_account.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
for forbidden in ["draw:", "columns:", "search:", "order:", "cursor:", "network:", "connection_uri", "postgresql://", "endpoint"] {
|
||||
assert!(!dto.contains(forbidden), "forbidden DataTables/physical account request material present: {forbidden}");
|
||||
}
|
||||
assert!(runtime.contains("25 | 50 | 100"));
|
||||
assert!(runtime.contains("inspection_page_from_request(request.offset.as_str(), request.limit)"));
|
||||
assert!(runtime.contains("offset_text.parse::<u64>()"));
|
||||
assert!(runtime.contains("parse_optional_pubkey(request.pubkey.as_deref())"));
|
||||
assert!(runtime.contains("RawSlotRange::new"));
|
||||
assert!(runtime.contains("request.pubkey.parse::<ksp_store_lib::Pubkey>()"));
|
||||
assert!(runtime.contains("decode_hex_32(request.state_hash.as_str())"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_copy_tracing_never_logs_identity_or_data_material() {
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
for required in ["account-pubkey", "account-state-hash", "account-owner", "account-data-preview"] {
|
||||
assert!(frontend.contains(required), "missing account copy field id: {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
r#"frontendDebug("main", "Store Desk long text copy requested", { fieldId, value })"#,
|
||||
r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId, value })"#,
|
||||
r#"frontendError("main", "Store Desk long text copy failed", { fieldId, value })"#,
|
||||
] {
|
||||
assert!(!frontend.contains(forbidden), "account copy tracing leaked copied value material: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_observation_ipc_and_tracing_keep_entity_values_and_source_bytes_out_of_logs() {
|
||||
let dto = include_str!("../src/dto_observation.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
for forbidden in ["draw:", "search:", "order:", "columns:", "cursor:", "network:"] {
|
||||
assert!(!dto.contains(forbidden), "observation IPC request leaked DataTables/physical field: {forbidden}");
|
||||
}
|
||||
for forbidden in ["source_payload_bytes", "payload_preview", "data_preview", "raw_bytes"] {
|
||||
assert!(!dto.contains(forbidden), "observation IPC leaked raw/source bytes: {forbidden}");
|
||||
}
|
||||
assert!(frontend.contains(r#"Store Desk transaction observation DataTables server-side query started", { pageLength: request.limit }"#));
|
||||
assert!(frontend.contains(r#"Store Desk account observation DataTables server-side query started", { pageLength: request.limit }"#));
|
||||
assert!(!frontend.contains(r#"query started", { signature"#));
|
||||
assert!(!frontend.contains(r#"query started", { pubkey"#));
|
||||
assert!(!frontend.contains(r#"query completed", { observationKey"#));
|
||||
let transaction_command = tauri.find("fn store_query_transaction_observations");
|
||||
let account_command = tauri.find("fn store_query_account_observations");
|
||||
assert!(transaction_command.is_some());
|
||||
assert!(account_command.is_some());
|
||||
if let (std::option::Option::Some(transaction_start), std::option::Option::Some(account_start)) = (transaction_command, account_command) {
|
||||
let transaction_source = &tauri[transaction_start..];
|
||||
let account_source = &tauri[account_start..transaction_start];
|
||||
assert!(transaction_source.contains("project_command_error"));
|
||||
assert!(transaction_source.contains(r#""store_query_transaction_observations""#));
|
||||
assert!(transaction_source.contains("crate::TRACING_DOMAIN_STORE"));
|
||||
assert!(account_source.contains("project_command_error"));
|
||||
assert!(account_source.contains(r#""store_query_account_observations""#));
|
||||
assert!(account_source.contains("crate::TRACING_DOMAIN_STORE"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_observation_page_hardening_rejects_hostile_draw_length_and_unsafe_exact_counts() {
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
assert!(frontend.contains("Number.isSafeInteger(page.draw)"));
|
||||
assert!(frontend.contains("Number.isSafeInteger(page.start)"));
|
||||
assert!(frontend.contains("[25, 50, 100].includes(page.length)"));
|
||||
assert!(frontend.contains("Number.MAX_SAFE_INTEGER"));
|
||||
assert!(frontend.contains("exact count exceeds frontend safe integer domain"));
|
||||
assert!(runtime.contains("inspection_page_from_request"));
|
||||
assert!(runtime.contains("request.offset.as_str(), request.limit"));
|
||||
assert!(runtime.contains("offset_text.parse::<u64>()"));
|
||||
}
|
||||
10
crates/ksp-app-store-desk/tests/public_api.rs
Normal file
10
crates/ksp-app-store-desk/tests/public_api.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// file: crates/ksp-app-store-desk/tests/public_api.rs
|
||||
// version: 2
|
||||
|
||||
//! Public API boundary test for the Store Desk scaffold.
|
||||
|
||||
#[test]
|
||||
fn pre_002_public_surface_exposes_only_application_run_entry_point() {
|
||||
let run: fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()> = ksp_app_store_desk_lib::run;
|
||||
let _ = run;
|
||||
}
|
||||
374
crates/ksp-app-store-desk/tests/release_completeness.rs
Normal file
374
crates/ksp-app-store-desk/tests/release_completeness.rs
Normal file
@@ -0,0 +1,374 @@
|
||||
// file: crates/ksp-app-store-desk/tests/release_completeness.rs
|
||||
// version: 2
|
||||
|
||||
//! Release-completeness firewall for the Store Desk V1 RAW surface.
|
||||
|
||||
fn parse_json(source: &str) -> serde_json::Result<serde_json::Value> {
|
||||
return serde_json::from_str::<serde_json::Value>(source);
|
||||
}
|
||||
|
||||
fn toml_table_keys<'a>(source: &'a str, table: &str) -> std::vec::Vec<&'a str> {
|
||||
let header = format!("[{table}]");
|
||||
let mut in_table = false;
|
||||
let mut keys = std::vec::Vec::<&str>::new();
|
||||
for line in source.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed == header {
|
||||
in_table = true;
|
||||
continue;
|
||||
}
|
||||
if in_table && trimmed.starts_with('[') {
|
||||
break;
|
||||
}
|
||||
if !in_table || trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let std::option::Option::Some((key, _)) = trimmed.split_once('=') {
|
||||
let key = key.trim();
|
||||
let normalized = match key.strip_suffix(".workspace") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => key,
|
||||
};
|
||||
keys.push(normalized);
|
||||
}
|
||||
}
|
||||
keys.sort_unstable();
|
||||
return keys;
|
||||
}
|
||||
|
||||
fn json_object_keys(value: &serde_json::Value) -> std::vec::Vec<&str> {
|
||||
let mut keys = match value.as_object() {
|
||||
std::option::Option::Some(object) => object.keys().map(std::string::String::as_str).collect::<std::vec::Vec<&str>>(),
|
||||
std::option::Option::None => std::vec::Vec::<&str>::new(),
|
||||
};
|
||||
keys.sort_unstable();
|
||||
return keys;
|
||||
}
|
||||
|
||||
fn source_segment<'a>(source: &'a str, start: &str, end: &str) -> &'a str {
|
||||
let after_start = match source.split_once(start) {
|
||||
std::option::Option::Some((_, tail)) => tail,
|
||||
std::option::Option::None => return "",
|
||||
};
|
||||
return match after_start.split_once(end) {
|
||||
std::option::Option::Some((head, _)) => head,
|
||||
std::option::Option::None => after_start,
|
||||
};
|
||||
}
|
||||
|
||||
fn handler_commands(source: &str) -> std::vec::Vec<&str> {
|
||||
let block = source_segment(source, "tauri::generate_handler![", "]);\n}");
|
||||
let mut commands = block
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| return !line.is_empty())
|
||||
.map(|line| return line.trim_end_matches(','))
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
commands.sort_unstable();
|
||||
return commands;
|
||||
}
|
||||
|
||||
fn crate_modules(source: &str) -> std::vec::Vec<&str> {
|
||||
let mut modules = source
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| return line.starts_with("mod ") && line.ends_with(';'))
|
||||
.map(|line| return line.trim_start_matches("mod ").trim_end_matches(';'))
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
modules.sort_unstable();
|
||||
return modules;
|
||||
}
|
||||
|
||||
fn visible_struct_names(source: &str) -> std::vec::Vec<&str> {
|
||||
let mut names = source
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| return line.starts_with("pub(crate) struct "))
|
||||
.filter_map(|line| return line.strip_prefix("pub(crate) struct "))
|
||||
.filter_map(|line| return line.split_whitespace().next())
|
||||
.map(|name| return name.trim_end_matches('{'))
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
names.sort_unstable();
|
||||
return names;
|
||||
}
|
||||
|
||||
fn const_line<'a>(source: &'a str, name: &str) -> &'a str {
|
||||
let prefix = format!("const {name}:");
|
||||
for line in source.lines() {
|
||||
if line.starts_with(prefix.as_str()) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_rust_and_frontend_dependency_inventories_are_exact() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
assert_eq!(toml_table_keys(manifest, "build-dependencies"), ["tauri-build"]);
|
||||
assert_eq!(
|
||||
toml_table_keys(manifest, "dependencies"),
|
||||
[
|
||||
"chrono",
|
||||
"fs2",
|
||||
"ksp-config-lib",
|
||||
"ksp-core-lib",
|
||||
"ksp-logging-lib",
|
||||
"ksp-store-lib",
|
||||
"serde",
|
||||
"tauri",
|
||||
"tauri-plugin-tracing",
|
||||
"tokio",
|
||||
"ts-rs",
|
||||
]
|
||||
);
|
||||
assert_eq!(toml_table_keys(manifest, "dev-dependencies"), ["serde_json"]);
|
||||
let package = parse_json(include_str!("../package.json"));
|
||||
assert!(package.is_ok(), "Store Desk package.json must stay valid JSON");
|
||||
if let std::result::Result::Ok(package) = package {
|
||||
assert_eq!(
|
||||
json_object_keys(&package["dependencies"]),
|
||||
[
|
||||
"@fltsci/tauri-plugin-tracing",
|
||||
"@fortawesome/fontawesome-free",
|
||||
"@tauri-apps/api",
|
||||
"bootstrap",
|
||||
"datatables.net-bs5",
|
||||
"resize-observer-polyfill",
|
||||
"simplebar",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
json_object_keys(&package["devDependencies"]),
|
||||
["@tauri-apps/cli", "@types/bootstrap", "@types/node", "sass-embedded", "typescript", "vite"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_module_command_and_tauri_capability_inventories_are_exact() {
|
||||
let lib = include_str!("../src/lib.rs");
|
||||
assert_eq!(
|
||||
crate_modules(lib),
|
||||
[
|
||||
"app_state",
|
||||
"bootstrap",
|
||||
"constants",
|
||||
"dto_account",
|
||||
"dto_common",
|
||||
"dto_observation",
|
||||
"dto_transaction",
|
||||
"errors",
|
||||
"frontend_logging",
|
||||
"logging_runtime",
|
||||
"splash",
|
||||
"store_runtime",
|
||||
"tauri",
|
||||
"tw_main",
|
||||
"tw_splash",
|
||||
]
|
||||
);
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
assert_eq!(
|
||||
handler_commands(tauri),
|
||||
[
|
||||
"emit_frontend_log",
|
||||
"get_shell_status",
|
||||
"splash_frontend_ready",
|
||||
"store_get_account_detail",
|
||||
"store_get_transaction_detail",
|
||||
"store_query_account_observations",
|
||||
"store_query_accounts",
|
||||
"store_query_transaction_observations",
|
||||
"store_query_transactions",
|
||||
"store_runtime_status",
|
||||
]
|
||||
);
|
||||
let capability = parse_json(include_str!("../capabilities/default.json"));
|
||||
assert!(capability.is_ok(), "Store Desk capability JSON must stay valid");
|
||||
if let std::result::Result::Ok(capability) = capability {
|
||||
assert_eq!(capability["identifier"], "default");
|
||||
assert_eq!(capability["windows"], serde_json::json!(["splash", "main"]));
|
||||
assert_eq!(capability["permissions"], serde_json::json!(["core:default", "tracing:default"]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_config_composition_is_exact_network_scoped_and_secret_free() {
|
||||
let source = include_str!("../../../config/composite.ksp-app-store-desk.json");
|
||||
let composite = parse_json(source);
|
||||
assert!(composite.is_ok(), "Store Desk composite must stay valid JSON");
|
||||
if let std::result::Result::Ok(composite) = composite {
|
||||
assert_eq!(composite["format_version"], 1);
|
||||
assert_eq!(composite["default_profile"], "mainnet");
|
||||
let profiles = composite["profiles"].as_array();
|
||||
assert!(profiles.is_some());
|
||||
if let std::option::Option::Some(profiles) = profiles {
|
||||
assert_eq!(profiles.len(), 3);
|
||||
let profile_ids = profiles
|
||||
.iter()
|
||||
.map(|profile| match profile["profile_id"].as_str() {
|
||||
std::option::Option::Some(value) => return value,
|
||||
std::option::Option::None => return "",
|
||||
})
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
assert_eq!(profile_ids, ["devnet", "mainnet", "testnet"]);
|
||||
for profile in profiles {
|
||||
let profile_id = match profile["profile_id"].as_str() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => "",
|
||||
};
|
||||
let documents = profile["documents"].as_array();
|
||||
assert!(documents.is_some());
|
||||
if let std::option::Option::Some(documents) = documents {
|
||||
assert_eq!(documents.len(), 2);
|
||||
assert_eq!(documents[0]["component_id"], "logging");
|
||||
assert_eq!(documents[0]["file_id"], "cfg.std.logging");
|
||||
assert_eq!(documents[0]["profile_id"], "supertrace");
|
||||
assert_eq!(documents[1]["component_id"], "store");
|
||||
assert_eq!(documents[1]["file_id"], "cfg.std.store");
|
||||
assert_eq!(documents[1]["profile_id"], profile_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for forbidden in ["postgres://", "postgresql://", "password", "credential", "connection_uri", "connectionUri", "KSP_STORE_POSTGRES"] {
|
||||
assert!(!source.contains(forbidden), "Store Desk composite leaked physical/secret material: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_frontend_security_scan_keeps_network_storage_native_dialogs_and_physical_store_out() {
|
||||
let frontend = [
|
||||
include_str!("../frontend/ts/frontend_log.ts"),
|
||||
include_str!("../frontend/ts/invoke.ts"),
|
||||
include_str!("../frontend/ts/main.ts"),
|
||||
include_str!("../frontend/ts/splash.ts"),
|
||||
include_str!("../frontend/main.html"),
|
||||
include_str!("../frontend/splash.html"),
|
||||
]
|
||||
.join("\n");
|
||||
for forbidden in [
|
||||
"fetch(",
|
||||
"XMLHttpRequest",
|
||||
"WebSocket(",
|
||||
"EventSource(",
|
||||
"localStorage",
|
||||
"sessionStorage",
|
||||
"indexedDB",
|
||||
"document.cookie",
|
||||
"window.open(",
|
||||
"alert(",
|
||||
"confirm(",
|
||||
"prompt(",
|
||||
"postgres://",
|
||||
"postgresql://",
|
||||
"connection_uri",
|
||||
"connectionUri",
|
||||
"ksp_store_postgres",
|
||||
"tokio_postgres",
|
||||
"deadpool_postgres",
|
||||
"SELECT ",
|
||||
"INSERT ",
|
||||
"UPDATE ",
|
||||
"DELETE ",
|
||||
] {
|
||||
assert!(!frontend.contains(forbidden), "Store Desk frontend crossed the release security firewall: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_ipc_dto_inventory_is_exact_and_table_contracts_remain_raw_byte_free() {
|
||||
let dto_account = include_str!("../src/dto_account.rs");
|
||||
let dto_common = include_str!("../src/dto_common.rs");
|
||||
let dto_observation = include_str!("../src/dto_observation.rs");
|
||||
let dto_transaction = include_str!("../src/dto_transaction.rs");
|
||||
let dto_sources = [dto_account, dto_common, dto_observation, dto_transaction].join("\n");
|
||||
assert_eq!(
|
||||
visible_struct_names(dto_sources.as_str()),
|
||||
[
|
||||
"CommandErrorDto",
|
||||
"ShellStatusDto",
|
||||
"StoreAccountDetailDto",
|
||||
"StoreAccountDetailRequestDto",
|
||||
"StoreAccountObservationQueryRequestDto",
|
||||
"StoreAccountObservationQueryResponseDto",
|
||||
"StoreAccountObservationRowDto",
|
||||
"StoreAccountQueryRequestDto",
|
||||
"StoreAccountQueryResponseDto",
|
||||
"StoreAccountRowDto",
|
||||
"StoreObservationProvenanceDto",
|
||||
"StoreRuntimeStatusDto",
|
||||
"StoreTransactionDetailDto",
|
||||
"StoreTransactionDetailRequestDto",
|
||||
"StoreTransactionObservationQueryRequestDto",
|
||||
"StoreTransactionObservationQueryResponseDto",
|
||||
"StoreTransactionObservationRowDto",
|
||||
"StoreTransactionQueryRequestDto",
|
||||
"StoreTransactionQueryResponseDto",
|
||||
"StoreTransactionRowDto",
|
||||
]
|
||||
);
|
||||
for request_marker in [
|
||||
"pub(crate) struct StoreTransactionQueryRequestDto",
|
||||
"pub(crate) struct StoreTransactionDetailRequestDto",
|
||||
"pub(crate) struct StoreAccountQueryRequestDto",
|
||||
"pub(crate) struct StoreAccountDetailRequestDto",
|
||||
"pub(crate) struct StoreTransactionObservationQueryRequestDto",
|
||||
"pub(crate) struct StoreAccountObservationQueryRequestDto",
|
||||
] {
|
||||
assert!(dto_sources.contains(request_marker), "missing Store Desk request DTO: {request_marker}");
|
||||
}
|
||||
for forbidden in ["RawPageCursor", "tokio_postgres", "deadpool_postgres", "ksp_store_postgres", "source_payload_bytes", "raw_bytes"] {
|
||||
assert!(!dto_sources.contains(forbidden), "Store Desk DTO surface leaked forbidden contract material: {forbidden}");
|
||||
}
|
||||
assert!(!source_segment(dto_transaction, "pub(crate) struct StoreTransactionRowDto", "/// Bounded detail projection").contains("payload_preview"));
|
||||
assert!(!source_segment(dto_account, "pub(crate) struct StoreAccountRowDto", "/// Bounded detail projection").contains("data_preview"));
|
||||
assert!(!dto_observation.contains("payload_preview"));
|
||||
assert!(!dto_observation.contains("data_preview"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_cursor_keyset_navigation_and_random_access_inspection_coexist_without_contract_collision() {
|
||||
let api = include_str!("../../ksp-store-api/src/lib.rs");
|
||||
let facade = include_str!("../../ksp-store-lib/src/lib.rs");
|
||||
let transaction = include_str!("../../ksp-store-postgres-lib/src/raw_transaction.rs");
|
||||
let account = include_str!("../../ksp-store-postgres-lib/src/raw_account.rs");
|
||||
let app_runtime = include_str!("../src/store_runtime.rs");
|
||||
for expected in ["RawPageCursor", "RawPageRequest", "RawInspectionPageRequest", "RawTransactionInspectionQuery", "RawAccountStateInspectionQuery"] {
|
||||
assert!(api.contains(expected), "Store API lost pagination/inspection contract: {expected}");
|
||||
assert!(facade.contains(expected), "Store facade lost pagination/inspection re-export: {expected}");
|
||||
}
|
||||
for cursor_statement in ["LIST_TRANSACTIONS_ASC_SQL", "LIST_TRANSACTIONS_DESC_SQL"] {
|
||||
let statement = const_line(transaction, cursor_statement);
|
||||
assert!(!statement.is_empty(), "missing transaction cursor statement: {cursor_statement}");
|
||||
assert!(!statement.contains("OFFSET"), "transaction cursor statement became OFFSET based: {cursor_statement}");
|
||||
}
|
||||
for cursor_statement in [
|
||||
"LIST_ACCOUNT_STATES_ASC_SQL",
|
||||
"LIST_ACCOUNT_STATES_DESC_SQL",
|
||||
"LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL",
|
||||
"LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL",
|
||||
] {
|
||||
let statement = const_line(account, cursor_statement);
|
||||
assert!(!statement.is_empty(), "missing account cursor statement: {cursor_statement}");
|
||||
assert!(!statement.contains("OFFSET"), "account cursor statement became OFFSET based: {cursor_statement}");
|
||||
}
|
||||
for inspection_statement in
|
||||
["INSPECT_TRANSACTIONS_ASC_SQL", "INSPECT_TRANSACTIONS_DESC_SQL", "INSPECT_OBSERVATIONS_ASC_SQL", "INSPECT_OBSERVATIONS_DESC_SQL"]
|
||||
{
|
||||
let statement = const_line(transaction, inspection_statement);
|
||||
assert!(statement.contains("OFFSET"), "transaction inspection statement lost random access: {inspection_statement}");
|
||||
}
|
||||
for inspection_statement in [
|
||||
"INSPECT_ACCOUNT_STATES_ASC_SQL",
|
||||
"INSPECT_ACCOUNT_STATES_DESC_SQL",
|
||||
"INSPECT_ACCOUNT_OBSERVATIONS_ASC_SQL",
|
||||
"INSPECT_ACCOUNT_OBSERVATIONS_DESC_SQL",
|
||||
] {
|
||||
let statement = const_line(account, inspection_statement);
|
||||
assert!(statement.contains("OFFSET"), "account inspection statement lost random access: {inspection_statement}");
|
||||
}
|
||||
assert!(app_runtime.contains("RawInspectionPageRequest::new"));
|
||||
assert!(!app_runtime.contains("RawPageCursor"));
|
||||
}
|
||||
31
crates/ksp-app-store-desk/tsconfig.json
Normal file
31
crates/ksp-app-store-desk/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"frontend",
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
56
crates/ksp-app-store-desk/unit_tests/dto_account.rs
Normal file
56
crates/ksp-app-store-desk/unit_tests/dto_account.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
// file: crates/ksp-app-store-desk/unit_tests/dto_account.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_rows_are_data_free_and_detail_preview_is_explicitly_bounded() {
|
||||
let row = crate::StoreAccountRowDto {
|
||||
data_length_decimal: "2048".to_owned(),
|
||||
executable: false,
|
||||
lamports_decimal: "12345678901234567890".to_owned(),
|
||||
owner: "11111111111111111111111111111111".to_owned(),
|
||||
pubkey: "SysvarRent111111111111111111111111111111111".to_owned(),
|
||||
rent_epoch_decimal: "18446744073709551615".to_owned(),
|
||||
slot_decimal: "987654321".to_owned(),
|
||||
state_hash: "11".repeat(32),
|
||||
};
|
||||
let serialized = serde_json::to_value(&row);
|
||||
assert!(serialized.is_ok());
|
||||
let serialized = match serialized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(serialized.get("dataPreviewHex").is_none());
|
||||
assert!(serialized.get("data").is_none());
|
||||
assert_eq!(serialized["dataLengthDecimal"], "2048");
|
||||
let detail = crate::StoreAccountDetailDto {
|
||||
data_length_decimal: "8192".to_owned(),
|
||||
data_preview_hex: "abcd".to_owned(),
|
||||
data_preview_truncated: true,
|
||||
executable: true,
|
||||
lamports_decimal: "42".to_owned(),
|
||||
owner: "11111111111111111111111111111111".to_owned(),
|
||||
pubkey: "SysvarRent111111111111111111111111111111111".to_owned(),
|
||||
rent_epoch_decimal: "7".to_owned(),
|
||||
slot_decimal: "99".to_owned(),
|
||||
state_hash: "22".repeat(32),
|
||||
};
|
||||
let serialized = serde_json::to_value(&detail);
|
||||
assert!(serialized.is_ok());
|
||||
let serialized = match serialized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(serialized["dataPreviewHex"], "abcd");
|
||||
assert_eq!(serialized["dataPreviewTruncated"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_query_request_contains_only_random_access_and_explicit_store_filters() {
|
||||
let source = include_str!("../src/dto_account.rs");
|
||||
for required in ["direction", "limit", "offset", "pubkey", "slot_max", "slot_min"] {
|
||||
assert!(source.contains(required), "missing Store account query field: {required}");
|
||||
}
|
||||
for forbidden in ["draw", "columns", "search", "order", "cursor", "network", "connection_uri", "endpoint"] {
|
||||
assert!(!source.contains(&format!("pub(crate) {forbidden}")), "DataTables/physical field leaked into Store account query DTO: {forbidden}");
|
||||
}
|
||||
}
|
||||
40
crates/ksp-app-store-desk/unit_tests/dto_common.rs
Normal file
40
crates/ksp-app-store-desk/unit_tests/dto_common.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
// file: crates/ksp-app-store-desk/unit_tests/dto_common.rs
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn command_error_projection_keeps_only_stable_public_fields() {
|
||||
let source = ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "safe message").with_context("secret_context", "must-not-cross-ipc");
|
||||
let projected = crate::CommandErrorDto::from_error(&source);
|
||||
assert_eq!(projected.domain, "store_desk");
|
||||
assert_eq!(projected.code, "app_state_invalid");
|
||||
assert_eq!(projected.message, "safe message");
|
||||
let debug = format!("{projected:?}");
|
||||
assert!(!debug.contains("must-not-cross-ipc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_runtime_status_dto_contains_only_safe_portable_fields() {
|
||||
let value = crate::StoreRuntimeStatusDto {
|
||||
backend_kind: std::option::Option::Some("postgres".to_owned()),
|
||||
diagnostic: std::option::Option::None,
|
||||
health_state: "ready".to_owned(),
|
||||
migration_version_decimal: std::option::Option::Some("2".to_owned()),
|
||||
network: std::option::Option::Some("mainnet".to_owned()),
|
||||
pending_migration_count: 0,
|
||||
pool_available: 1,
|
||||
pool_capacity: 8,
|
||||
pool_size: 1,
|
||||
pool_waiting: 0,
|
||||
profile_id: std::option::Option::Some("mainnet".to_owned()),
|
||||
store_open: true,
|
||||
};
|
||||
let serialized = serde_json::to_string(&value);
|
||||
assert!(serialized.is_ok(), "safe Store runtime DTO must serialize: {serialized:?}");
|
||||
if let std::result::Result::Ok(serialized) = serialized {
|
||||
assert!(serialized.contains("\"backendKind\":\"postgres\""));
|
||||
assert!(serialized.contains("\"migrationVersionDecimal\":\"2\""));
|
||||
for forbidden in ["postgresql://", "connection_uri", "connectionUri", "password", "payload", "dataBytes"] {
|
||||
assert!(!serialized.contains(forbidden), "Store runtime DTO leaked forbidden material: {forbidden}");
|
||||
}
|
||||
}
|
||||
}
|
||||
47
crates/ksp-app-store-desk/unit_tests/dto_observation.rs
Normal file
47
crates/ksp-app-store-desk/unit_tests/dto_observation.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
// file: crates/ksp-app-store-desk/unit_tests/dto_observation.rs
|
||||
// version: 1
|
||||
|
||||
//! Unit tests for Store Desk observation DTO safety and request boundaries.
|
||||
|
||||
#[test]
|
||||
fn pre_010_observation_requests_expose_only_random_access_and_exact_entity_identity() {
|
||||
let source = include_str!("../src/dto_observation.rs");
|
||||
for forbidden in ["draw:", "search:", "order:", "columns:", "cursor:", "network:", "payload_preview", "data_preview"] {
|
||||
assert!(!source.contains(forbidden), "observation request/row contract leaked forbidden field: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_observation_rows_keep_source_bytes_out_and_preserve_safe_provenance_metadata() {
|
||||
let provenance = crate::StoreObservationProvenanceDto {
|
||||
acquisition_method: "subscribe".to_owned(),
|
||||
capture_session_id: std::option::Option::Some("session-1".to_owned()),
|
||||
commitment: std::option::Option::Some("confirmed".to_owned()),
|
||||
endpoint_id: std::option::Option::Some("mainnet-primary".to_owned()),
|
||||
filter_id: std::option::Option::Some("wallet-filter".to_owned()),
|
||||
observed_at_unix_millis_decimal: std::option::Option::Some("10".to_owned()),
|
||||
origin: "live".to_owned(),
|
||||
protocol: "yellowstone-grpc".to_owned(),
|
||||
provider: "publicnode".to_owned(),
|
||||
received_at_unix_millis_decimal: "11".to_owned(),
|
||||
source_payload_hash: std::option::Option::Some("12".repeat(32)),
|
||||
source_payload_size_decimal: std::option::Option::Some("2048".to_owned()),
|
||||
};
|
||||
let row = crate::StoreAccountObservationRowDto {
|
||||
is_startup: std::option::Option::Some(false),
|
||||
observation_key: "34".repeat(32),
|
||||
provenance,
|
||||
transaction_signature: std::option::Option::Some("56".repeat(64)),
|
||||
write_version_decimal: std::option::Option::Some("9".to_owned()),
|
||||
};
|
||||
let json = serde_json::to_string(&row);
|
||||
let json = match json {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("observation DTO serialization failed: {error}"),
|
||||
};
|
||||
assert!(json.contains("sourcePayloadHash"));
|
||||
assert!(json.contains("sourcePayloadSizeDecimal"));
|
||||
for forbidden in ["payloadPreview", "dataPreview", "rawBytes", "sourcePayloadBytes"] {
|
||||
assert!(!json.contains(forbidden), "observation DTO serialized forbidden source material: {forbidden}");
|
||||
}
|
||||
}
|
||||
56
crates/ksp-app-store-desk/unit_tests/dto_transaction.rs
Normal file
56
crates/ksp-app-store-desk/unit_tests/dto_transaction.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
// file: crates/ksp-app-store-desk/unit_tests/dto_transaction.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_rows_and_detail_keep_raw_bytes_out_of_table_contract() {
|
||||
let row = crate::StoreTransactionRowDto {
|
||||
block_time_unix_millis_decimal: std::option::Option::Some("1720000000000".to_owned()),
|
||||
content_hash: "11".repeat(32),
|
||||
format_id: "ksp_raw_tx_v1".to_owned(),
|
||||
format_version: 1,
|
||||
payload_size_decimal: std::option::Option::Some("4096".to_owned()),
|
||||
retention_state: "full".to_owned(),
|
||||
signature: "22".repeat(64),
|
||||
slot_decimal: "123456789".to_owned(),
|
||||
};
|
||||
let serialized = serde_json::to_value(&row);
|
||||
assert!(serialized.is_ok());
|
||||
let serialized = match serialized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(serialized.get("payloadPreviewHex").is_none());
|
||||
assert!(serialized.get("payload").is_none());
|
||||
assert_eq!(serialized["payloadSizeDecimal"], "4096");
|
||||
let detail = crate::StoreTransactionDetailDto {
|
||||
block_time_unix_millis_decimal: std::option::Option::None,
|
||||
content_hash: "33".repeat(32),
|
||||
format_id: "ksp_raw_tx_v1".to_owned(),
|
||||
format_version: 1,
|
||||
payload_preview_hex: std::option::Option::Some("abcd".to_owned()),
|
||||
payload_preview_truncated: true,
|
||||
payload_size_decimal: std::option::Option::Some("8192".to_owned()),
|
||||
retention_state: "archived".to_owned(),
|
||||
signature: "44".repeat(64),
|
||||
slot_decimal: "987654321".to_owned(),
|
||||
};
|
||||
let serialized = serde_json::to_value(&detail);
|
||||
assert!(serialized.is_ok());
|
||||
let serialized = match serialized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(serialized["payloadPreviewHex"], "abcd");
|
||||
assert_eq!(serialized["payloadPreviewTruncated"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_query_request_contains_only_random_access_and_explicit_store_filters() {
|
||||
let source = include_str!("../src/dto_transaction.rs");
|
||||
for required in ["direction", "offset", "limit", "slot_max", "slot_min"] {
|
||||
assert!(source.contains(required), "missing Store transaction query field: {required}");
|
||||
}
|
||||
for forbidden in ["draw", "columns", "search", "order", "cursor", "connection_uri", "endpoint"] {
|
||||
assert!(!source.contains(&format!("pub(crate) {forbidden}")), "DataTables/physical field leaked into Store transaction query DTO: {forbidden}");
|
||||
}
|
||||
}
|
||||
40
crates/ksp-app-store-desk/unit_tests/frontend_logging.rs
Normal file
40
crates/ksp-app-store-desk/unit_tests/frontend_logging.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
// file: crates/ksp-app-store-desk/unit_tests/frontend_logging.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn frontend_log_levels_are_explicit_and_case_insensitive() {
|
||||
assert!(matches!(super::parse_level(" TRACE "), std::result::Result::Ok(super::FrontendLogLevel::Trace)));
|
||||
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::FrontendLogLevel::Debug)));
|
||||
assert!(matches!(super::parse_level("Info"), std::result::Result::Ok(super::FrontendLogLevel::Info)));
|
||||
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::FrontendLogLevel::Warn)));
|
||||
assert!(matches!(super::parse_level("ERROR"), std::result::Result::Ok(super::FrontendLogLevel::Error)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_frontend_log_level_is_rejected() {
|
||||
let result = super::parse_level("notice");
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontend_log_targets_are_whitelisted() {
|
||||
assert!(matches!(super::parse_target("frontend"), std::result::Result::Ok(super::FrontendLogTarget::Frontend)));
|
||||
assert!(matches!(super::parse_target(" MAIN "), std::result::Result::Ok(super::FrontendLogTarget::Main)));
|
||||
assert!(matches!(super::parse_target("Splash"), std::result::Result::Ok(super::FrontendLogTarget::Splash)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arbitrary_frontend_log_target_is_rejected() {
|
||||
let result = super::parse_target("ksp-app-store-desk.test.logging");
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID);
|
||||
}
|
||||
36
crates/ksp-app-store-desk/unit_tests/splash.rs
Normal file
36
crates/ksp-app-store-desk/unit_tests/splash.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// file: crates/ksp-app-store-desk/unit_tests/splash.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn fallback_splash_settings_are_short_and_ordered() {
|
||||
let settings = crate::SplashSettings::fallback();
|
||||
assert_eq!(settings.fade_in_ms(), 300);
|
||||
assert_eq!(settings.minimum_ms(), 1200);
|
||||
assert_eq!(settings.fade_out_ms(), 300);
|
||||
assert_eq!(settings.expected_backend_lifecycle_ms(), 1800);
|
||||
assert_eq!(settings.fade_in_source(), "fallback");
|
||||
assert_eq!(settings.minimum_source(), "fallback");
|
||||
assert_eq!(settings.fade_out_source(), "fallback");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splash_order_contract_keeps_action_message_status_and_duration() {
|
||||
let order =
|
||||
crate::SplashOrderDto::new("add_message", std::option::Option::Some("ready"), std::option::Option::Some("success"), std::option::Option::Some(300));
|
||||
assert_eq!(order.action, "add_message");
|
||||
assert_eq!(order.message.as_deref(), std::option::Option::Some("ready"));
|
||||
assert_eq!(order.status.as_deref(), std::option::Option::Some("success"));
|
||||
assert_eq!(order.duration_ms, std::option::Option::Some(300));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splash_duration_parser_rejects_invalid_or_unbounded_values() {
|
||||
let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000);
|
||||
let oversized_minimum = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "60001", 60_000);
|
||||
let oversized_fade_in = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_IN_MS", "10001", 10_000);
|
||||
let oversized_fade_out = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_OUT_MS", "10001", 10_000);
|
||||
assert!(malformed.is_err());
|
||||
assert!(oversized_minimum.is_err());
|
||||
assert!(oversized_fade_in.is_err());
|
||||
assert!(oversized_fade_out.is_err());
|
||||
}
|
||||
72
crates/ksp-app-store-desk/vite.config.ts
Normal file
72
crates/ksp-app-store-desk/vite.config.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// file: crates/ksp-app-store-desk/vite.config.ts
|
||||
// version: 2
|
||||
|
||||
import { NodePackageImporter } from "sass-embedded";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig, normalizePath } from "vite";
|
||||
|
||||
const appRoot = fileURLToPath(new URL(".", import.meta.url));
|
||||
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
|
||||
const frontendDist = normalizePath(resolve(appRoot, "../../../builds/khadhroony-solana-project/ksp-app-store-desk/dist"));
|
||||
const devHost = process.env.TAURI_DEV_HOST;
|
||||
|
||||
export default defineConfig({
|
||||
clearScreen: false,
|
||||
root: frontendRoot,
|
||||
publicDir: false,
|
||||
input: {
|
||||
main: normalizePath(resolve(frontendRoot, "main.html")),
|
||||
splash: normalizePath(resolve(frontendRoot, "splash.html")),
|
||||
},
|
||||
build: {
|
||||
outDir: frontendDist,
|
||||
emptyOutDir: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
cssCodeSplit: true,
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
entryFileNames: "js/[name]-[hash].js",
|
||||
chunkFileNames: "js/chunks/[name]-[hash].js",
|
||||
assetFileNames: assetInfo => {
|
||||
const originalName = assetInfo.names[0] ?? "";
|
||||
const extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
|
||||
if (extension === "css") {
|
||||
return "css/[name]-[hash][extname]";
|
||||
}
|
||||
if (["eot", "otf", "ttf", "woff", "woff2"].includes(extension)) {
|
||||
return "fonts/[name]-[hash][extname]";
|
||||
}
|
||||
if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(extension)) {
|
||||
return "imgs/[name][extname]";
|
||||
}
|
||||
return "otherassets/[name][extname]";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
quietDeps: true,
|
||||
silenceDeprecations: ["import", "color-functions", "global-builtin"],
|
||||
verbose: false,
|
||||
importers: [new NodePackageImporter()],
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 1438,
|
||||
strictPort: true,
|
||||
host: devHost || false,
|
||||
ws: {
|
||||
protocol: "ws",
|
||||
host: devHost || "localhost",
|
||||
port: 1439,
|
||||
},
|
||||
watch: {
|
||||
ignored: ["**/src/**"],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -54,18 +54,19 @@
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
||||
// version: 32
|
||||
// version: 33
|
||||
|
||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||
|
||||
@@ -432,7 +432,7 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 14);
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
|
||||
@@ -441,6 +441,10 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
|
||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-store-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-store-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-wallet-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-wallet-desk.json"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
//! Release-wide deterministic compliance canaries for Wallet Desk.
|
||||
|
||||
@@ -199,7 +199,7 @@ fn packaged_resources_include_only_registered_config_sources_and_schemas() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(resources.len(), 14);
|
||||
assert_eq!(resources.len(), 15);
|
||||
for (source, destination) in resources {
|
||||
let destination = destination.as_str();
|
||||
assert!(destination.is_some(), "resource destination must be textual");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/README.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# ksp-config-lib
|
||||
|
||||
@@ -35,7 +35,9 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
|
||||
Le registre par défaut connaît :
|
||||
|
||||
```text
|
||||
cfg.composite.ksp-app-backfill-desk -> config/composite.ksp-app-backfill-desk.json
|
||||
cfg.composite.ksp-app-solprices-desk -> config/composite.ksp-app-solprices-desk.json
|
||||
cfg.composite.ksp-app-store-desk -> config/composite.ksp-app-store-desk.json
|
||||
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
|
||||
cfg.std.logging -> config/std.logging.json
|
||||
cfg.std.offchain_transport -> config/std.offchain_transport.json
|
||||
@@ -54,7 +56,7 @@ schema.std.wallet -> config/schemas/std.wallet.schema.json
|
||||
|
||||
`ConfigManagement::read_source()` permet d'inspecter le texte brut d'un document Config enregistré même lorsque ce document est invalide. `save_source_candidate()` complète cette frontière : le candidat brut est parsé, validé contre son schema et les invariants sémantiques KSP, puis persisté atomiquement uniquement après validation complète. Le `file_id` doit appartenir au registre et désigner un document Config ; aucun path arbitraire n'est accepté.
|
||||
|
||||
`config/examples/composite.example.json` conserve l’exemple générique. Les composites runtime committed restent possédés par Config : `config/composite.ksp-app-solprices-desk.json` sélectionne Logging + Off-chain Transport, tandis que `config/composite.ksp-app-wallet-desk.json` sélectionne Logging + Off-chain Transport + On-chain Transport + Wallet. Tous référencent leurs documents par `file_id`, sans dépendre de filenames physiques.
|
||||
`config/examples/composite.example.json` conserve l’exemple générique. Les composites runtime committed restent possédés par Config : Backfill Desk compose Logging + Transport + Store, SOL Prices Desk compose Logging + Off-chain Transport, Store Desk compose Logging + Store, et Wallet Desk compose Logging + Off-chain Transport + On-chain Transport + Wallet. Tous référencent leurs documents par `file_id`, sans dépendre de filenames physiques.
|
||||
|
||||
Le fichier local d'environnement est :
|
||||
|
||||
@@ -100,6 +102,8 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
|
||||
- [`../../config/std.offchain_transport.json`](../../config/std.offchain_transport.json) — document standard Off-chain Transport V1, actuellement limité au domaine `market_price` SOL/USD ;
|
||||
- [`../../config/std.store.json`](../../config/std.store.json) — targets Store PostgreSQL Devnet/Mainnet/Testnet et settings runtime bornés ;
|
||||
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
|
||||
- [`../../config/composite.ksp-app-backfill-desk.json`](../../config/composite.ksp-app-backfill-desk.json) — composition Logging/Transport/Store de Backfill Desk ;
|
||||
- [`../../config/composite.ksp-app-solprices-desk.json`](../../config/composite.ksp-app-solprices-desk.json) — composition Logging/Off-chain Transport de SOL Prices Desk ;
|
||||
- [`../../config/composite.ksp-app-store-desk.json`](../../config/composite.ksp-app-store-desk.json) — composition Logging/Store de Store Desk ;
|
||||
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Off-chain Transport/On-chain Transport/Wallet de Wallet Desk ;
|
||||
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||
<!-- version: 14 -->
|
||||
<!-- version: 15 -->
|
||||
|
||||
# Utilisation de ksp-config-lib
|
||||
|
||||
@@ -250,7 +250,7 @@ Targets committed :
|
||||
|
||||
```text
|
||||
devnet -> network devnet -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI
|
||||
mainnet -> network mainnet-beta -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI
|
||||
mainnet -> network mainnet -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI
|
||||
testnet -> network testnet -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 22
|
||||
// version: 23
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -155,6 +155,8 @@ pub use self::registry::ConfigFileRegistry;
|
||||
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME;
|
||||
/// Default physical filename for the SOL Prices Desk composite configuration document.
|
||||
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME;
|
||||
/// Default physical filename for the Store Desk composite configuration document.
|
||||
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_STORE_DESK_FILENAME;
|
||||
/// Default physical filename for the Wallet Desk composite configuration document.
|
||||
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME;
|
||||
/// Default physical filename for the generic composite JSON Schema document.
|
||||
@@ -183,6 +185,8 @@ pub use self::registry::DEFAULT_STD_WALLET_SCHEMA_FILENAME;
|
||||
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK;
|
||||
/// Logical file identifier for the SOL Prices Desk composite configuration document.
|
||||
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK;
|
||||
/// Logical file identifier for the Store Desk composite configuration document.
|
||||
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK;
|
||||
/// Logical file identifier for the Wallet Desk composite configuration document.
|
||||
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
|
||||
/// Logical file identifier for the generic composite JSON Schema document.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 14
|
||||
// version: 15
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
@@ -7,6 +7,8 @@ pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
pub const DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME: &str = "composite.ksp-app-backfill-desk.json";
|
||||
/// Default physical filename for the SOL Prices Desk composite configuration document.
|
||||
pub const DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME: &str = "composite.ksp-app-solprices-desk.json";
|
||||
/// Default physical filename for the Store Desk composite configuration document.
|
||||
pub const DEFAULT_COMPOSITE_KSP_APP_STORE_DESK_FILENAME: &str = "composite.ksp-app-store-desk.json";
|
||||
/// Default physical filename for the Wallet Desk composite configuration document.
|
||||
pub const DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME: &str = "composite.ksp-app-wallet-desk.json";
|
||||
/// Default physical filename for the generic composite JSON Schema document.
|
||||
@@ -35,6 +37,8 @@ pub const DEFAULT_STD_WALLET_SCHEMA_FILENAME: &str = "std.wallet.schema.json";
|
||||
pub const FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK: &str = "cfg.composite.ksp-app-backfill-desk";
|
||||
/// Logical file identifier for the SOL Prices Desk composite configuration document.
|
||||
pub const FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK: &str = "cfg.composite.ksp-app-solprices-desk";
|
||||
/// Logical file identifier for the Store Desk composite configuration document.
|
||||
pub const FILE_ID_COMPOSITE_KSP_APP_STORE_DESK: &str = "cfg.composite.ksp-app-store-desk";
|
||||
/// Logical file identifier for the Wallet Desk composite configuration document.
|
||||
pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-wallet-desk";
|
||||
/// Logical file identifier for the generic composite JSON Schema document.
|
||||
@@ -184,6 +188,16 @@ impl ConfigFileRegistry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store_desk_composite = ConfigFileDescriptor::new(
|
||||
FILE_ID_COMPOSITE_KSP_APP_STORE_DESK,
|
||||
ConfigFileKind::Config,
|
||||
DEFAULT_COMPOSITE_KSP_APP_STORE_DESK_FILENAME,
|
||||
std::option::Option::Some(FILE_ID_SCHEMA_COMPOSITE),
|
||||
);
|
||||
let store_desk_composite = match store_desk_composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet_composite = ConfigFileDescriptor::new(
|
||||
FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK,
|
||||
ConfigFileKind::Config,
|
||||
@@ -287,6 +301,7 @@ impl ConfigFileRegistry {
|
||||
return crate::build_registry([
|
||||
backfill_composite,
|
||||
solprices_composite,
|
||||
store_desk_composite,
|
||||
wallet_composite,
|
||||
composite_schema,
|
||||
logging,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 27
|
||||
// version: 28
|
||||
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
||||
//! Logging/Transport/Store adapters and management contracts.
|
||||
@@ -80,22 +80,23 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
|
||||
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
assert_eq!(descriptors.len(), 14);
|
||||
assert_eq!(descriptors.len(), 15);
|
||||
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[13].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
let schema_file_id = descriptors[7].schema_file_id();
|
||||
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[13].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[14].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
let schema_file_id = descriptors[8].schema_file_id();
|
||||
assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association");
|
||||
if let std::option::Option::Some(schema_file_id) = schema_file_id {
|
||||
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
@@ -168,6 +169,8 @@ fn composite_schema_and_provenance_contracts_are_available_from_crate_root() {
|
||||
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME, "composite.ksp-app-backfill-desk.json");
|
||||
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK, "cfg.composite.ksp-app-solprices-desk");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME, "composite.ksp-app-solprices-desk.json");
|
||||
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK, "cfg.composite.ksp-app-store-desk");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_STORE_DESK_FILENAME, "composite.ksp-app-store-desk.json");
|
||||
assert_ne!(ksp_config_lib::ConfigProfileSelectionSource::Composite, ksp_config_lib::ConfigProfileSelectionSource::Explicit);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "mainnet",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "devnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "local_dev"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "devnet"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "local_dev"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "mainnet"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "testnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "local_dev"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "testnet"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"network": "mainnet-beta",
|
||||
"network": "mainnet",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"name": "fixture_helius_ws",
|
||||
"enabled": true,
|
||||
"provider": "helius",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"kind": "helius_laserstream",
|
||||
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}"
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"name": "fixture_http",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"url": "https://fallback.invalid",
|
||||
"connect_timeout_ms": 1000,
|
||||
"request_timeout_ms": 3000,
|
||||
@@ -73,7 +73,7 @@
|
||||
"name": "fixture_ws",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "wss://fallback.invalid"
|
||||
}
|
||||
@@ -83,7 +83,7 @@
|
||||
"name": "fixture_grpc",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"protocol": "solana_yellowstone",
|
||||
"url": "https://grpc.invalid",
|
||||
"metadata": [
|
||||
@@ -115,7 +115,7 @@
|
||||
"name": "fixture_http",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"url": "https://fallback.invalid",
|
||||
"connect_timeout_ms": 1000,
|
||||
"request_timeout_ms": 3000,
|
||||
@@ -143,7 +143,7 @@
|
||||
"name": "fixture_ws",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "wss://fallback.invalid"
|
||||
}
|
||||
@@ -153,7 +153,7 @@
|
||||
"name": "fixture_grpc",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"protocol": "solana_yellowstone",
|
||||
"url": "https://grpc.invalid",
|
||||
"metadata": [
|
||||
@@ -180,7 +180,7 @@
|
||||
"name": "fixture_http",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"url": "https://fallback.invalid",
|
||||
"connect_timeout_ms": 1000,
|
||||
"request_timeout_ms": 3000,
|
||||
@@ -208,7 +208,7 @@
|
||||
"name": "fixture_ws",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "wss://fallback.invalid"
|
||||
}
|
||||
@@ -218,7 +218,7 @@
|
||||
"name": "fixture_grpc",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"cluster": "mainnet",
|
||||
"protocol": "solana_yellowstone",
|
||||
"url": "https://grpc.invalid",
|
||||
"metadata": [],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
#[test]
|
||||
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
@@ -7,27 +7,30 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
assert_eq!(descriptors.len(), 14);
|
||||
assert_eq!(descriptors.len(), 15);
|
||||
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME));
|
||||
assert_eq!(descriptors[0].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
|
||||
assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[7].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
||||
assert_eq!(descriptors[7].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
|
||||
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[13].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
|
||||
assert!(descriptors[0..8].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
|
||||
assert!(descriptors[8..14].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
|
||||
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
|
||||
assert_eq!(descriptors[2].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_STORE_DESK_FILENAME));
|
||||
assert_eq!(descriptors[2].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
|
||||
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[8].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
||||
assert_eq!(descriptors[8].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
|
||||
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[13].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[14].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
|
||||
assert!(descriptors[0..9].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
|
||||
assert!(descriptors[9..15].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,35 +365,45 @@ fn absolute_fixture_path() -> std::path::PathBuf {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_register_generic_composite_schema_and_three_runtime_composites() {
|
||||
fn defaults_register_generic_composite_schema_and_four_runtime_composites() {
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
let backfill_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||
let solprices_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
let store_desk_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
|
||||
let wallet_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}");
|
||||
assert!(backfill_id.is_ok(), "Backfill Desk composite runtime file_id should be valid: {backfill_id:?}");
|
||||
assert!(solprices_id.is_ok(), "SOL Prices Desk composite runtime file_id should be valid: {solprices_id:?}");
|
||||
assert!(store_desk_id.is_ok(), "Store Desk composite runtime file_id should be valid: {store_desk_id:?}");
|
||||
assert!(wallet_id.is_ok(), "Wallet Desk composite runtime file_id should be valid: {wallet_id:?}");
|
||||
if let (
|
||||
std::result::Result::Ok(registry),
|
||||
std::result::Result::Ok(schema_id),
|
||||
std::result::Result::Ok(backfill_id),
|
||||
std::result::Result::Ok(solprices_id),
|
||||
std::result::Result::Ok(store_desk_id),
|
||||
std::result::Result::Ok(wallet_id),
|
||||
) = (registry, schema_id, backfill_id, solprices_id, wallet_id)
|
||||
) = (registry, schema_id, backfill_id, solprices_id, store_desk_id, wallet_id)
|
||||
{
|
||||
let schema = registry.descriptor(&schema_id);
|
||||
let backfill = registry.descriptor(&backfill_id);
|
||||
let solprices = registry.descriptor(&solprices_id);
|
||||
let store_desk = registry.descriptor(&store_desk_id);
|
||||
let wallet = registry.descriptor(&wallet_id);
|
||||
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
|
||||
assert!(backfill.is_ok(), "Backfill Desk runtime composite should be registered: {backfill:?}");
|
||||
assert!(solprices.is_ok(), "SOL Prices Desk runtime composite should be registered: {solprices:?}");
|
||||
assert!(store_desk.is_ok(), "Store Desk runtime composite should be registered: {store_desk:?}");
|
||||
assert!(wallet.is_ok(), "Wallet Desk runtime composite should be registered: {wallet:?}");
|
||||
if let (std::result::Result::Ok(schema), std::result::Result::Ok(backfill), std::result::Result::Ok(solprices), std::result::Result::Ok(wallet)) =
|
||||
(schema, backfill, solprices, wallet)
|
||||
if let (
|
||||
std::result::Result::Ok(schema),
|
||||
std::result::Result::Ok(backfill),
|
||||
std::result::Result::Ok(solprices),
|
||||
std::result::Result::Ok(store_desk),
|
||||
std::result::Result::Ok(wallet),
|
||||
) = (schema, backfill, solprices, store_desk, wallet)
|
||||
{
|
||||
assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
|
||||
@@ -400,6 +413,9 @@ fn defaults_register_generic_composite_schema_and_three_runtime_composites() {
|
||||
assert_eq!(solprices.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(solprices.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME));
|
||||
assert_eq!(solprices.schema_file_id(), std::option::Option::Some(&schema_id));
|
||||
assert_eq!(store_desk.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(store_desk.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_STORE_DESK_FILENAME));
|
||||
assert_eq!(store_desk.schema_file_id(), std::option::Option::Some(&schema_id));
|
||||
assert_eq!(wallet.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(wallet.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME));
|
||||
assert_eq!(wallet.schema_file_id(), std::option::Option::Some(&schema_id));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/store.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
#[test]
|
||||
fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() {
|
||||
@@ -122,7 +122,7 @@ fn named_store_targets_select_one_network_and_database_without_runtime_multiplex
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
for (target_id, network, expected_uri) in [
|
||||
("devnet", "devnet", "postgresql://devnet.invalid/ksp_devnet"),
|
||||
("mainnet", "mainnet-beta", "postgresql://mainnet.invalid/ksp_mainnet"),
|
||||
("mainnet", "mainnet", "postgresql://mainnet.invalid/ksp_mainnet"),
|
||||
("testnet", "testnet", "postgresql://testnet.invalid/ksp_testnet"),
|
||||
] {
|
||||
let resolved = engine.load_resolved_store_config(std::option::Option::Some(target_id), &environment);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/transport.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
#[test]
|
||||
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||
@@ -68,7 +68,7 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||
let helius = &ws.endpoints()[1];
|
||||
assert_eq!(helius.name(), "fixture_helius_ws");
|
||||
assert_eq!(helius.provider().as_str(), "helius");
|
||||
assert_eq!(helius.cluster().as_str(), "mainnet-beta");
|
||||
assert_eq!(helius.cluster().as_str(), "mainnet");
|
||||
assert_eq!(helius.protocol(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream);
|
||||
assert_eq!(helius.url().as_str(), "wss://mainnet.helius-rpc.com/?api-key=fixture-helius-key");
|
||||
let _connect_future = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect(helius.clone());
|
||||
@@ -122,13 +122,13 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
|
||||
if let std::result::Result::Ok(mainnet) = mainnet {
|
||||
assert_eq!(mainnet.profile_id(), "mainnet_public");
|
||||
assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||
assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet-beta");
|
||||
assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com");
|
||||
assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet");
|
||||
assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet.solana.com");
|
||||
assert!(mainnet.grpc_settings().is_none(), "generic Mainnet profile must stay distinct from the PublicNode gRPC profile");
|
||||
let ws = mainnet.ws_settings();
|
||||
assert!(ws.is_some(), "committed V3 Mainnet profile should expose WebSocket settings");
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet-beta.solana.com");
|
||||
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet.solana.com");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_with_n
|
||||
(
|
||||
"publicnode_mainnet",
|
||||
"publicnode_solana_mainnet_yellowstone",
|
||||
"mainnet-beta",
|
||||
"mainnet",
|
||||
"https://solana-yellowstone-grpc.publicnode.com:443",
|
||||
"KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN",
|
||||
"PUBLICNODE-MAINNET-GRPC-X-TOKEN-CANARY",
|
||||
@@ -480,7 +480,7 @@ fn helius_laserstream_mainnet_and_devnet_api_key_map_to_protocol_and_safe_redact
|
||||
assert_eq!(ws.endpoints().len(), 2);
|
||||
let endpoint = &ws.endpoints()[1];
|
||||
assert_eq!(endpoint.provider().as_str(), "helius");
|
||||
assert_eq!(endpoint.cluster().as_str(), "mainnet-beta");
|
||||
assert_eq!(endpoint.cluster().as_str(), "mainnet");
|
||||
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream);
|
||||
assert_eq!(endpoint.url().as_str(), "wss://mainnet.helius-rpc.com/?api-key=helius-api-key-canary");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-job-backfill-lib/Cargo.toml
|
||||
# version: 4
|
||||
# version: 7
|
||||
|
||||
[package]
|
||||
name = "ksp-job-backfill-lib"
|
||||
@@ -13,6 +13,7 @@ ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-job-api = { path = "../ksp-job-api" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||
ksp-raw-transaction-lib = { path = "../ksp-raw-transaction-lib" }
|
||||
ksp-store-lib = { path = "../ksp-store-lib", default-features = false }
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
@@ -20,6 +21,9 @@ tokio = { workspace = true, features = ["macros", "sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
tokio-tungstenite = { workspace = true, features = ["handshake"] }
|
||||
tonic = { workspace = true, features = ["codegen", "server"] }
|
||||
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: crates/ksp-job-backfill-lib/README.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# ksp-job-backfill-lib
|
||||
|
||||
`ksp-job-backfill-lib` implémente le premier job historique concret de KSP : un backfill borné de transactions Solana vers la couche RAW durable.
|
||||
|
||||
La crate compose les contrats Job, Transport observé et Store backend-neutral sans posséder leurs politiques internes. Elle couvre l'admission, la découverte historique, l'hydratation `getTransaction`, la conversion RAW v1, la persistance atomique, la concurrence bornée, la frontier contiguë, le checkpoint caller-owned, l'annulation coopérative et les snapshots latest-value.
|
||||
La crate compose les contrats Job, Transport observé, common RAW et Store backend-neutral sans posséder leurs politiques internes. Elle couvre l'admission, la découverte historique, l'hydratation `getTransaction`, l'adaptation vers `ksp-raw-transaction-lib`, la persistance atomique, la concurrence bornée, la frontier contiguë, le checkpoint caller-owned, l'annulation coopérative et les snapshots latest-value.
|
||||
|
||||
## Identité
|
||||
|
||||
@@ -94,6 +94,7 @@ ksp-core-lib
|
||||
ksp-job-api
|
||||
ksp-logging-lib
|
||||
ksp-onchain-transport-lib
|
||||
ksp-raw-transaction-lib
|
||||
ksp-store-lib (default-features = false)
|
||||
futures-util
|
||||
serde_json
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
// file: crates/ksp-job-backfill-lib/src/conversion.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
/// KSP-owned source-independent RAW transaction format identifier produced by this Backfill vertical.
|
||||
pub const RAW_TRANSACTION_FORMAT_ID: &str = "ksp.solana.raw_transaction";
|
||||
/// Initial KSP-owned RAW transaction format version produced by this Backfill vertical.
|
||||
pub const RAW_TRANSACTION_FORMAT_VERSION: u32 = 1;
|
||||
|
||||
const RAW_TRANSACTION_METHOD_CODE: &str = "getTransaction";
|
||||
const RAW_TRANSACTION_OBSERVATION_CONTRACT_VERSION: u32 = 1;
|
||||
const RAW_TRANSACTION_PROTOCOL_CODE: &str = "solana.http.json_rpc";
|
||||
|
||||
/// Complete in-memory RAW transaction acquisition ready for the later Store persistence tranche.
|
||||
/// Complete in-memory RAW transaction acquisition ready for Store persistence.
|
||||
#[derive(Debug)]
|
||||
pub struct BackfillRawAcquisition {
|
||||
inner: Box<BackfillRawAcquisitionInner>,
|
||||
@@ -20,35 +15,34 @@ pub struct BackfillRawAcquisition {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BackfillRawAcquisitionInner {
|
||||
transaction: ksp_store_lib::RawTransaction,
|
||||
observation: ksp_store_lib::RawTransactionObservation,
|
||||
acquisition: ksp_raw_transaction_lib::RawTransactionAcquisition,
|
||||
}
|
||||
|
||||
impl crate::BackfillRawAcquisition {
|
||||
/// Returns the canonical RAW transaction produced from the typed Transport response.
|
||||
/// Returns the canonical RAW transaction produced through the source-neutral common layer.
|
||||
#[must_use]
|
||||
pub const fn transaction(&self) -> &ksp_store_lib::RawTransaction {
|
||||
return &self.inner.transaction;
|
||||
pub fn transaction(&self) -> &ksp_store_lib::RawTransaction {
|
||||
return self.inner.acquisition.transaction();
|
||||
}
|
||||
|
||||
/// Returns the acquisition observation whose provenance records the actual successful endpoint.
|
||||
/// Returns the Backfill-owned acquisition observation linked by the common assembly layer.
|
||||
#[must_use]
|
||||
pub const fn observation(&self) -> &ksp_store_lib::RawTransactionObservation {
|
||||
return &self.inner.observation;
|
||||
pub fn observation(&self) -> &ksp_store_lib::RawTransactionObservation {
|
||||
return self.inner.acquisition.observation();
|
||||
}
|
||||
|
||||
/// Consumes the in-memory acquisition into the canonical transaction and its observation.
|
||||
/// Consumes the in-memory acquisition into the canonical transaction and its Backfill-owned observation.
|
||||
#[must_use]
|
||||
pub fn into_parts(self) -> (ksp_store_lib::RawTransaction, ksp_store_lib::RawTransactionObservation) {
|
||||
let inner = *self.inner;
|
||||
return (inner.transaction, inner.observation);
|
||||
return inner.acquisition.into_parts();
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of hydrating one deterministic Backfill candidate through observed `getTransaction`.
|
||||
#[derive(Debug)]
|
||||
pub enum BackfillHydrationOutcome {
|
||||
/// The RPC returned one complete transaction and conversion produced canonical RAW plus provenance.
|
||||
/// The RPC returned one complete transaction and common conversion produced canonical RAW plus Backfill provenance.
|
||||
Available(crate::BackfillRawAcquisition),
|
||||
/// The RPC returned JSON `null`; only the canonical transaction identity exists and no provenance is fabricated.
|
||||
Missing(ksp_store_lib::RawTransactionReference),
|
||||
@@ -71,11 +65,12 @@ impl crate::BackfillHydrationOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hydrates one candidate with the typed observed Transport path and converts it to canonical RAW v1.
|
||||
/// Hydrates one candidate with the typed observed Transport path and converts it through the common RAW v1 layer.
|
||||
///
|
||||
/// The caller supplies the local receipt timestamp because wall-clock ownership remains outside this
|
||||
/// pure conversion tranche. Transport retains endpoint selection and retry. This function never
|
||||
/// persists to Store; persistence begins in `pre.007`.
|
||||
/// pure conversion path. Transport retains endpoint selection and retry. Backfill retains campaign
|
||||
/// provenance and observation-key ownership; the common crate owns signature parsing, RAW v1
|
||||
/// canonicalization, content hashing and transaction/observation assembly.
|
||||
pub async fn hydrate_backfill_candidate(
|
||||
transport: &ksp_onchain_transport_lib::HttpTransportPool,
|
||||
request: &crate::BackfillRequest,
|
||||
@@ -105,12 +100,12 @@ pub async fn hydrate_backfill_candidate(
|
||||
std::option::Option::None => return std::result::Result::Ok(crate::BackfillHydrationOutcome::Missing(reference)),
|
||||
};
|
||||
let fields = CanonicalTransactionFields {
|
||||
slot: transaction.slot(),
|
||||
block_time: transaction.block_time(),
|
||||
transaction: transaction.transaction(),
|
||||
meta: transaction.meta(),
|
||||
version: transaction.version(),
|
||||
slot: transaction.slot(),
|
||||
transaction: transaction.transaction(),
|
||||
transaction_index: transaction.transaction_index(),
|
||||
version: transaction.version(),
|
||||
};
|
||||
let acquisition = convert_available_fields(request, reference, fields, provider.as_str(), endpoint.as_str(), received_at);
|
||||
return match acquisition {
|
||||
@@ -119,49 +114,22 @@ pub async fn hydrate_backfill_candidate(
|
||||
};
|
||||
}
|
||||
|
||||
/// Decodes one validated Base58 signature to exactly 64 canonical bytes without a Solana SDK dependency.
|
||||
/// Decodes one validated Base58 signature through the source-neutral common parser while preserving Backfill error semantics.
|
||||
pub(crate) fn decode_backfill_signature(signature: &crate::BackfillSignature) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionSignature> {
|
||||
let text = signature.as_str().as_bytes();
|
||||
let mut decoded = [0_u8; 64];
|
||||
let mut leading_zeroes = 0_usize;
|
||||
for byte in text {
|
||||
if *byte != b'1' {
|
||||
break;
|
||||
}
|
||||
leading_zeroes += 1;
|
||||
}
|
||||
for byte in text {
|
||||
let digit = match base58_digit(*byte) {
|
||||
std::option::Option::Some(digit) => digit,
|
||||
std::option::Option::None => return std::result::Result::Err(conversion_error("signature")),
|
||||
};
|
||||
let mut carry = u32::from(digit);
|
||||
for output in decoded.iter_mut().rev() {
|
||||
let value = (u32::from(*output) * 58) + carry;
|
||||
*output = (value & 0xff) as u8;
|
||||
carry = value >> 8;
|
||||
}
|
||||
if carry != 0 {
|
||||
return std::result::Result::Err(conversion_error("signature"));
|
||||
}
|
||||
}
|
||||
let significant_len = match decoded.iter().position(|byte| return *byte != 0) {
|
||||
std::option::Option::Some(index) => decoded.len() - index,
|
||||
std::option::Option::None => 0,
|
||||
let parsed = ksp_raw_transaction_lib::parse_raw_transaction_signature(signature.as_str());
|
||||
return match parsed {
|
||||
std::result::Result::Ok(signature) => std::result::Result::Ok(signature),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("signature")),
|
||||
};
|
||||
if leading_zeroes + significant_len != decoded.len() {
|
||||
return std::result::Result::Err(conversion_error("signature"));
|
||||
}
|
||||
return std::result::Result::Ok(ksp_store_lib::RawTransactionSignature::new(decoded));
|
||||
}
|
||||
|
||||
struct CanonicalTransactionFields<'a> {
|
||||
slot: u64,
|
||||
block_time: std::option::Option<i64>,
|
||||
transaction: &'a ksp_onchain_transport_lib::SolanaEncodedTransaction,
|
||||
meta: &'a ksp_onchain_transport_lib::SolanaWireField<serde_json::Value>,
|
||||
version: &'a ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
|
||||
slot: u64,
|
||||
transaction: &'a ksp_onchain_transport_lib::SolanaEncodedTransaction,
|
||||
transaction_index: &'a ksp_onchain_transport_lib::SolanaWireField<u32>,
|
||||
version: &'a ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
|
||||
}
|
||||
|
||||
fn canonical_reference(request: &crate::BackfillRequest, candidate: &crate::BackfillCandidate) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionReference> {
|
||||
@@ -184,30 +152,9 @@ fn convert_available_fields(
|
||||
endpoint: &str,
|
||||
received_at: ksp_store_lib::RawTimestamp,
|
||||
) -> ksp_core_lib::Result<crate::BackfillRawAcquisition> {
|
||||
let block_time = convert_block_time(fields.block_time);
|
||||
let block_time = match block_time {
|
||||
std::result::Result::Ok(block_time) => block_time,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let bytes = canonical_payload_bytes(&fields);
|
||||
let bytes = match bytes {
|
||||
std::result::Result::Ok(bytes) => bytes,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let hash: [u8; 32] = sha2::Sha256::digest(bytes.as_slice()).into();
|
||||
let format_id = ksp_store_lib::RawFormatId::new(crate::RAW_TRANSACTION_FORMAT_ID);
|
||||
let format_id = match format_id {
|
||||
std::result::Result::Ok(format_id) => format_id,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(conversion_error("payload.format_id")),
|
||||
};
|
||||
let payload = ksp_store_lib::RawPayload::try_new(
|
||||
format_id,
|
||||
crate::RAW_TRANSACTION_FORMAT_VERSION,
|
||||
bytes.into_boxed_slice(),
|
||||
ksp_store_lib::RawContentHash::new(hash),
|
||||
);
|
||||
let payload = match payload {
|
||||
std::result::Result::Ok(payload) => payload,
|
||||
let transaction = canonical_transaction(&reference, fields);
|
||||
let transaction = match transaction {
|
||||
std::result::Result::Ok(transaction) => transaction,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let provenance = build_provenance(request, provider, endpoint, received_at);
|
||||
@@ -215,182 +162,60 @@ fn convert_available_fields(
|
||||
std::result::Result::Ok(provenance) => provenance,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let observation_key = observation_key(request, &reference, provider, endpoint);
|
||||
let transaction = ksp_store_lib::RawTransaction::new(reference.clone(), fields.slot, block_time, payload);
|
||||
let observation = ksp_store_lib::RawTransactionObservation::new(observation_key, reference, provenance);
|
||||
return std::result::Result::Ok(crate::BackfillRawAcquisition { inner: Box::new(BackfillRawAcquisitionInner { transaction, observation }) });
|
||||
let observation_key = observation_key(request, transaction.reference(), provider, endpoint);
|
||||
let acquisition = ksp_raw_transaction_lib::assemble_raw_transaction_acquisition(transaction, observation_key, provenance);
|
||||
return std::result::Result::Ok(crate::BackfillRawAcquisition { inner: Box::new(BackfillRawAcquisitionInner { acquisition }) });
|
||||
}
|
||||
|
||||
fn convert_block_time(value: std::option::Option<i64>) -> ksp_core_lib::Result<std::option::Option<ksp_store_lib::RawTimestamp>> {
|
||||
let seconds = match value {
|
||||
std::option::Option::Some(seconds) => seconds,
|
||||
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
let seconds = match u64::try_from(seconds) {
|
||||
std::result::Result::Ok(seconds) => seconds,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(conversion_error("block_time")),
|
||||
};
|
||||
let millis = match seconds.checked_mul(1_000) {
|
||||
std::option::Option::Some(millis) => millis,
|
||||
std::option::Option::None => return std::result::Result::Err(conversion_error("block_time")),
|
||||
};
|
||||
let timestamp = ksp_store_lib::RawTimestamp::from_unix_millis(millis);
|
||||
return match timestamp {
|
||||
std::result::Result::Ok(timestamp) => std::result::Result::Ok(std::option::Option::Some(timestamp)),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("block_time")),
|
||||
};
|
||||
}
|
||||
|
||||
fn canonical_payload_bytes(fields: &CanonicalTransactionFields<'_>) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let (transaction_data, transaction_encoding) = match fields.transaction {
|
||||
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { data, encoding } => {
|
||||
if *encoding != ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64 {
|
||||
return std::result::Result::Err(conversion_error("transaction.encoding"));
|
||||
}
|
||||
(data.as_str(), "base64")
|
||||
fn canonical_transaction(
|
||||
reference: &ksp_store_lib::RawTransactionReference,
|
||||
fields: CanonicalTransactionFields<'_>,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawTransaction> {
|
||||
let transaction_data = match fields.transaction {
|
||||
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { data, encoding }
|
||||
if *encoding == ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64 =>
|
||||
{
|
||||
data.clone()
|
||||
},
|
||||
ksp_onchain_transport_lib::SolanaEncodedTransaction::LegacyBinary(_) | ksp_onchain_transport_lib::SolanaEncodedTransaction::Json(_) => {
|
||||
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { .. }
|
||||
| ksp_onchain_transport_lib::SolanaEncodedTransaction::LegacyBinary(_)
|
||||
| ksp_onchain_transport_lib::SolanaEncodedTransaction::Json(_) => {
|
||||
return std::result::Result::Err(conversion_error("transaction.encoding"));
|
||||
},
|
||||
};
|
||||
let mut bytes = std::vec::Vec::new();
|
||||
bytes.extend_from_slice(b"{\"transaction\":[");
|
||||
if let std::result::Result::Err(error) = append_json_string(&mut bytes, transaction_data) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
bytes.push(b',');
|
||||
if let std::result::Result::Err(error) = append_json_string(&mut bytes, transaction_encoding) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
bytes.push(b']');
|
||||
if let std::result::Result::Err(error) = append_wire_value(&mut bytes, "meta", fields.meta, |output, value| return append_canonical_json(output, value)) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let version_result = append_wire_value(&mut bytes, "version", fields.version, |output, value| {
|
||||
let meta = map_wire_field(fields.meta, |value| return value.clone());
|
||||
let version = map_wire_field(fields.version, |value| {
|
||||
return match value {
|
||||
ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy => append_json_string(output, "legacy"),
|
||||
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number) => {
|
||||
output.extend_from_slice(number.to_string().as_bytes());
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy => ksp_raw_transaction_lib::RawTransactionVersion::Legacy,
|
||||
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number) => ksp_raw_transaction_lib::RawTransactionVersion::Number(*number),
|
||||
};
|
||||
});
|
||||
if let std::result::Result::Err(error) = version_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let transaction_index_result = append_wire_value(&mut bytes, "transactionIndex", fields.transaction_index, |output, value| {
|
||||
output.extend_from_slice(value.to_string().as_bytes());
|
||||
return std::result::Result::Ok(());
|
||||
});
|
||||
if let std::result::Result::Err(error) = transaction_index_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
bytes.push(b'}');
|
||||
return std::result::Result::Ok(bytes);
|
||||
let transaction_index = map_wire_field(fields.transaction_index, |value| return *value);
|
||||
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||
reference.network().clone(),
|
||||
reference.signature(),
|
||||
fields.slot,
|
||||
fields.block_time,
|
||||
transaction_data,
|
||||
meta,
|
||||
version,
|
||||
transaction_index,
|
||||
);
|
||||
let canonical = ksp_raw_transaction_lib::canonicalize_raw_transaction(material);
|
||||
return match canonical {
|
||||
std::result::Result::Ok(transaction) => std::result::Result::Ok(transaction),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("common.canonicalization")),
|
||||
};
|
||||
}
|
||||
|
||||
fn append_wire_value<T, F>(
|
||||
output: &mut std::vec::Vec<u8>,
|
||||
key: &str,
|
||||
field: &ksp_onchain_transport_lib::SolanaWireField<T>,
|
||||
mut append_value: F,
|
||||
) -> ksp_core_lib::Result<()>
|
||||
fn map_wire_field<T, U, F>(field: &ksp_onchain_transport_lib::SolanaWireField<T>, mut map_value: F) -> ksp_raw_transaction_lib::RawTransactionWireField<U>
|
||||
where
|
||||
F: FnMut(&mut std::vec::Vec<u8>, &T) -> ksp_core_lib::Result<()>,
|
||||
F: FnMut(&T) -> U,
|
||||
{
|
||||
return match field {
|
||||
ksp_onchain_transport_lib::SolanaWireField::Omitted => std::result::Result::Ok(()),
|
||||
ksp_onchain_transport_lib::SolanaWireField::Null => {
|
||||
output.push(b',');
|
||||
let key_result = append_json_string(output, key);
|
||||
if let std::result::Result::Err(error) = key_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
output.extend_from_slice(b":null");
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(value) => {
|
||||
output.push(b',');
|
||||
let key_result = append_json_string(output, key);
|
||||
if let std::result::Result::Err(error) = key_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
output.push(b':');
|
||||
append_value(output, value)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn append_canonical_json(output: &mut std::vec::Vec<u8>, value: &serde_json::Value) -> ksp_core_lib::Result<()> {
|
||||
return match value {
|
||||
serde_json::Value::Null => {
|
||||
output.extend_from_slice(b"null");
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
serde_json::Value::Bool(value) => {
|
||||
if *value {
|
||||
output.extend_from_slice(b"true");
|
||||
} else {
|
||||
output.extend_from_slice(b"false");
|
||||
}
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
serde_json::Value::Number(value) => {
|
||||
output.extend_from_slice(value.to_string().as_bytes());
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
serde_json::Value::String(value) => append_json_string(output, value.as_str()),
|
||||
serde_json::Value::Array(values) => {
|
||||
output.push(b'[');
|
||||
for (index, item) in values.iter().enumerate() {
|
||||
if index != 0 {
|
||||
output.push(b',');
|
||||
}
|
||||
let item_result = append_canonical_json(output, item);
|
||||
if let std::result::Result::Err(error) = item_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
output.push(b']');
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
serde_json::Value::Object(values) => {
|
||||
output.push(b'{');
|
||||
let mut keys = values.keys().collect::<std::vec::Vec<_>>();
|
||||
keys.sort_unstable();
|
||||
for (index, key) in keys.iter().enumerate() {
|
||||
if index != 0 {
|
||||
output.push(b',');
|
||||
}
|
||||
let key_result = append_json_string(output, key.as_str());
|
||||
if let std::result::Result::Err(error) = key_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
output.push(b':');
|
||||
let item = values.get(key.as_str());
|
||||
let item = match item {
|
||||
std::option::Option::Some(item) => item,
|
||||
std::option::Option::None => return std::result::Result::Err(conversion_error("payload.meta")),
|
||||
};
|
||||
let item_result = append_canonical_json(output, item);
|
||||
if let std::result::Result::Err(error) = item_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
output.push(b'}');
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn append_json_string(output: &mut std::vec::Vec<u8>, value: &str) -> ksp_core_lib::Result<()> {
|
||||
let encoded = serde_json::to_vec(value);
|
||||
return match encoded {
|
||||
std::result::Result::Ok(encoded) => {
|
||||
output.extend_from_slice(encoded.as_slice());
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("payload.json")),
|
||||
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(map_value(value)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -463,18 +288,6 @@ fn hash_bytes(hasher: &mut sha2::Sha256, value: &[u8]) {
|
||||
hasher.update(value);
|
||||
}
|
||||
|
||||
fn base58_digit(byte: u8) -> std::option::Option<u8> {
|
||||
return match byte {
|
||||
b'1'..=b'9' => std::option::Option::Some(byte - b'1'),
|
||||
b'A'..=b'H' => std::option::Option::Some((byte - b'A') + 9),
|
||||
b'J'..=b'N' => std::option::Option::Some((byte - b'J') + 17),
|
||||
b'P'..=b'Z' => std::option::Option::Some((byte - b'P') + 22),
|
||||
b'a'..=b'k' => std::option::Option::Some((byte - b'a') + 33),
|
||||
b'm'..=b'z' => std::option::Option::Some((byte - b'm') + 44),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn conversion_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID, "invalid deterministic Backfill RAW conversion")
|
||||
.with_context("field", field);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-backfill-lib/src/lib.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -30,10 +30,6 @@ pub use self::checkpoint::BackfillCheckpoint;
|
||||
pub use self::conversion::BackfillHydrationOutcome;
|
||||
/// Complete in-memory RAW transaction acquisition ready for later Store persistence.
|
||||
pub use self::conversion::BackfillRawAcquisition;
|
||||
/// KSP-owned source-independent RAW transaction format identifier produced by this Backfill vertical.
|
||||
pub use self::conversion::RAW_TRANSACTION_FORMAT_ID;
|
||||
/// Initial KSP-owned RAW transaction format version produced by this Backfill vertical.
|
||||
pub use self::conversion::RAW_TRANSACTION_FORMAT_VERSION;
|
||||
/// Hydrates one candidate through observed Transport and converts a non-null response to canonical RAW v1.
|
||||
pub use self::conversion::hydrate_backfill_candidate;
|
||||
/// One deterministic transaction candidate produced by bounded discovery.
|
||||
@@ -112,6 +108,10 @@ pub use self::runtime::BackfillJobRuntime;
|
||||
pub use self::runtime::BackfillJobSnapshot;
|
||||
/// Cloneable runtime-neutral-facing latest-value source for concrete Backfill snapshots.
|
||||
pub use self::runtime::BackfillSnapshotSource;
|
||||
/// KSP-owned source-independent RAW transaction format identifier re-exported from the common RAW layer.
|
||||
pub use ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_ID;
|
||||
/// Frozen RAW transaction format version re-exported from the common RAW layer.
|
||||
pub use ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_VERSION;
|
||||
|
||||
/// Internal contiguous completion frontier used by bounded execution.
|
||||
pub(crate) use self::checkpoint::CompletionFrontier;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-backfill-lib/src/request.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
@@ -12,9 +12,9 @@ pub const MAX_BACKFILL_PAGES: usize = 10_000;
|
||||
/// Maximum page size admitted for one `getSignaturesForAddress` request.
|
||||
pub const MAX_BACKFILL_PAGE_SIZE: usize = 1_000;
|
||||
/// Maximum Base58 text length possible for one canonical 64-byte Solana signature.
|
||||
pub const MAX_BACKFILL_SIGNATURE_TEXT_BYTES: usize = 88;
|
||||
pub const MAX_BACKFILL_SIGNATURE_TEXT_BYTES: usize = ksp_raw_transaction_lib::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||
/// Minimum Base58 text length possible for one canonical 64-byte Solana signature.
|
||||
pub const MIN_BACKFILL_SIGNATURE_TEXT_BYTES: usize = 64;
|
||||
pub const MIN_BACKFILL_SIGNATURE_TEXT_BYTES: usize = ksp_raw_transaction_lib::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||
|
||||
/// Commitment levels intentionally admitted by the historical Backfill vertical.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
// file: crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
||||
// version: 6
|
||||
// version: 9
|
||||
|
||||
//! Dependency firewall canaries through the concrete cancellation and latest-value runtime tranche.
|
||||
|
||||
#[test]
|
||||
fn pre_009_manifest_uses_only_planned_ksp_edges_and_private_tokio_runtime() {
|
||||
fn pre_003_manifest_adds_common_raw_edge_without_crossing_store_or_runtime_boundaries() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
let production_manifest = match manifest.split_once("[dev-dependencies]") {
|
||||
std::option::Option::Some((production, _)) => production,
|
||||
std::option::Option::None => manifest,
|
||||
};
|
||||
for required in [
|
||||
"futures-util = { workspace = true, features = [\"std\"] }",
|
||||
"ksp-core-lib = { path = \"../ksp-core-lib\" }",
|
||||
"ksp-job-api = { path = \"../ksp-job-api\" }",
|
||||
"ksp-logging-lib = { path = \"../ksp-logging-lib\" }",
|
||||
"ksp-onchain-transport-lib = { path = \"../ksp-onchain-transport-lib\" }",
|
||||
"ksp-raw-transaction-lib = { path = \"../ksp-raw-transaction-lib\" }",
|
||||
"ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }",
|
||||
"serde_json.workspace = true",
|
||||
"sha2.workspace = true",
|
||||
"tokio = { workspace = true, features = [\"macros\", \"sync\"] }",
|
||||
] {
|
||||
assert!(manifest.contains(required), "required Backfill dependency missing: {required}");
|
||||
assert!(production_manifest.contains(required), "required Backfill dependency missing: {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
"ksp-config-lib",
|
||||
@@ -32,13 +37,56 @@ fn pre_009_manifest_uses_only_planned_ksp_edges_and_private_tokio_runtime() {
|
||||
"serde = {",
|
||||
"tonic",
|
||||
] {
|
||||
assert!(!manifest.contains(forbidden), "forbidden Backfill dependency present: {forbidden}");
|
||||
assert!(!production_manifest.contains(forbidden), "forbidden Backfill dependency present: {forbidden}");
|
||||
}
|
||||
let root = include_str!("../src/lib.rs");
|
||||
assert!(!root.contains("tokio::"), "Tokio implementation types must not leak through the public crate root");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_websocket_parity_dependency_stays_test_only() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
let parts = manifest.splitn(2, "[dev-dependencies]").collect::<std::vec::Vec<_>>();
|
||||
let (dependencies, dev_dependencies) = match parts.as_slice() {
|
||||
[dependencies, dev_dependencies] => (*dependencies, *dev_dependencies),
|
||||
_ => {
|
||||
assert_eq!(parts.len(), 2, "Backfill manifest must retain exactly one dev-dependencies section");
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert!(!dependencies.contains("tokio-tungstenite"), "WebSocket parity client must not become a production Backfill dependency");
|
||||
assert!(
|
||||
dev_dependencies.contains("tokio-tungstenite = { workspace = true, features = [\"handshake\"] }"),
|
||||
"WebSocket parity client must remain an explicit test-only dependency",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_yellowstone_parity_dependencies_stay_test_only() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
let parts = manifest.splitn(2, "[dev-dependencies]").collect::<std::vec::Vec<_>>();
|
||||
let (dependencies, dev_dependencies) = match parts.as_slice() {
|
||||
[dependencies, dev_dependencies] => (*dependencies, *dev_dependencies),
|
||||
_ => {
|
||||
assert_eq!(parts.len(), 2, "Backfill manifest must retain exactly one dev-dependencies section");
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert!(!dependencies.contains("tonic"), "Yellowstone parity server must not become a production Backfill dependency");
|
||||
assert!(!dependencies.contains("yellowstone-grpc-proto"), "Yellowstone protobuf must not become a production Backfill dependency");
|
||||
assert!(
|
||||
dev_dependencies.contains("tonic = { workspace = true, features = [\"codegen\", \"server\"] }"),
|
||||
"Yellowstone parity server must remain an explicit test-only dependency",
|
||||
);
|
||||
assert!(
|
||||
dev_dependencies.contains("yellowstone-grpc-proto = { workspace = true, features = [\"tonic\"] }"),
|
||||
"Yellowstone protobuf must remain an explicit test-only dependency",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_production_sources_keep_transport_store_and_scheduler_ownership_separate() {
|
||||
let neutral_sources = [
|
||||
@@ -56,7 +104,12 @@ fn pre_009_production_sources_keep_transport_store_and_scheduler_ownership_separ
|
||||
}
|
||||
}
|
||||
let conversion = include_str!("../src/conversion.rs");
|
||||
assert!(conversion.contains("serde_json::"));
|
||||
assert!(conversion.contains("ksp_raw_transaction_lib::canonicalize_raw_transaction"));
|
||||
assert!(conversion.contains("ksp_raw_transaction_lib::assemble_raw_transaction_acquisition"));
|
||||
assert!(conversion.contains("ksp_raw_transaction_lib::parse_raw_transaction_signature"));
|
||||
assert!(!conversion.contains("fn append_canonical_json"));
|
||||
assert!(!conversion.contains("fn base58_digit"));
|
||||
assert!(!conversion.contains("RawPayload::try_new"));
|
||||
assert!(conversion.contains("get_transaction_observed"));
|
||||
assert!(conversion.contains("SolanaTransactionEncoding::Base64"));
|
||||
assert!(conversion.contains("std::option::Option::Some(0)"));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||
// version: 4
|
||||
// version: 7
|
||||
|
||||
//! Adversarial, security, visibility and external-boundary hardening canaries for `pre.010`.
|
||||
|
||||
@@ -268,13 +268,14 @@ fn pre_010_manifest_dependency_surface_is_exact_and_backend_neutral() {
|
||||
"ksp-job-api",
|
||||
"ksp-logging-lib",
|
||||
"ksp-onchain-transport-lib",
|
||||
"ksp-raw-transaction-lib",
|
||||
"ksp-store-lib",
|
||||
"serde_json.workspace",
|
||||
"sha2.workspace",
|
||||
"tokio",
|
||||
])
|
||||
);
|
||||
assert_eq!(dev, std::collections::BTreeSet::from(["tokio"]));
|
||||
assert_eq!(dev, std::collections::BTreeSet::from(["tokio", "tokio-tungstenite", "tonic", "yellowstone-grpc-proto"]));
|
||||
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"));
|
||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\"] }"));
|
||||
assert!(!manifest.contains("ksp-store-postgres-lib"));
|
||||
@@ -328,6 +329,7 @@ fn pre_010_lower_layers_have_no_dependency_return_to_job() {
|
||||
include_str!("../../ksp-core-lib/Cargo.toml"),
|
||||
include_str!("../../ksp-logging-lib/Cargo.toml"),
|
||||
include_str!("../../ksp-onchain-transport-lib/Cargo.toml"),
|
||||
include_str!("../../ksp-raw-transaction-lib/Cargo.toml"),
|
||||
include_str!("../../ksp-store-api/Cargo.toml"),
|
||||
include_str!("../../ksp-store-lib/Cargo.toml"),
|
||||
include_str!("../../ksp-store-postgres-lib/Cargo.toml"),
|
||||
|
||||
158
crates/ksp-job-backfill-lib/tests/http_block_material.rs
Normal file
158
crates/ksp-job-backfill-lib/tests/http_block_material.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
// file: crates/ksp-job-backfill-lib/tests/http_block_material.rs
|
||||
// version: 2
|
||||
|
||||
//! Cross-layer canary for the future Worker-owned HTTP block adapter without changing production ownership.
|
||||
|
||||
fn pool_for_url(url: &str) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTransportPool> {
|
||||
let endpoint_url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse(url) {
|
||||
Ok(endpoint_url) => endpoint_url,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
||||
ksp_onchain_transport_lib::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()],
|
||||
10,
|
||||
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
),
|
||||
);
|
||||
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
||||
"http-block-fixture",
|
||||
true,
|
||||
ksp_onchain_transport_lib::HttpProviderName::new("fixture-provider"),
|
||||
ksp_onchain_transport_lib::HttpClusterName::new("devnet"),
|
||||
endpoint_url,
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::option::Option::Some(1),
|
||||
std::vec![role],
|
||||
);
|
||||
return ksp_onchain_transport_lib::HttpTransportPool::new(ksp_onchain_transport_lib::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
ksp_onchain_transport_lib::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
|
||||
));
|
||||
}
|
||||
|
||||
fn serve_once(body: &'static str) -> std::io::Result<(std::string::String, std::thread::JoinHandle<std::io::Result<()>>)> {
|
||||
let listener = match std::net::TcpListener::bind("127.0.0.1:0") {
|
||||
Ok(listener) => listener,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let address = match listener.local_addr() {
|
||||
Ok(address) => address,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (mut stream, _) = match listener.accept() {
|
||||
Ok(accepted) => accepted,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let mut bytes = std::vec::Vec::new();
|
||||
let mut buffer = [0_u8; 1024];
|
||||
loop {
|
||||
let count = match std::io::Read::read(&mut stream, &mut buffer) {
|
||||
Ok(count) => count,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
bytes.extend_from_slice(&buffer[..count]);
|
||||
if bytes.windows(4).any(|window| return window == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
|
||||
if let Err(error) = std::io::Write::write_all(&mut stream, response.as_bytes()) {
|
||||
return Err(error);
|
||||
}
|
||||
return Ok(());
|
||||
});
|
||||
return Ok((format!("http://{address}"), handle));
|
||||
}
|
||||
|
||||
fn map_meta(field: &ksp_onchain_transport_lib::SolanaWireField<serde_json::Value>) -> ksp_raw_transaction_lib::RawTransactionWireField<serde_json::Value> {
|
||||
return match field {
|
||||
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(value.clone()),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_version(
|
||||
field: &ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
|
||||
) -> ksp_raw_transaction_lib::RawTransactionWireField<ksp_raw_transaction_lib::RawTransactionVersion> {
|
||||
return match field {
|
||||
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy) => {
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Legacy)
|
||||
},
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number)) => {
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Number(*number))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn pre_004_http_observed_block_projects_each_base64_transaction_to_common_material_without_new_production_edge() {
|
||||
const BODY: &str = concat!(
|
||||
"{\"jsonrpc\":\"2.0\",\"result\":{\"previousBlockhash\":\"previous\",\"blockhash\":\"block\",\"parentSlot\":430000122,",
|
||||
"\"rewards\":[],\"numRewardPartitions\":0,\"blockTime\":1787072400,\"blockHeight\":410000000,",
|
||||
"\"transactions\":[{\"transaction\":[\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"AAAAAA\",\"base64\"],\"meta\":{\"err\":null,\"fee\":5000},\"version\":\"legacy\"},",
|
||||
"{\"transaction\":[\"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEA\",",
|
||||
"\"base64\"],\"meta\":{\"err\":null,\"fee\":6000},\"version\":0}]},\"id\":1}",
|
||||
);
|
||||
let (url, handle) = serve_once(BODY).expect("fixture server must start");
|
||||
let pool = pool_for_url(url.as_str()).expect("fixture pool must build");
|
||||
let config = ksp_onchain_transport_lib::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(0),
|
||||
std::option::Option::Some(false),
|
||||
);
|
||||
let observed = pool
|
||||
.get_block_observed(&ksp_onchain_transport_lib::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("observed block fixture must succeed");
|
||||
assert_eq!(observed.endpoint_name(), "http-block-fixture");
|
||||
assert_eq!(observed.provider().as_str(), "fixture-provider");
|
||||
let block = observed.value().as_ref().expect("fixture block must be present");
|
||||
let transactions = block.transactions().value().expect("fixture transactions must be present");
|
||||
assert_eq!(transactions.len(), 2);
|
||||
let network = ksp_store_lib::RawNetworkId::new("devnet").expect("fixture network must be valid");
|
||||
let expected_signatures = [[0_u8; 64], [1_u8; 64]];
|
||||
for (index, transaction) in transactions.iter().enumerate() {
|
||||
let (data, meta, version) = match transaction.transaction() {
|
||||
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary {
|
||||
data,
|
||||
encoding: ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64,
|
||||
} => (data.as_str(), map_meta(transaction.meta()), map_version(transaction.version())),
|
||||
_ => panic!("fixture must remain full Base64 transaction material"),
|
||||
};
|
||||
let transaction_index = u32::try_from(index).expect("fixture transaction index must fit u32");
|
||||
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||
network.clone(),
|
||||
430_000_123,
|
||||
block.block_time(),
|
||||
data,
|
||||
meta,
|
||||
version,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Value(transaction_index),
|
||||
)
|
||||
.expect("HTTP block material must map to common RAW material");
|
||||
let raw = ksp_raw_transaction_lib::canonicalize_raw_transaction(material).expect("mapped HTTP block material must canonicalize");
|
||||
assert_eq!(raw.reference().signature().as_bytes(), &expected_signatures[index]);
|
||||
assert_eq!(raw.slot(), 430_000_123);
|
||||
assert_eq!(raw.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_787_072_400_000));
|
||||
assert!(raw.payload().bytes().ends_with(format!("\"transactionIndex\":{transaction_index}}}").as_bytes()));
|
||||
}
|
||||
handle.join().expect("fixture server must join").expect("fixture server must complete successfully");
|
||||
return;
|
||||
}
|
||||
503
crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
Normal file
503
crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
Normal file
@@ -0,0 +1,503 @@
|
||||
// file: crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
|
||||
// version: 3
|
||||
|
||||
//! Cross-layer parity canaries qualifying standard `blockSubscribe` and Helius `transactionSubscribe` for RAW v1 ingestion.
|
||||
|
||||
use futures_util::SinkExt; // rust-rules: trait-import
|
||||
use futures_util::StreamExt; // rust-rules: trait-import
|
||||
|
||||
const FIXTURE_BLOCK_TIME_MILLIS: u64 = 1_787_072_400_000;
|
||||
const FIXTURE_SLOT: u64 = 430_000_123;
|
||||
const ZERO_SIGNATURE_TEXT: &str = "1111111111111111111111111111111111111111111111111111111111111111";
|
||||
const ZERO_TRANSACTION_BASE64: &str = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
|
||||
type FixtureWebSocket = tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>;
|
||||
|
||||
fn http_pool_for_url(url: &str) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTransportPool> {
|
||||
let endpoint_url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse(url) {
|
||||
Ok(endpoint_url) => endpoint_url,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
||||
ksp_onchain_transport_lib::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()],
|
||||
10,
|
||||
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
),
|
||||
);
|
||||
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
||||
"ws-parity-http-fixture",
|
||||
true,
|
||||
ksp_onchain_transport_lib::HttpProviderName::new("fixture-http-provider"),
|
||||
ksp_onchain_transport_lib::HttpClusterName::new("devnet"),
|
||||
endpoint_url,
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::option::Option::Some(1),
|
||||
std::vec![role],
|
||||
);
|
||||
return ksp_onchain_transport_lib::HttpTransportPool::new(ksp_onchain_transport_lib::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
ksp_onchain_transport_lib::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
|
||||
));
|
||||
}
|
||||
|
||||
fn serve_http_once(body: &'static str) -> std::io::Result<(std::string::String, std::thread::JoinHandle<std::io::Result<()>>)> {
|
||||
let listener = match std::net::TcpListener::bind("127.0.0.1:0") {
|
||||
Ok(listener) => listener,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let address = match listener.local_addr() {
|
||||
Ok(address) => address,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (mut stream, _) = match listener.accept() {
|
||||
Ok(accepted) => accepted,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let mut bytes = std::vec::Vec::new();
|
||||
let mut buffer = [0_u8; 1024];
|
||||
loop {
|
||||
let count = match std::io::Read::read(&mut stream, &mut buffer) {
|
||||
Ok(count) => count,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
bytes.extend_from_slice(&buffer[..count]);
|
||||
if bytes.windows(4).any(|window| return window == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
|
||||
if let Err(error) = std::io::Write::write_all(&mut stream, response.as_bytes()) {
|
||||
return Err(error);
|
||||
}
|
||||
return Ok(());
|
||||
});
|
||||
return Ok((format!("http://{address}"), handle));
|
||||
}
|
||||
|
||||
fn ws_endpoint(url: &str, protocol: ksp_onchain_transport_lib::WsProtocolKind) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsEndpointSettings> {
|
||||
let parsed = match ksp_onchain_transport_lib::WsEndpointUrl::parse(url) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
return Ok(ksp_onchain_transport_lib::WsEndpointSettings::new(
|
||||
"ws-raw-parity-fixture",
|
||||
true,
|
||||
ksp_onchain_transport_lib::WsProviderName::new("fixture-ws-provider"),
|
||||
ksp_onchain_transport_lib::WsClusterName::new("devnet"),
|
||||
protocol,
|
||||
parsed,
|
||||
ksp_onchain_transport_lib::WsSessionSettings::default(),
|
||||
));
|
||||
}
|
||||
|
||||
async fn read_ws_request(websocket: &mut FixtureWebSocket) -> std::result::Result<serde_json::Value, std::string::String> {
|
||||
let message = match websocket.next().await {
|
||||
std::option::Option::Some(std::result::Result::Ok(message)) => message,
|
||||
std::option::Option::Some(std::result::Result::Err(_)) => return Err("fixture WebSocket request decode failed".to_owned()),
|
||||
std::option::Option::None => return Err("fixture WebSocket request was absent".to_owned()),
|
||||
};
|
||||
let text = match message.to_text() {
|
||||
Ok(text) => text,
|
||||
Err(_) => return Err("fixture WebSocket request was not text".to_owned()),
|
||||
};
|
||||
return serde_json::from_str(text).map_err(|_| return "fixture WebSocket request JSON was invalid".to_owned());
|
||||
}
|
||||
|
||||
async fn send_ws_json(websocket: &mut FixtureWebSocket, value: serde_json::Value) -> std::result::Result<(), std::string::String> {
|
||||
return websocket
|
||||
.send(tokio_tungstenite::tungstenite::Message::Text(value.to_string().into()))
|
||||
.await
|
||||
.map_err(|_| return "fixture WebSocket response send failed".to_owned());
|
||||
}
|
||||
|
||||
async fn send_ws_result(
|
||||
websocket: &mut FixtureWebSocket,
|
||||
request: &serde_json::Value,
|
||||
result: serde_json::Value,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
let id = match request.get("id").and_then(serde_json::Value::as_u64) {
|
||||
std::option::Option::Some(id) => id,
|
||||
std::option::Option::None => return Err("fixture request id was absent".to_owned()),
|
||||
};
|
||||
return send_ws_json(websocket, serde_json::json!({"jsonrpc":"2.0","id":id,"result":result})).await;
|
||||
}
|
||||
|
||||
async fn wait_for_ws_close(websocket: &mut FixtureWebSocket) {
|
||||
loop {
|
||||
match websocket.next().await {
|
||||
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
|
||||
std::option::Option::Some(std::result::Result::Ok(_)) => {},
|
||||
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_meta(field: &ksp_onchain_transport_lib::SolanaWireField<serde_json::Value>) -> ksp_raw_transaction_lib::RawTransactionWireField<serde_json::Value> {
|
||||
return match field {
|
||||
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(value.clone()),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_version(
|
||||
field: &ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
|
||||
) -> ksp_raw_transaction_lib::RawTransactionWireField<ksp_raw_transaction_lib::RawTransactionVersion> {
|
||||
return match field {
|
||||
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy) => {
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Legacy)
|
||||
},
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number)) => {
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Number(*number))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn raw_from_block_transaction(
|
||||
network: ksp_store_lib::RawNetworkId,
|
||||
slot: u64,
|
||||
block_time: std::option::Option<i64>,
|
||||
transaction: &ksp_onchain_transport_lib::SolanaBlockTransaction,
|
||||
transaction_index: u32,
|
||||
) -> std::option::Option<ksp_store_lib::RawTransaction> {
|
||||
let data = match transaction.transaction() {
|
||||
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { data, encoding: ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64 } => {
|
||||
data.as_str()
|
||||
},
|
||||
_ => return std::option::Option::None,
|
||||
};
|
||||
let material = match ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||
network,
|
||||
slot,
|
||||
block_time,
|
||||
data,
|
||||
map_meta(transaction.meta()),
|
||||
map_version(transaction.version()),
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Value(transaction_index),
|
||||
) {
|
||||
Ok(material) => material,
|
||||
Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return ksp_raw_transaction_lib::canonicalize_raw_transaction(material).ok();
|
||||
}
|
||||
|
||||
fn raw_from_confirmed_transaction(
|
||||
network: ksp_store_lib::RawNetworkId,
|
||||
transaction: &ksp_onchain_transport_lib::SolanaConfirmedTransaction,
|
||||
) -> std::option::Option<ksp_store_lib::RawTransaction> {
|
||||
let data = match transaction.transaction() {
|
||||
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { data, encoding: ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64 } => {
|
||||
data.as_str()
|
||||
},
|
||||
_ => return std::option::Option::None,
|
||||
};
|
||||
let material = match ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||
network,
|
||||
transaction.slot(),
|
||||
transaction.block_time(),
|
||||
data,
|
||||
map_meta(transaction.meta()),
|
||||
map_version(transaction.version()),
|
||||
match transaction.transaction_index() {
|
||||
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(*value),
|
||||
},
|
||||
) {
|
||||
Ok(material) => material,
|
||||
Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return ksp_raw_transaction_lib::canonicalize_raw_transaction(material).ok();
|
||||
}
|
||||
|
||||
fn raw_from_helius_full(
|
||||
network: ksp_store_lib::RawNetworkId,
|
||||
notification: &ksp_onchain_transport_lib::HeliusFullTransactionNotification,
|
||||
) -> std::option::Option<ksp_store_lib::RawTransaction> {
|
||||
let object = match notification.transaction().as_object() {
|
||||
std::option::Option::Some(object) => object,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
let encoded = match object.get("transaction") {
|
||||
std::option::Option::Some(value) => match value.as_array() {
|
||||
std::option::Option::Some(encoded) => encoded,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
},
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
if encoded.len() != 2 {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let encoding = match encoded.get(1) {
|
||||
std::option::Option::Some(value) => match value.as_str() {
|
||||
std::option::Option::Some(encoding) => encoding,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
},
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
if encoding != "base64" {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let data = match encoded.first() {
|
||||
std::option::Option::Some(value) => match value.as_str() {
|
||||
std::option::Option::Some(data) => data,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
},
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
let meta = match object.get("meta") {
|
||||
std::option::Option::None => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
std::option::Option::Some(serde_json::Value::Null) => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||
std::option::Option::Some(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(value.clone()),
|
||||
};
|
||||
let signature = match ksp_raw_transaction_lib::parse_raw_transaction_signature(notification.signature()) {
|
||||
Ok(signature) => signature,
|
||||
Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let transaction_index = match u32::try_from(notification.transaction_index()) {
|
||||
Ok(transaction_index) => transaction_index,
|
||||
Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||
network,
|
||||
signature,
|
||||
notification.slot(),
|
||||
std::option::Option::None,
|
||||
data,
|
||||
meta,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Value(transaction_index),
|
||||
);
|
||||
return ksp_raw_transaction_lib::canonicalize_raw_transaction(material).ok();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn pre_005_standard_block_subscribe_full_base64_matches_http_get_block_raw_v1_for_legacy_transaction() {
|
||||
const HTTP_BODY: &str = concat!(
|
||||
"{\"jsonrpc\":\"2.0\",\"result\":{\"previousBlockhash\":\"previous\",\"blockhash\":\"block\",\"parentSlot\":430000122,",
|
||||
"\"rewards\":[],\"numRewardPartitions\":0,\"blockTime\":1787072400,\"blockHeight\":410000000,",
|
||||
"\"transactions\":[{\"transaction\":[\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"AAAAAA\",\"base64\"],\"meta\":{\"err\":null,\"fee\":5000},\"version\":\"legacy\"}]},\"id\":1}",
|
||||
);
|
||||
let (http_url, http_server) = serve_http_once(HTTP_BODY).expect("HTTP parity fixture must start");
|
||||
let http_pool = http_pool_for_url(http_url.as_str()).expect("HTTP parity pool must build");
|
||||
let http_config = ksp_onchain_transport_lib::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(0),
|
||||
std::option::Option::Some(false),
|
||||
);
|
||||
let http = http_pool
|
||||
.get_block_observed(&ksp_onchain_transport_lib::HttpRoleName::new("default"), FIXTURE_SLOT, std::option::Option::Some(&http_config))
|
||||
.await
|
||||
.expect("HTTP block parity fixture must decode");
|
||||
let http_block = http.value().as_ref().expect("HTTP parity block must be present").clone();
|
||||
http_server.join().expect("HTTP parity fixture must join").expect("HTTP parity fixture must complete");
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("WS parity listener must bind");
|
||||
let address = listener.local_addr().expect("WS parity listener address must resolve");
|
||||
let ws_block = serde_json::from_str::<serde_json::Value>(HTTP_BODY).expect("HTTP parity JSON must parse")["result"].clone();
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = match listener.accept().await {
|
||||
Ok(accepted) => accepted,
|
||||
Err(_) => return Err("WS parity server accept failed".to_owned()),
|
||||
};
|
||||
let mut websocket = match tokio_tungstenite::accept_async(stream).await {
|
||||
Ok(websocket) => websocket,
|
||||
Err(_) => return Err("WS parity handshake failed".to_owned()),
|
||||
};
|
||||
let subscribe = match read_ws_request(&mut websocket).await {
|
||||
Ok(subscribe) => subscribe,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if subscribe.get("method") != std::option::Option::Some(&serde_json::json!("blockSubscribe")) {
|
||||
return Err("WS parity server received unexpected subscribe method".to_owned());
|
||||
}
|
||||
if let Err(error) = send_ws_result(&mut websocket, &subscribe, serde_json::json!(501)).await {
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(error) = send_ws_json(
|
||||
&mut websocket,
|
||||
serde_json::json!({
|
||||
"jsonrpc":"2.0",
|
||||
"method":"blockNotification",
|
||||
"params":{"subscription":501,"result":{"context":{"slot":FIXTURE_SLOT},"value":{"slot":FIXTURE_SLOT,"block":ws_block,"err":null}}}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(error);
|
||||
}
|
||||
let unsubscribe = match read_ws_request(&mut websocket).await {
|
||||
Ok(unsubscribe) => unsubscribe,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if unsubscribe.get("method") != std::option::Option::Some(&serde_json::json!("blockUnsubscribe")) {
|
||||
return Err("WS parity server received unexpected unsubscribe method".to_owned());
|
||||
}
|
||||
if let Err(error) = send_ws_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await {
|
||||
return Err(error);
|
||||
}
|
||||
wait_for_ws_close(&mut websocket).await;
|
||||
return Ok::<(), std::string::String>(());
|
||||
});
|
||||
let endpoint = ws_endpoint(format!("ws://{address}").as_str(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard)
|
||||
.expect("standard WS parity endpoint must build");
|
||||
let session = ksp_onchain_transport_lib::SolanaStandardWsSession::connect(endpoint).await.expect("standard WS parity session must connect");
|
||||
let ws_config = ksp_onchain_transport_lib::SolanaBlockSubscribeConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(0),
|
||||
std::option::Option::Some(false),
|
||||
);
|
||||
let mut subscription = session
|
||||
.block_subscribe(&ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All, std::option::Option::Some(&ws_config))
|
||||
.await
|
||||
.expect("standard blockSubscribe parity subscription must register");
|
||||
let notification = subscription
|
||||
.recv()
|
||||
.await
|
||||
.expect("standard blockSubscribe parity notification must arrive")
|
||||
.expect("standard blockSubscribe parity notification must decode");
|
||||
let ws_block = notification.value().block().expect("standard blockSubscribe parity block must be present");
|
||||
assert_eq!(ws_block, &http_block);
|
||||
let network = ksp_store_lib::RawNetworkId::new("devnet").expect("parity fixture network must be valid");
|
||||
let http_transaction =
|
||||
http_block.transactions().value().expect("HTTP parity transactions must be present").first().expect("HTTP parity transaction must exist");
|
||||
let ws_transaction = ws_block.transactions().value().expect("WS parity transactions must be present").first().expect("WS parity transaction must exist");
|
||||
let http_raw = raw_from_block_transaction(network.clone(), FIXTURE_SLOT, http_block.block_time(), http_transaction, 0)
|
||||
.expect("HTTP block transaction must canonicalize");
|
||||
let ws_raw = raw_from_block_transaction(network, notification.value().slot(), ws_block.block_time(), ws_transaction, 0)
|
||||
.expect("WS block transaction must canonicalize");
|
||||
assert_eq!(ws_raw.reference(), http_raw.reference());
|
||||
assert_eq!(ws_raw.slot(), http_raw.slot());
|
||||
assert_eq!(ws_raw.block_time(), http_raw.block_time());
|
||||
assert_eq!(ws_raw.payload().bytes(), http_raw.payload().bytes());
|
||||
assert_eq!(ws_raw.payload().content_hash(), http_raw.payload().content_hash());
|
||||
assert!(subscription.unsubscribe().await.expect("standard blockSubscribe parity unsubscribe must complete"));
|
||||
session.close().await.expect("standard WS parity session must close");
|
||||
server.await.expect("standard WS parity server task must join").expect("standard WS parity server must complete");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn pre_005_helius_full_base64_without_block_time_and_version_requires_http_hydration_for_raw_v1() {
|
||||
const HTTP_BODY: &str = concat!(
|
||||
"{\"jsonrpc\":\"2.0\",\"result\":{\"slot\":430000123,\"blockTime\":1787072400,",
|
||||
"\"transaction\":[\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"base64\"],",
|
||||
"\"meta\":{\"err\":null,\"fee\":5000},\"version\":\"legacy\",\"transactionIndex\":0},\"id\":1}",
|
||||
);
|
||||
let (http_url, http_server) = serve_http_once(HTTP_BODY).expect("Helius hydration HTTP fixture must start");
|
||||
let http_pool = http_pool_for_url(http_url.as_str()).expect("Helius hydration HTTP pool must build");
|
||||
let http_config = ksp_onchain_transport_lib::SolanaGetTransactionConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
||||
std::option::Option::Some(0),
|
||||
);
|
||||
let http = http_pool
|
||||
.get_transaction_observed(&ksp_onchain_transport_lib::HttpRoleName::new("default"), ZERO_SIGNATURE_TEXT, std::option::Option::Some(&http_config))
|
||||
.await
|
||||
.expect("Helius hydration HTTP fixture must decode");
|
||||
let http_transaction = http.value().as_ref().expect("Helius hydration HTTP transaction must be present");
|
||||
let network = ksp_store_lib::RawNetworkId::new("devnet").expect("Helius hydration fixture network must be valid");
|
||||
let http_raw = raw_from_confirmed_transaction(network.clone(), http_transaction).expect("hydrated HTTP transaction must canonicalize");
|
||||
http_server.join().expect("Helius hydration HTTP fixture must join").expect("Helius hydration HTTP fixture must complete");
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("Helius parity listener must bind");
|
||||
let address = listener.local_addr().expect("Helius parity listener address must resolve");
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = match listener.accept().await {
|
||||
Ok(accepted) => accepted,
|
||||
Err(_) => return Err("Helius parity server accept failed".to_owned()),
|
||||
};
|
||||
let mut websocket = match tokio_tungstenite::accept_async(stream).await {
|
||||
Ok(websocket) => websocket,
|
||||
Err(_) => return Err("Helius parity handshake failed".to_owned()),
|
||||
};
|
||||
let subscribe = match read_ws_request(&mut websocket).await {
|
||||
Ok(subscribe) => subscribe,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if subscribe.get("method") != std::option::Option::Some(&serde_json::json!("transactionSubscribe")) {
|
||||
return Err("Helius parity server received unexpected subscribe method".to_owned());
|
||||
}
|
||||
if let Err(error) = send_ws_result(&mut websocket, &subscribe, serde_json::json!(601)).await {
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(error) = send_ws_json(
|
||||
&mut websocket,
|
||||
serde_json::json!({
|
||||
"jsonrpc":"2.0",
|
||||
"method":"transactionNotification",
|
||||
"params":{
|
||||
"subscription":601,
|
||||
"result":{
|
||||
"transaction":{"transaction":[ZERO_TRANSACTION_BASE64,"base64"],"meta":{"err":null,"fee":5000}},
|
||||
"signature":ZERO_SIGNATURE_TEXT,
|
||||
"slot":FIXTURE_SLOT,
|
||||
"transactionIndex":0
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(error);
|
||||
}
|
||||
let unsubscribe = match read_ws_request(&mut websocket).await {
|
||||
Ok(unsubscribe) => unsubscribe,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if unsubscribe.get("method") != std::option::Option::Some(&serde_json::json!("transactionUnsubscribe")) {
|
||||
return Err("Helius parity server received unexpected unsubscribe method".to_owned());
|
||||
}
|
||||
if let Err(error) = send_ws_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await {
|
||||
return Err(error);
|
||||
}
|
||||
wait_for_ws_close(&mut websocket).await;
|
||||
return Ok::<(), std::string::String>(());
|
||||
});
|
||||
let endpoint = ws_endpoint(format!("ws://{address}").as_str(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream)
|
||||
.expect("Helius parity endpoint must build");
|
||||
let session = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect(endpoint).await.expect("Helius parity session must connect");
|
||||
let request = ksp_onchain_transport_lib::HeliusTransactionSubscribeRequest::new(
|
||||
ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::default(),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeOptions::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeEncoding::Base64),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(false),
|
||||
std::option::Option::Some(0),
|
||||
)),
|
||||
);
|
||||
let mut subscription = session.transaction_subscribe(&request).await.expect("Helius parity transaction subscription must register");
|
||||
let notification = subscription.recv().await.expect("Helius parity notification must arrive").expect("Helius parity notification must decode");
|
||||
assert!(matches!(¬ification, ksp_onchain_transport_lib::HeliusTransactionNotification::Full(_)));
|
||||
let full = match notification {
|
||||
ksp_onchain_transport_lib::HeliusTransactionNotification::Full(full) => full,
|
||||
_ => return,
|
||||
};
|
||||
let helius_raw = raw_from_helius_full(network, &full).expect("best-effort Helius full projection must canonicalize for negative parity proof");
|
||||
assert_eq!(helius_raw.reference(), http_raw.reference());
|
||||
assert_eq!(helius_raw.slot(), http_raw.slot());
|
||||
assert!(helius_raw.block_time().is_none());
|
||||
assert_eq!(http_raw.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(FIXTURE_BLOCK_TIME_MILLIS),);
|
||||
assert_ne!(helius_raw.block_time(), http_raw.block_time());
|
||||
assert_ne!(helius_raw.payload().bytes(), http_raw.payload().bytes());
|
||||
assert_ne!(helius_raw.payload().content_hash(), http_raw.payload().content_hash());
|
||||
assert!(subscription.unsubscribe().await.expect("Helius parity transaction unsubscribe must complete"));
|
||||
session.close().await.expect("Helius parity session must close");
|
||||
server.await.expect("Helius parity server task must join").expect("Helius parity server must complete");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user