32 Commits

Author SHA1 Message Date
85f292a36d v0.3.8-rel.001 2026-09-04 10:51:31 +02:00
877411c6e5 v0.3.8-pre.014 2026-09-04 10:48:25 +02:00
081a5352c0 v0.3.8-pre.013-fix.001 2026-09-04 10:44:45 +02:00
c0b3190256 v0.3.8-pre.013 2026-09-04 09:58:20 +02:00
eaefec2259 v0.3.8-pre.012-fix.001 2026-09-04 07:23:33 +02:00
cfbf849e2a v0.3.8-pre.012 2026-09-03 23:21:23 +02:00
e01fe88594 v0.3.8-pre.011-fix.001 2026-09-03 22:34:02 +02:00
80fe2fd9e2 v0.3.8-pre.011 2026-09-03 22:31:28 +02:00
516c52ece9 v0.3.8-pre.010-fix.002 2026-09-03 22:05:45 +02:00
2b79818848 v0.3.8-pre.010-fix.001 2026-09-03 21:47:02 +02:00
e2e603ca6d v0.3.8-pre.010 2026-09-03 21:36:34 +02:00
01502678bf v0.3.8-pre.009-fix.001 2026-09-03 20:52:31 +02:00
32c4b67541 v0.3.8-pre.009 2026-09-03 20:44:21 +02:00
8c23625fee v0.3.8-pre.008-fix.001 2026-09-03 19:59:25 +02:00
489d686023 v0.3.8-pre.008 2026-09-03 19:53:25 +02:00
10515d1562 v0.3.8-pre.007-fix.003 2026-09-03 18:59:39 +02:00
c9e52d1926 v0.3.8-pre.007-fix.002 2026-09-03 18:51:37 +02:00
32577ea48d v0.3.8-pre.00-fix.001 2026-09-03 18:45:53 +02:00
d5b4aef194 v0.3.8-pre.007 2026-09-03 17:54:50 +02:00
8130e01b70 v0.3.8-pre.006-fix.001 2026-09-03 16:28:13 +02:00
3a38914be7 v0.3.8-pre.006 2026-09-03 16:26:18 +02:00
62d6cf9dac v0.3.8-pre.005-fix.003 2026-09-03 14:21:47 +02:00
4e2647cce1 v0.3.8-pre.005-fix.002 2026-09-03 14:19:01 +02:00
9b92900dd7 v0.3.8-pre.005-fix.001 2026-09-03 13:45:12 +02:00
bf73481180 v0.3.8-pre.005 2026-09-03 13:09:25 +02:00
3bb73e81e4 v0.3.8-pre.004-fix.001 2026-09-03 12:33:16 +02:00
c4198ac656 v0.3.8-pre.004 2026-09-03 12:32:53 +02:00
009b957523 v0.3.8-pre.003-fix.001 2026-09-03 11:45:43 +02:00
9300dccbe2 v0.3.8-pre.003 2026-09-03 11:39:09 +02:00
74890a6268 v0.3.8-pre.002-fix.001 2026-09-03 11:31:41 +02:00
6a1892792c v0.3.8-pre.002 2026-09-03 10:44:49 +02:00
3df8fc74c1 v0.3.8-pre.001 2026-09-03 09:56:41 +02:00
151 changed files with 17262 additions and 270 deletions

View File

@@ -1,8 +1,26 @@
<!-- file: CHANGELOG.md --> <!-- file: CHANGELOG.md -->
<!-- version: 27 --> <!-- version: 28 -->
# Changelog KSP # Changelog KSP
## 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 — 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. `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.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 444 # version: 475
[workspace] [workspace]
resolver = "3" 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-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
[workspace.package] [workspace.package]
version = "0.3.7" version = "0.3.8"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: README.md --> <!-- file: README.md -->
<!-- version: 9 --> <!-- version: 10 -->
# Khadhroony Solana Project # Khadhroony Solana Project
@@ -53,6 +53,8 @@ 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. `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.
`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.
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. 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.
## Points d'entrée ## Points d'entrée

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 102 --> <!-- version: 103 -->
# Roadmap KSP # Roadmap KSP
@@ -100,10 +100,11 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
- [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.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.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 nest exposé. - [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 nest 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. Lapplication évoluera ensuite avec STRUCTURAL, DECODED, processing/materialization et DOMAIN réellement persistés. - [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.
- [ ] `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.9` — Introduire `ksp-worker-api` comme API **générique et volontairement courte** de lifecycle/health/progression/snapshot pour services continus, distincte de `ksp-job-api` et sans dépendance Solana/Transport/Store/Tauri. Une fois l'API Worker fonctionnellement fermée, terminer la release par un audit fonctionnel exhaustif des sources/méthodes d'acquisition `RawTransaction` : HTTP, WS standard, extensions provider, Yellowstone gRPC, blocks/slots, discovery + hydration, replay/gap-repair et combinaisons multi-provider. Cet audit prépare `0.3.10` sans ajouter d'endpoint ni de Config provider dans `0.3.9`.
- [ ] `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.10` — Introduire `ksp-worker-raw-transaction-ingest-lib` **multi-source dès V1**. Le worker compose les stratégies retenues par l'audit `0.3.9` comme sources alternatives, complémentaires, redondantes ou spécialisées live/catch-up/gap-repair, converge vers `RawTransaction + RawTransactionObservation`, persiste par `ksp-store-lib` et utilise `ksp-worker-api` pour supervision/cancellation. Cette release porte aussi, et seulement si l'audit les justifie, les adaptations nécessaires de `ksp-onchain-transport-lib`/`ksp-config-lib` : futures sources Helius HTTP/WS Mainnet/Devnet avec réutilisation de `KSP_SECRET_HELIUS_API_KEY`, capabilities/tier réaudités au moment du travail, et éventuelle canonicalisation `mainnet`/`mainnet-beta` uniquement avec stratégie de compatibilité sûre. Aucun decode Program ni dépendance backend/provider directe dans le worker.
- [ ] `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`. - [ ] `0.3.11` — 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 `0.3.10` : 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 ; l'inspection détaillée des RAW persistés reste la responsabilité de `ksp-app-store-desk`.
- [ ] `0.3.12` — É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 (par exemple block/slot, replay provider lorsqu'il existe), sans supposer qu'une source live WS constitue un historique universel.
### TODO/IDEAS — applications spécialisées et control plane ### TODO/IDEAS — applications spécialisées et control plane
@@ -113,6 +114,9 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
### TODO/IDEAS — taxonomie N1, processing et rétention ### TODO/IDEAS — taxonomie N1, processing et rétention
- [ ] **TODO** — maintenir la matrice dadmission 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 dadmission 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 `0.3.9`** — produire après fermeture de `ksp-worker-api` une matrice RAW Transaction unique et réutilisable par `0.3.10` puis `0.3.12`, couvrant discovery, hydration/direct payload, filtres, ordering/duplicates, reconnect/replay, gap repair, backpressure, commitment, provenance, quotas, gaps Transport/Config et applicability continuous-ingest/catch-up/backfill.
- [ ] **TODO Helius futur** — ne pas ajouter d'URL/endpoints Helius en `0.3.8` ni pendant la construction générique de Worker API. L'audit `0.3.9` réévalue les capabilities/tier courants ; `0.3.10` peut ensuite ajouter les profils HTTP/WS réellement nécessaires en réutilisant exclusivement `KSP_SECRET_HELIUS_API_KEY` via Config.
- [ ] **TODO réseau** — auditer `mainnet` vs `mainnet-beta` avant tout renommage : Store persisté, Config, Transport, checkpoints/fingerprints et aliases externes doivent converger vers une identité KSP unique pour le même cluster ; aucune migration/canonicalisation n'est supposée avant cette preuve.
- [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. - [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 dexécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsquun consumer de commitment/snapshot réel apparaît, sans fusionner snapshot, transition et execution update dans un modèle Option-soup. - [ ] **TODO** — statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif dexécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsquun 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 dun 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-*`. - [ ] **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 dun 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-*`.

View 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"
}
]
}
]
}

View File

@@ -54,18 +54,19 @@
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json", "../../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-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/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/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.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.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.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.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"
} }
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs // file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
// version: 16 // version: 17
//! Structural desktop contract checks for the Backfill Desk scaffold. //! 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::Some(value) => value,
std::option::Option::None => return, std::option::Option::None => return,
}; };
assert_eq!(resources.len(), 14); assert_eq!(resources.len(), 15);
for required in [ for required in [
"../../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-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",

View File

@@ -54,18 +54,19 @@
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json", "../../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-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/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/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.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.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.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.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"
} }
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs // file: crates/ksp-app-config-desk/tests/desktop_contract.rs
// version: 12 // version: 13
//! Desktop build/shell contract audits for Config Desk. //! 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); let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some(), "packaged Config resources map must exist"); assert!(resources.is_some(), "packaged Config resources map must exist");
if let std::option::Option::Some(resources) = resources { if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 14); assert_eq!(resources.len(), 15);
assert_eq!( assert_eq!(
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str), 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"), 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), 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"), 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.logging.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.logging.json"));
assert_eq!( assert_eq!(
resources.get("../../config/std.offchain_transport.json").and_then(serde_json::Value::as_str), resources.get("../../config/std.offchain_transport.json").and_then(serde_json::Value::as_str),

View File

@@ -54,18 +54,19 @@
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json", "../../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-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/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/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.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.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.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.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"
} }
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs // 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`. //! 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] #[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 root = app_root();
let tauri = read_json(root.join("tauri.conf.json").as_path()); let tauri = read_json(root.join("tauri.conf.json").as_path());
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object); let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some()); assert!(resources.is_some());
if let std::option::Option::Some(resources) = resources { if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 14); assert_eq!(resources.len(), 15);
assert_eq!( assert_eq!(
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str), 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"), 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), 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"), 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/std.offchain_transport.json"));
assert!(resources.contains_key("../../config/schemas/std.offchain_transport.schema.json")); assert!(resources.contains_key("../../config/schemas/std.offchain_transport.schema.json"));
assert!(resources.contains_key("../../config/std.store.json")); assert!(resources.contains_key("../../config/std.store.json"));

View 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

View File

@@ -0,0 +1,106 @@
<!-- file: crates/ksp-app-store-desk/README.md -->
<!-- version: 1 -->
# `ksp-app-store-desk`
`ksp-app-store-desk` est lapplication desktop spécialisée dinspection 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 dinspection, et le backend physique reste entièrement masqué à lapplication 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
```
Lapplication ne dépend pas directement de `ksp-store-api`, `ksp-store-postgres-lib`, `tokio-postgres` ou dun 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 dinspection random-access de `ksp-store-lib` : `offset + limit` avec counts exacts. DataTables est lunique propriétaire de la pagination visible et les tailles admises sont `25`, `50` et `100`.
Cette voie dinspection 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 dacquisition 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 dun 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 noffre aucune action darchive, 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 najoute aucune capability Tauri : elle utilise le WebView et le tracing ne transporte que lidentifiant 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 nest ouvert par lapplication.
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 nest 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 dexcé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).

View 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 dinspection 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, lorsquun 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 darchive, purge ou force-rehydrate nest proposée par lapplication.
## 4. RAW Accounts
La vue **RAW Accounts** utilise le même modèle dinspection 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 lidentité `(pubkey, slot, state_hash)` puis affiche un preview hexadécimal de data limité à 512 bytes, la longueur exacte et lindicateur de troncature.
## 5. Observations
Les modals Transaction et Account contiennent une table **Observations** associée à lentité actuellement ouverte.
La table est server-side et expose uniquement de la provenance sûre : provider logique, protocole, méthode/origine dacquisition, timestamps, endpoint/session/filter/commitment lorsquils 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 loffset 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 dinspection est distincte de la pagination cursor/keyset utilisée par les consumers machine du Store. Store Desk naccepte 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 dinspection. 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 derreur 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 neffectue 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.

View 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()
}

View 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"
]
}

View 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`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

View 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 dacquisition</h3>
<p class="small text-body-secondary mb-0">Provenance sûre uniquement ; aucun payload source nest 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 dacquisition</h3>
<p class="small text-body-secondary mb-0">Provenance sûre et métadonnées Yellowstone optionnelles ; aucun byte Account nest 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>&copy; 2026 SASEDEV</small>
</div>
</footer>
<script type="module" src="ts/main.ts" defer></script>
</body>
</html>

View 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;
}

View 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;
}
}
}

View 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;

View 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;
}

View 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;

View 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";

View 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);
}
}

View 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>

View 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);
}

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View 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"
}
}

View 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>,
}

View 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),
});
}

View 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";

View 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;

View 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;

View 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;

View 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;

View 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");

View 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;

View 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;

View 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);
}

View 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(());
}
}

View 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;

View 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,
};
}

View 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)),
};
}

View 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(());
}

View 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),
),
};
}

View 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"
}
}
}

View 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}");
}
}

View 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"));
}

View 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>()"));
}

View 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;
}

View 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"));
}

View 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"
]
}

View 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}");
}
}

View File

@@ -0,0 +1,40 @@
// file: crates/ksp-app-store-desk/unit_tests/dto_common.rs
// version: 2
#[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-beta".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}");
}
}
}

View 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}");
}
}

View 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}");
}
}

View 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);
}

View 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());
}

View 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/**"],
},
},
});

View File

@@ -8,6 +8,7 @@
], ],
"permissions": [ "permissions": [
"core:default", "core:default",
"tracing:default" "tracing:default",
"dialog:default"
] ]
} }

View File

@@ -11,6 +11,7 @@
"@fltsci/tauri-plugin-tracing": "^0.3", "@fltsci/tauri-plugin-tracing": "^0.3",
"@fortawesome/fontawesome-free": "^7.3", "@fortawesome/fontawesome-free": "^7.3",
"@tauri-apps/api": "^2.11", "@tauri-apps/api": "^2.11",
"@tauri-apps/plugin-dialog": "^2.7",
"bootstrap": "^5.3", "bootstrap": "^5.3",
"datatables.net-bs5": "^3.0", "datatables.net-bs5": "^3.0",
"datatables.net-select-bs5": "^4.0", "datatables.net-select-bs5": "^4.0",

View File

@@ -54,18 +54,19 @@
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json", "../../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-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/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/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.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.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.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.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"
} }
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs // 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. //! 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); 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"); assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
if let std::option::Option::Some(resources) = resources { if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 14); assert_eq!(resources.len(), 15);
assert_eq!( assert_eq!(
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str), 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"), 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), 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"), 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!( assert_eq!(
resources.get("../../config/composite.ksp-app-wallet-desk.json").and_then(serde_json::Value::as_str), 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"), std::option::Option::Some("config/composite.ksp-app-wallet-desk.json"),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs // file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
// version: 11 // version: 12
//! Release-wide deterministic compliance canaries for Wallet Desk. //! 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::Some(value) => value,
std::option::Option::None => return, std::option::Option::None => return,
}; };
assert_eq!(resources.len(), 14); assert_eq!(resources.len(), 15);
for (source, destination) in resources { for (source, destination) in resources {
let destination = destination.as_str(); let destination = destination.as_str();
assert!(destination.is_some(), "resource destination must be textual"); assert!(destination.is_some(), "resource destination must be textual");

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md --> <!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 11 --> <!-- version: 12 -->
# ksp-config-lib # 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 : Le registre par défaut connaît :
```text ```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-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.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
cfg.std.logging -> config/std.logging.json cfg.std.logging -> config/std.logging.json
cfg.std.offchain_transport -> config/std.offchain_transport.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é. `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 lexemple 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 lexemple 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 : 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.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.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/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-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 ; - [`../../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. - [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs // file: crates/ksp-config-lib/src/lib.rs
// version: 22 // version: 23
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -155,6 +155,8 @@ pub use self::registry::ConfigFileRegistry;
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME; pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME;
/// Default physical filename for the SOL Prices Desk composite configuration document. /// Default physical filename for the SOL Prices Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME; 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. /// Default physical filename for the Wallet Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME; pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME;
/// Default physical filename for the generic composite JSON Schema document. /// 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; pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK;
/// Logical file identifier for the SOL Prices Desk composite configuration document. /// Logical file identifier for the SOL Prices Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK; 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. /// Logical file identifier for the Wallet Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK; pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
/// Logical file identifier for the generic composite JSON Schema document. /// Logical file identifier for the generic composite JSON Schema document.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/registry.rs // file: crates/ksp-config-lib/src/registry.rs
// version: 14 // version: 15
/// Bootstrap argument used to replace a known Config filename mapping. /// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap"; 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"; 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. /// 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"; 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. /// 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"; 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. /// 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"; 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. /// 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"; 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. /// 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"; 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. /// 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::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), 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( let wallet_composite = ConfigFileDescriptor::new(
FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK, FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK,
ConfigFileKind::Config, ConfigFileKind::Config,
@@ -287,6 +301,7 @@ impl ConfigFileRegistry {
return crate::build_registry([ return crate::build_registry([
backfill_composite, backfill_composite,
solprices_composite, solprices_composite,
store_desk_composite,
wallet_composite, wallet_composite,
composite_schema, composite_schema,
logging, logging,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs // 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, //! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport/Store adapters and management contracts. //! 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:?}"); assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect(); 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[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[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[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_STD_LOGGING); 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_OFFCHAIN_TRANSPORT); 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_STORE); 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_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_WALLET); 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_SCHEMA_COMPOSITE); 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_STD_LOGGING); 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_OFFCHAIN_TRANSPORT); 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_STORE); 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_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_WALLET); assert_eq!(descriptors[13].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
let schema_file_id = descriptors[7].schema_file_id(); 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"); 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 { 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); 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::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::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::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); assert_ne!(ksp_config_lib::ConfigProfileSelectionSource::Composite, ksp_config_lib::ConfigProfileSelectionSource::Explicit);
} }

View 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": "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"
}
]
}
]
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs // file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 12 // version: 13
#[test] #[test]
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() { 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:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect(); 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].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].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[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[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[2].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_LOGGING); assert_eq!(descriptors[2].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_STORE_DESK_FILENAME));
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT); assert_eq!(descriptors[2].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_STORE); assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT); assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_STD_WALLET); assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[7].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME)); assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_STORE);
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[7].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE); assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_STD_WALLET);
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(descriptors[8].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT); 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[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE); assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT); assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[13].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET); assert_eq!(descriptors[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
assert!(descriptors[0..8].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config)); assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE);
assert!(descriptors[8..14].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema)); 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] #[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 registry = crate::ConfigFileRegistry::defaults();
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_COMPOSITE); 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 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 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); 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!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}"); 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!(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!(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:?}"); assert!(wallet_id.is_ok(), "Wallet Desk composite runtime file_id should be valid: {wallet_id:?}");
if let ( if let (
std::result::Result::Ok(registry), std::result::Result::Ok(registry),
std::result::Result::Ok(schema_id), std::result::Result::Ok(schema_id),
std::result::Result::Ok(backfill_id), std::result::Result::Ok(backfill_id),
std::result::Result::Ok(solprices_id), std::result::Result::Ok(solprices_id),
std::result::Result::Ok(store_desk_id),
std::result::Result::Ok(wallet_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 schema = registry.descriptor(&schema_id);
let backfill = registry.descriptor(&backfill_id); let backfill = registry.descriptor(&backfill_id);
let solprices = registry.descriptor(&solprices_id); let solprices = registry.descriptor(&solprices_id);
let store_desk = registry.descriptor(&store_desk_id);
let wallet = registry.descriptor(&wallet_id); let wallet = registry.descriptor(&wallet_id);
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}"); assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
assert!(backfill.is_ok(), "Backfill Desk runtime composite should be registered: {backfill:?}"); 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!(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:?}"); 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)) = if let (
(schema, backfill, solprices, wallet) 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.kind(), crate::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME)); 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.kind(), crate::ConfigFileKind::Config);
assert_eq!(solprices.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME)); 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!(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.kind(), crate::ConfigFileKind::Config);
assert_eq!(wallet.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME)); 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)); assert_eq!(wallet.schema_file_id(), std::option::Option::Some(&schema_id));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/capability/raw_account.rs // file: crates/ksp-store-api/src/capability/raw_account.rs
// version: 2 // version: 4
/// Read capability for complete canonical RAW account states. /// Read capability for complete canonical RAW account states.
/// ///
@@ -24,6 +24,20 @@ pub trait RawAccountStateRead: std::marker::Send + std::marker::Sync {
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawPage<crate::RawAccountStateReference>>>; ) -> crate::StoreApiFuture<'a, crate::Result<crate::RawPage<crate::RawAccountStateReference>>>;
} }
/// Read capability for data-free random-access RAW account-state inspection.
///
/// This capability is intended for bounded operator/admin inspection. It must
/// return exact logical counts and summaries only; complete account bytes stay
/// behind [`RawAccountStateRead::get_raw_account_state`]. Worker, replay and
/// backfill traversal continue to use the opaque-cursor list operation.
pub trait RawAccountStateInspectionRead: std::marker::Send + std::marker::Sync {
/// Inspects one random-access account-state window with exact logical counts.
fn inspect_raw_account_states<'a>(
&'a self,
query: &'a crate::RawAccountStateInspectionQuery,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawInspectionPage<crate::RawAccountStateSummary>>>;
}
/// Write capability for complete RAW account-state acquisitions. /// Write capability for complete RAW account-state acquisitions.
/// ///
/// The account state and its acquisition observation form one logical /// The account state and its acquisition observation form one logical
@@ -48,6 +62,15 @@ pub trait RawAccountObservationRead: std::marker::Send + std::marker::Sync {
) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawAccountObservation>>>; ) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawAccountObservation>>>;
} }
/// Read capability for random-access RAW account-observation inspection.
pub trait RawAccountObservationInspectionRead: std::marker::Send + std::marker::Sync {
/// Inspects one account-observation window with exact logical counts.
fn inspect_raw_account_observations<'a>(
&'a self,
query: &'a crate::RawAccountObservationInspectionQuery,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawInspectionPage<crate::RawAccountObservationSummary>>>;
}
/// Write capability for an additional observation of an already persisted RAW account state. /// Write capability for an additional observation of an already persisted RAW account state.
/// ///
/// This capability allows repeated HTTP/WS/gRPC acquisitions to be retained /// This capability allows repeated HTTP/WS/gRPC acquisitions to be retained

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/capability/raw_transaction.rs // file: crates/ksp-store-api/src/capability/raw_transaction.rs
// version: 2 // version: 4
/// Read capability for canonical RAW transactions. /// Read capability for canonical RAW transactions.
/// ///
@@ -24,6 +24,20 @@ pub trait RawTransactionRead: std::marker::Send + std::marker::Sync {
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawPage<crate::RawTransactionReference>>>; ) -> crate::StoreApiFuture<'a, crate::Result<crate::RawPage<crate::RawTransactionReference>>>;
} }
/// Read capability for payload-free random-access RAW transaction inspection.
///
/// This capability is intended for bounded operator/admin inspection. It must
/// return exact logical counts and summaries only; canonical payload bytes stay
/// behind [`RawTransactionRead::get_raw_transaction`]. Worker, replay and
/// backfill traversal continue to use the opaque-cursor list operation.
pub trait RawTransactionInspectionRead: std::marker::Send + std::marker::Sync {
/// Inspects one random-access transaction window with exact logical counts.
fn inspect_raw_transactions<'a>(
&'a self,
query: &'a crate::RawTransactionInspectionQuery,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawInspectionPage<crate::RawTransactionSummary>>>;
}
/// Write capability for canonical RAW transaction acquisitions. /// Write capability for canonical RAW transaction acquisitions.
/// ///
/// The transaction and its acquisition observation form one logical persistence /// The transaction and its acquisition observation form one logical persistence
@@ -49,6 +63,15 @@ pub trait RawTransactionObservationRead: std::marker::Send + std::marker::Sync {
) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawTransactionObservation>>>; ) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawTransactionObservation>>>;
} }
/// Read capability for random-access RAW transaction-observation inspection.
pub trait RawTransactionObservationInspectionRead: std::marker::Send + std::marker::Sync {
/// Inspects one transaction-observation window with exact logical counts.
fn inspect_raw_transaction_observations<'a>(
&'a self,
query: &'a crate::RawTransactionObservationInspectionQuery,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawInspectionPage<crate::RawTransactionObservationSummary>>>;
}
/// Write capability for an additional observation of an already persisted RAW transaction. /// Write capability for an additional observation of an already persisted RAW transaction.
/// ///
/// This capability exists so repeated acquisitions can be recorded without /// This capability exists so repeated acquisitions can be recorded without

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/lib.rs // file: crates/ksp-store-api/src/lib.rs
// version: 5 // version: 7
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -22,10 +22,14 @@ mod model;
/// Boxed async operation returned by object-safe Store capability contracts. /// Boxed async operation returned by object-safe Store capability contracts.
pub use self::capability::StoreApiFuture; pub use self::capability::StoreApiFuture;
/// Read capability for random-access RAW account-observation inspection.
pub use self::capability::raw_account::RawAccountObservationInspectionRead;
/// Read capability for persisted RAW account-state observations. /// Read capability for persisted RAW account-state observations.
pub use self::capability::raw_account::RawAccountObservationRead; pub use self::capability::raw_account::RawAccountObservationRead;
/// Write capability for additional observations of already persisted RAW account states. /// Write capability for additional observations of already persisted RAW account states.
pub use self::capability::raw_account::RawAccountObservationWrite; pub use self::capability::raw_account::RawAccountObservationWrite;
/// Read capability for data-free random-access RAW account-state inspection.
pub use self::capability::raw_account::RawAccountStateInspectionRead;
/// Read capability for complete canonical RAW account states. /// Read capability for complete canonical RAW account states.
pub use self::capability::raw_account::RawAccountStateRead; pub use self::capability::raw_account::RawAccountStateRead;
/// Write capability for complete canonical RAW account-state acquisitions. /// Write capability for complete canonical RAW account-state acquisitions.
@@ -34,6 +38,10 @@ pub use self::capability::raw_account::RawAccountStateWrite;
pub use self::capability::raw_retention::RawTransactionRetentionRead; pub use self::capability::raw_retention::RawTransactionRetentionRead;
/// Write capability for policy-authorized RAW transaction retention transitions. /// Write capability for policy-authorized RAW transaction retention transitions.
pub use self::capability::raw_retention::RawTransactionRetentionWrite; pub use self::capability::raw_retention::RawTransactionRetentionWrite;
/// Read capability for payload-free random-access RAW transaction inspection.
pub use self::capability::raw_transaction::RawTransactionInspectionRead;
/// Read capability for random-access RAW transaction-observation inspection.
pub use self::capability::raw_transaction::RawTransactionObservationInspectionRead;
/// Read capability for persisted RAW transaction observations. /// Read capability for persisted RAW transaction observations.
pub use self::capability::raw_transaction::RawTransactionObservationRead; pub use self::capability::raw_transaction::RawTransactionObservationRead;
/// Write capability for additional observations of already persisted RAW transactions. /// Write capability for additional observations of already persisted RAW transactions.
@@ -60,6 +68,26 @@ pub use self::model::raw_account::RawAccountObservation;
pub use self::model::raw_account::RawAccountState; pub use self::model::raw_account::RawAccountState;
/// Durable backend-independent identity of one canonical RAW account state. /// Durable backend-independent identity of one canonical RAW account state.
pub use self::model::raw_account::RawAccountStateReference; pub use self::model::raw_account::RawAccountStateReference;
/// Backend-independent random-access inspection query for RAW account observations.
pub use self::model::raw_inspection::RawAccountObservationInspectionQuery;
/// Safe observation summary for one canonical RAW account-state acquisition.
pub use self::model::raw_inspection::RawAccountObservationSummary;
/// Backend-independent random-access inspection query for RAW account states.
pub use self::model::raw_inspection::RawAccountStateInspectionQuery;
/// Data-free summary of one canonical RAW account state for operator inspection.
pub use self::model::raw_inspection::RawAccountStateSummary;
/// One bounded random-access inspection page with exact logical counts.
pub use self::model::raw_inspection::RawInspectionPage;
/// Random-access page request dedicated to bounded interactive RAW inspection.
pub use self::model::raw_inspection::RawInspectionPageRequest;
/// Backend-independent random-access inspection query for RAW transactions.
pub use self::model::raw_inspection::RawTransactionInspectionQuery;
/// Backend-independent random-access inspection query for RAW transaction observations.
pub use self::model::raw_inspection::RawTransactionObservationInspectionQuery;
/// Safe observation summary for one canonical RAW transaction acquisition.
pub use self::model::raw_inspection::RawTransactionObservationSummary;
/// Payload-free summary of one canonical RAW transaction for operator inspection.
pub use self::model::raw_inspection::RawTransactionSummary;
/// Combined outcome of one atomic canonical RAW entity plus observation acquisition. /// Combined outcome of one atomic canonical RAW entity plus observation acquisition.
pub use self::model::raw_outcome::RawAcquisitionWriteOutcome; pub use self::model::raw_outcome::RawAcquisitionWriteOutcome;
/// Outcome for one canonical RAW entity in an idempotent persistence operation. /// Outcome for one canonical RAW entity in an idempotent persistence operation.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/model.rs // file: crates/ksp-store-api/src/model.rs
// version: 4 // version: 5
//! Private home for persistent Store models. //! Private home for persistent Store models.
//! //!
@@ -9,6 +9,7 @@
//! one or more models. //! one or more models.
pub(crate) mod raw_account; pub(crate) mod raw_account;
pub(crate) mod raw_inspection;
pub(crate) mod raw_outcome; pub(crate) mod raw_outcome;
pub(crate) mod raw_pagination; pub(crate) mod raw_pagination;
pub(crate) mod raw_primitives; pub(crate) mod raw_primitives;

View File

@@ -0,0 +1,562 @@
// file: crates/ksp-store-api/src/model/raw_inspection.rs
// version: 3
/// Random-access page request dedicated to bounded interactive RAW inspection.
///
/// This request is intentionally distinct from [`crate::RawPageRequest`]. It
/// provides offset/limit semantics for operator-facing inspection while the
/// canonical RAW list APIs keep their opaque backend cursors for replay,
/// backfill and worker traversal.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct RawInspectionPageRequest {
limit: crate::RawPageLimit,
offset: u64,
}
impl RawInspectionPageRequest {
/// Creates one inspection request from an absolute zero-based offset and positive page limit.
#[must_use]
pub const fn new(offset: u64, limit: crate::RawPageLimit) -> Self {
return Self { limit, offset };
}
/// Returns the exact caller-requested page size.
#[must_use]
pub const fn limit(&self) -> crate::RawPageLimit {
return self.limit;
}
/// Returns the absolute zero-based item offset within the filtered result set.
#[must_use]
pub const fn offset(&self) -> u64 {
return self.offset;
}
}
/// One bounded random-access inspection page with exact logical counts.
///
/// `total_items` is the exact count inside the mandatory query scope before
/// optional filters. `filtered_items` is the exact count after optional query
/// filters. The item vector contains only the requested page window.
#[derive(Debug)]
pub struct RawInspectionPage<T> {
filtered_items: u64,
items: std::vec::Vec<T>,
total_items: u64,
}
impl<T> RawInspectionPage<T> {
/// Creates one inspection page after validating count consistency.
pub fn try_new(items: std::vec::Vec<T>, total_items: u64, filtered_items: u64) -> crate::Result<Self> {
if filtered_items > total_items {
return std::result::Result::Err(raw_model_error("filtered_items"));
}
let item_count = u64::try_from(items.len());
let item_count = match item_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(raw_model_error("items")),
};
if item_count > filtered_items {
return std::result::Result::Err(raw_model_error("items"));
}
return std::result::Result::Ok(Self { filtered_items, items, total_items });
}
/// Returns the exact count after optional query filters.
#[must_use]
pub const fn filtered_items(&self) -> u64 {
return self.filtered_items;
}
/// Returns the current inspection page items.
#[must_use]
pub fn items(&self) -> &[T] {
return self.items.as_slice();
}
/// Consumes the page and returns its bounded inspection items.
#[must_use]
pub fn into_items(self) -> std::vec::Vec<T> {
return self.items;
}
/// Returns the exact count before optional query filters inside the mandatory scope.
#[must_use]
pub const fn total_items(&self) -> u64 {
return self.total_items;
}
}
/// Backend-independent random-access inspection query for RAW transaction observations.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawTransactionObservationInspectionQuery {
direction: crate::RawSortDirection,
network: crate::RawNetworkId,
page: crate::RawInspectionPageRequest,
transaction: std::option::Option<crate::RawTransactionReference>,
}
impl RawTransactionObservationInspectionQuery {
/// Creates one transaction-observation inspection query after enforcing network coherence.
pub fn try_new(
network: crate::RawNetworkId,
transaction: std::option::Option<crate::RawTransactionReference>,
direction: crate::RawSortDirection,
page: crate::RawInspectionPageRequest,
) -> crate::Result<Self> {
if let std::option::Option::Some(reference) = transaction.as_ref()
&& reference.network() != &network
{
return std::result::Result::Err(raw_model_error("transaction"));
}
return std::result::Result::Ok(Self { direction, network, page, transaction });
}
/// Returns the requested deterministic traversal direction.
#[must_use]
pub const fn direction(&self) -> crate::RawSortDirection {
return self.direction;
}
/// Returns the mandatory logical network scope.
#[must_use]
pub fn network(&self) -> &crate::RawNetworkId {
return &self.network;
}
/// Returns the random-access inspection page request.
#[must_use]
pub const fn page(&self) -> crate::RawInspectionPageRequest {
return self.page;
}
/// Returns the optional exact transaction identity used to filter observations.
#[must_use]
pub fn transaction(&self) -> std::option::Option<&crate::RawTransactionReference> {
return self.transaction.as_ref();
}
}
/// Backend-independent random-access inspection query for RAW account observations.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawAccountObservationInspectionQuery {
account: std::option::Option<crate::RawAccountStateReference>,
direction: crate::RawSortDirection,
network: crate::RawNetworkId,
page: crate::RawInspectionPageRequest,
}
impl RawAccountObservationInspectionQuery {
/// Creates one account-observation inspection query after enforcing network coherence.
pub fn try_new(
network: crate::RawNetworkId,
account: std::option::Option<crate::RawAccountStateReference>,
direction: crate::RawSortDirection,
page: crate::RawInspectionPageRequest,
) -> crate::Result<Self> {
if let std::option::Option::Some(reference) = account.as_ref()
&& reference.network() != &network
{
return std::result::Result::Err(raw_model_error("account"));
}
return std::result::Result::Ok(Self { account, direction, network, page });
}
/// Returns the optional exact account-state identity used to filter observations.
#[must_use]
pub fn account(&self) -> std::option::Option<&crate::RawAccountStateReference> {
return self.account.as_ref();
}
/// Returns the requested deterministic traversal direction.
#[must_use]
pub const fn direction(&self) -> crate::RawSortDirection {
return self.direction;
}
/// Returns the mandatory logical network scope.
#[must_use]
pub fn network(&self) -> &crate::RawNetworkId {
return &self.network;
}
/// Returns the random-access inspection page request.
#[must_use]
pub const fn page(&self) -> crate::RawInspectionPageRequest {
return self.page;
}
}
/// Safe observation summary for one canonical RAW transaction acquisition.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawTransactionObservationSummary {
observation_key: crate::RawObservationKey,
provenance: crate::RawAcquisitionProvenance,
transaction: crate::RawTransactionReference,
}
impl RawTransactionObservationSummary {
/// Creates one safe transaction-observation summary without RAW payload bytes.
#[must_use]
pub fn new(observation_key: crate::RawObservationKey, transaction: crate::RawTransactionReference, provenance: crate::RawAcquisitionProvenance) -> Self {
return Self { observation_key, provenance, transaction };
}
/// Returns the producer-owned deterministic observation key.
#[must_use]
pub const fn observation_key(&self) -> crate::RawObservationKey {
return self.observation_key;
}
/// Returns safe source-independent acquisition provenance.
#[must_use]
pub fn provenance(&self) -> &crate::RawAcquisitionProvenance {
return &self.provenance;
}
/// Returns the durable transaction identity observed by this acquisition.
#[must_use]
pub fn transaction(&self) -> &crate::RawTransactionReference {
return &self.transaction;
}
}
/// Safe observation summary for one canonical RAW account-state acquisition.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawAccountObservationSummary {
account: crate::RawAccountStateReference,
is_startup: std::option::Option<bool>,
observation_key: crate::RawObservationKey,
provenance: crate::RawAcquisitionProvenance,
transaction_signature: std::option::Option<crate::RawTransactionSignature>,
write_version: std::option::Option<u64>,
}
impl RawAccountObservationSummary {
/// Creates one safe account-observation summary without account data bytes.
#[must_use]
pub fn new(
observation_key: crate::RawObservationKey,
account: crate::RawAccountStateReference,
provenance: crate::RawAcquisitionProvenance,
is_startup: std::option::Option<bool>,
transaction_signature: std::option::Option<crate::RawTransactionSignature>,
write_version: std::option::Option<u64>,
) -> Self {
return Self { account, is_startup, observation_key, provenance, transaction_signature, write_version };
}
/// Returns the durable account-state identity observed by this acquisition.
#[must_use]
pub fn account(&self) -> &crate::RawAccountStateReference {
return &self.account;
}
/// Returns the optional source-reported startup/replay marker.
#[must_use]
pub const fn is_startup(&self) -> std::option::Option<bool> {
return self.is_startup;
}
/// Returns the producer-owned deterministic observation key.
#[must_use]
pub const fn observation_key(&self) -> crate::RawObservationKey {
return self.observation_key;
}
/// Returns safe source-independent acquisition provenance.
#[must_use]
pub fn provenance(&self) -> &crate::RawAcquisitionProvenance {
return &self.provenance;
}
/// Returns the optional transaction signature associated with the observed account write.
#[must_use]
pub const fn transaction_signature(&self) -> std::option::Option<crate::RawTransactionSignature> {
return self.transaction_signature;
}
/// Returns the optional source-specific account write version.
#[must_use]
pub const fn write_version(&self) -> std::option::Option<u64> {
return self.write_version;
}
}
/// Backend-independent random-access inspection query for RAW transactions.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawTransactionInspectionQuery {
direction: crate::RawSortDirection,
network: crate::RawNetworkId,
page: crate::RawInspectionPageRequest,
slots: crate::RawSlotRange,
}
impl RawTransactionInspectionQuery {
/// Creates one transaction inspection query.
#[must_use]
pub fn new(network: crate::RawNetworkId, slots: crate::RawSlotRange, direction: crate::RawSortDirection, page: crate::RawInspectionPageRequest) -> Self {
return Self { direction, network, page, slots };
}
/// Returns the requested deterministic traversal direction.
#[must_use]
pub const fn direction(&self) -> crate::RawSortDirection {
return self.direction;
}
/// Returns the mandatory logical network scope.
#[must_use]
pub fn network(&self) -> &crate::RawNetworkId {
return &self.network;
}
/// Returns the random-access inspection page request.
#[must_use]
pub const fn page(&self) -> crate::RawInspectionPageRequest {
return self.page;
}
/// Returns optional inclusive slot filters.
#[must_use]
pub const fn slots(&self) -> crate::RawSlotRange {
return self.slots;
}
}
/// Backend-independent random-access inspection query for RAW account states.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawAccountStateInspectionQuery {
direction: crate::RawSortDirection,
network: crate::RawNetworkId,
page: crate::RawInspectionPageRequest,
pubkey: std::option::Option<crate::Pubkey>,
slots: crate::RawSlotRange,
}
impl RawAccountStateInspectionQuery {
/// Creates one account-state inspection query.
#[must_use]
pub fn new(
network: crate::RawNetworkId,
pubkey: std::option::Option<crate::Pubkey>,
slots: crate::RawSlotRange,
direction: crate::RawSortDirection,
page: crate::RawInspectionPageRequest,
) -> Self {
return Self { direction, network, page, pubkey, slots };
}
/// Returns the requested deterministic traversal direction.
#[must_use]
pub const fn direction(&self) -> crate::RawSortDirection {
return self.direction;
}
/// Returns the mandatory logical network scope.
#[must_use]
pub fn network(&self) -> &crate::RawNetworkId {
return &self.network;
}
/// Returns the random-access inspection page request.
#[must_use]
pub const fn page(&self) -> crate::RawInspectionPageRequest {
return self.page;
}
/// Returns an optional exact account-address filter.
#[must_use]
pub fn pubkey(&self) -> std::option::Option<&crate::Pubkey> {
return self.pubkey.as_ref();
}
/// Returns optional inclusive slot filters.
#[must_use]
pub const fn slots(&self) -> crate::RawSlotRange {
return self.slots;
}
}
/// Payload-free summary of one canonical RAW transaction for operator inspection.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawTransactionSummary {
block_time: std::option::Option<crate::RawTimestamp>,
content_hash: crate::RawContentHash,
format_id: crate::RawFormatId,
format_version: u32,
payload_size_bytes: std::option::Option<u64>,
reference: crate::RawTransactionReference,
retention_state: crate::RawRetentionState,
slot: u64,
}
impl RawTransactionSummary {
/// Creates one payload-free transaction summary after validating summary invariants.
pub fn try_new(
reference: crate::RawTransactionReference,
slot: u64,
block_time: std::option::Option<crate::RawTimestamp>,
format_id: crate::RawFormatId,
format_version: u32,
content_hash: crate::RawContentHash,
payload_size_bytes: std::option::Option<u64>,
retention_state: crate::RawRetentionState,
) -> crate::Result<Self> {
if format_version == 0 {
return std::result::Result::Err(raw_model_error("format_version"));
}
if let std::option::Option::Some(size) = payload_size_bytes {
let size = usize::try_from(size);
let size = match size {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(raw_model_error("payload_size_bytes")),
};
if size == 0 || size > crate::MAX_RAW_PAYLOAD_BYTES {
return std::result::Result::Err(raw_model_error("payload_size_bytes"));
}
}
if retention_state == crate::RawRetentionState::Purged {
if payload_size_bytes.is_some() {
return std::result::Result::Err(raw_model_error("payload_size_bytes"));
}
} else if payload_size_bytes.is_none() {
return std::result::Result::Err(raw_model_error("payload_size_bytes"));
}
return std::result::Result::Ok(Self {
block_time,
content_hash,
format_id,
format_version,
payload_size_bytes,
reference,
retention_state,
slot,
});
}
/// Returns the optional canonical block timestamp.
#[must_use]
pub const fn block_time(&self) -> std::option::Option<crate::RawTimestamp> {
return self.block_time;
}
/// Returns the deterministic canonical content digest.
#[must_use]
pub const fn content_hash(&self) -> crate::RawContentHash {
return self.content_hash;
}
/// Returns the KSP-owned canonical RAW format identifier.
#[must_use]
pub fn format_id(&self) -> &crate::RawFormatId {
return &self.format_id;
}
/// Returns the KSP-owned canonical RAW format version.
#[must_use]
pub const fn format_version(&self) -> u32 {
return self.format_version;
}
/// Returns the canonical payload size when payload content is logically retained.
#[must_use]
pub const fn payload_size_bytes(&self) -> std::option::Option<u64> {
return self.payload_size_bytes;
}
/// Returns the durable backend-independent transaction identity.
#[must_use]
pub fn reference(&self) -> &crate::RawTransactionReference {
return &self.reference;
}
/// Returns the logical RAW transaction retention state.
#[must_use]
pub const fn retention_state(&self) -> crate::RawRetentionState {
return self.retention_state;
}
/// Returns the Solana slot containing the transaction.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
}
/// Data-free summary of one canonical RAW account state for operator inspection.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawAccountStateSummary {
data_length_bytes: u64,
executable: bool,
lamports: u64,
owner: crate::Pubkey,
reference: crate::RawAccountStateReference,
rent_epoch: u64,
}
impl RawAccountStateSummary {
/// Creates one data-free account-state summary after validating its bounded data length.
pub fn try_new(
reference: crate::RawAccountStateReference,
lamports: u64,
owner: crate::Pubkey,
executable: bool,
rent_epoch: u64,
data_length_bytes: u64,
) -> crate::Result<Self> {
let data_length = usize::try_from(data_length_bytes);
let data_length = match data_length {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(raw_model_error("data_length_bytes")),
};
if data_length > crate::MAX_RAW_ACCOUNT_DATA_BYTES {
return std::result::Result::Err(raw_model_error("data_length_bytes"));
}
return std::result::Result::Ok(Self { data_length_bytes, executable, lamports, owner, reference, rent_epoch });
}
/// Returns the complete canonical account-data length without exposing account bytes.
#[must_use]
pub const fn data_length_bytes(&self) -> u64 {
return self.data_length_bytes;
}
/// Returns whether the account is executable.
#[must_use]
pub const fn executable(&self) -> bool {
return self.executable;
}
/// Returns the account lamport balance.
#[must_use]
pub const fn lamports(&self) -> u64 {
return self.lamports;
}
/// Returns the account owner program public key.
#[must_use]
pub const fn owner(&self) -> &crate::Pubkey {
return &self.owner;
}
/// Returns the durable backend-independent account-state identity.
#[must_use]
pub fn reference(&self) -> &crate::RawAccountStateReference {
return &self.reference;
}
/// Returns the rent epoch reported for this account state.
#[must_use]
pub const fn rent_epoch(&self) -> u64 {
return self.rent_epoch;
}
}
fn raw_model_error(field: &'static str) -> crate::Error {
return crate::Error::new(crate::ERROR_CODE_RAW_MODEL_INVALID, "invalid backend-agnostic RAW inspection model").with_context("field", field);
}
#[cfg(test)]
#[path = "../../unit_tests/model/raw_inspection.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/tests/dependency_boundary.rs // file: crates/ksp-store-api/tests/dependency_boundary.rs
// version: 5 // version: 7
//! Dependency canaries for the Store API RAW foundation. //! Dependency canaries for the Store API RAW foundation.
@@ -53,6 +53,7 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() {
let crate_root = include_str!("../src/lib.rs"); let crate_root = include_str!("../src/lib.rs");
let model_home = include_str!("../src/model.rs"); let model_home = include_str!("../src/model.rs");
let raw_account = include_str!("../src/model/raw_account.rs"); let raw_account = include_str!("../src/model/raw_account.rs");
let raw_inspection = include_str!("../src/model/raw_inspection.rs");
let raw_outcome = include_str!("../src/model/raw_outcome.rs"); let raw_outcome = include_str!("../src/model/raw_outcome.rs");
let raw_pagination = include_str!("../src/model/raw_pagination.rs"); let raw_pagination = include_str!("../src/model/raw_pagination.rs");
let raw_primitives = include_str!("../src/model/raw_primitives.rs"); let raw_primitives = include_str!("../src/model/raw_primitives.rs");
@@ -66,6 +67,7 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() {
assert!(crate_root.contains("mod error;")); assert!(crate_root.contains("mod error;"));
assert!(crate_root.contains("mod model;")); assert!(crate_root.contains("mod model;"));
assert!(model_home.contains("raw_account")); assert!(model_home.contains("raw_account"));
assert!(model_home.contains("raw_inspection"));
assert!(model_home.contains("raw_outcome")); assert!(model_home.contains("raw_outcome"));
assert!(model_home.contains("raw_pagination")); assert!(model_home.contains("raw_pagination"));
assert!(model_home.contains("raw_primitives")); assert!(model_home.contains("raw_primitives"));
@@ -78,6 +80,7 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() {
crate_root, crate_root,
model_home, model_home,
raw_account, raw_account,
raw_inspection,
raw_outcome, raw_outcome,
raw_pagination, raw_pagination,
raw_primitives, raw_primitives,
@@ -109,12 +112,16 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() {
assert!(!crate_root.contains(forbidden), "deferred pre.006 model leaked into Store API surface: {forbidden}"); assert!(!crate_root.contains(forbidden), "deferred pre.006 model leaked into Store API surface: {forbidden}");
} }
assert!(raw_transaction_capability.contains("trait RawTransactionRead")); assert!(raw_transaction_capability.contains("trait RawTransactionRead"));
assert!(raw_transaction_capability.contains("trait RawTransactionInspectionRead"));
assert!(raw_transaction_capability.contains("trait RawTransactionObservationInspectionRead"));
assert!(raw_transaction_capability.contains("trait RawTransactionWrite")); assert!(raw_transaction_capability.contains("trait RawTransactionWrite"));
assert!(raw_transaction_capability.contains("trait RawTransactionObservationRead")); assert!(raw_transaction_capability.contains("trait RawTransactionObservationRead"));
assert!(raw_transaction_capability.contains("trait RawTransactionObservationWrite")); assert!(raw_transaction_capability.contains("trait RawTransactionObservationWrite"));
assert!(raw_retention_capability.contains("trait RawTransactionRetentionRead")); assert!(raw_retention_capability.contains("trait RawTransactionRetentionRead"));
assert!(raw_retention_capability.contains("trait RawTransactionRetentionWrite")); assert!(raw_retention_capability.contains("trait RawTransactionRetentionWrite"));
assert!(raw_account_capability.contains("trait RawAccountStateRead")); assert!(raw_account_capability.contains("trait RawAccountStateRead"));
assert!(raw_account_capability.contains("trait RawAccountStateInspectionRead"));
assert!(raw_account_capability.contains("trait RawAccountObservationInspectionRead"));
assert!(raw_account_capability.contains("trait RawAccountStateWrite")); assert!(raw_account_capability.contains("trait RawAccountStateWrite"));
assert!(raw_account_capability.contains("trait RawAccountObservationRead")); assert!(raw_account_capability.contains("trait RawAccountObservationRead"));
assert!(raw_account_capability.contains("trait RawAccountObservationWrite")); assert!(raw_account_capability.contains("trait RawAccountObservationWrite"));
@@ -126,6 +133,9 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() {
} }
assert!(!raw_pagination.contains("u64::MAX")); assert!(!raw_pagination.contains("u64::MAX"));
assert!(!raw_pagination.contains("MAX_RAW_PAGE_ITEMS")); assert!(!raw_pagination.contains("MAX_RAW_PAGE_ITEMS"));
for forbidden in ["DataTable", "recordsTotal", "recordsFiltered", "draw", "SQL", "OFFSET ", "LIMIT "] {
assert!(!raw_inspection.contains(forbidden), "UI/physical inspection concept leaked into Store API: {forbidden}");
}
return; return;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/tests/external_backend.rs // file: crates/ksp-store-api/tests/external_backend.rs
// version: 2 // version: 4
//! External-implementation canary for object-safe Store API capabilities. //! External-implementation canary for object-safe Store API capabilities.
@@ -27,6 +27,18 @@ impl ksp_store_api::RawTransactionRead for ExternalMemoryBackend {
} }
} }
impl ksp_store_api::RawTransactionInspectionRead for ExternalMemoryBackend {
fn inspect_raw_transactions<'a>(
&'a self,
query: &'a ksp_store_api::RawTransactionInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionSummary>>> {
let _ = query;
return std::boxed::Box::pin(async {
return ksp_store_api::RawInspectionPage::try_new(std::vec::Vec::new(), 0, 0);
});
}
}
impl ksp_store_api::RawTransactionWrite for ExternalMemoryBackend { impl ksp_store_api::RawTransactionWrite for ExternalMemoryBackend {
fn persist_raw_transaction_acquisition<'a>( fn persist_raw_transaction_acquisition<'a>(
&'a self, &'a self,
@@ -46,6 +58,18 @@ impl ksp_store_api::RawTransactionWrite for ExternalMemoryBackend {
} }
} }
impl ksp_store_api::RawTransactionObservationInspectionRead for ExternalMemoryBackend {
fn inspect_raw_transaction_observations<'a>(
&'a self,
query: &'a ksp_store_api::RawTransactionObservationInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionObservationSummary>>> {
let _ = query;
return std::boxed::Box::pin(async {
return ksp_store_api::RawInspectionPage::try_new(std::vec::Vec::new(), 0, 0);
});
}
}
impl ksp_store_api::RawTransactionObservationRead for ExternalMemoryBackend { impl ksp_store_api::RawTransactionObservationRead for ExternalMemoryBackend {
fn get_raw_transaction_observation<'a>( fn get_raw_transaction_observation<'a>(
&'a self, &'a self,
@@ -92,6 +116,18 @@ impl ksp_store_api::RawAccountStateRead for ExternalMemoryBackend {
} }
} }
impl ksp_store_api::RawAccountStateInspectionRead for ExternalMemoryBackend {
fn inspect_raw_account_states<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountStateInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountStateSummary>>> {
let _ = query;
return std::boxed::Box::pin(async {
return ksp_store_api::RawInspectionPage::try_new(std::vec::Vec::new(), 0, 0);
});
}
}
impl ksp_store_api::RawAccountStateWrite for ExternalMemoryBackend { impl ksp_store_api::RawAccountStateWrite for ExternalMemoryBackend {
fn persist_raw_account_acquisition<'a>( fn persist_raw_account_acquisition<'a>(
&'a self, &'a self,
@@ -109,6 +145,18 @@ impl ksp_store_api::RawAccountStateWrite for ExternalMemoryBackend {
} }
} }
impl ksp_store_api::RawAccountObservationInspectionRead for ExternalMemoryBackend {
fn inspect_raw_account_observations<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountObservationInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountObservationSummary>>> {
let _ = query;
return std::boxed::Box::pin(async {
return ksp_store_api::RawInspectionPage::try_new(std::vec::Vec::new(), 0, 0);
});
}
}
impl ksp_store_api::RawAccountObservationRead for ExternalMemoryBackend { impl ksp_store_api::RawAccountObservationRead for ExternalMemoryBackend {
fn get_raw_account_observation<'a>( fn get_raw_account_observation<'a>(
&'a self, &'a self,
@@ -168,24 +216,32 @@ impl ksp_store_api::RawTransactionRetentionWrite for ExternalMemoryBackend {
} }
#[test] #[test]
fn pre_006_external_backend_implements_each_capability_without_store_runtime_crate() { fn v0_3_8_pre_003_external_backend_implements_canonical_and_inspection_capabilities_without_runtime_crate() {
let backend = ExternalMemoryBackend; let backend = ExternalMemoryBackend;
let transaction_read: &dyn ksp_store_api::RawTransactionRead = &backend; let transaction_read: &dyn ksp_store_api::RawTransactionRead = &backend;
let transaction_write: &dyn ksp_store_api::RawTransactionWrite = &backend; let transaction_write: &dyn ksp_store_api::RawTransactionWrite = &backend;
let transaction_inspection: &dyn ksp_store_api::RawTransactionInspectionRead = &backend;
let transaction_observation_inspection: &dyn ksp_store_api::RawTransactionObservationInspectionRead = &backend;
let transaction_observation_read: &dyn ksp_store_api::RawTransactionObservationRead = &backend; let transaction_observation_read: &dyn ksp_store_api::RawTransactionObservationRead = &backend;
let transaction_observation_write: &dyn ksp_store_api::RawTransactionObservationWrite = &backend; let transaction_observation_write: &dyn ksp_store_api::RawTransactionObservationWrite = &backend;
let account_read: &dyn ksp_store_api::RawAccountStateRead = &backend; let account_read: &dyn ksp_store_api::RawAccountStateRead = &backend;
let account_write: &dyn ksp_store_api::RawAccountStateWrite = &backend; let account_write: &dyn ksp_store_api::RawAccountStateWrite = &backend;
let account_inspection: &dyn ksp_store_api::RawAccountStateInspectionRead = &backend;
let account_observation_inspection: &dyn ksp_store_api::RawAccountObservationInspectionRead = &backend;
let account_observation_read: &dyn ksp_store_api::RawAccountObservationRead = &backend; let account_observation_read: &dyn ksp_store_api::RawAccountObservationRead = &backend;
let account_observation_write: &dyn ksp_store_api::RawAccountObservationWrite = &backend; let account_observation_write: &dyn ksp_store_api::RawAccountObservationWrite = &backend;
let retention_read: &dyn ksp_store_api::RawTransactionRetentionRead = &backend; let retention_read: &dyn ksp_store_api::RawTransactionRetentionRead = &backend;
let retention_write: &dyn ksp_store_api::RawTransactionRetentionWrite = &backend; let retention_write: &dyn ksp_store_api::RawTransactionRetentionWrite = &backend;
let _ = transaction_read; let _ = transaction_read;
let _ = transaction_write; let _ = transaction_write;
let _ = transaction_inspection;
let _ = transaction_observation_inspection;
let _ = transaction_observation_read; let _ = transaction_observation_read;
let _ = transaction_observation_write; let _ = transaction_observation_write;
let _ = account_read; let _ = account_read;
let _ = account_write; let _ = account_write;
let _ = account_inspection;
let _ = account_observation_inspection;
let _ = account_observation_read; let _ = account_observation_read;
let _ = account_observation_write; let _ = account_observation_write;
let _ = retention_read; let _ = retention_read;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/tests/public_api.rs // file: crates/ksp-store-api/tests/public_api.rs
// version: 6 // version: 8
//! Integration canaries for the public `ksp-store-api` surface. //! Integration canaries for the public `ksp-store-api` surface.
@@ -186,3 +186,25 @@ fn public_pre_007_retention_race_outcome_is_available_from_crate_root() {
assert_ne!(ksp_store_api::RawRetentionWriteOutcome::AlreadyAtTarget, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch); assert_ne!(ksp_store_api::RawRetentionWriteOutcome::AlreadyAtTarget, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch);
return; return;
} }
#[test]
fn public_v0_3_8_pre_003_inspection_contracts_are_backend_neutral_and_dyn_compatible() {
let limit = match ksp_store_api::RawPageLimit::new(25) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let request = ksp_store_api::RawInspectionPageRequest::new(50, limit);
assert_eq!(request.offset(), 50);
assert_eq!(request.limit().get(), 25);
let page = ksp_store_api::RawInspectionPage::<u64>::try_new(std::vec![1, 2], 10, 5);
assert!(page.is_ok());
let transaction_inspection: std::option::Option<&dyn ksp_store_api::RawTransactionInspectionRead> = std::option::Option::None;
let transaction_observation_inspection: std::option::Option<&dyn ksp_store_api::RawTransactionObservationInspectionRead> = std::option::Option::None;
let account_inspection: std::option::Option<&dyn ksp_store_api::RawAccountStateInspectionRead> = std::option::Option::None;
let account_observation_inspection: std::option::Option<&dyn ksp_store_api::RawAccountObservationInspectionRead> = std::option::Option::None;
assert!(transaction_inspection.is_none());
assert!(transaction_observation_inspection.is_none());
assert!(account_inspection.is_none());
assert!(account_observation_inspection.is_none());
return;
}

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-store-api/tests/release_completeness.rs // file: crates/ksp-store-api/tests/release_completeness.rs
// version: 1 // version: 3
//! Release-level boundary and completeness canaries for the `0.3.1` Store API RAW surface. //! Release-level boundary and completeness canaries for the backend-neutral Store API RAW surface.
#[test] #[test]
fn pre_007_exact_crate_root_export_inventory_is_stable() { fn v0_3_8_pre_003_exact_crate_root_export_inventory_is_stable() {
let crate_root = include_str!("../src/lib.rs"); let crate_root = include_str!("../src/lib.rs");
let mut actual = std::vec::Vec::new(); let mut actual = std::vec::Vec::new();
for line in crate_root.lines() { for line in crate_root.lines() {
@@ -21,15 +21,19 @@ fn pre_007_exact_crate_root_export_inventory_is_stable() {
"pub use ksp_core_lib::Pubkey;", "pub use ksp_core_lib::Pubkey;",
"pub use ksp_core_lib::Result;", "pub use ksp_core_lib::Result;",
"pub use self::capability::StoreApiFuture;", "pub use self::capability::StoreApiFuture;",
"pub use self::capability::raw_account::RawAccountObservationInspectionRead;",
"pub use self::capability::raw_account::RawAccountObservationRead;", "pub use self::capability::raw_account::RawAccountObservationRead;",
"pub use self::capability::raw_account::RawAccountObservationWrite;", "pub use self::capability::raw_account::RawAccountObservationWrite;",
"pub use self::capability::raw_account::RawAccountStateRead;", "pub use self::capability::raw_account::RawAccountStateRead;",
"pub use self::capability::raw_account::RawAccountStateInspectionRead;",
"pub use self::capability::raw_account::RawAccountStateWrite;", "pub use self::capability::raw_account::RawAccountStateWrite;",
"pub use self::capability::raw_retention::RawTransactionRetentionRead;", "pub use self::capability::raw_retention::RawTransactionRetentionRead;",
"pub use self::capability::raw_retention::RawTransactionRetentionWrite;", "pub use self::capability::raw_retention::RawTransactionRetentionWrite;",
"pub use self::capability::raw_transaction::RawTransactionObservationInspectionRead;",
"pub use self::capability::raw_transaction::RawTransactionObservationRead;", "pub use self::capability::raw_transaction::RawTransactionObservationRead;",
"pub use self::capability::raw_transaction::RawTransactionObservationWrite;", "pub use self::capability::raw_transaction::RawTransactionObservationWrite;",
"pub use self::capability::raw_transaction::RawTransactionRead;", "pub use self::capability::raw_transaction::RawTransactionRead;",
"pub use self::capability::raw_transaction::RawTransactionInspectionRead;",
"pub use self::capability::raw_transaction::RawTransactionWrite;", "pub use self::capability::raw_transaction::RawTransactionWrite;",
"pub use self::error::ERROR_CODE_RAW_CONFLICT;", "pub use self::error::ERROR_CODE_RAW_CONFLICT;",
"pub use self::error::ERROR_CODE_RAW_MODEL_INVALID;", "pub use self::error::ERROR_CODE_RAW_MODEL_INVALID;",
@@ -40,6 +44,16 @@ fn pre_007_exact_crate_root_export_inventory_is_stable() {
"pub use self::model::raw_account::RawAccountObservation;", "pub use self::model::raw_account::RawAccountObservation;",
"pub use self::model::raw_account::RawAccountState;", "pub use self::model::raw_account::RawAccountState;",
"pub use self::model::raw_account::RawAccountStateReference;", "pub use self::model::raw_account::RawAccountStateReference;",
"pub use self::model::raw_inspection::RawAccountObservationInspectionQuery;",
"pub use self::model::raw_inspection::RawAccountObservationSummary;",
"pub use self::model::raw_inspection::RawAccountStateInspectionQuery;",
"pub use self::model::raw_inspection::RawAccountStateSummary;",
"pub use self::model::raw_inspection::RawInspectionPage;",
"pub use self::model::raw_inspection::RawInspectionPageRequest;",
"pub use self::model::raw_inspection::RawTransactionInspectionQuery;",
"pub use self::model::raw_inspection::RawTransactionObservationInspectionQuery;",
"pub use self::model::raw_inspection::RawTransactionObservationSummary;",
"pub use self::model::raw_inspection::RawTransactionSummary;",
"pub use self::model::raw_outcome::RawAcquisitionWriteOutcome;", "pub use self::model::raw_outcome::RawAcquisitionWriteOutcome;",
"pub use self::model::raw_outcome::RawEntityWriteOutcome;", "pub use self::model::raw_outcome::RawEntityWriteOutcome;",
"pub use self::model::raw_outcome::RawObservationWriteOutcome;", "pub use self::model::raw_outcome::RawObservationWriteOutcome;",
@@ -83,7 +97,7 @@ fn pre_007_exact_crate_root_export_inventory_is_stable() {
} }
#[test] #[test]
fn pre_007_exact_production_module_inventory_is_raw_only() { fn v0_3_8_pre_003_exact_production_module_inventory_is_raw_only() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let root_names = rust_file_names(root.as_path()); let root_names = rust_file_names(root.as_path());
assert!(root_names.is_ok()); assert!(root_names.is_ok());
@@ -100,7 +114,7 @@ fn pre_007_exact_production_module_inventory_is_raw_only() {
}; };
assert_eq!( assert_eq!(
model_names, model_names,
std::vec!["raw_account.rs", "raw_outcome.rs", "raw_pagination.rs", "raw_primitives.rs", "raw_retention.rs", "raw_transaction.rs"] std::vec!["raw_account.rs", "raw_inspection.rs", "raw_outcome.rs", "raw_pagination.rs", "raw_primitives.rs", "raw_retention.rs", "raw_transaction.rs"]
); );
let capability_names = rust_file_names(root.join("capability").as_path()); let capability_names = rust_file_names(root.join("capability").as_path());
assert!(capability_names.is_ok()); assert!(capability_names.is_ok());
@@ -141,6 +155,7 @@ fn pre_007_interface_store_ownership_and_negative_scope_remain_explicit() {
include_str!("../src/lib.rs"), include_str!("../src/lib.rs"),
include_str!("../src/model.rs"), include_str!("../src/model.rs"),
include_str!("../src/model/raw_account.rs"), include_str!("../src/model/raw_account.rs"),
include_str!("../src/model/raw_inspection.rs"),
include_str!("../src/model/raw_outcome.rs"), include_str!("../src/model/raw_outcome.rs"),
include_str!("../src/model/raw_pagination.rs"), include_str!("../src/model/raw_pagination.rs"),
include_str!("../src/model/raw_primitives.rs"), include_str!("../src/model/raw_primitives.rs"),
@@ -176,7 +191,7 @@ fn pre_007_interface_store_ownership_and_negative_scope_remain_explicit() {
} }
#[test] #[test]
fn pre_007_capability_inventory_stays_fine_grained_without_runtime_facade() { fn v0_3_8_pre_003_capability_inventory_stays_fine_grained_without_runtime_facade() {
let sources = [ let sources = [
include_str!("../src/capability/raw_account.rs"), include_str!("../src/capability/raw_account.rs"),
include_str!("../src/capability/raw_retention.rs"), include_str!("../src/capability/raw_retention.rs"),
@@ -193,13 +208,17 @@ fn pre_007_capability_inventory_stays_fine_grained_without_runtime_facade() {
} }
traits.sort_unstable(); traits.sort_unstable();
let mut expected = std::vec![ let mut expected = std::vec![
"pub trait RawAccountObservationInspectionRead: std::marker::Send + std::marker::Sync {",
"pub trait RawAccountObservationRead: std::marker::Send + std::marker::Sync {", "pub trait RawAccountObservationRead: std::marker::Send + std::marker::Sync {",
"pub trait RawAccountObservationWrite: std::marker::Send + std::marker::Sync {", "pub trait RawAccountObservationWrite: std::marker::Send + std::marker::Sync {",
"pub trait RawAccountStateRead: std::marker::Send + std::marker::Sync {", "pub trait RawAccountStateRead: std::marker::Send + std::marker::Sync {",
"pub trait RawAccountStateInspectionRead: std::marker::Send + std::marker::Sync {",
"pub trait RawAccountStateWrite: std::marker::Send + std::marker::Sync {", "pub trait RawAccountStateWrite: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionObservationInspectionRead: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionObservationRead: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionObservationRead: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionObservationWrite: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionObservationWrite: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionRead: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionRead: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionInspectionRead: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionRetentionRead: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionRetentionRead: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionRetentionWrite: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionRetentionWrite: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionWrite: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionWrite: std::marker::Send + std::marker::Sync {",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/tests/security_hardening.rs // file: crates/ksp-store-api/tests/security_hardening.rs
// version: 1 // version: 2
//! Adversarial and retention-race canaries for the Store API RAW foundation. //! Adversarial and retention-race canaries for the Store API RAW foundation.
@@ -122,3 +122,47 @@ fn pre_007_retention_outcome_distinguishes_lost_compare_and_transition_race() {
assert_ne!(ksp_store_api::RawRetentionWriteOutcome::AlreadyAtTarget, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch); assert_ne!(ksp_store_api::RawRetentionWriteOutcome::AlreadyAtTarget, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch);
return; return;
} }
#[test]
fn v0_3_8_pre_003_inspection_summaries_and_counts_reject_payload_shaped_or_inconsistent_state() {
let network = match ksp_store_api::RawNetworkId::new("mainnet-beta".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let format = match ksp_store_api::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let transaction_reference = ksp_store_api::RawTransactionReference::new(network.clone(), ksp_store_api::RawTransactionSignature::new([0x61_u8; 64]));
let summary = ksp_store_api::RawTransactionSummary::try_new(
transaction_reference,
42,
std::option::Option::None,
format,
1,
ksp_store_api::RawContentHash::new([0x62_u8; 32]),
std::option::Option::Some(512),
ksp_store_api::RawRetentionState::Full,
);
assert!(summary.is_ok());
let summary = match summary {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let debug = std::format!("{summary:?}");
assert!(!debug.contains(HOSTILE_MARKER));
assert!(!debug.contains("payload:"));
let account_reference = ksp_store_api::RawAccountStateReference::new(
network,
ksp_store_api::Pubkey::new_from_array([0x63_u8; 32]),
43,
ksp_store_api::RawContentHash::new([0x64_u8; 32]),
);
assert!(
ksp_store_api::RawAccountStateSummary::try_new(account_reference, 1, ksp_store_api::Pubkey::new_from_array([0x65_u8; 32]), false, 0, 16_777_217,)
.is_err()
);
assert!(ksp_store_api::RawInspectionPage::<u64>::try_new(std::vec![1], 0, 0).is_err());
assert!(ksp_store_api::RawInspectionPage::<u64>::try_new(std::vec::Vec::new(), 1, 2).is_err());
return;
}

View File

@@ -0,0 +1,271 @@
// file: crates/ksp-store-api/unit_tests/model/raw_inspection.rs
// version: 2
//! Unit tests for backend-neutral RAW inspection contracts.
fn network() -> std::option::Option<crate::RawNetworkId> {
return match crate::RawNetworkId::new("mainnet-beta".to_owned()) {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(_) => std::option::Option::None,
};
}
fn format_id() -> std::option::Option<crate::RawFormatId> {
return match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(_) => std::option::Option::None,
};
}
#[test]
fn inspection_page_request_preserves_large_offset_and_caller_limit_without_cursor_semantics() {
let limit = match crate::RawPageLimit::new(100) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let request = crate::RawInspectionPageRequest::new(u64::MAX, limit);
assert_eq!(request.offset(), u64::MAX);
assert_eq!(request.limit().get(), 100);
return;
}
#[test]
fn inspection_page_rejects_inconsistent_exact_counts() {
assert!(crate::RawInspectionPage::<u64>::try_new(std::vec![1], 0, 0).is_err());
assert!(crate::RawInspectionPage::<u64>::try_new(std::vec::Vec::new(), 4, 5).is_err());
let page = crate::RawInspectionPage::<u64>::try_new(std::vec![1, 2], 5, 3);
assert!(page.is_ok());
let page = match page {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(page.total_items(), 5);
assert_eq!(page.filtered_items(), 3);
assert_eq!(page.items(), &[1, 2]);
return;
}
#[test]
fn inspection_queries_keep_backend_neutral_filters_and_random_access_page() {
let network = match network() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let slots = match crate::RawSlotRange::new(std::option::Option::Some(10), std::option::Option::Some(20)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let limit = match crate::RawPageLimit::new(25) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let transaction =
crate::RawTransactionInspectionQuery::new(network.clone(), slots, crate::RawSortDirection::Descending, crate::RawInspectionPageRequest::new(50, limit));
assert_eq!(transaction.network().as_str(), "mainnet-beta");
assert_eq!(transaction.page().offset(), 50);
assert_eq!(transaction.slots().end_inclusive(), std::option::Option::Some(20));
assert_eq!(transaction.direction(), crate::RawSortDirection::Descending);
let pubkey = crate::Pubkey::new_from_array([0x21_u8; 32]);
let account = crate::RawAccountStateInspectionQuery::new(
network,
std::option::Option::Some(pubkey),
slots,
crate::RawSortDirection::Ascending,
crate::RawInspectionPageRequest::new(75, limit),
);
assert_eq!(account.page().offset(), 75);
assert_eq!(account.pubkey(), std::option::Option::Some(&pubkey));
return;
}
#[test]
fn transaction_summary_is_payload_free_and_retention_consistent() {
let network = match network() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let format_id = match format_id() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let reference = crate::RawTransactionReference::new(network, crate::RawTransactionSignature::new([0x31_u8; 64]));
let full = crate::RawTransactionSummary::try_new(
reference.clone(),
42,
std::option::Option::None,
format_id.clone(),
1,
crate::RawContentHash::new([0x32_u8; 32]),
std::option::Option::Some(123),
crate::RawRetentionState::Full,
);
assert!(full.is_ok());
let full = match full {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(full.slot(), 42);
assert_eq!(full.payload_size_bytes(), std::option::Option::Some(123));
assert_eq!(full.retention_state(), crate::RawRetentionState::Full);
assert!(
crate::RawTransactionSummary::try_new(
reference.clone(),
42,
std::option::Option::None,
format_id.clone(),
1,
crate::RawContentHash::new([0x32_u8; 32]),
std::option::Option::None,
crate::RawRetentionState::Full,
)
.is_err()
);
assert!(
crate::RawTransactionSummary::try_new(
reference,
42,
std::option::Option::None,
format_id,
1,
crate::RawContentHash::new([0x32_u8; 32]),
std::option::Option::Some(123),
crate::RawRetentionState::Purged,
)
.is_err()
);
return;
}
#[test]
fn account_summary_exposes_length_only_and_enforces_raw_account_bound() {
let network = match network() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let reference = crate::RawAccountStateReference::new(network, crate::Pubkey::new_from_array([0x41_u8; 32]), 88, crate::RawContentHash::new([0x42_u8; 32]));
let summary = crate::RawAccountStateSummary::try_new(reference.clone(), 1000, crate::Pubkey::new_from_array([0x43_u8; 32]), false, 9, 512);
assert!(summary.is_ok());
let summary = match summary {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(summary.reference().slot(), 88);
assert_eq!(summary.data_length_bytes(), 512);
assert!(crate::RawAccountStateSummary::try_new(reference, 1000, crate::Pubkey::new_from_array([0x43_u8; 32]), false, 9, 16_777_217,).is_err());
return;
}
#[test]
fn observation_inspection_queries_require_network_coherence_and_exact_entity_filters() {
let network = match network() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let other_network = match crate::RawNetworkId::new("devnet".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let limit = match crate::RawPageLimit::new(25) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let page = crate::RawInspectionPageRequest::new(125, limit);
let transaction_reference = crate::RawTransactionReference::new(network.clone(), crate::RawTransactionSignature::new([0x51_u8; 64]));
let transaction = crate::RawTransactionObservationInspectionQuery::try_new(
network.clone(),
std::option::Option::Some(transaction_reference.clone()),
crate::RawSortDirection::Descending,
page,
);
assert!(transaction.is_ok());
let transaction = match transaction {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(transaction.page().offset(), 125);
assert_eq!(transaction.transaction(), std::option::Option::Some(&transaction_reference));
assert!(
crate::RawTransactionObservationInspectionQuery::try_new(
other_network.clone(),
std::option::Option::Some(transaction_reference),
crate::RawSortDirection::Ascending,
page,
)
.is_err()
);
let account_reference =
crate::RawAccountStateReference::new(network.clone(), crate::Pubkey::new_from_array([0x52_u8; 32]), 99, crate::RawContentHash::new([0x53_u8; 32]));
let account = crate::RawAccountObservationInspectionQuery::try_new(
network,
std::option::Option::Some(account_reference.clone()),
crate::RawSortDirection::Ascending,
page,
);
assert!(account.is_ok());
let account = match account {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(account.account(), std::option::Option::Some(&account_reference));
let mismatched_account = crate::RawAccountStateReference::new(
other_network.clone(),
crate::Pubkey::new_from_array([0x54_u8; 32]),
100,
crate::RawContentHash::new([0x55_u8; 32]),
);
assert!(
crate::RawAccountObservationInspectionQuery::try_new(
other_network,
std::option::Option::Some(account_reference),
crate::RawSortDirection::Ascending,
page,
)
.is_err()
);
let _ = mismatched_account;
return;
}
#[test]
fn observation_summaries_expose_safe_provenance_without_raw_entity_bytes() {
let network = match network() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let provider = match crate::RawProvenanceCode::new("publicnode".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let protocol = match crate::RawProvenanceCode::new("solana-json-rpc".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let method = match crate::RawProvenanceCode::new("getTransaction".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let received_at = match crate::RawTimestamp::from_unix_millis(1_750_000_000_000) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let provenance = crate::RawAcquisitionProvenance::new(provider, protocol, method, crate::RawAcquisitionOrigin::Backfill, received_at);
let transaction_reference = crate::RawTransactionReference::new(network.clone(), crate::RawTransactionSignature::new([0x61_u8; 64]));
let transaction =
crate::RawTransactionObservationSummary::new(crate::RawObservationKey::new([0x62_u8; 32]), transaction_reference.clone(), provenance.clone());
assert_eq!(transaction.transaction(), &transaction_reference);
assert_eq!(transaction.provenance().received_at(), received_at);
let account_reference =
crate::RawAccountStateReference::new(network, crate::Pubkey::new_from_array([0x63_u8; 32]), 700, crate::RawContentHash::new([0x64_u8; 32]));
let account = crate::RawAccountObservationSummary::new(
crate::RawObservationKey::new([0x65_u8; 32]),
account_reference.clone(),
provenance,
std::option::Option::Some(true),
std::option::Option::Some(crate::RawTransactionSignature::new([0x66_u8; 64])),
std::option::Option::Some(u64::MAX),
);
assert_eq!(account.account(), &account_reference);
assert_eq!(account.is_startup(), std::option::Option::Some(true));
assert_eq!(account.write_version(), std::option::Option::Some(u64::MAX));
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/lib.rs // file: crates/ksp-store-lib/src/lib.rs
// version: 9 // version: 12
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -10,8 +10,10 @@
//! The runtime facade owns backend selection, lifecycle, safe diagnostics and //! The runtime facade owns backend selection, lifecycle, safe diagnostics and
//! backend-neutral capability dispatch. `0.3.3-pre.008` completed the six PostgreSQL //! backend-neutral capability dispatch. `0.3.3-pre.008` completed the six PostgreSQL
//! `RawTransaction` capabilities. `0.3.4-pre.008` adds the four `RawAccount*` capabilities //! `RawTransaction` capabilities. `0.3.4-pre.008` adds the four `RawAccount*` capabilities
//! on both the physical backend and this common facade, completing the RAW inventory at //! on both the physical backend and this common facade, completing the canonical RAW runtime inventory at
//! ten capabilities without exposing physical types. //! ten capabilities without exposing physical types. The `0.3.8` inspection
//! contracts are reexported here. `0.3.8-pre.004` dispatches transaction inspection
//! and `0.3.8-pre.005` completes inspection dispatch with RAW account states.
//! //!
//! The default `postgres` feature compiles the official PostgreSQL backend as //! The default `postgres` feature compiles the official PostgreSQL backend as
//! an optional implementation dependency. No backend implementation type is //! an optional implementation dependency. No backend implementation type is
@@ -119,18 +121,30 @@ pub use ksp_store_api::MAX_RAW_UNIX_MILLIS;
pub use ksp_store_api::Pubkey; pub use ksp_store_api::Pubkey;
/// Persistable acquisition observation linked to one complete canonical RAW account state. /// Persistable acquisition observation linked to one complete canonical RAW account state.
pub use ksp_store_api::RawAccountObservation; pub use ksp_store_api::RawAccountObservation;
/// Backend-independent random-access inspection query for RAW account observations.
pub use ksp_store_api::RawAccountObservationInspectionQuery;
/// Read capability for random-access RAW account-observation inspection.
pub use ksp_store_api::RawAccountObservationInspectionRead;
/// Read capability for persisted RAW account-state observations. /// Read capability for persisted RAW account-state observations.
pub use ksp_store_api::RawAccountObservationRead; pub use ksp_store_api::RawAccountObservationRead;
/// Safe observation summary for one canonical RAW account-state acquisition.
pub use ksp_store_api::RawAccountObservationSummary;
/// Write capability for additional observations of already persisted RAW account states. /// Write capability for additional observations of already persisted RAW account states.
pub use ksp_store_api::RawAccountObservationWrite; pub use ksp_store_api::RawAccountObservationWrite;
/// Canonical complete N1 RAW account state independent from acquisition transport. /// Canonical complete N1 RAW account state independent from acquisition transport.
pub use ksp_store_api::RawAccountState; pub use ksp_store_api::RawAccountState;
/// Backend-independent random-access inspection query for RAW account states.
pub use ksp_store_api::RawAccountStateInspectionQuery;
/// Read capability for data-free random-access RAW account-state inspection.
pub use ksp_store_api::RawAccountStateInspectionRead;
/// Backend-independent list query for complete canonical RAW account states. /// Backend-independent list query for complete canonical RAW account states.
pub use ksp_store_api::RawAccountStateQuery; pub use ksp_store_api::RawAccountStateQuery;
/// Read capability for complete canonical RAW account states. /// Read capability for complete canonical RAW account states.
pub use ksp_store_api::RawAccountStateRead; pub use ksp_store_api::RawAccountStateRead;
/// Durable backend-independent identity of one canonical RAW account state. /// Durable backend-independent identity of one canonical RAW account state.
pub use ksp_store_api::RawAccountStateReference; pub use ksp_store_api::RawAccountStateReference;
/// Data-free summary of one canonical RAW account state for operator inspection.
pub use ksp_store_api::RawAccountStateSummary;
/// Write capability for complete canonical RAW account-state acquisitions. /// Write capability for complete canonical RAW account-state acquisitions.
pub use ksp_store_api::RawAccountStateWrite; pub use ksp_store_api::RawAccountStateWrite;
/// Origin category describing why one acquisition was performed. /// Origin category describing why one acquisition was performed.
@@ -145,6 +159,10 @@ pub use ksp_store_api::RawContentHash;
pub use ksp_store_api::RawEntityWriteOutcome; pub use ksp_store_api::RawEntityWriteOutcome;
/// Bounded identifier of one KSP-owned source-independent RAW persistence format. /// Bounded identifier of one KSP-owned source-independent RAW persistence format.
pub use ksp_store_api::RawFormatId; pub use ksp_store_api::RawFormatId;
/// One bounded random-access inspection page with exact logical counts.
pub use ksp_store_api::RawInspectionPage;
/// Random-access page request dedicated to bounded interactive RAW inspection.
pub use ksp_store_api::RawInspectionPageRequest;
/// Bounded logical network/cluster identifier used in backend-independent Store identities. /// Bounded logical network/cluster identifier used in backend-independent Store identities.
pub use ksp_store_api::RawNetworkId; pub use ksp_store_api::RawNetworkId;
/// Stable deterministic idempotence key for one persisted acquisition observation. /// Stable deterministic idempotence key for one persisted acquisition observation.
@@ -177,10 +195,20 @@ pub use ksp_store_api::RawTimestamp;
pub use ksp_store_api::RawTransaction; pub use ksp_store_api::RawTransaction;
/// Explicit write mode for canonical RAW transaction acquisitions. /// Explicit write mode for canonical RAW transaction acquisitions.
pub use ksp_store_api::RawTransactionAcquisitionMode; pub use ksp_store_api::RawTransactionAcquisitionMode;
/// Backend-independent random-access inspection query for RAW transactions.
pub use ksp_store_api::RawTransactionInspectionQuery;
/// Read capability for payload-free random-access RAW transaction inspection.
pub use ksp_store_api::RawTransactionInspectionRead;
/// Persistable acquisition observation linked to one canonical RAW transaction. /// Persistable acquisition observation linked to one canonical RAW transaction.
pub use ksp_store_api::RawTransactionObservation; pub use ksp_store_api::RawTransactionObservation;
/// Backend-independent random-access inspection query for RAW transaction observations.
pub use ksp_store_api::RawTransactionObservationInspectionQuery;
/// Read capability for random-access RAW transaction-observation inspection.
pub use ksp_store_api::RawTransactionObservationInspectionRead;
/// Read capability for persisted RAW transaction observations. /// Read capability for persisted RAW transaction observations.
pub use ksp_store_api::RawTransactionObservationRead; pub use ksp_store_api::RawTransactionObservationRead;
/// Safe observation summary for one canonical RAW transaction acquisition.
pub use ksp_store_api::RawTransactionObservationSummary;
/// Write capability for additional observations of already persisted RAW transactions. /// Write capability for additional observations of already persisted RAW transactions.
pub use ksp_store_api::RawTransactionObservationWrite; pub use ksp_store_api::RawTransactionObservationWrite;
/// Backend-independent list query for canonical RAW transactions. /// Backend-independent list query for canonical RAW transactions.
@@ -197,6 +225,8 @@ pub use ksp_store_api::RawTransactionRetentionTransition;
pub use ksp_store_api::RawTransactionRetentionWrite; pub use ksp_store_api::RawTransactionRetentionWrite;
/// Canonical 64-byte Solana transaction signature used by Store identities. /// Canonical 64-byte Solana transaction signature used by Store identities.
pub use ksp_store_api::RawTransactionSignature; pub use ksp_store_api::RawTransactionSignature;
/// Payload-free summary of one canonical RAW transaction for operator inspection.
pub use ksp_store_api::RawTransactionSummary;
/// Minimal durable identity retained after a canonical RAW transaction payload is purged. /// Minimal durable identity retained after a canonical RAW transaction payload is purged.
pub use ksp_store_api::RawTransactionTombstone; pub use ksp_store_api::RawTransactionTombstone;
/// Write capability for canonical RAW transaction acquisitions. /// Write capability for canonical RAW transaction acquisitions.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/store.rs // file: crates/ksp-store-lib/src/store.rs
// version: 7 // version: 10
/// Opaque common Store runtime facade. /// Opaque common Store runtime facade.
/// ///
@@ -116,6 +116,36 @@ impl std::fmt::Debug for Store {
} }
} }
impl ksp_store_api::RawAccountObservationInspectionRead for Store {
fn inspect_raw_account_observations<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountObservationInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountObservationSummary>>> {
let network_check = validate_operation_network(&self.network, query.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.inspect_raw_account_observations(query).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = query;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawAccountObservationRead for Store { impl ksp_store_api::RawAccountObservationRead for Store {
fn get_raw_account_observation<'a>( fn get_raw_account_observation<'a>(
&'a self, &'a self,
@@ -228,6 +258,36 @@ impl ksp_store_api::RawAccountStateRead for Store {
} }
} }
impl ksp_store_api::RawAccountStateInspectionRead for Store {
fn inspect_raw_account_states<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountStateInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountStateSummary>>> {
let network_check = validate_operation_network(&self.network, query.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.inspect_raw_account_states(query).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = query;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawAccountStateWrite for Store { impl ksp_store_api::RawAccountStateWrite for Store {
fn persist_raw_account_acquisition<'a>( fn persist_raw_account_acquisition<'a>(
&'a self, &'a self,
@@ -324,6 +384,36 @@ impl ksp_store_api::RawTransactionRead for Store {
} }
} }
impl ksp_store_api::RawTransactionInspectionRead for Store {
fn inspect_raw_transactions<'a>(
&'a self,
query: &'a ksp_store_api::RawTransactionInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionSummary>>> {
let network_check = validate_operation_network(&self.network, query.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.inspect_raw_transactions(query).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = query;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawTransactionWrite for Store { impl ksp_store_api::RawTransactionWrite for Store {
fn persist_raw_transaction_acquisition<'a>( fn persist_raw_transaction_acquisition<'a>(
&'a self, &'a self,
@@ -364,6 +454,36 @@ impl ksp_store_api::RawTransactionWrite for Store {
} }
} }
impl ksp_store_api::RawTransactionObservationInspectionRead for Store {
fn inspect_raw_transaction_observations<'a>(
&'a self,
query: &'a ksp_store_api::RawTransactionObservationInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionObservationSummary>>> {
let network_check = validate_operation_network(&self.network, query.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.inspect_raw_transaction_observations(query).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = query;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawTransactionObservationRead for Store { impl ksp_store_api::RawTransactionObservationRead for Store {
fn get_raw_transaction_observation<'a>( fn get_raw_transaction_observation<'a>(
&'a self, &'a self,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/dependency_boundary.rs // file: crates/ksp-store-lib/tests/dependency_boundary.rs
// version: 8 // version: 11
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -60,13 +60,17 @@ fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() {
} }
#[test] #[test]
fn pre_008_facade_dispatches_exact_ten_raw_capabilities_without_physical_leak() { fn v0_3_8_pre_009_facade_dispatches_fourteen_raw_capabilities_without_physical_leak() {
let store = include_str!("../src/store.rs"); let store = include_str!("../src/store.rs");
for required in [ for required in [
"impl ksp_store_api::RawAccountObservationInspectionRead for Store",
"impl ksp_store_api::RawAccountObservationRead for Store", "impl ksp_store_api::RawAccountObservationRead for Store",
"impl ksp_store_api::RawAccountObservationWrite for Store", "impl ksp_store_api::RawAccountObservationWrite for Store",
"impl ksp_store_api::RawAccountStateInspectionRead for Store",
"impl ksp_store_api::RawAccountStateRead for Store", "impl ksp_store_api::RawAccountStateRead for Store",
"impl ksp_store_api::RawAccountStateWrite for Store", "impl ksp_store_api::RawAccountStateWrite for Store",
"impl ksp_store_api::RawTransactionInspectionRead for Store",
"impl ksp_store_api::RawTransactionObservationInspectionRead for Store",
"impl ksp_store_api::RawTransactionObservationRead for Store", "impl ksp_store_api::RawTransactionObservationRead for Store",
"impl ksp_store_api::RawTransactionObservationWrite for Store", "impl ksp_store_api::RawTransactionObservationWrite for Store",
"impl ksp_store_api::RawTransactionRead for Store", "impl ksp_store_api::RawTransactionRead for Store",
@@ -79,7 +83,7 @@ fn pre_008_facade_dispatches_exact_ten_raw_capabilities_without_physical_leak()
] { ] {
assert!(store.contains(required), "missing pre.008 Store capability dispatch contract: {required}"); assert!(store.contains(required), "missing pre.008 Store capability dispatch contract: {required}");
} }
assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 10); assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 14);
for forbidden in ["tokio_postgres::", "deadpool_postgres::", "CREATE TABLE", "INSERT INTO", "UPDATE ksp_", "DELETE FROM"] { for forbidden in ["tokio_postgres::", "deadpool_postgres::", "CREATE TABLE", "INSERT INTO", "UPDATE ksp_", "DELETE FROM"] {
assert!(!store.contains(forbidden), "pre.008 facade leaked physical backend material: {forbidden}"); assert!(!store.contains(forbidden), "pre.008 facade leaked physical backend material: {forbidden}");
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/hardening_completeness.rs // file: crates/ksp-store-lib/tests/hardening_completeness.rs
// version: 6 // version: 10
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -155,12 +155,18 @@ fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
"PostgresTlsMode", "PostgresTlsMode",
"Pubkey", "Pubkey",
"RawAccountObservation", "RawAccountObservation",
"RawAccountObservationInspectionQuery",
"RawAccountObservationInspectionRead",
"RawAccountObservationRead", "RawAccountObservationRead",
"RawAccountObservationSummary",
"RawAccountObservationWrite", "RawAccountObservationWrite",
"RawAccountState", "RawAccountState",
"RawAccountStateInspectionQuery",
"RawAccountStateInspectionRead",
"RawAccountStateQuery", "RawAccountStateQuery",
"RawAccountStateRead", "RawAccountStateRead",
"RawAccountStateReference", "RawAccountStateReference",
"RawAccountStateSummary",
"RawAccountStateWrite", "RawAccountStateWrite",
"RawAcquisitionOrigin", "RawAcquisitionOrigin",
"RawAcquisitionProvenance", "RawAcquisitionProvenance",
@@ -168,6 +174,8 @@ fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
"RawContentHash", "RawContentHash",
"RawEntityWriteOutcome", "RawEntityWriteOutcome",
"RawFormatId", "RawFormatId",
"RawInspectionPage",
"RawInspectionPageRequest",
"RawNetworkId", "RawNetworkId",
"RawObservationKey", "RawObservationKey",
"RawObservationWriteOutcome", "RawObservationWriteOutcome",
@@ -184,8 +192,13 @@ fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
"RawTimestamp", "RawTimestamp",
"RawTransaction", "RawTransaction",
"RawTransactionAcquisitionMode", "RawTransactionAcquisitionMode",
"RawTransactionInspectionQuery",
"RawTransactionInspectionRead",
"RawTransactionObservation", "RawTransactionObservation",
"RawTransactionObservationInspectionQuery",
"RawTransactionObservationInspectionRead",
"RawTransactionObservationRead", "RawTransactionObservationRead",
"RawTransactionObservationSummary",
"RawTransactionObservationWrite", "RawTransactionObservationWrite",
"RawTransactionQuery", "RawTransactionQuery",
"RawTransactionRead", "RawTransactionRead",
@@ -194,6 +207,7 @@ fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
"RawTransactionRetentionTransition", "RawTransactionRetentionTransition",
"RawTransactionRetentionWrite", "RawTransactionRetentionWrite",
"RawTransactionSignature", "RawTransactionSignature",
"RawTransactionSummary",
"RawTransactionTombstone", "RawTransactionTombstone",
"RawTransactionWrite", "RawTransactionWrite",
"Result", "Result",
@@ -208,7 +222,7 @@ fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
]; ];
expected.sort_unstable(); expected.sort_unstable();
assert_eq!(actual.as_slice(), expected.as_slice()); assert_eq!(actual.as_slice(), expected.as_slice());
assert_eq!(actual.len(), 91); assert_eq!(actual.len(), 105);
return; return;
} }
@@ -284,13 +298,17 @@ fn pre_009_facade_production_sources_keep_config_env_physical_sql_and_backend_ha
} }
#[test] #[test]
fn pre_010_facade_raw_capability_inventory_is_exactly_ten() { fn v0_3_8_pre_009_facade_raw_capability_inventory_is_exactly_fourteen() {
let store = include_str!("../src/store.rs"); let store = include_str!("../src/store.rs");
let capability_impls = [ let capability_impls = [
"impl ksp_store_api::RawAccountObservationInspectionRead for Store",
"impl ksp_store_api::RawAccountObservationRead for Store", "impl ksp_store_api::RawAccountObservationRead for Store",
"impl ksp_store_api::RawAccountObservationWrite for Store", "impl ksp_store_api::RawAccountObservationWrite for Store",
"impl ksp_store_api::RawAccountStateInspectionRead for Store",
"impl ksp_store_api::RawAccountStateRead for Store", "impl ksp_store_api::RawAccountStateRead for Store",
"impl ksp_store_api::RawAccountStateWrite for Store", "impl ksp_store_api::RawAccountStateWrite for Store",
"impl ksp_store_api::RawTransactionInspectionRead for Store",
"impl ksp_store_api::RawTransactionObservationInspectionRead for Store",
"impl ksp_store_api::RawTransactionObservationRead for Store", "impl ksp_store_api::RawTransactionObservationRead for Store",
"impl ksp_store_api::RawTransactionObservationWrite for Store", "impl ksp_store_api::RawTransactionObservationWrite for Store",
"impl ksp_store_api::RawTransactionRead for Store", "impl ksp_store_api::RawTransactionRead for Store",
@@ -301,20 +319,24 @@ fn pre_010_facade_raw_capability_inventory_is_exactly_ten() {
for implementation in capability_impls { for implementation in capability_impls {
assert_eq!(store.matches(implementation).count(), 1, "unexpected Store capability implementation inventory: {implementation}"); assert_eq!(store.matches(implementation).count(), 1, "unexpected Store capability implementation inventory: {implementation}");
} }
assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 10); assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 14);
assert_eq!(store.matches("validate_operation_network(").count(), 14); assert_eq!(store.matches("validate_operation_network(").count(), 18);
return; return;
} }
#[test] #[test]
fn pre_010_facade_and_backend_raw_capability_sets_match_exactly_without_account_retention() { fn v0_3_8_pre_005_facade_and_backend_capability_sets_match_with_both_inspection_capabilities() {
let store = include_str!("../src/store.rs"); let store = include_str!("../src/store.rs");
let backend = include_str!("../../ksp-store-postgres-lib/src/runtime.rs"); let backend = include_str!("../../ksp-store-postgres-lib/src/runtime.rs");
let store_traits = raw_capability_trait_names(store, " for Store"); let store_traits = raw_capability_trait_names(store, " for Store");
let backend_traits = raw_capability_trait_names(backend, " for PostgresBackend"); let backend_traits = raw_capability_trait_names(backend, " for PostgresBackend");
assert_eq!(store_traits.len(), 10); assert_eq!(store_traits.len(), 14);
assert_eq!(backend_traits.len(), 10); assert_eq!(backend_traits.len(), 14);
assert_eq!(store_traits, backend_traits); assert_eq!(store_traits, backend_traits);
assert!(store_traits.contains(&"RawTransactionInspectionRead"));
assert!(store_traits.contains(&"RawAccountStateInspectionRead"));
assert!(store_traits.contains(&"RawTransactionObservationInspectionRead"));
assert!(store_traits.contains(&"RawAccountObservationInspectionRead"));
for forbidden in ["RawAccountRetentionRead", "RawAccountRetentionWrite", "RawAccountDelete", "RawAccountCompaction"] { for forbidden in ["RawAccountRetentionRead", "RawAccountRetentionWrite", "RawAccountDelete", "RawAccountCompaction"] {
assert!(!store_traits.contains(&forbidden), "unexpected account capability added to Store: {forbidden}"); assert!(!store_traits.contains(&forbidden), "unexpected account capability added to Store: {forbidden}");
assert!(!backend_traits.contains(&forbidden), "unexpected account capability added to PostgreSQL backend: {forbidden}"); assert!(!backend_traits.contains(&forbidden), "unexpected account capability added to PostgreSQL backend: {forbidden}");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/public_api.rs // file: crates/ksp-store-lib/tests/public_api.rs
// version: 8 // version: 12
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -57,11 +57,14 @@ fn pre_005_common_and_postgres_error_codes_are_stable_and_store_owned() {
} }
#[test] #[test]
fn pre_003_facade_reexports_backend_agnostic_store_api_types() { fn v0_3_8_pre_003_facade_reexports_backend_agnostic_inspection_types_without_dispatch() {
let _raw_transaction = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransaction>>(); let _raw_transaction = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransaction>>();
let _raw_account_state = std::mem::size_of::<std::option::Option<ksp_store_lib::RawAccountState>>(); let _raw_account_state = std::mem::size_of::<std::option::Option<ksp_store_lib::RawAccountState>>();
let _query = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransactionQuery>>(); let _query = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransactionQuery>>();
let _inspection_query = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransactionInspectionQuery>>();
let _inspection_page = std::mem::size_of::<std::option::Option<ksp_store_lib::RawInspectionPage<ksp_store_lib::RawTransactionSummary>>>();
let _capability = std::mem::size_of::<std::option::Option<&dyn ksp_store_lib::RawTransactionRead>>(); let _capability = std::mem::size_of::<std::option::Option<&dyn ksp_store_lib::RawTransactionRead>>();
let _inspection_capability = std::mem::size_of::<std::option::Option<&dyn ksp_store_lib::RawTransactionInspectionRead>>();
let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(()); let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(());
return; return;
} }
@@ -76,10 +79,14 @@ fn pre_007_health_and_runtime_snapshot_types_are_portable_crate_root_contracts()
fn assert_raw_capabilities<T>() fn assert_raw_capabilities<T>()
where where
T: ksp_store_lib::RawAccountObservationRead T: ksp_store_lib::RawAccountObservationInspectionRead
+ ksp_store_lib::RawAccountObservationRead
+ ksp_store_lib::RawAccountObservationWrite + ksp_store_lib::RawAccountObservationWrite
+ ksp_store_lib::RawAccountStateInspectionRead
+ ksp_store_lib::RawAccountStateRead + ksp_store_lib::RawAccountStateRead
+ ksp_store_lib::RawAccountStateWrite + ksp_store_lib::RawAccountStateWrite
+ ksp_store_lib::RawTransactionInspectionRead
+ ksp_store_lib::RawTransactionObservationInspectionRead
+ ksp_store_lib::RawTransactionObservationRead + ksp_store_lib::RawTransactionObservationRead
+ ksp_store_lib::RawTransactionObservationWrite + ksp_store_lib::RawTransactionObservationWrite
+ ksp_store_lib::RawTransactionRead + ksp_store_lib::RawTransactionRead
@@ -92,7 +99,7 @@ where
} }
#[test] #[test]
fn pre_008_store_facade_implements_exact_raw_capability_set_10_of_10() { fn v0_3_8_pre_009_store_facade_implements_entity_and_observation_inspection_capabilities_as_14_of_14() {
assert_raw_capabilities::<ksp_store_lib::Store>(); assert_raw_capabilities::<ksp_store_lib::Store>();
return; return;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/error.rs // file: crates/ksp-store-postgres-lib/src/error.rs
// version: 8 // version: 9
/// Stable KSP error code reserved for PostgreSQL retention transitions that require unsupported physical compaction. /// Stable KSP error code reserved for PostgreSQL retention transitions that require unsupported physical compaction.
pub const ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED: ksp_store_api::ErrorCode = pub const ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED: ksp_store_api::ErrorCode =
@@ -23,7 +23,7 @@ pub enum PostgresBackendErrorKind {
DataInvalid, DataInvalid,
/// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL. /// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL.
MigrationFailed, MigrationFailed,
/// The requested RAW page size cannot be represented by PostgreSQL LIMIT plus the continuation probe row. /// The requested RAW page size cannot be represented by the physical PostgreSQL LIMIT domain.
PageLimitUnsupported, PageLimitUnsupported,
/// Applied PostgreSQL migration history diverges from the embedded immutable KSP history. /// Applied PostgreSQL migration history diverges from the embedded immutable KSP history.
MigrationMismatch, MigrationMismatch,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/lib.rs // file: crates/ksp-store-postgres-lib/src/lib.rs
// version: 21 // version: 24
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -33,6 +33,11 @@
//! deterministic account keyset pagination with the fixed `KSPA` cursor. `0.3.4-pre.008` //! deterministic account keyset pagination with the fixed `KSPA` cursor. `0.3.4-pre.008`
//! implements the four `RawAccount*` capabilities directly on `PostgresBackend`, completing //! implements the four `RawAccount*` capabilities directly on `PostgresBackend`, completing
//! the backend RAW capability inventory at ten without exposing physical PostgreSQL types. //! the backend RAW capability inventory at ten without exposing physical PostgreSQL types.
//! `0.3.8-pre.004` adds payload-free random-access RawTransaction inspection;
//! `0.3.8-pre.005` adds the corresponding data-free RawAccountState inspection
//! with exact counts while preserving both keyset traversal families unchanged.
//! `0.3.8-pre.009` adds safe random-access observation inspection for both RAW
//! families without changing the physical schema or read-by-key contracts.
//! //!
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The //! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
//! common facade consumes only this crate's narrow backend bridge and never //! common facade consumes only this crate's narrow backend bridge and never
@@ -82,6 +87,10 @@ pub(crate) use self::raw_account::cursor::raw_account_physical_page_limit;
pub(crate) use self::raw_account::get_raw_account_observation; pub(crate) use self::raw_account::get_raw_account_observation;
/// Private RAW account state reader consumed by the physical backend runtime. /// Private RAW account state reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::get_raw_account_state; pub(crate) use self::raw_account::get_raw_account_state;
/// Private safe RAW account-observation inspection reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::inspect_raw_account_observations;
/// Private data-free RAW account-state inspection reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::inspect_raw_account_states;
/// Private RAW account-state list reader consumed by the physical backend runtime. /// Private RAW account-state list reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::list_raw_account_states; pub(crate) use self::raw_account::list_raw_account_states;
/// Private atomic RAW account acquisition writer consumed by the physical backend runtime. /// Private atomic RAW account acquisition writer consumed by the physical backend runtime.
@@ -102,6 +111,10 @@ pub(crate) use self::raw_transaction::get_raw_transaction_observation;
pub(crate) use self::raw_transaction::get_raw_transaction_retention_state; pub(crate) use self::raw_transaction::get_raw_transaction_retention_state;
/// Private RAW transaction tombstone reader consumed by the physical backend runtime. /// Private RAW transaction tombstone reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::get_raw_transaction_tombstone; pub(crate) use self::raw_transaction::get_raw_transaction_tombstone;
/// Private safe RAW transaction-observation inspection reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::inspect_raw_transaction_observations;
/// Private payload-free RAW transaction inspection reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::inspect_raw_transactions;
/// Private RAW transaction list reader consumed by the physical backend runtime. /// Private RAW transaction list reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::list_raw_transactions; pub(crate) use self::raw_transaction::list_raw_transactions;
/// Private atomic RAW transaction acquisition writer consumed by the physical backend runtime. /// Private atomic RAW transaction acquisition writer consumed by the physical backend runtime.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/raw_account.rs // file: crates/ksp-store-postgres-lib/src/raw_account.rs
// version: 5 // version: 8
pub(crate) mod cursor; pub(crate) mod cursor;
@@ -7,6 +7,10 @@ const GET_ACCOUNT_OBSERVATION_SQL: &str = "SELECT observation_key, account_pubke
const GET_ACCOUNT_STATE_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash, lamports::text AS lamports_text, owner, executable, rent_epoch::text AS rent_epoch_text, data FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3"; const GET_ACCOUNT_STATE_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash, lamports::text AS lamports_text, owner, executable, rent_epoch::text AS rent_epoch_text, data FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3";
const INSERT_ACCOUNT_OBSERVATION_SQL: &str = "INSERT INTO ksp_raw_account_observations (observation_key, account_pubkey, account_slot, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version) VALUES ($1, $2, $3::TEXT::NUMERIC, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19::TEXT::NUMERIC) ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key"; const INSERT_ACCOUNT_OBSERVATION_SQL: &str = "INSERT INTO ksp_raw_account_observations (observation_key, account_pubkey, account_slot, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version) VALUES ($1, $2, $3::TEXT::NUMERIC, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19::TEXT::NUMERIC) ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key";
const INSERT_ACCOUNT_STATE_SQL: &str = "INSERT INTO ksp_raw_account_states (pubkey, slot, state_hash, lamports, owner, executable, rent_epoch, data) VALUES ($1, $2::TEXT::NUMERIC, $3, $4::TEXT::NUMERIC, $5, $6, $7::TEXT::NUMERIC, $8) ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING pubkey"; const INSERT_ACCOUNT_STATE_SQL: &str = "INSERT INTO ksp_raw_account_states (pubkey, slot, state_hash, lamports, owner, executable, rent_epoch, data) VALUES ($1, $2::TEXT::NUMERIC, $3, $4::TEXT::NUMERIC, $5, $6, $7::TEXT::NUMERIC, $8) ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING pubkey";
const INSPECT_ACCOUNT_OBSERVATIONS_ASC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_account_observations WHERE ($1::BYTEA IS NULL OR (account_pubkey = $1 AND account_slot = $2::TEXT::NUMERIC AND account_state_hash = $3))), counts AS (SELECT filtered_count_text, CASE WHEN $1::BYTEA IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_account_observations) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.observation_key IS NOT NULL AS page_present, page.observation_key, page.account_pubkey, page.account_slot_text, page.account_state_hash, page.provider, page.protocol, page.acquisition_method, page.origin, page.received_at_unix_millis, page.capture_session_id, page.commitment, page.endpoint_id, page.filter_id, page.observed_at_unix_millis, page.source_payload_hash, page.source_payload_size_bytes, page.is_startup, page.transaction_signature, page.write_version_text FROM counts LEFT JOIN LATERAL (SELECT observation_key, account_pubkey, account_slot::TEXT AS account_slot_text, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version::TEXT AS write_version_text FROM ksp_raw_account_observations WHERE ($1::BYTEA IS NULL OR (account_pubkey = $1 AND account_slot = $2::TEXT::NUMERIC AND account_state_hash = $3)) ORDER BY received_at_unix_millis ASC, observation_key ASC LIMIT $4 OFFSET $5) AS page ON TRUE";
const INSPECT_ACCOUNT_OBSERVATIONS_DESC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_account_observations WHERE ($1::BYTEA IS NULL OR (account_pubkey = $1 AND account_slot = $2::TEXT::NUMERIC AND account_state_hash = $3))), counts AS (SELECT filtered_count_text, CASE WHEN $1::BYTEA IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_account_observations) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.observation_key IS NOT NULL AS page_present, page.observation_key, page.account_pubkey, page.account_slot_text, page.account_state_hash, page.provider, page.protocol, page.acquisition_method, page.origin, page.received_at_unix_millis, page.capture_session_id, page.commitment, page.endpoint_id, page.filter_id, page.observed_at_unix_millis, page.source_payload_hash, page.source_payload_size_bytes, page.is_startup, page.transaction_signature, page.write_version_text FROM counts LEFT JOIN LATERAL (SELECT observation_key, account_pubkey, account_slot::TEXT AS account_slot_text, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version::TEXT AS write_version_text FROM ksp_raw_account_observations WHERE ($1::BYTEA IS NULL OR (account_pubkey = $1 AND account_slot = $2::TEXT::NUMERIC AND account_state_hash = $3)) ORDER BY received_at_unix_millis DESC, observation_key DESC LIMIT $4 OFFSET $5) AS page ON TRUE";
const INSPECT_ACCOUNT_STATES_ASC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_account_states WHERE ($1::BYTEA IS NULL OR pubkey = $1::BYTEA) AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC)), counts AS (SELECT filtered_count_text, CASE WHEN $1::BYTEA IS NULL AND $2::TEXT IS NULL AND $3::TEXT IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_account_states) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.pubkey IS NOT NULL AS page_present, page.pubkey, page.slot_text, page.state_hash, page.lamports_text, page.owner, page.executable, page.rent_epoch_text, page.data_length_bytes FROM counts LEFT JOIN LATERAL (SELECT account_row.pubkey, account_row.slot::TEXT AS slot_text, account_row.state_hash, account_row.lamports::TEXT AS lamports_text, account_row.owner, account_row.executable, account_row.rent_epoch::TEXT AS rent_epoch_text, OCTET_LENGTH(account_row.data)::BIGINT AS data_length_bytes FROM ksp_raw_account_states AS account_row WHERE ($1::BYTEA IS NULL OR account_row.pubkey = $1::BYTEA) AND ($2::TEXT IS NULL OR account_row.slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR account_row.slot <= $3::TEXT::NUMERIC) ORDER BY account_row.slot ASC, account_row.pubkey ASC, account_row.state_hash ASC LIMIT $4 OFFSET $5) AS page ON TRUE";
const INSPECT_ACCOUNT_STATES_DESC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_account_states WHERE ($1::BYTEA IS NULL OR pubkey = $1::BYTEA) AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC)), counts AS (SELECT filtered_count_text, CASE WHEN $1::BYTEA IS NULL AND $2::TEXT IS NULL AND $3::TEXT IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_account_states) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.pubkey IS NOT NULL AS page_present, page.pubkey, page.slot_text, page.state_hash, page.lamports_text, page.owner, page.executable, page.rent_epoch_text, page.data_length_bytes FROM counts LEFT JOIN LATERAL (SELECT account_row.pubkey, account_row.slot::TEXT AS slot_text, account_row.state_hash, account_row.lamports::TEXT AS lamports_text, account_row.owner, account_row.executable, account_row.rent_epoch::TEXT AS rent_epoch_text, OCTET_LENGTH(account_row.data)::BIGINT AS data_length_bytes FROM ksp_raw_account_states AS account_row WHERE ($1::BYTEA IS NULL OR account_row.pubkey = $1::BYTEA) AND ($2::TEXT IS NULL OR account_row.slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR account_row.slot <= $3::TEXT::NUMERIC) ORDER BY account_row.slot DESC, account_row.pubkey DESC, account_row.state_hash DESC LIMIT $4 OFFSET $5) AS page ON TRUE";
const LIST_ACCOUNT_STATES_ASC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, pubkey, state_hash) > ($3::TEXT::NUMERIC, $4::BYTEA, $5::BYTEA)) ORDER BY slot ASC, pubkey ASC, state_hash ASC LIMIT $6"; const LIST_ACCOUNT_STATES_ASC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, pubkey, state_hash) > ($3::TEXT::NUMERIC, $4::BYTEA, $5::BYTEA)) ORDER BY slot ASC, pubkey ASC, state_hash ASC LIMIT $6";
const LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE pubkey = $1 AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC) AND ($4::TEXT IS NULL OR (slot, pubkey, state_hash) > ($4::TEXT::NUMERIC, $5::BYTEA, $6::BYTEA)) ORDER BY slot ASC, pubkey ASC, state_hash ASC LIMIT $7"; const LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE pubkey = $1 AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC) AND ($4::TEXT IS NULL OR (slot, pubkey, state_hash) > ($4::TEXT::NUMERIC, $5::BYTEA, $6::BYTEA)) ORDER BY slot ASC, pubkey ASC, state_hash ASC LIMIT $7";
const LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE pubkey = $1 AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC) AND ($4::TEXT IS NULL OR (slot, pubkey, state_hash) < ($4::TEXT::NUMERIC, $5::BYTEA, $6::BYTEA)) ORDER BY slot DESC, pubkey DESC, state_hash DESC LIMIT $7"; const LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE pubkey = $1 AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC) AND ($4::TEXT IS NULL OR (slot, pubkey, state_hash) < ($4::TEXT::NUMERIC, $5::BYTEA, $6::BYTEA)) ORDER BY slot DESC, pubkey DESC, state_hash DESC LIMIT $7";
@@ -22,6 +26,45 @@ struct RawAccountListDbRow {
state_hash: std::vec::Vec<u8>, state_hash: std::vec::Vec<u8>,
} }
struct RawAccountInspectionDbRow {
data_length_bytes: std::option::Option<i64>,
executable: std::option::Option<bool>,
filtered_count_text: std::string::String,
lamports_text: std::option::Option<std::string::String>,
owner: std::option::Option<std::vec::Vec<u8>>,
page_present: bool,
pubkey: std::option::Option<std::vec::Vec<u8>>,
rent_epoch_text: std::option::Option<std::string::String>,
slot_text: std::option::Option<std::string::String>,
state_hash: std::option::Option<std::vec::Vec<u8>>,
total_count_text: std::string::String,
}
struct RawAccountObservationInspectionDbRow {
account_pubkey: std::option::Option<std::vec::Vec<u8>>,
account_slot_text: std::option::Option<std::string::String>,
account_state_hash: std::option::Option<std::vec::Vec<u8>>,
acquisition_method: std::option::Option<std::string::String>,
capture_session_id: std::option::Option<std::string::String>,
commitment: std::option::Option<std::string::String>,
endpoint_id: std::option::Option<std::string::String>,
filter_id: std::option::Option<std::string::String>,
filtered_count_text: std::string::String,
is_startup: std::option::Option<bool>,
observation_key: std::option::Option<std::vec::Vec<u8>>,
observed_at_unix_millis: std::option::Option<i64>,
origin: std::option::Option<std::string::String>,
page_present: bool,
protocol: std::option::Option<std::string::String>,
provider: std::option::Option<std::string::String>,
received_at_unix_millis: std::option::Option<i64>,
source_payload_hash: std::option::Option<std::vec::Vec<u8>>,
source_payload_size_bytes: std::option::Option<i64>,
total_count_text: std::string::String,
transaction_signature: std::option::Option<std::vec::Vec<u8>>,
write_version_text: std::option::Option<std::string::String>,
}
struct RawAccountObservationDbRow { struct RawAccountObservationDbRow {
account_pubkey: std::vec::Vec<u8>, account_pubkey: std::vec::Vec<u8>,
account_slot_text: std::string::String, account_slot_text: std::string::String,
@@ -203,6 +246,219 @@ pub(crate) async fn list_raw_account_states(
return std::result::Result::Ok(ksp_store_api::RawPage::new(items, next_cursor)); return std::result::Result::Ok(ksp_store_api::RawPage::new(items, next_cursor));
} }
/// Inspects one data-free random-access RAW account-state window with exact logical counts.
pub(crate) async fn inspect_raw_account_states(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
query: &ksp_store_api::RawAccountStateInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountStateSummary>, crate::PostgresBackendError> {
if query.network() != network {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WrongNetwork, "raw_account_inspection_network"));
}
let sql = match query.direction() {
ksp_store_api::RawSortDirection::Ascending => INSPECT_ACCOUNT_STATES_ASC_SQL,
ksp_store_api::RawSortDirection::Descending => INSPECT_ACCOUNT_STATES_DESC_SQL,
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::QueryInvalid,
"raw_account_inspection_direction",
));
},
};
let (sql_limit, sql_offset) = match raw_account_inspection_sql_window(query.page()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let client_result = pool.get().await;
let client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let pubkey_bytes = query.pubkey().map(|value| return value.to_bytes().to_vec());
let slots = query.slots();
let start_text = slots.start_inclusive().map(|value| return value.to_string());
let end_text = slots.end_inclusive().map(|value| return value.to_string());
let rows_result = client.query(sql, &[&pubkey_bytes, &start_text, &end_text, &sql_limit, &sql_offset]).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_account_inspection_query"));
},
};
if rows.is_empty() {
return std::result::Result::Err(data_invalid("raw_account_inspection_cardinality"));
}
let row_count = rows.len();
let mut total_items = std::option::Option::None;
let mut filtered_items = std::option::Option::None;
let mut items = std::vec::Vec::new();
for row in rows {
let physical = match raw_account_inspection_db_row(&row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_total = match decode_u64_decimal(physical.total_count_text.as_str(), "raw_account_inspection_total") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_filtered = match decode_u64_decimal(physical.filtered_count_text.as_str(), "raw_account_inspection_filtered") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match (total_items, filtered_items) {
(std::option::Option::None, std::option::Option::None) => {
total_items = std::option::Option::Some(row_total);
filtered_items = std::option::Option::Some(row_filtered);
},
(std::option::Option::Some(total), std::option::Option::Some(filtered)) if total == row_total && filtered == row_filtered => {},
_ => return std::result::Result::Err(data_invalid("raw_account_inspection_counts")),
}
if physical.page_present {
let summary = match decode_raw_account_inspection_summary(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
items.push(summary);
} else if row_count != 1 || !raw_account_inspection_empty_page_is_clean(&physical) {
return std::result::Result::Err(data_invalid("raw_account_inspection_page"));
}
}
let total_items = match total_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_inspection_total")),
};
let filtered_items = match filtered_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_inspection_filtered")),
};
let item_count = match u64::try_from(items.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_page")),
};
if item_count > query.page().limit().get() {
return std::result::Result::Err(data_invalid("raw_account_inspection_page"));
}
return match ksp_store_api::RawInspectionPage::try_new(items, total_items, filtered_items) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_account_inspection_page")),
};
}
/// Inspects one safe random-access RAW account-observation window with exact counts.
pub(crate) async fn inspect_raw_account_observations(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
query: &ksp_store_api::RawAccountObservationInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountObservationSummary>, crate::PostgresBackendError> {
if query.network() != network {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::WrongNetwork,
"raw_account_observation_inspection_network",
));
}
let sql = match query.direction() {
ksp_store_api::RawSortDirection::Ascending => INSPECT_ACCOUNT_OBSERVATIONS_ASC_SQL,
ksp_store_api::RawSortDirection::Descending => INSPECT_ACCOUNT_OBSERVATIONS_DESC_SQL,
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::QueryInvalid,
"raw_account_observation_inspection_direction",
));
},
};
let (sql_limit, sql_offset) = match raw_account_inspection_sql_window(query.page()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let account_pubkey = query.account().map(|reference| return reference.pubkey().to_bytes().to_vec());
let account_slot_text = query.account().map(|reference| return reference.slot().to_string());
let account_state_hash = query.account().map(|reference| return reference.state_hash().as_bytes().to_vec());
let client_result = pool.get().await;
let client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let rows_result = client.query(sql, &[&account_pubkey, &account_slot_text, &account_state_hash, &sql_limit, &sql_offset]).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::ReadFailed,
"raw_account_observation_inspection_query",
));
},
};
if rows.is_empty() {
return std::result::Result::Err(data_invalid("raw_account_observation_inspection_cardinality"));
}
let row_count = rows.len();
let mut total_items = std::option::Option::None;
let mut filtered_items = std::option::Option::None;
let mut items = std::vec::Vec::new();
for row in rows {
let physical = match raw_account_observation_inspection_db_row(&row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_total = match decode_u64_decimal(physical.total_count_text.as_str(), "raw_account_observation_inspection_total") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_filtered = match decode_u64_decimal(physical.filtered_count_text.as_str(), "raw_account_observation_inspection_filtered") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match (total_items, filtered_items) {
(std::option::Option::None, std::option::Option::None) => {
total_items = std::option::Option::Some(row_total);
filtered_items = std::option::Option::Some(row_filtered);
},
(std::option::Option::Some(total), std::option::Option::Some(filtered)) if total == row_total && filtered == row_filtered => {},
_ => return std::result::Result::Err(data_invalid("raw_account_observation_inspection_counts")),
}
if physical.page_present {
let observation = match decode_raw_account_observation_inspection_row(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(reference) = query.account()
&& observation.account() != reference
{
return std::result::Result::Err(data_invalid("raw_account_observation_inspection_reference"));
}
items.push(ksp_store_api::RawAccountObservationSummary::new(
observation.observation_key(),
observation.account().clone(),
observation.provenance().clone(),
observation.is_startup(),
observation.transaction_signature(),
observation.write_version(),
));
} else if row_count != 1 || !raw_account_observation_inspection_empty_page_is_clean(&physical) {
return std::result::Result::Err(data_invalid("raw_account_observation_inspection_page"));
}
}
let total_items = match total_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_observation_inspection_total")),
};
let filtered_items = match filtered_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_observation_inspection_filtered")),
};
let item_count = match u64::try_from(items.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_inspection_page")),
};
if item_count > query.page().limit().get() {
return std::result::Result::Err(data_invalid("raw_account_observation_inspection_page"));
}
return match ksp_store_api::RawInspectionPage::try_new(items, total_items, filtered_items) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_account_observation_inspection_page")),
};
}
/// Reads one persisted RAW account observation by producer-owned idempotence key. /// Reads one persisted RAW account observation by producer-owned idempotence key.
pub(crate) async fn get_raw_account_observation( pub(crate) async fn get_raw_account_observation(
pool: &deadpool_postgres::Pool, pool: &deadpool_postgres::Pool,
@@ -381,6 +637,141 @@ fn decode_raw_account_list_row(
return std::result::Result::Ok((slot, reference)); return std::result::Result::Ok((slot, reference));
} }
fn raw_account_observation_inspection_empty_page_is_clean(row: &RawAccountObservationInspectionDbRow) -> bool {
return row.account_pubkey.is_none()
&& row.account_slot_text.is_none()
&& row.account_state_hash.is_none()
&& row.acquisition_method.is_none()
&& row.capture_session_id.is_none()
&& row.commitment.is_none()
&& row.endpoint_id.is_none()
&& row.filter_id.is_none()
&& row.is_startup.is_none()
&& row.observation_key.is_none()
&& row.observed_at_unix_millis.is_none()
&& row.origin.is_none()
&& row.protocol.is_none()
&& row.provider.is_none()
&& row.received_at_unix_millis.is_none()
&& row.source_payload_hash.is_none()
&& row.source_payload_size_bytes.is_none()
&& row.transaction_signature.is_none()
&& row.write_version_text.is_none();
}
fn raw_account_observation_inspection_db_row(
row: &tokio_postgres::Row,
) -> std::result::Result<RawAccountObservationInspectionDbRow, crate::PostgresBackendError> {
macro_rules! get_optional {
($name:literal, $ty:ty) => {
match row.try_get::<_, std::option::Option<$ty>>($name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_inspection_decode")),
}
};
}
let filtered_count_text = match row.try_get::<_, std::string::String>("filtered_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_inspection_decode")),
};
let page_present = match row.try_get::<_, bool>("page_present") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_inspection_decode")),
};
let total_count_text = match row.try_get::<_, std::string::String>("total_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_inspection_decode")),
};
return std::result::Result::Ok(RawAccountObservationInspectionDbRow {
account_pubkey: get_optional!("account_pubkey", std::vec::Vec<u8>),
account_slot_text: get_optional!("account_slot_text", std::string::String),
account_state_hash: get_optional!("account_state_hash", std::vec::Vec<u8>),
acquisition_method: get_optional!("acquisition_method", std::string::String),
capture_session_id: get_optional!("capture_session_id", std::string::String),
commitment: get_optional!("commitment", std::string::String),
endpoint_id: get_optional!("endpoint_id", std::string::String),
filter_id: get_optional!("filter_id", std::string::String),
filtered_count_text,
is_startup: get_optional!("is_startup", bool),
observation_key: get_optional!("observation_key", std::vec::Vec<u8>),
observed_at_unix_millis: get_optional!("observed_at_unix_millis", i64),
origin: get_optional!("origin", std::string::String),
page_present,
protocol: get_optional!("protocol", std::string::String),
provider: get_optional!("provider", std::string::String),
received_at_unix_millis: get_optional!("received_at_unix_millis", i64),
source_payload_hash: get_optional!("source_payload_hash", std::vec::Vec<u8>),
source_payload_size_bytes: get_optional!("source_payload_size_bytes", i64),
total_count_text,
transaction_signature: get_optional!("transaction_signature", std::vec::Vec<u8>),
write_version_text: get_optional!("write_version_text", std::string::String),
});
}
fn decode_raw_account_observation_inspection_row(
network: &ksp_store_api::RawNetworkId,
row: RawAccountObservationInspectionDbRow,
) -> std::result::Result<ksp_store_api::RawAccountObservation, crate::PostgresBackendError> {
let account_pubkey = match inspection_required(row.account_pubkey, "raw_account_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let account_slot_text = match inspection_required(row.account_slot_text, "raw_account_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let account_state_hash = match inspection_required(row.account_state_hash, "raw_account_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let acquisition_method = match inspection_required(row.acquisition_method, "raw_account_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let observation_key = match inspection_required(row.observation_key, "raw_account_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let origin = match inspection_required(row.origin, "raw_account_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let protocol = match inspection_required(row.protocol, "raw_account_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider = match inspection_required(row.provider, "raw_account_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let received_at_unix_millis = match inspection_required(row.received_at_unix_millis, "raw_account_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let physical = RawAccountObservationDbRow {
account_pubkey,
account_slot_text,
account_state_hash,
acquisition_method,
capture_session_id: row.capture_session_id,
commitment: row.commitment,
endpoint_id: row.endpoint_id,
filter_id: row.filter_id,
is_startup: row.is_startup,
observation_key,
observed_at_unix_millis: row.observed_at_unix_millis,
origin,
protocol,
provider,
received_at_unix_millis,
source_payload_hash: row.source_payload_hash,
source_payload_size_bytes: row.source_payload_size_bytes,
transaction_signature: row.transaction_signature,
write_version_text: row.write_version_text,
};
return decode_raw_account_observation_row(network, physical);
}
fn raw_account_observation_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountObservationDbRow, crate::PostgresBackendError> { fn raw_account_observation_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountObservationDbRow, crate::PostgresBackendError> {
let account_pubkey: std::vec::Vec<u8> = match row.try_get("account_pubkey") { let account_pubkey: std::vec::Vec<u8> = match row.try_get("account_pubkey") {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -913,6 +1304,177 @@ fn decode_timestamp_i64(value: i64, phase: &'static str) -> std::result::Result<
}; };
} }
fn raw_account_inspection_empty_page_is_clean(row: &RawAccountInspectionDbRow) -> bool {
return !row.page_present
&& row.data_length_bytes.is_none()
&& row.executable.is_none()
&& row.lamports_text.is_none()
&& row.owner.is_none()
&& row.pubkey.is_none()
&& row.rent_epoch_text.is_none()
&& row.slot_text.is_none()
&& row.state_hash.is_none();
}
fn raw_account_inspection_sql_window(page: ksp_store_api::RawInspectionPageRequest) -> std::result::Result<(i64, i64), crate::PostgresBackendError> {
let sql_limit = match i64::try_from(page.limit().get()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::PageLimitUnsupported,
"raw_account_inspection_limit",
));
},
};
let sql_offset = match i64::try_from(page.offset()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::QueryInvalid, "raw_account_inspection_offset"));
},
};
return std::result::Result::Ok((sql_limit, sql_offset));
}
fn raw_account_inspection_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountInspectionDbRow, crate::PostgresBackendError> {
let data_length_bytes = match row.try_get::<_, std::option::Option<i64>>("data_length_bytes") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let executable = match row.try_get::<_, std::option::Option<bool>>("executable") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let filtered_count_text = match row.try_get::<_, std::string::String>("filtered_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let lamports_text = match row.try_get::<_, std::option::Option<std::string::String>>("lamports_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let owner = match row.try_get::<_, std::option::Option<std::vec::Vec<u8>>>("owner") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let page_present = match row.try_get::<_, bool>("page_present") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let pubkey = match row.try_get::<_, std::option::Option<std::vec::Vec<u8>>>("pubkey") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let rent_epoch_text = match row.try_get::<_, std::option::Option<std::string::String>>("rent_epoch_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let slot_text = match row.try_get::<_, std::option::Option<std::string::String>>("slot_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let state_hash = match row.try_get::<_, std::option::Option<std::vec::Vec<u8>>>("state_hash") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let total_count_text = match row.try_get::<_, std::string::String>("total_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
return std::result::Result::Ok(RawAccountInspectionDbRow {
data_length_bytes,
executable,
filtered_count_text,
lamports_text,
owner,
page_present,
pubkey,
rent_epoch_text,
slot_text,
state_hash,
total_count_text,
});
}
fn decode_raw_account_inspection_summary(
network: &ksp_store_api::RawNetworkId,
row: RawAccountInspectionDbRow,
) -> std::result::Result<ksp_store_api::RawAccountStateSummary, crate::PostgresBackendError> {
if !row.page_present {
return std::result::Result::Err(data_invalid("raw_account_inspection_page"));
}
let pubkey = match inspection_required(row.pubkey, "raw_account_inspection_pubkey") {
std::result::Result::Ok(value) => match fixed_bytes::<32>(value, "raw_account_inspection_pubkey") {
std::result::Result::Ok(bytes) => ksp_store_api::Pubkey::new_from_array(bytes),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot_text = match inspection_required(row.slot_text, "raw_account_inspection_slot") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot = match decode_u64_decimal(slot_text.as_str(), "raw_account_inspection_slot") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let state_hash_raw = match inspection_required(row.state_hash, "raw_account_inspection_state_hash") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let state_hash = match fixed_bytes::<32>(state_hash_raw, "raw_account_inspection_state_hash") {
std::result::Result::Ok(value) => ksp_store_api::RawContentHash::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let lamports_text = match inspection_required(row.lamports_text, "raw_account_inspection_lamports") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let lamports = match decode_u64_decimal(lamports_text.as_str(), "raw_account_inspection_lamports") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner_raw = match inspection_required(row.owner, "raw_account_inspection_owner") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner = match fixed_bytes::<32>(owner_raw, "raw_account_inspection_owner") {
std::result::Result::Ok(value) => ksp_store_api::Pubkey::new_from_array(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let executable = match inspection_required(row.executable, "raw_account_inspection_executable") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rent_epoch_text = match inspection_required(row.rent_epoch_text, "raw_account_inspection_rent_epoch") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rent_epoch = match decode_u64_decimal(rent_epoch_text.as_str(), "raw_account_inspection_rent_epoch") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let data_length_raw = match inspection_required(row.data_length_bytes, "raw_account_inspection_data_length") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let data_length_bytes = match u64::try_from(data_length_raw) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_data_length")),
};
let reference = ksp_store_api::RawAccountStateReference::new(network.clone(), pubkey, slot, state_hash);
return match ksp_store_api::RawAccountStateSummary::try_new(reference, lamports, owner, executable, rent_epoch, data_length_bytes) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_account_inspection_summary")),
};
}
fn inspection_required<T>(value: std::option::Option<T>, phase: &'static str) -> std::result::Result<T, crate::PostgresBackendError> {
return match value {
std::option::Option::Some(inner) => std::result::Result::Ok(inner),
std::option::Option::None => std::result::Result::Err(data_invalid(phase)),
};
}
fn decode_u64_decimal(value: &str, phase: &'static str) -> std::result::Result<u64, crate::PostgresBackendError> { fn decode_u64_decimal(value: &str, phase: &'static str) -> std::result::Result<u64, crate::PostgresBackendError> {
return match value.parse::<u64>() { return match value.parse::<u64>() {
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded), std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/raw_transaction.rs // file: crates/ksp-store-postgres-lib/src/raw_transaction.rs
// version: 4 // version: 7
pub(crate) mod cursor; pub(crate) mod cursor;
@@ -12,6 +12,10 @@ const GET_TRANSACTION_SQL: &str = "SELECT transaction_row.signature, transaction
const INSERT_ARCHIVE_PAYLOAD_SQL: &str = "INSERT INTO ksp_raw_transaction_archive_payloads (signature, payload) VALUES ($1, $2)"; const INSERT_ARCHIVE_PAYLOAD_SQL: &str = "INSERT INTO ksp_raw_transaction_archive_payloads (signature, payload) VALUES ($1, $2)";
const INSERT_OBSERVATION_SQL: &str = "INSERT INTO ksp_raw_transaction_observations (observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key"; const INSERT_OBSERVATION_SQL: &str = "INSERT INTO ksp_raw_transaction_observations (observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key";
const INSERT_TRANSACTION_SQL: &str = "INSERT INTO ksp_raw_transactions (signature, slot, block_time_unix_millis, format_id, format_version, content_hash, payload, retention_state) VALUES ($1, $2::TEXT::NUMERIC, $3, $4, $5, $6, $7, 'full') ON CONFLICT (signature) DO NOTHING RETURNING signature"; const INSERT_TRANSACTION_SQL: &str = "INSERT INTO ksp_raw_transactions (signature, slot, block_time_unix_millis, format_id, format_version, content_hash, payload, retention_state) VALUES ($1, $2::TEXT::NUMERIC, $3, $4, $5, $6, $7, 'full') ON CONFLICT (signature) DO NOTHING RETURNING signature";
const INSPECT_OBSERVATIONS_ASC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_transaction_observations WHERE ($1::BYTEA IS NULL OR transaction_signature = $1)), counts AS (SELECT filtered_count_text, CASE WHEN $1::BYTEA IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_transaction_observations) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.observation_key IS NOT NULL AS page_present, page.observation_key, page.transaction_signature, page.provider, page.protocol, page.acquisition_method, page.origin, page.received_at_unix_millis, page.capture_session_id, page.commitment, page.endpoint_id, page.filter_id, page.observed_at_unix_millis, page.source_payload_hash, page.source_payload_size_bytes FROM counts LEFT JOIN LATERAL (SELECT observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes FROM ksp_raw_transaction_observations WHERE ($1::BYTEA IS NULL OR transaction_signature = $1) ORDER BY received_at_unix_millis ASC, observation_key ASC LIMIT $2 OFFSET $3) AS page ON TRUE";
const INSPECT_OBSERVATIONS_DESC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_transaction_observations WHERE ($1::BYTEA IS NULL OR transaction_signature = $1)), counts AS (SELECT filtered_count_text, CASE WHEN $1::BYTEA IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_transaction_observations) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.observation_key IS NOT NULL AS page_present, page.observation_key, page.transaction_signature, page.provider, page.protocol, page.acquisition_method, page.origin, page.received_at_unix_millis, page.capture_session_id, page.commitment, page.endpoint_id, page.filter_id, page.observed_at_unix_millis, page.source_payload_hash, page.source_payload_size_bytes FROM counts LEFT JOIN LATERAL (SELECT observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes FROM ksp_raw_transaction_observations WHERE ($1::BYTEA IS NULL OR transaction_signature = $1) ORDER BY received_at_unix_millis DESC, observation_key DESC LIMIT $2 OFFSET $3) AS page ON TRUE";
const INSPECT_TRANSACTIONS_ASC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_transactions WHERE ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC)), counts AS (SELECT filtered_count_text, CASE WHEN $1::TEXT IS NULL AND $2::TEXT IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_transactions) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.signature IS NOT NULL AS page_present, page.signature, page.slot_text, page.block_time_unix_millis, page.format_id, page.format_version, page.content_hash, page.retention_state, page.payload_size_bytes, page.hot_payload_present, page.archive_payload_present FROM counts LEFT JOIN LATERAL (SELECT transaction_row.signature, transaction_row.slot::TEXT AS slot_text, transaction_row.block_time_unix_millis, transaction_row.format_id, transaction_row.format_version, transaction_row.content_hash, transaction_row.retention_state, CASE WHEN transaction_row.retention_state = 'full' THEN OCTET_LENGTH(transaction_row.payload)::BIGINT WHEN transaction_row.retention_state = 'archived' THEN OCTET_LENGTH(archive_row.payload)::BIGINT ELSE NULL END AS payload_size_bytes, transaction_row.payload IS NOT NULL AS hot_payload_present, archive_row.payload IS NOT NULL AS archive_payload_present FROM ksp_raw_transactions AS transaction_row LEFT JOIN ksp_raw_transaction_archive_payloads AS archive_row ON archive_row.signature = transaction_row.signature WHERE ($1::TEXT IS NULL OR transaction_row.slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR transaction_row.slot <= $2::TEXT::NUMERIC) ORDER BY transaction_row.slot ASC, transaction_row.signature ASC LIMIT $3 OFFSET $4) AS page ON TRUE";
const INSPECT_TRANSACTIONS_DESC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_transactions WHERE ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC)), counts AS (SELECT filtered_count_text, CASE WHEN $1::TEXT IS NULL AND $2::TEXT IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_transactions) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.signature IS NOT NULL AS page_present, page.signature, page.slot_text, page.block_time_unix_millis, page.format_id, page.format_version, page.content_hash, page.retention_state, page.payload_size_bytes, page.hot_payload_present, page.archive_payload_present FROM counts LEFT JOIN LATERAL (SELECT transaction_row.signature, transaction_row.slot::TEXT AS slot_text, transaction_row.block_time_unix_millis, transaction_row.format_id, transaction_row.format_version, transaction_row.content_hash, transaction_row.retention_state, CASE WHEN transaction_row.retention_state = 'full' THEN OCTET_LENGTH(transaction_row.payload)::BIGINT WHEN transaction_row.retention_state = 'archived' THEN OCTET_LENGTH(archive_row.payload)::BIGINT ELSE NULL END AS payload_size_bytes, transaction_row.payload IS NOT NULL AS hot_payload_present, archive_row.payload IS NOT NULL AS archive_payload_present FROM ksp_raw_transactions AS transaction_row LEFT JOIN ksp_raw_transaction_archive_payloads AS archive_row ON archive_row.signature = transaction_row.signature WHERE ($1::TEXT IS NULL OR transaction_row.slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR transaction_row.slot <= $2::TEXT::NUMERIC) ORDER BY transaction_row.slot DESC, transaction_row.signature DESC LIMIT $3 OFFSET $4) AS page ON TRUE";
const LIST_TRANSACTIONS_ASC_SQL: &str = "SELECT signature, slot::text AS slot_text FROM ksp_raw_transactions WHERE retention_state <> 'purged' AND ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, signature) > ($3::TEXT::NUMERIC, $4::BYTEA)) ORDER BY slot ASC, signature ASC LIMIT $5"; const LIST_TRANSACTIONS_ASC_SQL: &str = "SELECT signature, slot::text AS slot_text FROM ksp_raw_transactions WHERE retention_state <> 'purged' AND ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, signature) > ($3::TEXT::NUMERIC, $4::BYTEA)) ORDER BY slot ASC, signature ASC LIMIT $5";
const LIST_TRANSACTIONS_DESC_SQL: &str = "SELECT signature, slot::text AS slot_text FROM ksp_raw_transactions WHERE retention_state <> 'purged' AND ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, signature) < ($3::TEXT::NUMERIC, $4::BYTEA)) ORDER BY slot DESC, signature DESC LIMIT $5"; const LIST_TRANSACTIONS_DESC_SQL: &str = "SELECT signature, slot::text AS slot_text FROM ksp_raw_transactions WHERE retention_state <> 'purged' AND ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, signature) < ($3::TEXT::NUMERIC, $4::BYTEA)) ORDER BY slot DESC, signature DESC LIMIT $5";
const LOCK_OBSERVATION_SQL: &str = "SELECT observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes FROM ksp_raw_transaction_observations WHERE observation_key = $1 FOR UPDATE"; const LOCK_OBSERVATION_SQL: &str = "SELECT observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes FROM ksp_raw_transaction_observations WHERE observation_key = $1 FOR UPDATE";
@@ -30,6 +34,42 @@ struct RawListDbRow {
slot_text: std::string::String, slot_text: std::string::String,
} }
struct RawInspectionDbRow {
archive_payload_present: std::option::Option<bool>,
block_time_unix_millis: std::option::Option<i64>,
content_hash: std::option::Option<std::vec::Vec<u8>>,
filtered_count_text: std::string::String,
format_id: std::option::Option<std::string::String>,
format_version: std::option::Option<i64>,
hot_payload_present: std::option::Option<bool>,
page_present: bool,
payload_size_bytes: std::option::Option<i64>,
retention_state: std::option::Option<std::string::String>,
signature: std::option::Option<std::vec::Vec<u8>>,
slot_text: std::option::Option<std::string::String>,
total_count_text: std::string::String,
}
struct RawObservationInspectionDbRow {
acquisition_method: std::option::Option<std::string::String>,
capture_session_id: std::option::Option<std::string::String>,
commitment: std::option::Option<std::string::String>,
endpoint_id: std::option::Option<std::string::String>,
filter_id: std::option::Option<std::string::String>,
filtered_count_text: std::string::String,
observation_key: std::option::Option<std::vec::Vec<u8>>,
observed_at_unix_millis: std::option::Option<i64>,
origin: std::option::Option<std::string::String>,
page_present: bool,
protocol: std::option::Option<std::string::String>,
provider: std::option::Option<std::string::String>,
received_at_unix_millis: std::option::Option<i64>,
source_payload_hash: std::option::Option<std::vec::Vec<u8>>,
source_payload_size_bytes: std::option::Option<i64>,
total_count_text: std::string::String,
transaction_signature: std::option::Option<std::vec::Vec<u8>>,
}
struct RawObservationDbRow { struct RawObservationDbRow {
acquisition_method: std::string::String, acquisition_method: std::string::String,
capture_session_id: std::option::Option<std::string::String>, capture_session_id: std::option::Option<std::string::String>,
@@ -192,6 +232,213 @@ pub(crate) async fn list_raw_transactions(
return std::result::Result::Ok(ksp_store_api::RawPage::new(items, next_cursor)); return std::result::Result::Ok(ksp_store_api::RawPage::new(items, next_cursor));
} }
/// Inspects one payload-free random-access RAW transaction window with exact counts.
pub(crate) async fn inspect_raw_transactions(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
query: &ksp_store_api::RawTransactionInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionSummary>, crate::PostgresBackendError> {
if query.network() != network {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WrongNetwork, "raw_transaction_inspection_network"));
}
let sql = match query.direction() {
ksp_store_api::RawSortDirection::Ascending => INSPECT_TRANSACTIONS_ASC_SQL,
ksp_store_api::RawSortDirection::Descending => INSPECT_TRANSACTIONS_DESC_SQL,
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::QueryInvalid,
"raw_transaction_inspection_direction",
));
},
};
let (sql_limit, sql_offset) = match raw_transaction_inspection_sql_window(query.page()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let client_result = pool.get().await;
let client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let slots = query.slots();
let start_text = slots.start_inclusive().map(|value| return value.to_string());
let end_text = slots.end_inclusive().map(|value| return value.to_string());
let rows_result = client.query(sql, &[&start_text, &end_text, &sql_limit, &sql_offset]).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_transaction_inspection_query"));
},
};
if rows.is_empty() {
return std::result::Result::Err(data_invalid("raw_transaction_inspection_cardinality"));
}
let row_count = rows.len();
let mut total_items = std::option::Option::None;
let mut filtered_items = std::option::Option::None;
let mut items = std::vec::Vec::new();
for row in rows {
let physical = match raw_inspection_db_row(&row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_total = match decode_u64_decimal(physical.total_count_text.as_str(), "raw_transaction_inspection_total") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_filtered = match decode_u64_decimal(physical.filtered_count_text.as_str(), "raw_transaction_inspection_filtered") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match (total_items, filtered_items) {
(std::option::Option::None, std::option::Option::None) => {
total_items = std::option::Option::Some(row_total);
filtered_items = std::option::Option::Some(row_filtered);
},
(std::option::Option::Some(total), std::option::Option::Some(filtered)) if total == row_total && filtered == row_filtered => {},
_ => return std::result::Result::Err(data_invalid("raw_transaction_inspection_counts")),
}
if physical.page_present {
let summary = match decode_raw_inspection_summary(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
items.push(summary);
} else if row_count != 1 || !raw_inspection_empty_page_is_clean(&physical) {
return std::result::Result::Err(data_invalid("raw_transaction_inspection_page"));
}
}
let total_items = match total_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_transaction_inspection_total")),
};
let filtered_items = match filtered_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_transaction_inspection_filtered")),
};
let item_count = match u64::try_from(items.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_page")),
};
if item_count > query.page().limit().get() {
return std::result::Result::Err(data_invalid("raw_transaction_inspection_page"));
}
return match ksp_store_api::RawInspectionPage::try_new(items, total_items, filtered_items) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_transaction_inspection_page")),
};
}
/// Inspects one safe random-access RAW transaction-observation window with exact counts.
pub(crate) async fn inspect_raw_transaction_observations(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
query: &ksp_store_api::RawTransactionObservationInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionObservationSummary>, crate::PostgresBackendError> {
if query.network() != network {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::WrongNetwork,
"raw_transaction_observation_inspection_network",
));
}
let sql = match query.direction() {
ksp_store_api::RawSortDirection::Ascending => INSPECT_OBSERVATIONS_ASC_SQL,
ksp_store_api::RawSortDirection::Descending => INSPECT_OBSERVATIONS_DESC_SQL,
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::QueryInvalid,
"raw_transaction_observation_inspection_direction",
));
},
};
let (sql_limit, sql_offset) = match raw_transaction_inspection_sql_window(query.page()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let signature = query.transaction().map(|reference| return reference.signature().as_bytes().to_vec());
let client_result = pool.get().await;
let client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let rows_result = client.query(sql, &[&signature, &sql_limit, &sql_offset]).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::ReadFailed,
"raw_transaction_observation_inspection_query",
));
},
};
if rows.is_empty() {
return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_cardinality"));
}
let row_count = rows.len();
let mut total_items = std::option::Option::None;
let mut filtered_items = std::option::Option::None;
let mut items = std::vec::Vec::new();
for row in rows {
let physical = match raw_observation_inspection_db_row(&row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_total = match decode_u64_decimal(physical.total_count_text.as_str(), "raw_transaction_observation_inspection_total") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_filtered = match decode_u64_decimal(physical.filtered_count_text.as_str(), "raw_transaction_observation_inspection_filtered") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match (total_items, filtered_items) {
(std::option::Option::None, std::option::Option::None) => {
total_items = std::option::Option::Some(row_total);
filtered_items = std::option::Option::Some(row_filtered);
},
(std::option::Option::Some(total), std::option::Option::Some(filtered)) if total == row_total && filtered == row_filtered => {},
_ => return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_counts")),
}
if physical.page_present {
let observation = match decode_raw_observation_inspection_row(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(reference) = query.transaction()
&& observation.transaction() != reference
{
return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_reference"));
}
items.push(ksp_store_api::RawTransactionObservationSummary::new(
observation.observation_key(),
observation.transaction().clone(),
observation.provenance().clone(),
));
} else if row_count != 1 || !raw_observation_inspection_empty_page_is_clean(&physical) {
return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_page"));
}
}
let total_items = match total_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_total")),
};
let filtered_items = match filtered_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_filtered")),
};
let item_count = match u64::try_from(items.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_page")),
};
if item_count > query.page().limit().get() {
return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_page"));
}
return match ksp_store_api::RawInspectionPage::try_new(items, total_items, filtered_items) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_page")),
};
}
/// Reads one RAW transaction observation from the physical PostgreSQL backend. /// Reads one RAW transaction observation from the physical PostgreSQL backend.
pub(crate) async fn get_raw_transaction_observation( pub(crate) async fn get_raw_transaction_observation(
pool: &deadpool_postgres::Pool, pool: &deadpool_postgres::Pool,
@@ -1009,6 +1256,221 @@ fn write_failed(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WriteFailed, phase); return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WriteFailed, phase);
} }
fn raw_inspection_empty_page_is_clean(row: &RawInspectionDbRow) -> bool {
return !row.page_present
&& row.archive_payload_present.is_none()
&& row.block_time_unix_millis.is_none()
&& row.content_hash.is_none()
&& row.format_id.is_none()
&& row.format_version.is_none()
&& row.hot_payload_present.is_none()
&& row.payload_size_bytes.is_none()
&& row.retention_state.is_none()
&& row.signature.is_none()
&& row.slot_text.is_none();
}
fn raw_transaction_inspection_sql_window(page: ksp_store_api::RawInspectionPageRequest) -> std::result::Result<(i64, i64), crate::PostgresBackendError> {
let sql_limit = match i64::try_from(page.limit().get()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::PageLimitUnsupported,
"raw_transaction_inspection_limit",
));
},
};
let sql_offset = match i64::try_from(page.offset()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::QueryInvalid,
"raw_transaction_inspection_offset",
));
},
};
return std::result::Result::Ok((sql_limit, sql_offset));
}
fn raw_inspection_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawInspectionDbRow, crate::PostgresBackendError> {
let archive_payload_present = match row.try_get::<_, std::option::Option<bool>>("archive_payload_present") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let block_time_unix_millis = match row.try_get::<_, std::option::Option<i64>>("block_time_unix_millis") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let content_hash = match row.try_get::<_, std::option::Option<std::vec::Vec<u8>>>("content_hash") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let filtered_count_text = match row.try_get::<_, std::string::String>("filtered_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let format_id = match row.try_get::<_, std::option::Option<std::string::String>>("format_id") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let format_version = match row.try_get::<_, std::option::Option<i64>>("format_version") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let hot_payload_present = match row.try_get::<_, std::option::Option<bool>>("hot_payload_present") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let page_present = match row.try_get::<_, bool>("page_present") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let payload_size_bytes = match row.try_get::<_, std::option::Option<i64>>("payload_size_bytes") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let retention_state = match row.try_get::<_, std::option::Option<std::string::String>>("retention_state") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let signature = match row.try_get::<_, std::option::Option<std::vec::Vec<u8>>>("signature") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let slot_text = match row.try_get::<_, std::option::Option<std::string::String>>("slot_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
let total_count_text = match row.try_get::<_, std::string::String>("total_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_decode")),
};
return std::result::Result::Ok(RawInspectionDbRow {
archive_payload_present,
block_time_unix_millis,
content_hash,
filtered_count_text,
format_id,
format_version,
hot_payload_present,
page_present,
payload_size_bytes,
retention_state,
signature,
slot_text,
total_count_text,
});
}
fn decode_raw_inspection_summary(
network: &ksp_store_api::RawNetworkId,
row: RawInspectionDbRow,
) -> std::result::Result<ksp_store_api::RawTransactionSummary, crate::PostgresBackendError> {
if !row.page_present {
return std::result::Result::Err(data_invalid("raw_transaction_inspection_page"));
}
let signature = match inspection_required(row.signature, "raw_transaction_inspection_signature") {
std::result::Result::Ok(value) => match fixed_bytes::<64>(value) {
std::result::Result::Ok(bytes) => ksp_store_api::RawTransactionSignature::new(bytes),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot_text = match inspection_required(row.slot_text, "raw_transaction_inspection_slot") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot = match decode_u64_decimal(slot_text.as_str(), "raw_transaction_inspection_slot") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let block_time = match decode_optional_timestamp(row.block_time_unix_millis, "raw_transaction_inspection_block_time") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let format_id_raw = match inspection_required(row.format_id, "raw_transaction_inspection_format_id") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let format_id = match decode_format_id(format_id_raw) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let format_version_raw = match inspection_required(row.format_version, "raw_transaction_inspection_format_version") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let format_version = match decode_u32_i64(format_version_raw, "raw_transaction_inspection_format_version") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let content_hash_raw = match inspection_required(row.content_hash, "raw_transaction_inspection_content_hash") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let content_hash = match fixed_bytes::<32>(content_hash_raw) {
std::result::Result::Ok(value) => ksp_store_api::RawContentHash::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let retention_raw = match inspection_required(row.retention_state, "raw_transaction_inspection_retention") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let retention_state = match decode_retention_state(retention_raw.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let hot_payload_present = match inspection_required(row.hot_payload_present, "raw_transaction_inspection_hot_payload") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let archive_payload_present = match inspection_required(row.archive_payload_present, "raw_transaction_inspection_archive_payload") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let payload_size_bytes = match row.payload_size_bytes {
std::option::Option::Some(value) => match u64::try_from(value) {
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_inspection_payload_size")),
},
std::option::Option::None => std::option::Option::None,
};
match retention_state {
ksp_store_api::RawRetentionState::Full if !hot_payload_present || archive_payload_present => {
return std::result::Result::Err(data_invalid("raw_transaction_inspection_full_shape"));
},
ksp_store_api::RawRetentionState::Archived if hot_payload_present || !archive_payload_present => {
return std::result::Result::Err(data_invalid("raw_transaction_inspection_archived_shape"));
},
ksp_store_api::RawRetentionState::Purged if hot_payload_present || archive_payload_present || block_time.is_some() => {
return std::result::Result::Err(data_invalid("raw_transaction_inspection_purged_shape"));
},
ksp_store_api::RawRetentionState::Full | ksp_store_api::RawRetentionState::Archived | ksp_store_api::RawRetentionState::Purged => {},
_ => return std::result::Result::Err(data_invalid("raw_transaction_inspection_retention")),
}
let reference = ksp_store_api::RawTransactionReference::new(network.clone(), signature);
return match ksp_store_api::RawTransactionSummary::try_new(
reference,
slot,
block_time,
format_id,
format_version,
content_hash,
payload_size_bytes,
retention_state,
) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_transaction_inspection_summary")),
};
}
fn inspection_required<T>(value: std::option::Option<T>, phase: &'static str) -> std::result::Result<T, crate::PostgresBackendError> {
return match value {
std::option::Option::Some(inner) => std::result::Result::Ok(inner),
std::option::Option::None => std::result::Result::Err(data_invalid(phase)),
};
}
fn raw_list_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawListDbRow, crate::PostgresBackendError> { fn raw_list_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawListDbRow, crate::PostgresBackendError> {
let signature = match row.try_get::<_, std::vec::Vec<u8>>("signature") { let signature = match row.try_get::<_, std::vec::Vec<u8>>("signature") {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -1071,6 +1533,116 @@ fn raw_transaction_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawT
}); });
} }
fn raw_observation_inspection_empty_page_is_clean(row: &RawObservationInspectionDbRow) -> bool {
return row.acquisition_method.is_none()
&& row.capture_session_id.is_none()
&& row.commitment.is_none()
&& row.endpoint_id.is_none()
&& row.filter_id.is_none()
&& row.observation_key.is_none()
&& row.observed_at_unix_millis.is_none()
&& row.origin.is_none()
&& row.protocol.is_none()
&& row.provider.is_none()
&& row.received_at_unix_millis.is_none()
&& row.source_payload_hash.is_none()
&& row.source_payload_size_bytes.is_none()
&& row.transaction_signature.is_none();
}
fn raw_observation_inspection_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawObservationInspectionDbRow, crate::PostgresBackendError> {
macro_rules! get_optional {
($name:literal, $ty:ty) => {
match row.try_get::<_, std::option::Option<$ty>>($name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_decode")),
}
};
}
let filtered_count_text = match row.try_get::<_, std::string::String>("filtered_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_decode")),
};
let page_present = match row.try_get::<_, bool>("page_present") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_decode")),
};
let total_count_text = match row.try_get::<_, std::string::String>("total_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_observation_inspection_decode")),
};
return std::result::Result::Ok(RawObservationInspectionDbRow {
acquisition_method: get_optional!("acquisition_method", std::string::String),
capture_session_id: get_optional!("capture_session_id", std::string::String),
commitment: get_optional!("commitment", std::string::String),
endpoint_id: get_optional!("endpoint_id", std::string::String),
filter_id: get_optional!("filter_id", std::string::String),
filtered_count_text,
observation_key: get_optional!("observation_key", std::vec::Vec<u8>),
observed_at_unix_millis: get_optional!("observed_at_unix_millis", i64),
origin: get_optional!("origin", std::string::String),
page_present,
protocol: get_optional!("protocol", std::string::String),
provider: get_optional!("provider", std::string::String),
received_at_unix_millis: get_optional!("received_at_unix_millis", i64),
source_payload_hash: get_optional!("source_payload_hash", std::vec::Vec<u8>),
source_payload_size_bytes: get_optional!("source_payload_size_bytes", i64),
total_count_text,
transaction_signature: get_optional!("transaction_signature", std::vec::Vec<u8>),
});
}
fn decode_raw_observation_inspection_row(
network: &ksp_store_api::RawNetworkId,
row: RawObservationInspectionDbRow,
) -> std::result::Result<ksp_store_api::RawTransactionObservation, crate::PostgresBackendError> {
let observation_key = match inspection_required(row.observation_key, "raw_transaction_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let received_at_unix_millis = match inspection_required(row.received_at_unix_millis, "raw_transaction_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transaction_signature = match inspection_required(row.transaction_signature, "raw_transaction_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let acquisition_method = match inspection_required(row.acquisition_method, "raw_transaction_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let origin = match inspection_required(row.origin, "raw_transaction_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let protocol = match inspection_required(row.protocol, "raw_transaction_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider = match inspection_required(row.provider, "raw_transaction_observation_inspection_shape") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let physical = RawObservationDbRow {
acquisition_method,
capture_session_id: row.capture_session_id,
commitment: row.commitment,
endpoint_id: row.endpoint_id,
filter_id: row.filter_id,
observation_key,
observed_at_unix_millis: row.observed_at_unix_millis,
origin,
protocol,
provider,
received_at_unix_millis,
source_payload_hash: row.source_payload_hash,
source_payload_size_bytes: row.source_payload_size_bytes,
transaction_signature,
};
return decode_raw_observation_row(network, physical);
}
fn raw_observation_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawObservationDbRow, crate::PostgresBackendError> { fn raw_observation_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawObservationDbRow, crate::PostgresBackendError> {
macro_rules! required { macro_rules! required {
($name:literal, $ty:ty) => { ($name:literal, $ty:ty) => {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/runtime.rs // file: crates/ksp-store-postgres-lib/src/runtime.rs
// version: 15 // version: 18
const APPLICATION_NAME: &str = "ksp-store"; const APPLICATION_NAME: &str = "ksp-store";
const MAX_CONNECTION_URI_BYTES: usize = 4_096; const MAX_CONNECTION_URI_BYTES: usize = 4_096;
@@ -348,6 +348,22 @@ impl PostgresBackend {
return crate::list_raw_account_states(&self.pool, &self.network, query).await; return crate::list_raw_account_states(&self.pool, &self.network, query).await;
} }
/// Inspects one data-free random-access RAW account-state window with exact logical counts.
pub async fn inspect_raw_account_states(
&self,
query: &ksp_store_api::RawAccountStateInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountStateSummary>, crate::PostgresBackendError> {
return crate::inspect_raw_account_states(&self.pool, &self.network, query).await;
}
/// Inspects one safe random-access RAW account-observation window with exact logical counts.
pub async fn inspect_raw_account_observations(
&self,
query: &ksp_store_api::RawAccountObservationInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountObservationSummary>, crate::PostgresBackendError> {
return crate::inspect_raw_account_observations(&self.pool, &self.network, query).await;
}
/// Reads one persisted RAW account observation by producer-owned idempotence key. /// Reads one persisted RAW account observation by producer-owned idempotence key.
pub async fn get_raw_account_observation( pub async fn get_raw_account_observation(
&self, &self,
@@ -389,6 +405,22 @@ impl PostgresBackend {
return crate::list_raw_transactions(&self.pool, &self.network, query).await; return crate::list_raw_transactions(&self.pool, &self.network, query).await;
} }
/// Inspects one payload-free random-access RAW transaction window with exact logical counts.
pub async fn inspect_raw_transactions(
&self,
query: &ksp_store_api::RawTransactionInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionSummary>, crate::PostgresBackendError> {
return crate::inspect_raw_transactions(&self.pool, &self.network, query).await;
}
/// Inspects one safe random-access RAW transaction-observation window with exact logical counts.
pub async fn inspect_raw_transaction_observations(
&self,
query: &ksp_store_api::RawTransactionObservationInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionObservationSummary>, crate::PostgresBackendError> {
return crate::inspect_raw_transaction_observations(&self.pool, &self.network, query).await;
}
/// Reads one persisted RAW transaction observation by producer-owned idempotence key. /// Reads one persisted RAW transaction observation by producer-owned idempotence key.
pub async fn get_raw_transaction_observation( pub async fn get_raw_transaction_observation(
&self, &self,
@@ -469,6 +501,18 @@ impl std::fmt::Debug for PostgresBackend {
} }
} }
impl ksp_store_api::RawAccountObservationInspectionRead for PostgresBackend {
fn inspect_raw_account_observations<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountObservationInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountObservationSummary>>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::inspect_raw_account_observations(self, query).await;
return result.map_err(map_capability_error);
});
}
}
impl ksp_store_api::RawAccountObservationRead for PostgresBackend { impl ksp_store_api::RawAccountObservationRead for PostgresBackend {
fn get_raw_account_observation<'a>( fn get_raw_account_observation<'a>(
&'a self, &'a self,
@@ -515,6 +559,18 @@ impl ksp_store_api::RawAccountStateRead for PostgresBackend {
} }
} }
impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend {
fn inspect_raw_account_states<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountStateInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountStateSummary>>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::inspect_raw_account_states(self, query).await;
return result.map_err(map_capability_error);
});
}
}
impl ksp_store_api::RawAccountStateWrite for PostgresBackend { impl ksp_store_api::RawAccountStateWrite for PostgresBackend {
fn persist_raw_account_acquisition<'a>( fn persist_raw_account_acquisition<'a>(
&'a self, &'a self,
@@ -550,6 +606,18 @@ impl ksp_store_api::RawTransactionRead for PostgresBackend {
} }
} }
impl ksp_store_api::RawTransactionInspectionRead for PostgresBackend {
fn inspect_raw_transactions<'a>(
&'a self,
query: &'a ksp_store_api::RawTransactionInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionSummary>>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::inspect_raw_transactions(self, query).await;
return result.map_err(map_capability_error);
});
}
}
impl ksp_store_api::RawTransactionWrite for PostgresBackend { impl ksp_store_api::RawTransactionWrite for PostgresBackend {
fn persist_raw_transaction_acquisition<'a>( fn persist_raw_transaction_acquisition<'a>(
&'a self, &'a self,
@@ -564,6 +632,18 @@ impl ksp_store_api::RawTransactionWrite for PostgresBackend {
} }
} }
impl ksp_store_api::RawTransactionObservationInspectionRead for PostgresBackend {
fn inspect_raw_transaction_observations<'a>(
&'a self,
query: &'a ksp_store_api::RawTransactionObservationInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawTransactionObservationSummary>>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::inspect_raw_transaction_observations(self, query).await;
return result.map_err(map_capability_error);
});
}
}
impl ksp_store_api::RawTransactionObservationRead for PostgresBackend { impl ksp_store_api::RawTransactionObservationRead for PostgresBackend {
fn get_raw_transaction_observation<'a>( fn get_raw_transaction_observation<'a>(
&'a self, &'a self,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs // file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
// version: 24 // version: 29
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -180,12 +180,14 @@ fn pre_005_raw_account_acquisition_is_atomic_idempotent_and_keeps_trait_impls_ou
assert!(raw.contains(required), "missing private RAW account acquisition contract: {required}"); assert!(raw.contains(required), "missing private RAW account acquisition contract: {required}");
} }
assert!(runtime.contains("pub async fn persist_raw_account_acquisition")); assert!(runtime.contains("pub async fn persist_raw_account_acquisition"));
for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE", " OFFSET "] { for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE"] {
assert!(!raw.contains(forbidden), "pre.005 account module contains later/destructive scope: {forbidden}"); assert!(!raw.contains(forbidden), "pre.005 account module contains later/destructive scope: {forbidden}");
} }
for implementation in [ for implementation in [
"impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend", "impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend", "impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend", "impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend", "impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
] { ] {
@@ -214,12 +216,14 @@ fn pre_006_raw_account_additional_observation_is_reference_guarded_cancellation_
assert!(raw.contains(required), "missing pre.006 additional-observation contract: {required}"); assert!(raw.contains(required), "missing pre.006 additional-observation contract: {required}");
} }
assert!(runtime.contains("pub async fn record_raw_account_observation")); assert!(runtime.contains("pub async fn record_raw_account_observation"));
for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE", " OFFSET "] { for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE"] {
assert!(!raw.contains(forbidden), "pre.006 account module contains later/destructive scope: {forbidden}"); assert!(!raw.contains(forbidden), "pre.006 account module contains later/destructive scope: {forbidden}");
} }
for implementation in [ for implementation in [
"impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend", "impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend", "impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend", "impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend", "impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
] { ] {
@@ -315,7 +319,21 @@ fn pre_006_raw_pagination_is_keyset_cursor_bound_and_policy_free() {
} }
assert!(index.contains("ON ksp_raw_transactions (slot, signature)")); assert!(index.contains("ON ksp_raw_transactions (slot, signature)"));
assert!(index.contains("WHERE retention_state <> 'purged'")); assert!(index.contains("WHERE retention_state <> 'purged'"));
for forbidden in [" OFFSET ", "limit.min(", "clamp(", "500", "1000"] { let ascending = raw.lines().find(|line| return line.starts_with("const LIST_TRANSACTIONS_ASC_SQL"));
let ascending = match ascending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical ascending keyset SQL"),
};
let descending = raw.lines().find(|line| return line.starts_with("const LIST_TRANSACTIONS_DESC_SQL"));
let descending = match descending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical descending keyset SQL"),
};
for statement in [ascending, descending] {
assert!(!statement.contains(" OFFSET "), "pre.006 canonical keyset SQL must remain OFFSET-free");
}
assert!(!cursor.contains(" OFFSET "), "pre.006 cursor contains forbidden OFFSET material");
for forbidden in ["limit.min(", "clamp(", "500", "1000"] {
assert!(!raw.contains(forbidden), "pre.006 contains forbidden pagination/policy/later-scope material: {forbidden}"); assert!(!raw.contains(forbidden), "pre.006 contains forbidden pagination/policy/later-scope material: {forbidden}");
assert!(!cursor.contains(forbidden), "pre.006 cursor contains forbidden pagination/policy/later-scope material: {forbidden}"); assert!(!cursor.contains(forbidden), "pre.006 cursor contains forbidden pagination/policy/later-scope material: {forbidden}");
} }
@@ -360,13 +378,39 @@ fn pre_007_raw_account_pagination_is_keyset_cursor_bound_and_policy_free() {
assert!(cursor.contains(required), "missing pre.007 account cursor/binding contract: {required}"); assert!(cursor.contains(required), "missing pre.007 account cursor/binding contract: {required}");
} }
assert!(index.contains("ON ksp_raw_account_states (slot, pubkey, state_hash)")); assert!(index.contains("ON ksp_raw_account_states (slot, pubkey, state_hash)"));
for forbidden in [" OFFSET ", "limit.min(", "clamp(", "500", "1000"] { let ascending = raw.lines().find(|line| return line.starts_with("const LIST_ACCOUNT_STATES_ASC_SQL"));
let ascending = match ascending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical account ascending keyset SQL"),
};
let descending = raw.lines().find(|line| return line.starts_with("const LIST_ACCOUNT_STATES_DESC_SQL"));
let descending = match descending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical account descending keyset SQL"),
};
let by_pubkey_ascending = raw.lines().find(|line| return line.starts_with("const LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL"));
let by_pubkey_ascending = match by_pubkey_ascending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical account pubkey ascending keyset SQL"),
};
let by_pubkey_descending = raw.lines().find(|line| return line.starts_with("const LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL"));
let by_pubkey_descending = match by_pubkey_descending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical account pubkey descending keyset SQL"),
};
for statement in [ascending, descending, by_pubkey_ascending, by_pubkey_descending] {
assert!(!statement.contains(" OFFSET "), "pre.007 canonical account keyset SQL must remain OFFSET-free");
}
assert!(!cursor.contains(" OFFSET "), "pre.007 account cursor contains forbidden OFFSET material");
for forbidden in ["limit.min(", "clamp(", "500", "1000"] {
assert!(!raw.contains(forbidden), "pre.007 account source contains forbidden pagination/policy material: {forbidden}"); assert!(!raw.contains(forbidden), "pre.007 account source contains forbidden pagination/policy material: {forbidden}");
assert!(!cursor.contains(forbidden), "pre.007 account cursor contains forbidden pagination/policy material: {forbidden}"); assert!(!cursor.contains(forbidden), "pre.007 account cursor contains forbidden pagination/policy material: {forbidden}");
} }
for forbidden in [ for forbidden in [
"impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend", "impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend", "impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend", "impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend", "impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
] { ] {
@@ -408,13 +452,17 @@ fn pre_007_raw_retention_is_atomic_compare_and_transition_without_fake_compactio
} }
#[test] #[test]
fn pre_008_backend_trait_implementations_cover_exact_ten_raw_capabilities_in_runtime_bridge() { fn v0_3_8_pre_009_backend_trait_implementations_cover_exact_fourteen_raw_capabilities_in_runtime_bridge() {
let runtime = include_str!("../src/runtime.rs"); let runtime = include_str!("../src/runtime.rs");
for implementation in [ for implementation in [
"impl ksp_store_api::RawAccountObservationInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend", "impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend", "impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
"impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend", "impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend", "impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionInspectionRead for PostgresBackend",
"impl ksp_store_api::RawTransactionObservationInspectionRead for PostgresBackend",
"impl ksp_store_api::RawTransactionObservationRead for PostgresBackend", "impl ksp_store_api::RawTransactionObservationRead for PostgresBackend",
"impl ksp_store_api::RawTransactionObservationWrite for PostgresBackend", "impl ksp_store_api::RawTransactionObservationWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionRead for PostgresBackend", "impl ksp_store_api::RawTransactionRead for PostgresBackend",
@@ -424,6 +472,6 @@ fn pre_008_backend_trait_implementations_cover_exact_ten_raw_capabilities_in_run
] { ] {
assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL RAW capability inventory: {implementation}"); assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL RAW capability inventory: {implementation}");
} }
assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 10); assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 14);
return; return;
} }

Some files were not shown because too many files have changed in this diff Show More