124 Commits

Author SHA1 Message Date
7328be6997 v0.3.15-rel.001 2026-09-19 12:40:28 +02:00
d32299ea18 v0.3.15-pre.018 2026-09-19 12:40:04 +02:00
cd5c79c253 v0.3.15-pre.017 2026-09-19 08:36:52 +02:00
5f19786ce9 0.3.15-pre.016 2026-09-19 05:08:05 +02:00
b1b0e91616 0.3.15-pre.015-fix.001 2026-09-18 23:33:08 +02:00
582a286baa 0.3.15-pre.015 2026-09-18 21:06:58 +02:00
b266019ea8 0.3.15-pre.014-fix.003 2026-09-18 13:36:51 +02:00
463c82bfcf 0.3.15-pre.014-fix.002 2026-09-18 00:42:56 +02:00
6d820bf0d7 0.3.15-pre.014-fix.001 2026-09-17 23:09:21 +02:00
ad24cb36fa 0.3.15-pre.014 2026-09-17 13:02:10 +02:00
6ee4e3bdf4 0.3.15-pre.013 2026-09-17 09:34:11 +02:00
1573273d27 0.3.15-pre.012-fix.004 2026-09-16 23:49:07 +02:00
d56de4ace9 0.3.15-pre.012-fix.003 2026-09-16 22:22:48 +02:00
888ce3f35f 0.3.15-pre.012-fix.001 2026-09-16 17:02:28 +02:00
1c28f3155f 0.3.15-pre.012-fix.001 2026-09-16 11:38:02 +02:00
31ae38853d 0.3.15-pre.012 2026-09-16 10:58:29 +02:00
2f587e22be 0.3.15-pre.011-fix.001 2026-09-16 09:38:30 +02:00
a248be04ca 0.3.15-pre.011 2026-09-16 09:07:31 +02:00
e919945d1e 0.3.15-pre.010-fix.001 2026-09-15 05:53:36 +02:00
9fd51c5a41 0.3.15-pre.009-fix.003 2026-09-14 21:06:32 +02:00
8ae4d0effd v0.3.15-pre.009-fix.002 2026-09-14 15:19:55 +02:00
d7c2931c10 v0.3.15-pre.009-fix.001 2026-09-14 14:47:04 +02:00
11105fac28 v0.3.15-pre.009 2026-09-14 11:38:55 +02:00
d3eea1aa12 v0.3.15-pre.008-fix.004 2026-09-14 09:24:27 +02:00
11e5247fbf v0.3.15-pre.008-fix.003 2026-09-13 23:05:18 +02:00
ee658eb862 v0.3.15-pre.008-fix.002 2026-09-13 22:40:15 +02:00
fe512637d4 v0.3.15-pre.008-fix.001 2026-09-13 22:34:26 +02:00
c71fb2d8fe v0.3.15-pre.008 2026-09-13 22:28:21 +02:00
35bd1bb226 v0.3.15-pre.007-fix.002 2026-09-13 21:59:35 +02:00
320699e61a v0.3.15-pre.007-fix.001 2026-09-13 17:51:44 +02:00
0d9ec5c490 v0.3.15-pre.007 2026-09-13 17:36:06 +02:00
487bcbf0c1 v0.3.15-pre.006-fix.003 2026-09-13 17:11:21 +02:00
f12e67bdc7 v0.3.15-pre.006-fix.002 2026-09-13 16:34:18 +02:00
f3ed86e54c v0.3.15-pre.006-fix.001 2026-09-13 15:47:32 +02:00
0a8ce6e5f9 v0.3.15-pre.006 2026-09-13 11:30:56 +02:00
9ee3cd4a53 v0.3.15-pre.005-fix.002 2026-09-13 11:29:38 +02:00
b8d46fbb2b v0.3.15-pre.005-fix.001 2026-09-13 10:10:20 +02:00
b5554a9f9c v0.3.15-pre.005 2026-09-13 09:57:39 +02:00
8adf1bda3f v0.3.15-pre.004 2026-09-13 08:47:43 +02:00
dc8863a46d v0.3.15-pre.004 2026-09-13 08:47:31 +02:00
bd376dc479 v0.3.15-pre.003 2026-09-13 08:11:35 +02:00
dc126497db v0.3.15-pre.002-fix.001 2026-09-13 08:09:39 +02:00
8ea261d3a4 v0.3.15-pre.002 2026-09-13 06:07:42 +02:00
84ef8bb1f1 v0.3.15-pre.001-fix.001 2026-09-13 00:33:44 +02:00
4ec80889c2 v0.3.15-pre.001 2026-09-13 00:27:37 +02:00
002d08ba5f v0.3.14-rel.001 2026-09-12 23:46:51 +02:00
a7ac30f39b v0.3.14-pre.016 2026-09-12 23:31:19 +02:00
cd7cf2c4c6 v0.3.14-pre.015 2026-09-12 23:07:58 +02:00
8d8b6b9bcb v0.3.14-pre.014 2026-09-12 21:29:00 +02:00
02012f105a v0.3.14-pre.013-fix.001 2026-09-12 20:55:57 +02:00
09c91172ae v0.3.14-pre.013 2026-09-12 20:43:43 +02:00
8a0717c839 v0.3.14-pre.012 2026-09-12 20:28:00 +02:00
f3c4169752 v0.3.14-pre.011-fix.002 2026-09-12 19:29:26 +02:00
eedad4b81e v0.3.14-pre.011-fix.001 2026-09-12 19:07:27 +02:00
4e39ddd5d0 v0.3.14-pre.011 2026-09-12 19:05:43 +02:00
c99cb048bf v0.3.14-pre.010-fix.001 2026-09-12 18:11:16 +02:00
8c4b458724 v0.3.14-pre.010 2026-09-12 14:20:02 +02:00
792560191c v0.3.14-pre.009-fix.002 2026-09-12 13:04:49 +02:00
c884ce88ba v0.3.14-pre.009-fix.001 2026-09-12 12:30:27 +02:00
4bcd942928 v0.3.14-pre.009 2026-09-12 11:22:53 +02:00
bf394e0e4b v0.3.14-pre.008-fix.002 2026-09-12 09:45:58 +02:00
ec94001ab8 v0.3.14-pre.008-fix.001 2026-09-12 09:44:36 +02:00
505cc8c3f6 v0.3.14-pre.008 2026-09-12 09:00:36 +02:00
9fc30d3ab9 v0.3.14-pre.007-fix.001 2026-09-12 08:04:19 +02:00
887664f3c0 v0.3.14-pre.007 2026-09-12 07:47:16 +02:00
ae712eae64 v0.3.14-pre.006 2026-09-11 23:48:53 +02:00
a3df8f6ef5 v0.3.14-pre.005 2026-09-11 23:15:00 +02:00
1006079654 v0.3.14-pre.004 2026-09-11 22:22:51 +02:00
6721a4142a v0.3.14-pre.003 2026-09-11 21:54:19 +02:00
7bf938c19f v0.3.14-pre.002-fix.002 2026-09-11 20:55:53 +02:00
5932250314 v0.3.14-pre.002-fix.001 2026-09-11 20:00:56 +02:00
b2068e9ef2 v0.3.14-pre.002 2026-09-11 19:58:32 +02:00
2ca56dbaf4 v0.3.14-pre.001 2026-09-11 19:37:36 +02:00
9785cda39d v0.3.13-rel.001 2026-09-11 00:41:22 +02:00
0cea99cd5e v0.3.13-pre.015 2026-09-11 00:39:51 +02:00
71bc754feb v0.3.13-pre.014 2026-09-10 23:25:05 +02:00
cabbfed730 v0.3.13-pre.013 2026-09-10 22:15:21 +02:00
8b6ef7778a v0.3.13-pre.012 2026-09-10 21:55:02 +02:00
06319655b0 v0.3.13-pre.011 2026-09-10 21:27:18 +02:00
f636169783 v0.3.13-pre.010 2026-09-10 21:02:29 +02:00
0f8f38ff52 v0.3.13-pre.009-fix.002 2026-09-10 20:24:12 +02:00
142998afb3 v0.3.13-pre.009-fix.001 2026-09-10 20:13:01 +02:00
665d7d2bb6 v0.3.13-pre.009 2026-09-10 19:56:16 +02:00
1746d47412 v0.3.13-pre.008-fix.002 2026-09-10 18:25:54 +02:00
9bb2f7bcae v0.3.13-pre.008-fix.001 2026-09-10 17:55:46 +02:00
1f2720463b v0.3.13-pre.008 2026-09-10 17:52:35 +02:00
8bb530e9b4 v0.3.13-pre.007 2026-09-10 17:33:02 +02:00
72df965a9c v0.3.13-pre.006-fix.001 2026-09-10 16:17:50 +02:00
f688325170 v0.3.13-pre.006 2026-09-10 16:05:52 +02:00
c711213dcc v0.3.13-pre.005-fix.001 2026-09-10 15:23:09 +02:00
11ecdccd65 v0.3.13-pre.005 2026-09-10 15:01:00 +02:00
27ab84d198 v0.3.13-pre.004-fix.001 2026-09-10 14:51:28 +02:00
28e4da3879 v0.3.13-pre.004 2026-09-10 14:25:30 +02:00
31c0a83e51 v0.3.13-pre.003-fix.003 2026-09-10 13:44:17 +02:00
34fddc3bf5 v0.3.13-pre.003-fix.002 2026-09-10 13:20:09 +02:00
13c3dac554 v0.3.13-pre.003-fix.001 2026-09-10 13:12:25 +02:00
f7ed053e8e v0.3.13-pre.003 2026-09-10 11:38:34 +02:00
2e8feb56dd v0.3.13-pre.002-fix.001 2026-09-10 10:00:42 +02:00
793178b345 v0.3.13-pre.002 2026-09-10 09:50:42 +02:00
ee0359efd5 v0.3.13-pre.001 2026-09-10 09:01:00 +02:00
1dc57a85c2 v0.3.12-rel.001 2026-09-10 00:09:44 +02:00
e7e8960fec v0.3.12-pre.013 2026-09-10 00:00:12 +02:00
79e278064d v0.3.12-pre.012 2026-09-09 23:24:20 +02:00
474b894d1c v0.3.12-pre.011 2026-09-09 23:07:52 +02:00
0c76b74afe v0.3.12-pre.010 2026-09-09 22:35:30 +02:00
b608ed2c50 v0.3.12-pre.009-fix.001 2026-09-09 22:14:29 +02:00
f9d4e6bc74 v0.3.12-pre.009 2026-09-09 22:06:18 +02:00
223f11b989 v0.3.12-pre.008-fix.002 2026-09-09 20:57:54 +02:00
5095be0b0d v0.3.12-pre.008-fix.001 2026-09-09 20:46:44 +02:00
2aa64cf0a4 v0.3.12-pre.008 2026-09-09 20:42:32 +02:00
90c266d68a v0.3.12-pre.007-fix.002 2026-09-09 17:14:47 +02:00
8410b2f4ef v0.3.12-pre.007-fix.001 2026-09-09 16:22:09 +02:00
ba69a5ad20 v0.3.12-pre.007 2026-09-09 16:21:23 +02:00
07b9b20eb5 v0.3.12-pre.006-fix.001 2026-09-09 15:00:27 +02:00
901a0515b7 v0.3.12-pre.006 2026-09-09 13:30:15 +02:00
50c01a19c9 v0.3.12-pre.005-fix.001 2026-09-09 12:59:51 +02:00
3a8fa87acb v0.3.12-pre.005 2026-09-09 12:45:29 +02:00
df1dbbb8fb v0.3.12-pre.004-fix.001 2026-09-09 11:55:36 +02:00
531ca851c4 v0.3.12-pre.004 2026-09-09 11:44:36 +02:00
b3fd74529d v0.3.12-pre.003-fix.002 2026-09-09 11:43:19 +02:00
00b376d234 v0.3.12-pre.003-fix.001 2026-09-09 08:14:45 +02:00
fa720d0054 v0.3.12-pre.003 2026-09-09 07:39:42 +02:00
91438e8214 v0.3.12-pre.002 2026-09-09 07:12:37 +02:00
08e9a34183 v0.3.12-pre.001 2026-09-08 23:44:19 +02:00
284 changed files with 63852 additions and 548 deletions

View File

@@ -1,8 +1,58 @@
<!-- file: CHANGELOG.md -->
<!-- version: 31 -->
<!-- version: 35 -->
# Changelog KSP
## 0.3.15 — Raw Transaction Ingest Desk multi-route et fermeture live Mainnet — 2026-09-19
`0.3.15` livre `ksp-app-raw-transaction-ingest-desk` comme Desk Tauri KSP de composition et supervision des routes live `RawTransaction`. La Desk raisonne en routes complètes plutôt quen endpoints : une route nest composable que lorsque toutes ses capabilities Config/Transport requises existent sur le même réseau, puis le Start reconstruit les ressources backend et lance une instance `ksp-worker-raw-transaction-ingest-lib` indépendante par route sélectionnée. Plusieurs routes peuvent partager le même Store sans partager implicitement lifecycle, gaps, `TargetCoverage` ou réparation. Lapplication expose inventaire, Start/Stop ciblé, lifecycle, health, activité, backpressure, reconnect/replay, gaps/repair et compteurs sûrs sans déplacer Transport, Store ou logique dingestion dans le frontend.
Les cinq familles live du Worker sont composables selon les capacités réellement déclarées : Yellowstone hydraté, Standard Logs hydraté, Standard Block direct lorsquun `blockSubscribe` compatible existe, Helius Transaction hydraté et HTTP Block Polling. Sur Mainnet, Yellowstone/PublicNode est traité comme capability du réseau canonique `mainnet`, pas comme réseau séparé. Le chemin Yellowstone Block final utilise le stream gRPC comme déclencheur de bloc puis un HTTP `getBlock` `Full/Base64` avec `maxSupportedTransactionVersion = 1`, au lieu dun fan-out `getTransaction` par transaction. La queue dupdates Yellowstone reste bornée mais applique désormais une backpressure asynchrone vers le stream au lieu de transformer une saturation locale prolongée en `grpc_backpressure_overflow`; le Stop explicite accepte aussi comme fermeture coopérative un `Status` provider reçu après le half-close local, sans masquer un `grpc_status` sur session active.
Le live multi-provider a également révélé un cas de convergence RAW où deux acquisitions de la même transaction, du même slot et du même bloc différaient uniquement par `meta.logMessages`, PublicNode renvoyant un marqueur explicite `Log truncated`. Store journalise désormais des diagnostics bornés et sans matériau sensible, puis accepte le cas strictement prouvé **canonique complet déjà stocké + observation entrante tronquée compatible** comme observation supplémentaire sans remplacer le canonique ni arrêter le Worker. Cette exception reste volontairement asymétrique en `0.3.15` : un canonique tronqué nest pas encore promu automatiquement vers une version complète et toute autre divergence non prouvée conserve la sémantique `content_conflict`.
Le gate technique final `pre.016` passe format, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, les suites Store PostgreSQL, Transport, Worker, Raw Transaction Ingest Desk et le rejeu `cargo test --workspace --all-targets --all-features`, ainsi que les graphes Cargo et `cargo tree --duplicates`. `cargo tauri build` produit les trois bundles Linux `.deb`, `.rpm` et `.AppImage`. Le live de référence immédiatement antérieur fait tourner Yellowstone Mainnet et HTTP Block Polling environ dix-neuf minutes en parallèle sans `grpc_backpressure_overflow`, sans terminal `content_conflict` et sans route `Faulted`; les deux routes terminent ensuite `Stopped/Healthy`. La réconciliation documentaire `pre.017` repasse ensuite format, audits Rust/Markdown et `cargo check --workspace` sans réouvrir le runtime.
`prompts/035-V0_3_16_START_PROMPT.md` ouvre `0.3.16` exclusivement depuis le futur tag stable `v0.3.15`. Cette release est dédiée à la résilience RAW et à la gestion durable des conflits : variantes conservées, comparaison `Exact / CompatibleLessComplete / CompatibleMoreComplete / Conflict`, promotions réversibles, quarantaine non terminale des conflits, historique de résolution, Store retry/backpressure, reconnexion Transport configurable et nouvelles vues de `ksp-app-store-desk`. `0.3.17` reste réservé au Backfill multi-route/multi-stratégie et `0.3.18` à ladaptation de `ksp-app-backfill-desk`.
## 0.3.14 — gap repair et hardening multi-source du Worker RawTransaction — 2026-09-12
`0.3.14` ferme `ksp-worker-raw-transaction-ingest-lib` par une continuité run-local explicite au-dessus des cinq familles live de `0.3.13`, sans transformer le Worker en moteur historique. Le Worker distingue désormais reconnect, tentative de replay, delivery de replay et preuve de coverage ; il maintient des gaps bornés avec plage inclusive, `TargetCoverage` conservative et frontier de continuité distincte de la processing frontier. Une reprise live ou l'acceptation d'un `from_slot` ne vaut jamais à elle seule preuve de réparation.
La réconciliation peut s'appuyer sur une coverage réellement prouvée d'une source redondante, un scan HTTP borné de blocs/slots ou l'hydration `getTransaction observed` des références connues. Les bornes finales restent 64 gaps ouverts, 4096 slots par plage, fenêtres de discovery de 512 slots, 4 `getBlock` logiques en vol et un seul gap activement réparé. Les matériaux de repair repassent par la même canonicalisation Common RAW, la même admission, la même hydration globale et le même Store ; aucun second pipeline, retry réseau Worker, Job Backfill automatique ou backend Store direct n'est introduit.
La politique multi-source ne fault plus mécaniquement sur toute perte de source : la continuation n'est autorisée que lorsque les gaps concernés sont réconciliés et que la coverage présente/future requise est réellement démontrée. Les snapshots publics ajoutent une observabilité source-neutral et bornée des gaps avec états/raisons/méthodes et compteurs checked, sans source key, endpoint, token, signature, payload ou erreur provider arbitraire. La health policy reste conservative, la fairness alterne trafic nominal/repair sur les capacités existantes et le shutdown borne également le drain interne des sources avec abort+join des siblings non coopératifs.
La fermeture cross-layer confirme le pipeline unique `Transport -> Worker -> ksp-raw-transaction-lib -> Store`, la non-régression Legacy/V0/V1, les dependency firewalls et l'indépendance Worker/Backfill. Le gate technique final passe fmt, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features` et les graphes Cargo : `1 931` tests passent, `0` échoue et `15` restent ignored opt-in/operator-only. Les smokes keyless Solana HTTP Devnet et WebSocket Devnet passent `1/1` chacun ; Yellowstone/provider-gated, Helius live, PostgreSQL live et Worker->Store end-to-end restent explicitement non exécutés lorsqu'aucune ressource correspondante n'a été provisionnée.
`prompts/034-V0_3_15_START_PROMPT.md` ouvre `0.3.15` exclusivement depuis le futur tag stable `v0.3.14`. Cette release introduit `ksp-app-raw-transaction-ingest-desk` comme Desk Tauri de composition/supervision : une **route** est une stratégie complète pouvant nécessiter plusieurs capacités Config/Transport, elle n'est sélectionnable que si toutes ses requirements sont composables, puis une instance Worker réelle est démarrée par route sélectionnée et valide l'opérabilité runtime au Start. Le frontend reste une surface sûre ; Config, Transport, Store et Worker conservent leurs responsabilités.
## 0.3.13 — convergence live multi-source du Worker RawTransaction — 2026-09-10
`0.3.13` généralise `ksp-worker-raw-transaction-ingest-lib` de la verticale Yellowstone de `0.3.12` vers une composition caller-owned de `1..32` sources logiques appartenant au même réseau. Cinq familles live convergent désormais vers le même pipeline Common RAW / admission / Store : Yellowstone, Standard Logs avec hydration `getTransaction observed`, Standard Block RAW-direct, Helius `transactionSubscribe` avec hydration et HTTP Block Polling run-local RAW-direct. Le Worker reste sans Config, Job Backfill, backend Store physique, client réseau direct ou SDK provider parallèle.
La convergence partage un registre global d'hydration entre Yellowstone, Standard Logs et Helius afin de coalescer `(network, signature, commitment)` avant fan-out HTTP. Après canonicalisation, les acquisitions de même `(network, signature)` sont sérialisées de manière bornée : un contenu identique conserve les observations/provenances distinctes, tandis qu'une divergence devient un `content_conflict` terminal explicite. Aucune majorité provider, préférence silencieuse ou stratégie `first-provider-wins` n'est introduite. Standard Block et HTTP Block Polling admettent directement les transactions Legacy/V0/V1 qualifiées avec `maxSupportedTransactionVersion = 1`.
Le supervisor multi-source possède toutes les tâches et applique des bornes source-neutral sur admission, pending signals, hydrations et persistence avec une fairness minimale entre sources reference-bearing. Les snapshots agrègent activity/health/backpressure/reconnect/replay/gap sans exposer le matériau provider. La politique reste conservative : une source configurée qui échoue est terminale faute d'équivalence de coverage prouvée ; le gap repair, les rôles degraded/failover et la preuve de couverture restent donc `0.3.14`. Le shutdown stop/fault/drain/abort+join est durci contre les races, leaders d'hydration abandonnés, Store lent et publications tardives.
Le gate déterministe final passe `cargo fmt`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features` et les graphes Cargo : `1 850` tests passent, `0` échoue et `15` restent ignored opt-in/operator-only. Les smokes keyless Solana HTTP Devnet et WebSocket Devnet passent `1/1` chacun. Yellowstone provider-gated, Helius `transactionSubscribe` live, `blockSubscribe` provider dédié, HTTP polling Worker end-to-end et Worker multi-source vers Store restent explicitement non exécutés faute de gate provisionné ; aucune fixture ne les requalifie en PASS.
`prompts/033-V0_3_14_START_PROMPT.md` ouvre `0.3.14` exclusivement depuis le futur tag stable `v0.3.13`. Cette release doit fermer le gap repair/hardening multi-source limité au run actif — replay adressable, coverage redondante, scan HTTP borné, hydration de réparation, unresolved gaps et health policy — sans transformer le Worker en moteur historique ni créer de dépendance vers `ksp-job-backfill-lib`.
## 0.3.12 — Yellowstone + hydration HTTP + continuité de run du Worker RawTransaction — 2026-09-09
`0.3.12` ouvre la première source réseau productive de `ksp-worker-raw-transaction-ingest-lib` sans remettre en cause la fondation source-neutral de `0.3.11`. Le Worker dépend désormais de la façade `ksp-onchain-transport-lib` mais reste sans Config, Job Backfill, backend Store physique, `reqwest`, `tonic` ou proto provider direct. `RawTransactionIngestRuntimeResources` reçoit une source Yellowstone caller-composed et un pool HTTP cohérent ; `start` conserve la fondation sans source, tandis que `start_with_runtime_resources` lance la verticale live supervisée.
Les updates Yellowstone `Transaction`, `TransactionStatus` et les transactions incluses dans `Block` sont projetées vers des signaux source-neutral, coalescées de manière bornée par `(network, signature, commitment)`, hydratées via `getTransaction` observed puis converties par `ksp-raw-transaction-lib` avant admission/persistence Store. `BlockMeta` et `Slot` restent continuity-only. La provenance composite conserve les identités logiques sûres de la source et de la route dhydration, sans URL, secret ni payload distant. Les gardes réseau/signature/slot/index/commitment empêchent les croisements de matériau avant admission.
La release matérialise une processing frontier strictement run-local : le Worker distingue travail pending/settled, navance jamais au travers dun pending plus ancien et compacte ses états sans revendiquer une preuve de complétude durable. Le reconnect/replay reste Transport-owned : `from_slot` et `SubscribeReplayInfo` ne sont pas pilotés par le Worker. Le Worker projette un état source-neutral `Active/Reconnecting/Closing/Closed/Failed`, compte reconnect/replay-attempt/continuity-gap et fault avec `source_failed` lorsquune hausse du gap Transport prouve que la rétention ne permet plus la reprise demandée. Aucun Backfill ou repair automatique nest déclenché.
Le hardening final ferme les duplicate storms, saturation/coalescence, Store lent/fautif, stop/fault pendant hydration/reconnect, overflow de compteurs, abandon des pending sans faux `settled`, abort/join des tâches privées et absence de retry HTTP possédé par le Worker. `pre.010` ajoute les canaris cross-layer Transport -> Worker -> Common RAW -> Store et verrouille les dependency firewalls, fixtures Legacy/V0, redaction et inventaires publics.
Le gate déterministe `pre.011` passe rustfmt check, audits Rust/Markdown, `cargo check --workspace`, Clippy strict, `cargo test --workspace --all-targets --all-features`, les suites ciblées et les trois inspections Cargo demandées. Les smokes live keyless HTTP Devnet et WebSocket Devnet sont exécutés séparément et passent ; les smokes Yellowstone token-gated et le Worker end-to-end Yellowstone + HTTP + Store restent explicitement non exécutés faute de ressources fournies, sans faux PASS. `pre.012` réconcilie ensuite README/USAGE et les architectures concernées avec cette verticale réellement matérialisée.
La suite reste découpée : `0.3.13` ajoute WS standard, Helius `transactionSubscribe`, HTTP live polling et la convergence multi-source ; `0.3.14` ferme le gap repair/hardening multi-source. `prompts/032-V0_3_13_START_PROMPT.md` ouvre `0.3.13` uniquement depuis le tag stable `v0.3.12` et impose un `pre.001` daudit/sizing avant toute extension productive.
## 0.3.11 — Fondation runtime source-neutral du Worker RawTransaction ingest — 2026-09-08
`0.3.11` introduit `ksp-worker-raw-transaction-ingest-lib` comme premier Worker concret KSP pour lacquisition continue de `RawTransaction`, tout en fermant volontairement la release **sans source réseau productive**. La crate consomme `ksp-worker-api`, `ksp-raw-transaction-lib` et la seule façade `ksp-store-lib`; elle ne dépend ni de Config, ni dun Job, ni dun backend Store physique, ni encore de `ksp-onchain-transport-lib`. Son identité stable est `raw_transaction_ingest`, ses settings bornent la capacité dadmission, la concurrence de persistence et la deadline de drain, et son `start` utilise exclusivement le runtime Tokio actif fourni par le caller.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 518
# version: 637
[workspace]
resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
[workspace.package]
version = "0.3.11"
version = "0.3.15"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 12 -->
<!-- version: 15 -->
# Khadhroony Solana Project
@@ -57,7 +57,9 @@ La couche RAW dispose d'une façade Store backend-neutral et d'un premier job hi
`ksp-worker-api` fournit désormais la fondation générique des services continus : identité Worker, lifecycle borné, health/activity, intention de stop coopératif et observation latest-value. Cette API reste Core-only, runtime-neutral et sans connaissance Solana, Transport, Store ou Job. Elle ne démarre ni n'arrête elle-même un runtime concret.
`ksp-worker-raw-transaction-ingest-lib` est désormais le premier Worker concret distinct du Job Backfill. Sa fondation source-neutral possède le lifecycle Start/Stop, l'admission bornée, la canonicalisation Common RAW, la persistance Store backend-neutral, le shutdown borné et les snapshots latest-value, mais aucune source réseau productive ni dépendance Transport. Les adapters live/catch-up seront ajoutés dans les tranches suivantes sans transformer le Worker en campagne historique. `ksp-job-backfill-lib` conserve de son côté son rôle historique paramétré et borné ; les deux producteurs restent indépendants et convergent uniquement vers les mêmes contrats RAW/Store.
`ksp-worker-raw-transaction-ingest-lib` est le premier Worker concret distinct du Job Backfill. Il conserve sa fondation Start/Stop, admission bornée, Common RAW, Store backend-neutral, shutdown borné et snapshots latest-value. Ses cinq familles productives couvrent Yellowstone, Standard WS `logsSubscribe`, Standard WS `blockSubscribe`, Helius `transactionSubscribe` et Solana HTTP live block polling. Les signaux transactionnels Yellowstone/Standard Logs/Helius utilisent l'hydration HTTP `getTransaction`, tandis que le mode Yellowstone Block utilisé par la route Mainnet du Desk emploie le bloc gRPC comme trigger puis réconcilie le slot par HTTP `getBlock` Full/Base64 avec concurrence bornée. Standard Block et HTTP Block Polling qualifient directement leurs transactions Legacy/V0/V1 vers Common RAW. Les queues restent bornées ; une saturation durable de la livraison Yellowstone applique désormais une backpressure asynchrone au stream gRPC au lieu de produire un overflow local. Le Store accepte aussi l'exception étroite déjà prouvée où un canonique complet reçoit ensuite une observation identique hors `logMessages` explicitement tronqués ; le canonique complet est conservé et les autres divergences restent fail-closed en `0.3.15`. Le reconnect/replay reste propriétaire de Transport, la frontier de traitement reste run-local et un gap de rétention prouvé provoque un fault au lieu de lancer une campagne historique. `ksp-job-backfill-lib` conserve son rôle historique paramétré et borné ; les deux producteurs restent indépendants et convergent uniquement vers les mêmes contrats RAW/Store.
`ksp-app-raw-transaction-ingest-desk` compose et supervise ces routes live sans déplacer leurs responsabilités dans l'UI. Les profils applicatifs sont réseau-centriques, plusieurs routes d'un même réseau peuvent partager un Store tout en gardant des Workers indépendants, et le monitoring latest-value expose lifecycle, health, activité, persistence, reconnect/replay, continuité, gaps et repair sans URL, secret, payload RAW ou handle physique. Le gate `0.3.15` valide notamment le fonctionnement parallèle Mainnet de Yellowstone Block + HTTP `getBlock` et HTTP Block Polling, la fermeture coopérative de Yellowstone après half-close et le build Tauri Linux de la Desk.
## Points d'entrée

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 109 -->
<!-- version: 114 -->
# Roadmap KSP
@@ -104,11 +104,13 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
- [X] `0.3.9``ksp-worker-api` stable comme API générique et volontairement courte pour services continus : identité/kind bornés, lifecycle explicite, health/activity sûrs, stop token partagé, séquence/snapshot latest-value et `WorkerSnapshotSource` object-safe, avec dépendance Core-only et sans runtime/start-stop universel, Solana, Transport, Store, Config ou Tauri. Après freeze Worker API, l'audit `RawTransaction` a été consolidé dans une synthèse Store-centrique exhaustive séparant possibilités/support/preuve et classant HTTP, WS standard/provider, Yellowstone gRPC, blocs/slots, discovery + hydration, replay de continuité, archives et sources EARLY. Le Job Backfill historique paramétré et le Worker Ingest live start/stop sont deux producteurs indépendants du même Store ; `mainnet` est désormais l'identité KSP canonique et `mainnet-beta` un alias legacy/externe. Le handoff retient `ksp-raw-transaction-lib` comme lower-layer commune de canonicalisation RAW v1 pour `0.3.10`, sans edge Job ↔ Worker.
- [X] `0.3.10` — Lower-layer RAW Transaction commune stabilisée : `ksp-raw-transaction-lib` possède la canonicalisation RAW v1 source-neutral partagée, le Backfill est migré sans changement des golden bytes/hash, Transport expose `get_block_observed`, le wire Solana Legacy/V0/V1 est matérialisé et les canaris HTTP/WS standard/Helius/Yellowstone qualifient précisément les voies directes ou nécessitant hydration. La release se ferme sans runtime Worker concret ; celui-ci commence en `0.3.11` selon le redécoupage `0.3.11``0.3.14`.
- [X] `0.3.11` — Fondation runtime de `ksp-worker-raw-transaction-ingest-lib` livrée : crate/dependency firewall, settings source-neutral, handle/start-stop, lifecycle, snapshots, supervisor, admission bornée, canonicalisation Common RAW, persistence/déduplication Store déterministe, backpressure et shutdown/fault hardening. La release se ferme volontairement sans source réseau productive ; Yellowstone + hydration HTTP commencent en `0.3.12`.
- [ ] `0.3.12`Ajouter au Worker la voie **Yellowstone + hydration HTTP** : transactions/blocks/status, projection vers la common RAW, provenance sûre, `from_slot`, replay info, frontier de run et continuité propre au run, avec fixtures déterministes et smokes accessibles sans prétendre à un RAW-direct lorsque les métadonnées restent incomplètes.
- [ ] `0.3.13`Ajouter les voies **WS standard / Helius / HTTP live** et leur convergence multi-source : `logsSubscribe + getTransaction`, `blockSubscribe`, `transactionSubscribe + hydration`, polling blocs HTTP, observations multiples, content conflict explicite, coalescence et backpressure sans second actor Transport.
- [ ] `0.3.14`Fermer le Worker par le **gap repair/hardening multi-source** : replay natif, source redondante, scan HTTP blocs, hydration de réparation, unresolved gaps visibles, politiques degraded/unhealthy/faulted, shutdown pendant repair, smokes provider accessibles et adapter EARLY seulement si réellement prouvé.
- [ ] `0.3.15` Introduire `ksp-app-raw-transaction-ingest-desk`, Desk Tauri KSP spécialisée qui choisit et supervise une ou plusieurs sources/méthodes réellement admises par le Worker finalisé : lifecycle, health, rates, backpressure, reconnect/recovery, gap state et compteurs sûrs. La Desk ne réimplémente ni discovery, hydration, déduplication, reprise ni persistence.
- [ ] `0.3.16`Étendre `ksp-job-backfill-lib` et `ksp-app-backfill-desk` au **backfill multi-source/multi-stratégie** à partir de la même matrice d'acquisition auditée en `0.3.9`. Conserver la stratégie actuelle `getSignaturesForAddress + getTransaction` comme première voie HTTP valide, puis ajouter seulement les voies historiques/catch-up/gap-repair réellement pertinentes et sûres, sans supposer qu'une source live WS constitue un historique universel.
- [X] `0.3.12`Première source live productive du Worker livrée : Yellowstone `Transaction`/`TransactionStatus`/`Block` vers signaux source-neutral, coalescence bornée puis hydration HTTP `getTransaction` observed avant Common RAW/Store ; `BlockMeta`/`Slot` restent continuity-only. Processing frontier run-local, reconnect/from_slot/ReplayInfo Transport-owned, compteurs source-neutral et fault sur gap de rétention prouvé sans Backfill automatique. Hardening duplicate/backpressure/stop/fault et canaris cross-layer fermés ; gate workspace complet vert, smokes live HTTP Devnet + WebSocket Devnet verts, Yellowstone token-gated/Worker end-to-end laissés explicitement non exécutés.
- [X] `0.3.13`Convergence live multi-source du Worker RAW livrée : composition caller-owned `1..32` sources dun même réseau, cinq familles Yellowstone / Standard Logs / Standard Block / Helius Transaction / HTTP Block Polling, hydration globale des trois voies reference-bearing, RAW-direct Legacy/V0/V1 pour les deux voies blocs, observations multiples, content conflict explicite, coalescence cross-source, fairness/backpressure/health source-neutral et shutdown hardening sans second actor Transport. Gate workspace complet vert (`1 850` PASS / `0` échec / `15` ignored) et smokes keyless HTTP + WebSocket Devnet verts ; les preuves provider/resource-gated non exécutées restent explicitement non revendiquées.
- [X] `0.3.14`Gap repair/hardening multi-source du Worker RAW livré : gaps run-local bornés, distinction reconnect/replay/delivery/coverage, TargetCoverage conservative, coverage redondante prouvée, discovery HTTP bornée, known-reference hydration, réconciliation source-loss, health gap-aware, fairness nominal/repair, snapshots publics source-neutral et shutdown/drain renforcé. Le pipeline Common RAW/Store reste unique, Worker/Backfill restent indépendants, EARLY nest pas ajouté. Gate workspace complet vert (`1 931` PASS / `0` échec / `15` ignored) et smokes keyless HTTP + WebSocket Devnet verts ; preuves provider/resource-gated non provisionnées explicitement non revendiquées.
- [X] `0.3.15``ksp-app-raw-transaction-ingest-desk` livrée comme Desk Tauri KSP multi-route : composition capability-driven par réseau, une instance Worker indépendante par route sélectionnée, Store partagé, Start/Stop ciblé et supervision lifecycle/health/backpressure/reconnect/replay/gaps/repair sans réimplémenter Transport ni persistence. Les cinq familles live sont projetées selon les capacités réellement configurées ; Yellowstone Mainnet utilise le trigger gRPC Block + HTTP `getBlock` et applique une backpressure bornée sans overflow local prolongé. Le Stop Yellowstone post-half-close est coopératif, et Store accepte le cas strict `canonique complet + incoming logMessages explicitement tronqué compatible` sans remplacer le canonique ni arrêter le Worker. Gate workspace/Clippy/tests/graphes/bundles Linux vert et live Mainnet Yellowstone + HTTP polling denviron 19 minutes fermé `Stopped/Healthy`. La généralisation variantes/conflits/promotions/retry/reconnect reste explicitement `0.3.16`.
- [ ] `0.3.16`**RAW resilience / conflict management** : faire converger Store API/façade/PostgreSQL, Worker live et Store Desk autour de variantes RAW durables, résolution automatique `Exact / CompatibleLessComplete / CompatibleMoreComplete / Conflict`, quarantaine des conflits non résolus sans arrêt du Worker, historique réversible des promotions/résolutions, retry Store sans perte silencieuse et reconnexion Transport configurable/bornée. Une provenance provider n'est jamais une priorité canonique en soi ; la représentation prouvée la plus complète gagne.
- [ ] `0.3.17` — Étendre `ksp-job-backfill-lib` au **backfill multi-route/multi-stratégie** à partir de la matrice d'acquisition auditée en `0.3.9` et de la convergence Store `0.3.16`. Conserver `getSignaturesForAddress + getTransaction` comme première voie valide puis ajouter les stratégies historiques/catch-up réellement pertinentes sans edge Job ↔ Worker.
- [ ] `0.3.18` — Adapter `ksp-app-backfill-desk` au Job multi-route `0.3.17` : inventaire/composition de routes, sélection/supervision et progression sur le modèle structurel de `ksp-app-raw-transaction-ingest-desk`, sans réimplémenter la logique du Job.
### TODO/IDEAS — applications spécialisées et control plane
@@ -118,8 +120,8 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
### TODO/IDEAS — taxonomie D1, processing et rétention
- [ ] **TODO** — maintenir la matrice dadmission HTTP/WS/gRPC/provider lors de toute nouvelle famille D1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
- [X] **TODO `0.3.9`** — matrice RAW Transaction unique produite et consolidée dans `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`, avec séparation possibilités/support/preuve, capabilities orthogonales aux producteurs, provenance/déduplication/continuité et handoffs désormais scindés `0.3.11``0.3.14` Worker live / `0.3.16` Backfill historique.
- [ ] **TODO Helius `0.3.13`**ajouter uniquement les profils/capabilities Helius réellement nécessaires au Worker live après revalidation des surfaces/tier courants ; réutiliser exclusivement `KSP_SECRET_HELIUS_API_KEY` via Config et la surface `transactionSubscribe` déjà Transport-owned, sans SDK provider ni second client parallèle.
- [X] **TODO `0.3.9`** — matrice RAW Transaction unique produite et consolidée dans `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`, avec séparation possibilités/support/preuve, capabilities orthogonales aux producteurs, provenance/déduplication/continuité et handoffs désormais scindés `0.3.11``0.3.14` Worker live / `0.3.16` résilience RAW / `0.3.17``0.3.18` Backfill multi-route.
- [X] **TODO Helius `0.3.13`**la voie Worker Helius `transactionSubscribe` réutilise la surface Transport et les profils Config Helius déjà existants avec `KSP_SECRET_HELIUS_API_KEY`; aucun nouveau profil, secret, SDK provider, tier codé ou second client parallèle na été nécessaire. Le payload riche reste Transport-owned et la voie Worker hydrate via `getTransaction observed` tant que le RAW complet nest pas prouvé directement.
- [X] **TODO réseau** — identité KSP canonique fixée à `mainnet` dans Config/Store/Transport/tests ; `mainnet-beta` reste uniquement un alias legacy/externe lorsque la frontière provider l'exige. Les données D1 RAW antérieures restent non autoritaires pendant cette phase et aucune compatibilité de base de test n'impose l'ancien libellé.
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique est complétée en `0.3.4` avec les quatre capabilities account et la conformance RAW 10/10.
- [ ] **TODO** — statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif 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.

View File

@@ -0,0 +1,91 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "transport.helius",
"file_id": "cfg.std.transport",
"profile_id": "helius_devnet"
},
{
"component_id": "transport.yellowstone",
"file_id": "cfg.std.transport",
"profile_id": "orbitflare_devnet"
},
{
"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": "transport",
"file_id": "cfg.std.transport",
"profile_id": "mainnet_public"
},
{
"component_id": "transport.helius",
"file_id": "cfg.std.transport",
"profile_id": "helius_mainnet"
},
{
"component_id": "transport.yellowstone",
"file_id": "cfg.std.transport",
"profile_id": "publicnode_mainnet"
},
{
"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": "transport",
"file_id": "cfg.std.transport",
"profile_id": "testnet_public"
},
{
"component_id": "transport.yellowstone",
"file_id": "cfg.std.transport",
"profile_id": "publicnode_testnet"
},
{
"component_id": "store",
"file_id": "cfg.std.store",
"profile_id": "testnet"
}
]
}
]
}

View File

@@ -87,7 +87,15 @@
"provider": "solana-public",
"cluster": "mainnet",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot"
]
},
{
"name": "mainnet_helius_ws",
@@ -99,7 +107,17 @@
"session": {
"notification_queue_capacity": 512,
"max_active_subscriptions": 2048
}
},
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
}
]
},
@@ -140,7 +158,17 @@
"provider": "helius",
"cluster": "devnet",
"kind": "helius_laserstream",
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}"
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
}
]
}

View File

@@ -348,6 +348,26 @@
},
"session": {
"$ref": "#/$defs/wsSessionOverride"
},
"capabilities": {
"type": "array",
"minItems": 1,
"maxItems": 10,
"uniqueItems": true,
"items": {
"enum": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"vote",
"helius_transaction"
]
}
}
}
},

View File

@@ -75,7 +75,67 @@
"provider": "solana-public",
"cluster": "devnet",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}"
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
]
},
{
"profile_id": "helius_devnet",
"endpoints": [
{
"name": "solana_devnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "devnet",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
],
"ws_endpoints": [
{
"name": "helius_devnet_ws",
"enabled": true,
"provider": "helius",
"cluster": "devnet",
"kind": "helius_laserstream",
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
}
]
},
@@ -116,7 +176,16 @@
"provider": "solana-public",
"cluster": "devnet",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}"
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
],
"grpc_endpoints": [
@@ -174,7 +243,67 @@
"provider": "solana-public",
"cluster": "mainnet",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
]
},
{
"profile_id": "helius_mainnet",
"endpoints": [
{
"name": "solana_mainnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
],
"ws_endpoints": [
{
"name": "helius_mainnet_ws",
"enabled": true,
"provider": "helius",
"cluster": "mainnet",
"kind": "helius_laserstream",
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
}
]
},
@@ -269,7 +398,16 @@
"provider": "solana-public",
"cluster": "mainnet",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
]
},
@@ -277,7 +415,31 @@
"profile_id": "publicnode_mainnet",
"endpoints": [
{
"name": "solana_mainnet_public",
"name": "publicnode_solana_mainnet_rpc",
"enabled": true,
"provider": "publicnode",
"cluster": "mainnet",
"url": "https://solana-rpc.publicnode.com",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
},
{
"name": "solana_mainnet_public_repair",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet",
@@ -310,7 +472,16 @@
"provider": "solana-public",
"cluster": "mainnet",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
],
"grpc_endpoints": [
@@ -330,6 +501,56 @@
}
]
},
{
"profile_id": "testnet_public",
"endpoints": [
{
"name": "solana_testnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "testnet",
"url": "https://api.testnet.solana.com",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
],
"ws_endpoints": [
{
"name": "solana_testnet_public_ws",
"enabled": true,
"provider": "solana-public",
"cluster": "testnet",
"kind": "solana_standard",
"url": "wss://api.testnet.solana.com",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
]
},
{
"profile_id": "publicnode_testnet",
"endpoints": [
@@ -367,7 +588,16 @@
"provider": "solana-public",
"cluster": "testnet",
"kind": "solana_standard",
"url": "wss://api.testnet.solana.com"
"url": "wss://api.testnet.solana.com",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
],
"grpc_endpoints": [

View File

@@ -53,6 +53,7 @@
],
"resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-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",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
// version: 17
// version: 18
//! Structural desktop contract checks for the Backfill Desk scaffold.
@@ -145,9 +145,10 @@ fn pre_003_composite_packaging_follows_atomic_config_registry_contract() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(resources.len(), 15);
assert_eq!(resources.len(), 16);
for required in [
"../../config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json",
"../../config/composite.ksp-app-solprices-desk.json",
"../../config/composite.ksp-app-store-desk.json",
"../../config/composite.ksp-app-wallet-desk.json",

View File

@@ -53,6 +53,7 @@
],
"resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-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",

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
// version: 8
// version: 9
#[test]
fn profile_inventory_exposes_registered_profile_documents() {
@@ -21,6 +21,9 @@ fn profile_inventory_exposes_registered_profile_documents() {
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_STD_WALLET;
}));
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK;
}));
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
}));

View File

@@ -0,0 +1,44 @@
# file: crates/ksp-app-raw-transaction-ingest-desk/Cargo.toml
# version: 4
[package]
name = "ksp-app-raw-transaction-ingest-desk"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lib]
name = "ksp_app_raw_transaction_ingest_desk_lib"
path = "src/lib.rs"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "ksp-app-raw-transaction-ingest-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-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
ksp-store-lib = { path = "../ksp-store-lib" }
ksp-worker-api = { path = "../ksp-worker-api" }
ksp-worker-raw-transaction-ingest-lib = { path = "../ksp-worker-raw-transaction-ingest-lib" }
serde = { workspace = true, features = ["derive"] }
tauri.workspace = true
tauri-plugin-tracing.workspace = true
tokio = { workspace = true, features = ["time"] }
ts-rs.workspace = true
[dev-dependencies]
serde_json.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,76 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
<!-- version: 15 -->
# `ksp-app-raw-transaction-ingest-desk`
Application desktop Tauri KSP destinée à composer et superviser les routes live alimentant le Store commun en `RawTransaction`.
Le composant conserve les frontières KSP : Config possède les profils/endpoints/secrets, Transport les clients et sessions, Store la persistance et le Worker la logique d'ingestion. Le shell desktop ne déplace jamais ces responsabilités vers le frontend.
## Package
```text
package : ksp-app-raw-transaction-ingest-desk
lib : ksp_app_raw_transaction_ingest_desk_lib
bin : ksp-app-raw-transaction-ingest-desk
```
## Frontières
```text
Config -> composite, profils, secrets et résolution effective
Transport -> capabilities HTTP/WS/gRPC et ressources physiques
Desk -> catalogue logique, revalidation Start et ownership du lifecycle mono/multi-route
Store -> persistance via la façade KSP
Worker -> validation des contrats source, acquisition continue et lifecycle d'exécution
```
L'inventaire de routes est calculé côté Rust à partir du composite Config validé et des settings Transport/Store résolus. Il ne lance aucun probe réseau et n'ouvre ni Store ni Worker.
Au Start, le backend revalide une sélection logique par `profile_id + route_id + inventory_generation + commitment`. Il recharge Config, reproouve le réseau Store/Transport, reconstruit le pool HTTP ou l'endpoint WS/gRPC exact requis, ouvre le Store via `ksp-store-lib`, exige un health `Ready` puis lance le Worker exact via `RawTransactionIngestWorker::start_with_runtime_resources`. Les ressources physiques et handles restent exclusivement côté Rust.
Le runtime accepte désormais plusieurs routes simultanées sur un même réseau. Chaque route possède son Worker indépendant ; deux Starts de la même identité logique `profile_id + route_id` sont refusés, et un Start d'un autre réseau est refusé tant que le Store partagé reste ouvert. Le premier Worker ouvre et valide le Store, les suivants réutilisent le même `Arc<Store>`, puis la dernière route terminale déclenche seule la fermeture explicite du Store. Un fault d'une route ne stoppe pas les Workers siblings. Stop est ciblé par route et reste idempotent lorsqu'un terminal sûr a déjà été retenu.
Les races de contrôle sont fermées côté backend : un Stop qui rencontre une route encore `Starting` marque cette réservation pour arrêt coopératif et attend son cleanup au lieu de la considérer inactive. Dès qu'une fermeture de l'application commence, tout nouveau Start est refusé, les routes actives reçoivent Stop et les Starts déjà réservés sont marqués avant activation. La fenêtre principale ne quitte le processus qu'après cleanup borné des Workers et du Store partagé, ou avec un code d'échec après expiration de la borne.
Les profils applicatifs sont réseau-centriques (`devnet`, `mainnet`, `testnet`). Un profil peut agréger plusieurs profils Transport du même réseau : le provider reste une source de capability et ne devient jamais une identité réseau ni un choix de profil applicatif.
Une route explicitement liée à un provider nest projetée pour un réseau que si le composite déclare une source Transport de ce provider. Ainsi, la route Helius Transaction existe sur Devnet/Mainnet mais nest pas présentée sur Testnet tant quHelius ne fournit pas de source Testnet configurée.
Le frontend ne reçoit aucune URL, credential, URI Store, metadata secrète, source key, handle runtime ou payload `RawTransaction`.
Les requêtes IPC Start/Stop sont shape-strict : les champs inconnus sont rejetés et `profile_id` est borné/validé avant toute reconstruction. Le bridge de logging frontend applique également des bornes strictes et refuse les caractères de contrôle avant émission dans la façade Logging.
## Routes V1
```text
yellowstone-hydrated
standard-logs-hydrated
standard-block-direct
helius-transaction-hydrated
http-block-polling
```
Les profils standard publics committés déclarent `Block` en plus de `Logs`; `standard-block-direct` est donc composable depuis Config sur Devnet, Mainnet et Testnet. Cette déclaration ne transforme pas `blockSubscribe` en méthode stable et ne remplace pas la revalidation runtime.
Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle du Store et du Worker est revalidée lors du Start avant publication de l'accusé runtime. Le backend relaie désormais chaque Worker actif par un snapshot latest-value sûr : lifecycle, health, activity, admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. Les mises à jour sont coalescées côté backend à une cadence bornée de `500 ms` maximum par route, émises via l'événement Tauri `ksp-raw-ingest-route-status` et peuvent être resynchronisées avec `get_route_monitoring`. Les compteurs et slots `u64` sont projetés en texte décimal afin de rester exacts côté JavaScript. Le frontend consomme maintenant ce flux en temps réel et via resynchronisation explicite : chaque route affiche lifecycle/health/activity, persisted/source/gap summaries et ouvre un détail complet pipeline/persistence, reconnect/replay, continuité, gaps et repair. Les événements monitoring mettent aussi à jour les états actifs/terminaux afin que le sélecteur réseau et le Store partagé suivent le lifecycle réellement publié par le Worker. Les snapshots steady-state patchent les champs de carte en place ; la reconstruction des contrôles Start/Stop est réservée aux changements de lifecycle, afin de préserver la réactivité des interactions.
Pour `yellowstone-hydrated`, la stratégie Mainnet n'effectue plus un `getTransaction` par notification transactionnelle. Le Desk construit un abonnement Yellowstone Block léger ; chaque bloc observé déclenche une reconciliation HTTP `getBlock` Full/Base64 sur le même réseau. Le profil `publicnode_mainnet` expose un pool HTTP logique `default` contenant PublicNode et le RPC public Solana comme fallback de même priorité. Le pool conserve les limites propres à chaque endpoint, son health/cooldown et son round-robin interne. La reconciliation Yellowstone tolère un décalage bref entre le gRPC et les RPC HTTP grâce à un retry borné et interruptible par Stop. Les slots gRPC et hydrations `getBlock` restent bornés ; lorsqu'un consumer est saturé, la queue Transport attend de la capacité et propage la backpressure au stream au lieu de produire un `grpc_backpressure_overflow` local. Après un Stop explicite, un status provider reçu pendant le half-close ne remplace plus la fermeture coopérative ; un status observé pendant une session active reste soumis au reconnect/failure normal. Le chemin protobuf -> RAW direct reste différé tant que la canonicalisation exacte du meta n'est pas prouvée pour toutes les formes supportées.
La convergence Store de `0.3.15` conserve également une exception volontairement étroite : si un canonique complet existe déjà et qu'une route apporte le même RAW hors `logMessages` avec un marqueur exact `Log truncated` après un préfixe identique, l'observation moins complète est acceptée sans remplacer le canonique ni fault la route. Le sens inverse et toute divergence non prouvée restent des conflits terminaux dans cette version ; la gestion durable de variantes/résolutions appartient à `0.3.16`.
## Ports de développement
```text
Vite HTTP : 1440
Vite WS : 1441
```
Lancement :
```bash
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Le cycle frontend est déclenché par Tauri via ses hooks configurés ; les scripts npm applicatifs ne sont pas lancés directement par l'opérateur.
Voir [`USAGE.md`](USAGE.md) et [`../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md`](../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md).

View File

@@ -0,0 +1,84 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
<!-- version: 14 -->
# Utilisation de `ksp-app-raw-transaction-ingest-desk`
## 1. Lancement
Depuis la racine du workspace :
```bash
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Tauri déclenche lui-même les hooks frontend configurés. Les scripts npm applicatifs de développement, contrôle ou build ne sont pas lancés directement par l'opérateur.
Le runtime prépare les ressources Config nécessaires, charge le composite dédié, initialise Logging puis ouvre le shell desktop.
## 2. Vue Routes
La vue Routes charge un inventaire déterministe construit côté Rust depuis le composite Config validé. Le sélecteur présente les réseaux logiques `devnet`, `mainnet` et `testnet`, jamais les providers. Chaque réseau agrège les profils Transport same-network configurés pour ses capabilities. La vue projette, pour chaque route V1 :
```text
route_id
famille et label sûrs
réseau logique lorsque résolu
état Config
selectable oui/non
raison sûre lorsque la route est indisponible
génération d'inventaire
```
`Configured` signifie uniquement que toutes les requirements Config nécessaires à la route sont cohérentes. Les profils `solana-public` standard committés déclarent explicitement les capabilities `Logs` et `Block`; `standard-block-direct` est donc Configured sur les trois réseaux logiques tant que ce socle Config reste valide. L'inventaire seul ne fait aucun probe réseau et n'ouvre aucun Store ni Worker.
Un refresh reconstruit l'inventaire depuis Config et publie une nouvelle génération. La sélection visuelle d'un réseau ne choisit aucune URL, provider ou ressource physique et ne démarre aucune acquisition. Une capability optionnelle dont le secret n'est pas résolu rend uniquement la route qui en dépend indisponible ; les routes satisfaites par le Transport de base du même réseau restent composables.
Une route provider-specific dont aucune source nest déclarée pour le réseau nest pas une erreur de configuration : elle est hors inventaire pour ce réseau. Par exemple, Helius Transaction est projetée sur Devnet/Mainnet lorsque `transport.helius` existe, mais pas sur Testnet lorsquaucune source Helius Testnet nest déclarée.
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker, URI Store, source keys et payloads RAW restent backend-only.
Les commandes Start/Stop refusent les champs IPC inconnus et les identifiants logiques non bornés ou contenant des caractères de contrôle. Le logging frontend est lui aussi borné avant l'IPC ; une entrée hostile ne doit jamais être recopiée dans les diagnostics backend.
### Start/Stop multi-route
Pour une route `Configured`, choisir `confirmed` ou `finalized` puis utiliser `Start`. Le backend revalide la génération d'inventaire et la composition Config, ouvre le Store correspondant lorsqu'aucun Store compatible n'est encore actif, exige son état `Ready`, construit le Worker exact puis installe son handle avant de renvoyer l'accusé runtime.
Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert.
Un Stop peut être demandé même si le Start ciblé n'a pas encore fini d'ouvrir le Store ou d'installer le Worker : la réservation est alors marquée pour arrêt et le backend attend sa résolution. Lors de la fermeture de la fenêtre principale, le Desk ferme d'abord l'admission Start, demande l'arrêt de toutes les routes actives ou encore en démarrage et attend un cleanup borné avant de quitter. Il n'est donc pas nécessaire d'arrêter manuellement chaque route avant de fermer l'application.
Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. Le bouton `Resync monitoring` recharge explicitement les latest values backend ; les événements `ksp-raw-ingest-route-status` actualisent ensuite les cartes sans polling frontend. Le relais backend coalesce les changements à une cadence maximale d'une émission toutes les `500 ms` par route, et les changements steady-state mettent à jour la carte en place sans recréer les boutons Start/Stop. Une carte disposant d'un snapshot montre un résumé sûr (`health`, `activity`, persisted, sources actives, gaps) et le bouton `Supervision` ouvre le détail pipeline/persistence, sources, reconnect/replay, continuité et repair. Les derniers terminaux de la session restent consultables jusqu'à remplacement par un nouveau Start ou changement de session réseau.
Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La livraison gRPC et les hydrations restent bornées : une saturation locale applique de la backpressure au stream au lieu de dropper les updates ou de fault sur `grpc_backpressure_overflow`. Lors d'un Stop ciblé, un status distant reçu après le half-close local est une fermeture coopérative ; une erreur gRPC observée avant Stop reste une panne source/reconnect normale. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée.
Lorsque plusieurs routes observent la même transaction, l'entité canonique reste unique et les provenances restent séparées. Le cas `canonique complet + entrant explicitement Log truncated compatible` est accepté sans remplacer le canonique ni passer la route en `Faulted`. Les autres divergences de contenu restent fail-closed en `0.3.15`; les variantes conflictuelles durables et leur résolution via Store Desk sont prévues pour `0.3.16`.
## 3. Raisons d'indisponibilité
Les reasons exposées sont des codes applicatifs bornés, par exemple :
```text
profile_unresolved
network_mismatch
missing_http_get_block
missing_http_get_transaction
missing_http_block_scan
missing_ws_logs_capability
missing_ws_block_capability
missing_helius_transaction_capability
missing_yellowstone_grpc
missing_required_secret
```
Le texte arbitraire d'une erreur provider ou d'un secret Config n'est jamais projeté dans l'inventaire.
## 4. Diagnostics
La vue Diagnostics expose uniquement des informations techniques sûres nécessaires à l'exploitation du Desk, notamment l'identité de version, l'état du shell, la composition Config bootstrap, l'état Logging et la génération d'inventaire.
Les diagnostics ne doivent pas exposer de credentials, d'URI Store, d'URL d'endpoint, de source key, de handle runtime ou de payload `RawTransaction`.
## 5. Runtime packagé
Le build Tauri embarque les documents Config et schemas enregistrés. `ksp-config-lib` prépare ensuite la racine KSP user-writable sans embarquer `.env`.
Les fichiers Config et les secrets restent administrés par les mécanismes Config KSP ; l'interface ne contourne pas cette ownership.

View File

@@ -0,0 +1,12 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/build.rs
// version: 1
//! Build script for the KSP Raw Transaction Ingest 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": "Default capability for KSP Raw Transaction Ingest Desk",
"windows": [
"splash",
"main"
],
"permissions": [
"core:default",
"tracing:default"
]
}

View File

@@ -0,0 +1,6 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/fonts/README.md -->
<!-- version: 1 -->
# Fonts
`DOS_Amazigh.ttf` est repris byte-exact du gabarit Desk KSP existant pour préserver l'identité visuelle commune.

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,231 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html -->
<!-- version: 7 -->
<!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>Raw Transaction Ingest Desk — Routes</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">Raw Transaction Ingest 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">Routes</span>
</div>
<div class="d-flex align-items-center gap-2 ms-auto">
<span id="headerProfile" class="badge text-bg-light border text-dark"></span>
<span id="headerStore" class="badge text-bg-secondary">Store idle</span>
<span id="headerPhase" class="badge text-bg-secondary">inventory</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 Raw Transaction Ingest Desk">
<button class="nav-link text-start active" type="button" data-view="routes">
<i class="fa-solid fa-route me-2" aria-hidden="true"></i>Routes
</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="routes">
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Routes live</h1>
<p class="text-body-secondary mb-0">Inventaire Config et runtime multi-route avec un Worker indépendant par route et Store partagé same-network.</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<button id="refreshRouteMonitoring" class="btn btn-outline-secondary btn-sm" type="button">
<i class="fa-solid fa-wave-square me-2" aria-hidden="true"></i>Resync monitoring
</button>
<button id="refreshRouteInventory" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh inventory
</button>
</div>
</div>
<div class="row g-3 align-items-end mb-4">
<div class="col-12 col-md-4">
<label class="form-label" for="routeProfile">Réseau logique</label>
<select id="routeProfile" class="form-select" aria-label="Réseau Raw Transaction Ingest Desk"></select>
</div>
<div class="col-12 col-md-3">
<label class="form-label" for="routeCommitment">Commitment</label>
<select id="routeCommitment" class="form-select" aria-label="Commitment Raw Transaction Ingest Desk">
<option value="confirmed" selected>confirmed</option>
<option value="finalized">finalized</option>
</select>
</div>
<div class="col-6 col-md-2">
<div class="small text-body-secondary">Réseau</div>
<div id="routeNetwork" class="fw-semibold app-route-code"></div>
</div>
<div class="col-6 col-md-3">
<div class="small text-body-secondary">Génération</div>
<div id="routeInventoryGeneration" class="fw-semibold app-route-code"></div>
</div>
</div>
<div class="alert alert-info" role="status">
<strong>Configured</strong> signifie que Config prouve la composabilité. Start revalide Config puis lance un Worker indépendant. Les routes du même réseau partagent le Store ; chaque fault reste isolé à sa route.
</div>
<div id="routeCommandFeedback" class="alert alert-info" role="status" aria-live="polite" hidden></div>
<div class="card border-primary-subtle mb-4">
<div class="card-body d-flex align-items-center justify-content-between gap-3 flex-wrap">
<div>
<div class="small text-body-secondary">Runtime multi-route / Store partagé</div>
<div class="d-flex flex-wrap gap-2 mt-1">
<span id="routeRuntimeState" class="badge text-bg-secondary">idle</span>
<span id="routeRuntimeIdentity" class="app-route-code small text-body-secondary">aucun Worker actif</span>
</div>
</div>
</div>
</div>
<div class="small text-body-secondary mb-3">Réseau affiché : <span id="routeSelectedProfile" class="app-route-code"></span></div>
<div id="routeFoundation" class="row g-3 app-route-grid" aria-live="polite"></div>
<section id="routeMonitoringDetail" class="card shadow-sm mt-4 app-monitoring-detail" aria-live="polite" hidden>
<div class="card-header d-flex align-items-center justify-content-between gap-3 flex-wrap">
<div>
<div class="fw-semibold">Supervision route</div>
<div id="monitorRouteId" class="small text-body-secondary app-route-code"></div>
</div>
<div class="d-flex gap-2 flex-wrap align-items-center">
<span id="monitorLifecycle" class="badge text-bg-secondary"></span>
<span id="monitorHealthBadge" class="badge text-bg-secondary"></span>
<span id="monitorActivityBadge" class="badge text-bg-light border text-dark"></span>
</div>
</div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Network</div><div id="monitorNetwork" class="app-route-code fw-semibold"></div></div>
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Commitment</div><div id="monitorCommitment" class="app-route-code fw-semibold"></div></div>
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Sequence</div><div id="monitorSequence" class="app-route-code fw-semibold"></div></div>
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Fault</div><div id="monitorFault" class="app-route-code fw-semibold"></div></div>
</div>
<div class="row g-3">
<div class="col-12 col-xl-6">
<div class="card h-100 border-light-subtle">
<div class="card-header fw-semibold">Pipeline / persistence</div>
<div class="card-body">
<dl class="row mb-0 app-monitoring-list">
<dt class="col-7">Health</dt><dd id="monitorHealth" class="col-5"></dd>
<dt class="col-7">Activity</dt><dd id="monitorActivity" class="col-5"></dd>
<dt class="col-7">Admission queue</dt><dd id="monitorAdmissionQueue" class="col-5"></dd>
<dt class="col-7">Persistence in-flight</dt><dd id="monitorPersistence" class="col-5"></dd>
<dt class="col-7">Admitted</dt><dd id="monitorAdmittedTotal" class="col-5"></dd>
<dt class="col-7">Canonicalized</dt><dd id="monitorCanonicalizedTotal" class="col-5"></dd>
<dt class="col-7">Persisted</dt><dd id="monitorPersistedTotal" class="col-5"></dd>
<dt class="col-7">RAW inserted</dt><dd id="monitorEntityInsertedTotal" class="col-5"></dd>
<dt class="col-7">RAW already present</dt><dd id="monitorEntityAlreadyPresentTotal" class="col-5"></dd>
<dt class="col-7">RAW purged skipped</dt><dd id="monitorEntitySkippedPurgedTotal" class="col-5"></dd>
<dt class="col-7">Observations inserted</dt><dd id="monitorObservationInsertedTotal" class="col-5"></dd>
<dt class="col-7">Observations already present</dt><dd id="monitorObservationAlreadyPresentTotal" class="col-5"></dd>
<dt class="col-7">Content conflicts</dt><dd id="monitorContentConflictTotal" class="col-5"></dd>
<dt class="col-7">Store failures</dt><dd id="monitorStoreFailureTotal" class="col-5"></dd>
<dt class="col-7">Source failures</dt><dd id="monitorSourceFailureTotal" class="col-5"></dd>
<dt class="col-7">Backpressure waits</dt><dd id="monitorBackpressureWaitTotal" class="col-5"></dd>
<dt class="col-7">Hydration pending</dt><dd id="monitorHydrationPending" class="col-5"></dd>
<dt class="col-7">Processing frontier</dt><dd id="monitorProcessingFrontier" class="col-5"></dd>
<dt class="col-7">Oldest pending slot</dt><dd id="monitorOldestPending" class="col-5"></dd>
</dl>
</div>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="card h-100 border-light-subtle">
<div class="card-header fw-semibold">Sources / continuité / repair</div>
<div class="card-body">
<dl class="row mb-0 app-monitoring-list">
<dt class="col-7">Source state</dt><dd id="monitorSourceState" class="col-5"></dd>
<dt class="col-7">Source counts</dt><dd id="monitorSourceCounts" class="col-5"></dd>
<dt class="col-7">Reconnects</dt><dd id="monitorSourceReconnectTotal" class="col-5"></dd>
<dt class="col-7">Replay attempts</dt><dd id="monitorSourceReplayAttemptTotal" class="col-5"></dd>
<dt class="col-7">Continuity gaps source</dt><dd id="monitorSourceContinuityGapTotal" class="col-5"></dd>
<dt class="col-7">Policy observed</dt><dd id="monitorContinuityPolicyObserved" class="col-5"></dd>
<dt class="col-7">Continuity frontier</dt><dd id="monitorContinuityFrontier" class="col-5"></dd>
<dt class="col-7">Open gaps</dt><dd id="monitorContinuityHasOpenGaps" class="col-5"></dd>
<dt class="col-7">Failed losses reconciled</dt><dd id="monitorFailedSourceLossesReconciled" class="col-5"></dd>
<dt class="col-7">Future coverage</dt><dd id="monitorFutureTargetCoverage" class="col-5"></dd>
<dt class="col-7">Open / repairing</dt><dd class="col-5"><span id="monitorOpenGapCount"></span> / <span id="monitorRepairingGapCount"></span></dd>
<dt class="col-7">Repaired / unresolved</dt><dd class="col-5"><span id="monitorRepairedGapTotal"></span> / <span id="monitorUnresolvedGapTotal"></span></dd>
<dt class="col-7">Oldest open gap</dt><dd id="monitorOldestOpenGap" class="col-5"></dd>
<dt class="col-7">Replay repair</dt><dd id="monitorReplayRepairTotal" class="col-5"></dd>
<dt class="col-7">Redundant coverage repair</dt><dd id="monitorRedundantCoverageRepairTotal" class="col-5"></dd>
<dt class="col-7">HTTP scan repair</dt><dd id="monitorHttpScanRepairTotal" class="col-5"></dd>
<dt class="col-7">Block fetch repair</dt><dd id="monitorRepairBlockFetchTotal" class="col-5"></dd>
<dt class="col-7">Transaction hydration repair</dt><dd id="monitorRepairTransactionHydrationTotal" class="col-5"></dd>
</dl>
</div>
</div>
</div>
</div>
<div class="mt-3">
<div class="fw-semibold mb-2">Gaps courants / récents</div>
<div id="monitorGapList" class="list-group app-monitoring-gaps"></div>
</div>
</div>
</section>
</section>
<section data-view-panel="diagnostics" hidden>
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Diagnostics</h1>
<p class="text-body-secondary mb-0">État sûr du shell Config, Logging et de la génération d'inventaire.</p>
</div>
<button id="refreshRuntimeStatus" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
</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 composite bootstrap</dt><dd id="runtimeCompositeProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Logging</dt><dd id="runtimeLoggingProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Fallback Logging</dt><dd id="runtimeLoggingFallback" class="col-sm-7"></dd>
<dt class="col-sm-5">Inventaire default</dt><dd id="runtimeInventoryDefaultProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Génération inventaire</dt><dd id="runtimeInventoryGeneration" class="col-sm-7"></dd>
</dl>
<div id="runtimeDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
</div>
</div>
</section>
</div>
</div>
</div>
</section>
</div>
</main>
<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,167 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_app.scss
// version: 3
$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-logo {
display: block;
width: auto;
height: 42px;
}
.app-scrollable {
height: 100%;
max-height: 100%;
}
.app-shell-card {
width: 100%;
max-width: 1320px;
min-width: 0;
}
.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-route-code {
font-family: var(--bs-font-monospace);
}
.app-route-grid {
margin-right: 0;
margin-left: 0;
}
.app-route-grid > * {
min-width: 0;
}
.app-route-card {
min-width: 0;
max-width: 100%;
overflow: hidden;
border-left: 4px solid rgba($primary, 0.55);
}
.app-route-code {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.app-route-monitoring-summary {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.app-monitoring-detail {
min-width: 0;
overflow: hidden;
}
.app-monitoring-list dt,
.app-monitoring-list dd {
margin-bottom: 0.45rem;
}
.app-monitoring-list dd,
.app-monitoring-gaps,
.app-monitoring-detail .app-route-code {
font-family: var(--bs-font-monospace);
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.app-monitoring-gaps .list-group-item {
min-width: 0;
}
.app-placeholder {
min-height: 320px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 2rem;
border: 1px dashed var(--bs-border-color);
border-radius: var(--bs-border-radius-lg);
color: var(--bs-secondary-color);
background: var(--bs-body-bg);
text-align: center;
}
.app-placeholder i {
font-size: 2.5rem;
}
@media (max-width: 991.98px) {
.app-sidebar {
width: 190px;
min-width: 190px;
max-width: 190px;
}
}

View File

@@ -0,0 +1,160 @@
// file: crates/ksp-app-raw-transaction-ingest-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-raw-transaction-ingest-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-raw-transaction-ingest-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-raw-transaction-ingest-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-raw-transaction-ingest-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-raw-transaction-ingest-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-raw-transaction-ingest-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 Raw Transaction Ingest Desk — Chargement</title>
</head>
<body>
<div id="splash-container" style="opacity: 0;">
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Raw Transaction Ingest Desk">
<div id="app-name">RAW Ingest</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,116 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/frontend_log.ts
// version: 2
import { invoke } from "@tauri-apps/api/core";
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_raw_transaction_ingest_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 FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024;
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(" ");
}
function boundMessage(message: string): string {
return message.length <= FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS ? message : `${message.slice(0, FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS)}`;
}
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
const payload: FrontendLogPayloadDto = {
level,
targetId,
message: boundMessage(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-raw-transaction-ingest-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;
}
}

View File

@@ -0,0 +1,760 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
// version: 8
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import { listen } from "@tauri-apps/api/event";
import type { ShellStatusDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts";
import type { RawIngestCommitment } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestCommitment.ts";
import type { RawIngestRouteId } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteId.ts";
import type { RawIngestRouteInventoryDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts";
import type { RawIngestRouteRuntimeDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteRuntimeDto.ts";
import type { RawIngestRouteStartRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts";
import type { RawIngestRouteStopRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts";
import type { RawIngestRouteMonitoringDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteMonitoringDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
installFrontendConsoleBridge("main");
type ViewId = "routes" | "diagnostics";
const viewTitles: Record<ViewId, string> = {
routes: "Routes",
diagnostics: "Diagnostics",
};
const activeRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
const lastRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
const routeMonitoring = new Map<string, RawIngestRouteMonitoringDto>();
const routeCards = new Map<string, HTMLElement>();
let routeInventory: RawIngestRouteInventoryDto | null = null;
let selectedProfileId: string | null = null;
let selectedMonitoringKey: string | null = null;
function isViewId(value: string | undefined): value is ViewId {
return value === "routes" || value === "diagnostics";
}
function activateView(viewId: ViewId, source: "startup" | "user"): void {
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
panel.hidden = panel.dataset.viewPanel !== viewId;
});
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
const active = button.dataset.view === viewId;
button.classList.toggle("active", active);
button.setAttribute("aria-current", active ? "page" : "false");
});
const header = document.querySelector<HTMLElement>("#headerViewTitle");
if (header) {
header.textContent = viewTitles[viewId];
}
document.title = `Raw Transaction Ingest Desk — ${viewTitles[viewId]}`;
frontendTrace("main", "Raw Transaction Ingest Desk view DOM updated", { viewId, source });
}
function bindFrontendInteractions(): void {
document.addEventListener(
"click",
event => {
const source = event.target;
if (!(source instanceof Element)) {
return;
}
const control = source.closest<HTMLElement>('button, a, input, select, textarea, [role="button"], [role="tab"], [data-view]');
if (!control) {
return;
}
frontendTrace("main", "Raw Transaction Ingest Desk frontend control clicked", {
controlId: control.id || null,
role: control.getAttribute("role"),
tagName: control.tagName.toLowerCase(),
viewId: control.dataset.view ?? null,
});
},
true,
);
}
function bindNavigation(): void {
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
button.addEventListener("click", () => {
const viewId = button.dataset.view;
frontendDebug("main", "Raw Transaction Ingest Desk navigation tab clicked", { viewId: viewId ?? "unknown" });
if (isViewId(viewId)) {
activateView(viewId, "user");
}
});
});
}
function reasonLabel(reason: string | null): string {
const labels: Record<string, string> = {
profile_unresolved: "profil Config non résolu",
network_mismatch: "réseau Transport/Store incohérent",
missing_http_get_block: "HTTP getBlock indisponible",
missing_http_get_transaction: "HTTP getTransaction indisponible",
missing_http_block_scan: "HTTP block scan incomplet",
missing_ws_logs_capability: "capability WS Logs absente",
missing_ws_block_capability: "capability WS Block absente",
missing_helius_transaction_capability: "capability Helius Transaction absente",
missing_yellowstone_grpc: "Yellowstone gRPC absent",
missing_required_secret: "secret Config requis non résolu",
};
if (reason === null) {
return "composable depuis Config";
}
return labels[reason] ?? reason;
}
function runtimeKey(profileId: string, routeId: RawIngestRouteId): string {
return `${profileId}::${routeId}`;
}
function lifecycleBadgeClass(state: string): string {
if (state === "faulted") {
return "badge text-bg-danger";
}
if (state === "running") {
return "badge text-bg-success";
}
if (state === "starting" || state === "stopping") {
return "badge text-bg-primary";
}
if (state === "configured") {
return "badge text-bg-info";
}
return "badge text-bg-secondary";
}
function yesNo(value: boolean): string {
return value ? "oui" : "non";
}
function optionalText(value: string | null): string {
return value ?? "—";
}
function safeCommandErrorText(caughtError: unknown, fallback: string): string {
if (typeof caughtError === "object" && caughtError !== null) {
const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
return `${candidate.domain}/${candidate.code}: ${candidate.message}`;
}
}
return fallback;
}
function renderRouteFeedback(message: string, level: "danger" | "info" | "success"): void {
const feedback = document.querySelector<HTMLElement>("#routeCommandFeedback");
if (!feedback) {
return;
}
feedback.hidden = false;
feedback.classList.remove("alert-danger", "alert-info", "alert-success");
feedback.classList.add(`alert-${level}`);
feedback.textContent = message;
}
function clearRouteFeedback(): void {
const feedback = document.querySelector<HTMLElement>("#routeCommandFeedback");
if (!feedback) {
return;
}
feedback.hidden = true;
feedback.textContent = "";
}
function monitoringRuntime(status: RawIngestRouteMonitoringDto): RawIngestRouteRuntimeDto {
return {
commitment: status.commitment,
inventoryGeneration: status.inventoryGeneration,
network: status.network,
profileId: status.profileId,
routeId: status.routeId,
state: status.state,
};
}
function recordRouteMonitoring(status: RawIngestRouteMonitoringDto): void {
const key = runtimeKey(status.profileId, status.routeId);
const runtime = monitoringRuntime(status);
routeMonitoring.set(key, status);
lastRuntimes.set(key, runtime);
if (runtimeIsActive(runtime)) {
activeRuntimes.set(key, runtime);
} else {
activeRuntimes.delete(key);
}
}
function selectRouteMonitoring(profileId: string, routeId: RawIngestRouteId, family: string, source: "event" | "user"): void {
const key = runtimeKey(profileId, routeId);
selectedMonitoringKey = key;
renderMonitoringDetail();
frontendDebug("main", "Raw Transaction Ingest Desk route supervision selected", {
family,
routeId,
selected: routeMonitoring.has(key),
source,
});
}
function renderSelectedProfile(): void {
if (routeInventory === null) {
return;
}
const profile = routeInventory.profiles.find(candidate => candidate.profileId === selectedProfileId) ?? routeInventory.profiles[0];
if (!profile) {
return;
}
selectedProfileId = profile.profileId;
text("#routeNetwork", profile.network ?? "non résolu");
text("#routeSelectedProfile", profile.profileId);
text("#headerProfile", profile.profileId);
const routeRoot = document.querySelector<HTMLElement>("#routeFoundation");
if (routeRoot) {
routeCards.clear();
routeRoot.replaceChildren();
for (const route of profile.routes) {
const key = runtimeKey(profile.profileId, route.routeId);
const runtime = activeRuntimes.get(key) ?? lastRuntimes.get(key) ?? null;
const monitoring = routeMonitoring.get(key) ?? null;
const projectedState = monitoring?.state ?? runtime?.state ?? route.state;
const active = projectedState === "starting" || projectedState === "running" || projectedState === "stopping";
const column = document.createElement("div");
column.className = "col-12 col-xl-6 app-route-column";
const card = document.createElement("div");
card.className = "card h-100 shadow-sm app-route-card";
const body = document.createElement("div");
body.className = "card-body";
const heading = document.createElement("div");
heading.className = "d-flex align-items-start justify-content-between gap-3";
const headingText = document.createElement("div");
const title = document.createElement("h2");
title.className = "h6 mb-1";
title.textContent = route.label;
const code = document.createElement("div");
code.className = "app-route-code text-body-secondary small";
code.textContent = route.routeId;
headingText.append(title, code);
const family = document.createElement("span");
family.className = "badge text-bg-light border text-dark";
family.textContent = route.family;
heading.append(headingText, family);
const meta = document.createElement("div");
meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta";
const state = document.createElement("span");
state.className = lifecycleBadgeClass(projectedState);
state.dataset.monitoringField = "state";
state.textContent = projectedState;
const network = document.createElement("span");
network.className = "badge text-bg-light border text-dark";
network.textContent = route.network ?? "network unresolved";
const reason = document.createElement("span");
reason.className = route.selectable ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
reason.textContent = reasonLabel(route.reason);
meta.append(state, network, reason);
body.append(heading, meta);
if (monitoring) {
const summary = document.createElement("div");
summary.className = "app-route-monitoring-summary mt-3";
const health = document.createElement("span");
health.className = monitoring.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
health.dataset.monitoringField = "health";
health.textContent = `health: ${monitoring.health}`;
const activity = document.createElement("span");
activity.className = "badge text-bg-light border text-dark";
activity.dataset.monitoringField = "activity";
activity.textContent = `activity: ${monitoring.activity}`;
const persisted = document.createElement("span");
persisted.className = "badge text-bg-light border text-dark";
persisted.dataset.monitoringField = "persisted";
persisted.textContent = `persisted: ${monitoring.persistedTotal}`;
const sources = document.createElement("span");
sources.className = "badge text-bg-light border text-dark";
sources.dataset.monitoringField = "sources";
sources.textContent = `sources: ${monitoring.sourceActive}/${monitoring.sourceTotal}`;
const gaps = document.createElement("span");
gaps.className = monitoring.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
gaps.dataset.monitoringField = "gaps";
gaps.textContent = `gaps: ${monitoring.openGapCount}`;
summary.append(health, activity, persisted, sources, gaps);
body.append(summary);
}
const actions = document.createElement("div");
actions.className = "d-flex flex-wrap gap-2 mt-3";
if (route.selectable) {
const start = document.createElement("button");
start.className = "btn btn-primary btn-sm";
start.type = "button";
start.textContent = "Start";
start.disabled = active;
start.dataset.routeId = route.routeId;
start.addEventListener("click", () => {
selectedMonitoringKey = key;
clearRouteFeedback();
void startRoute(route.routeId).catch(caughtError => {
renderRouteFeedback(safeCommandErrorText(caughtError, "Le démarrage de la route a été refusé."), "danger");
frontendWarn("main", "Raw Transaction Ingest Desk route Start failed", { routeId: route.routeId });
});
});
actions.append(start);
if (active && runtime !== null) {
const stop = document.createElement("button");
stop.className = "btn btn-outline-danger btn-sm";
stop.type = "button";
stop.textContent = "Stop";
stop.disabled = projectedState === "stopping";
stop.dataset.routeId = route.routeId;
stop.addEventListener("click", () => {
clearRouteFeedback();
void stopRoute(runtime).catch(caughtError => {
renderRouteFeedback(safeCommandErrorText(caughtError, "L'arrêt de la route a été refusé."), "danger");
frontendWarn("main", "Raw Transaction Ingest Desk route Stop failed", { routeId: route.routeId });
});
});
actions.append(stop);
}
}
if (monitoring) {
const supervise = document.createElement("button");
supervise.className = selectedMonitoringKey === key ? "btn btn-secondary btn-sm" : "btn btn-outline-secondary btn-sm";
supervise.type = "button";
supervise.textContent = "Supervision";
supervise.dataset.routeId = route.routeId;
supervise.addEventListener("click", () => selectRouteMonitoring(profile.profileId, route.routeId, route.family, "user"));
actions.append(supervise);
}
if (actions.childElementCount > 0) {
body.append(actions);
}
card.append(body);
routeCards.set(key, card);
column.append(card);
routeRoot.append(column);
}
}
renderMonitoringDetail();
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory profile rendered", {
activeRouteCount: activeRuntimes.size,
monitoringCount: routeMonitoring.size,
profileId: profile.profileId,
routeCount: profile.routes.length,
selectableCount: profile.routes.filter(route => route.selectable).length,
});
}
function renderMonitoringDetail(): void {
const panel = document.querySelector<HTMLElement>("#routeMonitoringDetail");
if (!panel) {
return;
}
const monitoring = selectedMonitoringKey === null ? null : (routeMonitoring.get(selectedMonitoringKey) ?? null);
if (!monitoring) {
panel.hidden = true;
return;
}
panel.hidden = false;
text("#monitorRouteId", monitoring.routeId);
text("#monitorNetwork", monitoring.network);
text("#monitorCommitment", monitoring.commitment);
text("#monitorSequence", monitoring.sequence);
text("#monitorHealth", monitoring.health);
text("#monitorActivity", monitoring.activity);
text("#monitorSourceState", optionalText(monitoring.sourceState));
text("#monitorFault", monitoring.faultDomain === null || monitoring.faultCode === null ? "—" : `${monitoring.faultDomain}/${monitoring.faultCode}`);
const lifecycle = document.querySelector<HTMLElement>("#monitorLifecycle");
if (lifecycle) {
lifecycle.className = lifecycleBadgeClass(monitoring.state);
lifecycle.textContent = monitoring.state;
}
const health = document.querySelector<HTMLElement>("#monitorHealthBadge");
if (health) {
health.className = monitoring.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
health.textContent = monitoring.health;
}
const activity = document.querySelector<HTMLElement>("#monitorActivityBadge");
if (activity) {
activity.className = "badge text-bg-light border text-dark";
activity.textContent = monitoring.activity;
}
const values: Record<string, string> = {
monitorAdmissionQueue: `${monitoring.admissionQueueDepth} / ${monitoring.admissionQueueCapacity}`,
monitorPersistence: `${monitoring.inFlightPersistence} / ${monitoring.persistenceConcurrency}`,
monitorAdmittedTotal: monitoring.admittedTotal,
monitorCanonicalizedTotal: monitoring.canonicalizedTotal,
monitorPersistedTotal: monitoring.persistedTotal,
monitorEntityInsertedTotal: monitoring.entityInsertedTotal,
monitorEntityAlreadyPresentTotal: monitoring.entityAlreadyPresentTotal,
monitorEntitySkippedPurgedTotal: monitoring.entitySkippedPurgedTotal,
monitorObservationInsertedTotal: monitoring.observationInsertedTotal,
monitorObservationAlreadyPresentTotal: monitoring.observationAlreadyPresentTotal,
monitorContentConflictTotal: monitoring.contentConflictTotal,
monitorStoreFailureTotal: monitoring.storeFailureTotal,
monitorSourceFailureTotal: monitoring.sourceFailureTotal,
monitorBackpressureWaitTotal: monitoring.backpressureWaitTotal,
monitorHydrationPending: monitoring.hydrationPending.toString(),
monitorProcessingFrontier: optionalText(monitoring.processingFrontierSlot),
monitorOldestPending: optionalText(monitoring.oldestPendingSlot),
monitorSourceCounts: `${monitoring.sourceActive} active / ${monitoring.sourceReconnecting} reconnecting / ${monitoring.sourceFailed} failed / ${monitoring.sourceTotal} total`,
monitorSourceReconnectTotal: monitoring.sourceReconnectTotal,
monitorSourceReplayAttemptTotal: monitoring.sourceReplayAttemptTotal,
monitorSourceContinuityGapTotal: monitoring.sourceContinuityGapTotal,
monitorContinuityPolicyObserved: yesNo(monitoring.continuityPolicyObserved),
monitorContinuityFrontier: optionalText(monitoring.continuityFrontierSlot),
monitorContinuityHasOpenGaps: yesNo(monitoring.continuityHasOpenGaps),
monitorFailedSourceLossesReconciled: yesNo(monitoring.failedSourceLossesReconciled),
monitorFutureTargetCoverage: yesNo(monitoring.futureTargetCoverage),
monitorOpenGapCount: monitoring.openGapCount.toString(),
monitorRepairingGapCount: monitoring.repairingGapCount.toString(),
monitorRepairedGapTotal: monitoring.repairedGapTotal,
monitorUnresolvedGapTotal: monitoring.unresolvedGapTotal,
monitorOldestOpenGap: optionalText(monitoring.oldestOpenGapStartSlot),
monitorReplayRepairTotal: monitoring.replayRepairTotal,
monitorRedundantCoverageRepairTotal: monitoring.redundantCoverageRepairTotal,
monitorHttpScanRepairTotal: monitoring.httpScanRepairTotal,
monitorRepairBlockFetchTotal: monitoring.repairBlockFetchTotal,
monitorRepairTransactionHydrationTotal: monitoring.repairTransactionHydrationTotal,
};
for (const [id, value] of Object.entries(values)) {
text(`#${id}`, value);
}
const gaps = document.querySelector<HTMLElement>("#monitorGapList");
if (gaps) {
gaps.replaceChildren();
if (monitoring.gaps.length === 0) {
const empty = document.createElement("div");
empty.className = "text-body-secondary small";
empty.textContent = "Aucun gap courant ou récent projeté.";
gaps.append(empty);
} else {
for (const gap of monitoring.gaps) {
const item = document.createElement("div");
item.className = "list-group-item";
const top = document.createElement("div");
top.className = "d-flex justify-content-between gap-3 flex-wrap";
const range = document.createElement("span");
range.className = "app-route-code";
range.textContent = `${gap.startSlot} ${gap.endSlot}`;
const badge = document.createElement("span");
badge.className = gap.state === "open" ? "badge text-bg-warning" : "badge text-bg-secondary";
badge.textContent = gap.state;
top.append(range, badge);
const meta = document.createElement("div");
meta.className = "small text-body-secondary mt-1";
meta.textContent = `gap ${gap.gapId} · ${gap.reason} · repair ${gap.lastMethod ?? "—"}`;
item.append(top, meta);
gaps.append(item);
}
}
}
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring detail rendered", {
gapCount: monitoring.gaps.length,
routeId: monitoring.routeId,
sequence: monitoring.sequence,
state: monitoring.state,
});
}
function renderRouteMonitoringCard(status: RawIngestRouteMonitoringDto): boolean {
const key = runtimeKey(status.profileId, status.routeId);
const card = routeCards.get(key);
if (!card) {
return false;
}
const field = (name: string): HTMLElement | null => card.querySelector<HTMLElement>(`[data-monitoring-field="${name}"]`);
const state = field("state");
const health = field("health");
const activity = field("activity");
const persisted = field("persisted");
const sources = field("sources");
const gaps = field("gaps");
if (!state || !health || !activity || !persisted || !sources || !gaps) {
return false;
}
state.className = lifecycleBadgeClass(status.state);
state.textContent = status.state;
health.className = status.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
health.textContent = `health: ${status.health}`;
activity.textContent = `activity: ${status.activity}`;
persisted.textContent = `persisted: ${status.persistedTotal}`;
sources.textContent = `sources: ${status.sourceActive}/${status.sourceTotal}`;
gaps.className = status.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
gaps.textContent = `gaps: ${status.openGapCount}`;
return true;
}
function selectedCommitment(): RawIngestCommitment {
const selector = document.querySelector<HTMLSelectElement>("#routeCommitment");
return selector?.value === "finalized" ? "finalized" : "confirmed";
}
function runtimeIsActive(runtime: RawIngestRouteRuntimeDto): boolean {
return runtime.state === "starting" || runtime.state === "running" || runtime.state === "stopping";
}
function renderMultiRouteRuntime(): void {
const active = Array.from(activeRuntimes.values());
const state = document.querySelector<HTMLElement>("#routeRuntimeState");
if (state) {
state.textContent = active.length === 0 ? "idle" : `${active.length} active`;
state.className = active.length === 0 ? "badge text-bg-secondary" : "badge text-bg-primary";
}
const identities = active.map(runtime => `${runtime.routeId}/${runtime.commitment}`).join(", ");
text("#routeRuntimeIdentity", identities.length === 0 ? "aucun Worker actif" : identities);
const store = document.querySelector<HTMLElement>("#headerStore");
if (store) {
store.textContent = active.length === 0 ? "Store idle" : "Store shared";
store.className = active.length === 0 ? "badge text-bg-secondary" : "badge text-bg-success";
}
const profile = document.querySelector<HTMLSelectElement>("#routeProfile");
if (profile) {
profile.disabled = active.length > 0;
}
}
async function startRoute(routeId: RawIngestRouteId): Promise<void> {
if (routeInventory === null || selectedProfileId === null) {
return;
}
const key = runtimeKey(selectedProfileId, routeId);
const existing = activeRuntimes.get(key);
if (existing !== undefined && runtimeIsActive(existing)) {
return;
}
const request: RawIngestRouteStartRequestDto = {
commitment: selectedCommitment(),
inventoryGeneration: routeInventory.generation,
profileId: selectedProfileId,
routeId,
};
frontendDebug("main", "Raw Transaction Ingest Desk route Start requested", {
commitment: request.commitment,
profileId: request.profileId,
routeId: request.routeId,
});
const response = await invokeKsp<RawIngestRouteRuntimeDto>("main", "start_route", { request });
lastRuntimes.set(key, response);
if (runtimeIsActive(response)) {
activeRuntimes.set(key, response);
} else {
activeRuntimes.delete(key);
}
renderMultiRouteRuntime();
renderSelectedProfile();
renderRouteFeedback(`Route ${response.routeId} démarrée (${response.state}).`, "success");
await syncRouteMonitoring("start");
frontendInfo("main", "Raw Transaction Ingest Desk route Start acknowledged", {
activeRouteCount: activeRuntimes.size,
profileId: response.profileId,
routeId: response.routeId,
state: response.state,
});
}
async function stopRoute(runtime: RawIngestRouteRuntimeDto): Promise<void> {
const key = runtimeKey(runtime.profileId, runtime.routeId);
const request: RawIngestRouteStopRequestDto = {
profileId: runtime.profileId,
routeId: runtime.routeId,
};
frontendDebug("main", "Raw Transaction Ingest Desk targeted route Stop requested", {
profileId: request.profileId,
routeId: request.routeId,
});
const response = await invokeKsp<RawIngestRouteRuntimeDto>("main", "stop_route", { request });
lastRuntimes.set(key, response);
if (runtimeIsActive(response)) {
activeRuntimes.set(key, response);
} else {
activeRuntimes.delete(key);
}
renderMultiRouteRuntime();
renderSelectedProfile();
renderRouteFeedback(`Route ${response.routeId} arrêtée (${response.state}).`, "info");
await syncRouteMonitoring("stop");
frontendInfo("main", "Raw Transaction Ingest Desk targeted route Stop completed", {
activeRouteCount: activeRuntimes.size,
profileId: response.profileId,
routeId: response.routeId,
state: response.state,
});
}
function applyRouteMonitoring(status: RawIngestRouteMonitoringDto, source: "event" | "startup" | "start" | "stop" | "user"): void {
const key = runtimeKey(status.profileId, status.routeId);
const previous = routeMonitoring.get(key) ?? null;
recordRouteMonitoring(status);
if (selectedMonitoringKey === null && status.profileId === selectedProfileId) {
selectedMonitoringKey = key;
}
renderMultiRouteRuntime();
const lifecycleChanged = previous === null || previous.state !== status.state;
if (lifecycleChanged || !renderRouteMonitoringCard(status)) {
renderSelectedProfile();
} else if (selectedMonitoringKey === key) {
renderMonitoringDetail();
}
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring latest value applied", {
lifecycleChanged,
routeId: status.routeId,
sequence: status.sequence,
source,
state: status.state,
terminal: status.terminal,
});
}
async function syncRouteMonitoring(source: "startup" | "start" | "stop" | "user"): Promise<void> {
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring resynchronization requested", { source });
const statuses = await invokeKsp<RawIngestRouteMonitoringDto[]>("main", "get_route_monitoring");
routeMonitoring.clear();
activeRuntimes.clear();
lastRuntimes.clear();
for (const status of statuses) {
recordRouteMonitoring(status);
}
if (selectedMonitoringKey !== null && !routeMonitoring.has(selectedMonitoringKey)) {
selectedMonitoringKey = null;
}
if (selectedMonitoringKey === null) {
const preferred = statuses.find(status => status.profileId === selectedProfileId) ?? statuses[0];
if (preferred) {
selectedMonitoringKey = runtimeKey(preferred.profileId, preferred.routeId);
}
}
renderMultiRouteRuntime();
renderSelectedProfile();
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring resynchronization completed", {
activeRouteCount: activeRuntimes.size,
source,
statusCount: statuses.length,
});
}
async function bindRouteMonitoring(): Promise<void> {
await listen<RawIngestRouteMonitoringDto>("ksp-raw-ingest-route-status", event => {
applyRouteMonitoring(event.payload, "event");
});
document.querySelector<HTMLButtonElement>("#refreshRouteMonitoring")?.addEventListener("click", () => {
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring refresh button clicked");
void syncRouteMonitoring("user").catch(caughtError => {
renderRouteFeedback(safeCommandErrorText(caughtError, "La resynchronisation du monitoring a échoué."), "danger");
frontendWarn("main", "Raw Transaction Ingest Desk route monitoring resynchronization failed");
});
});
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring listener and resynchronization control installed");
}
function renderRouteInventory(inventory: RawIngestRouteInventoryDto): void {
routeInventory = inventory;
const requestedProfile = selectedProfileId;
const selectedStillExists = requestedProfile !== null && inventory.profiles.some(profile => profile.profileId === requestedProfile);
selectedProfileId = selectedStillExists ? requestedProfile : inventory.defaultProfile;
const selector = document.querySelector<HTMLSelectElement>("#routeProfile");
if (selector) {
selector.replaceChildren();
for (const profile of inventory.profiles) {
const option = document.createElement("option");
option.value = profile.profileId;
option.textContent = profile.isDefault ? `${profile.profileId} (default)` : profile.profileId;
option.selected = profile.profileId === selectedProfileId;
selector.append(option);
}
}
text("#routeInventoryGeneration", inventory.generation.toString());
text("#runtimeInventoryGeneration", inventory.generation.toString());
text("#runtimeInventoryDefaultProfile", inventory.defaultProfile);
renderSelectedProfile();
renderMultiRouteRuntime();
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory rendered", {
generation: inventory.generation,
profileCount: inventory.profiles.length,
});
}
function text(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
function renderRuntimeStatus(status: ShellStatusDto): void {
text("#runtimeVersion", status.applicationVersion);
text("#runtimeShellPhase", status.shellPhase);
text("#runtimeConfigDocuments", status.configDocumentCount.toString());
text("#runtimeCompositeProfile", status.activeCompositeProfile ?? "fallback/unresolved");
text("#runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback");
text("#runtimeLoggingFallback", status.fallbackLoggingActive ? "oui" : "non");
text("#headerPhase", status.shellPhase);
const diagnostic = document.querySelector<HTMLElement>("#runtimeDiagnostic");
if (diagnostic) {
diagnostic.hidden = status.startupDiagnostic === null;
diagnostic.textContent = status.startupDiagnostic ? `${status.startupDiagnostic.domain}/${status.startupDiagnostic.code}: ${status.startupDiagnostic.message}` : "";
}
frontendTrace("main", "Raw Transaction Ingest Desk runtime status rendered", {
fallbackLoggingActive: status.fallbackLoggingActive,
hasStartupDiagnostic: status.startupDiagnostic !== null,
});
}
async function refreshRouteInventory(): Promise<void> {
frontendDebug("main", "Raw Transaction Ingest Desk Config route inventory refresh requested");
const inventory = await invokeKsp<RawIngestRouteInventoryDto>("main", "get_route_inventory");
renderRouteInventory(inventory);
}
async function refreshRuntimeStatus(): Promise<void> {
frontendDebug("main", "Raw Transaction Ingest Desk runtime status refresh requested");
const status = await invokeKsp<ShellStatusDto>("main", "get_runtime_status");
renderRuntimeStatus(status);
}
function bindRefreshControls(): void {
document.querySelector<HTMLButtonElement>("#refreshRouteInventory")?.addEventListener("click", () => {
void refreshRouteInventory().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk Config route inventory refresh failed"));
});
document.querySelector<HTMLButtonElement>("#refreshRuntimeStatus")?.addEventListener("click", () => {
void refreshRuntimeStatus().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk runtime status refresh failed"));
});
document.querySelector<HTMLSelectElement>("#routeProfile")?.addEventListener("change", event => {
const selector = event.currentTarget;
if (!(selector instanceof HTMLSelectElement)) {
return;
}
selectedProfileId = selector.value;
selectedMonitoringKey = null;
const selected = routeInventory?.profiles.find(profile => profile.profileId === selectedProfileId) ?? null;
frontendDebug("main", "Raw Transaction Ingest Desk logical network selected", {
profileId: selectedProfileId,
routeCount: selected?.routes.length ?? 0,
selectionResolved: selected !== null,
});
renderSelectedProfile();
});
document.querySelector<HTMLSelectElement>("#routeCommitment")?.addEventListener("change", event => {
const selector = event.currentTarget;
if (!(selector instanceof HTMLSelectElement)) {
return;
}
frontendDebug("main", "Raw Transaction Ingest Desk commitment selection changed", { commitment: selectedCommitment() });
});
}
async function initializeMain(): Promise<void> {
frontendInfo("main", "Raw Transaction Ingest Desk main frontend loaded");
bindFrontendInteractions();
bindNavigation();
bindRefreshControls();
await bindRouteMonitoring();
activateView("routes", "startup");
await refreshRouteInventory();
await Promise.all([refreshRuntimeStatus(), syncRouteMonitoring("startup")]);
frontendInfo("main", "Raw Transaction Ingest Desk multi-route shared-Store runtime frontend ready");
}
void initializeMain().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk frontend initialization failed"));

View File

@@ -0,0 +1,131 @@
// file: crates/ksp-app-raw-transaction-ingest-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_raw_transaction_ingest_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", "Raw Transaction Ingest 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,26 @@
{
"name": "ksp-app-raw-transaction-ingest-desk",
"private": true,
"version": "0.3.15",
"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",
"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,222 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
// version: 9
//! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application.
/// Shared Raw Transaction Ingest Desk application state managed by Tauri.
pub(crate) struct AppState {
active_composite_profile: std::option::Option<String>,
config_management: ksp_config_lib::ConfigManagement,
inventory_generation: std::sync::Mutex<u32>,
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
route_runtime: std::sync::Arc<crate::RouteRuntimeState>,
splash_settings: crate::SplashSettings,
splash_sequence_started: std::sync::atomic::AtomicBool,
}
impl crate::AppState {
/// Initializes Config ownership and composite-managed Logging without constructing Transport, Store or Worker resources.
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 composite = crate::load_raw_transaction_ingest_desk_composite(&config_management);
let active_composite_profile = match composite {
std::result::Result::Ok(value) => std::option::Option::Some(value.profile_id().to_owned()),
std::result::Result::Err(_) => std::option::Option::None,
};
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 Raw Transaction Ingest Desk splash timings");
return std::result::Result::Ok(Self {
active_composite_profile,
config_management,
inventory_generation: std::sync::Mutex::new(0),
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
guard: logging_startup.guard,
active_profile_id: logging_startup.active_profile_id,
fallback_active: logging_startup.fallback_active,
startup_diagnostic: logging_startup.startup_diagnostic,
}),
route_runtime: std::sync::Arc::new(crate::RouteRuntimeState::new()),
splash_settings,
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
});
}
/// Returns the static V1 route/state vocabulary without making any Config composability claim.
#[must_use]
pub(crate) fn route_foundation(&self) -> crate::RawIngestRouteFoundationDto {
let _keep_config_owner_alive = &self.config_management;
return crate::RawIngestRouteFoundationDto::scaffold();
}
/// Rebuilds the complete Config-only route inventory and publishes a fresh monotonic generation.
pub(crate) fn route_inventory(&self) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
let generation = self.inventory_generation.lock();
let mut generation = match generation {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
));
},
};
let next_generation = generation.checked_add(1);
let next_generation = match next_generation {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_INVENTORY_INVALID,
"Raw Transaction Ingest Desk route inventory generation is exhausted",
));
},
};
let inventory = crate::build_route_inventory(&self.config_management, next_generation);
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
*generation = next_generation;
return std::result::Result::Ok(inventory);
}
/// Revalidates one future Start request and reconstructs its exact Transport/Worker source contract without launching a Worker.
pub(crate) fn validate_route_start(&self, request: &crate::RawIngestRouteStartRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteStartValidationDto> {
let generation = self.inventory_generation.lock();
let generation = match generation {
std::result::Result::Ok(value) => *value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
));
},
};
return crate::validate_route_start(&self.config_management, generation, request);
}
/// Starts one independent route Worker after repeating complete Start-time revalidation, sharing Store only on an exact network match.
pub(crate) async fn start_route(&self, request: &crate::RawIngestRouteStartRequestDto) -> ksp_core_lib::Result<crate::RouteRuntimeLaunch> {
let admission = self.route_runtime.ensure_start_admission_open();
if let std::result::Result::Err(error) = admission {
return std::result::Result::Err(error);
}
let prepared = {
let generation = self.inventory_generation.lock();
let generation = match generation {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
));
},
};
let prepared = crate::prepare_route_start(&self.config_management, *generation, request);
match prepared {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
};
return crate::start_route_runtime(std::sync::Arc::clone(&self.route_runtime), prepared).await;
}
/// Returns complete latest-value monitoring projections for active and recent terminal routes in the current same-network runtime session.
pub(crate) fn route_monitoring(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>> {
return self.route_runtime.monitoring_statuses();
}
/// Requests cooperative Stop for one exact logical route and waits until its Worker cleanup is complete.
pub(crate) async fn stop_route(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
return self.route_runtime.stop_and_wait(request).await;
}
/// Closes route Start admission once and requests cooperative Stop for all routes during application shutdown.
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
return self.route_runtime.begin_shutdown();
}
/// Waits for bounded cleanup of all route Workers and the shared Store during application shutdown.
pub(crate) async fn shutdown_routes(&self) -> ksp_core_lib::Result<()> {
return self.route_runtime.shutdown_and_wait().await;
}
/// Builds the safe scaffold status exposed by the shell.
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
let document_count = self.config_management.engine().registry().descriptors().count();
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 Raw Transaction Ingest 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,
"Raw Transaction Ingest Desk Logging runtime state lock is poisoned",
));
},
};
let _keep_guard_alive = &runtime.guard;
return std::result::Result::Ok(crate::ShellStatusDto {
active_composite_profile: self.active_composite_profile.clone(),
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-runtime-monitoring".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
});
}
/// 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();
}
}
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,228 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/bootstrap.rs
// version: 2
//! Config composite and Logging bootstrap for Raw Transaction Ingest 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>,
}
enum LoggingStartupPlan {
Managed {
active_profile_id: String,
settings: ksp_logging_lib::LoggingSettings,
},
Fallback {
initial_error: ksp_core_lib::Error,
diagnostic: crate::CommandErrorDto,
settings: ksp_logging_lib::LoggingSettings,
},
}
/// 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 Raw Transaction Ingest Desk Config composite using its registered logical `file_id` and autonomous default profile.
pub(crate) fn load_raw_transaction_ingest_desk_composite(
management: &ksp_config_lib::ConfigManagement,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
return crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::None);
}
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile by registered logical file identifier.
pub(crate) fn load_raw_transaction_ingest_desk_composite_profile(
management: &ksp_config_lib::ConfigManagement,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return management.engine().load_resolved_composite(&file_id, requested_profile);
}
/// Returns one required standard profile from the Raw Transaction Ingest 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, "Raw Transaction Ingest 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,
"Raw Transaction Ingest 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 Logging from the Raw Transaction Ingest Desk composite, with a trace-level development fallback.
pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<crate::LoggingStartup> {
let plan = resolve_logging_startup(management);
return match plan {
LoggingStartupPlan::Managed { active_profile_id, settings } => {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
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(),
"initialized Raw Transaction Ingest Desk logging from composite-managed configuration"
);
std::result::Result::Ok(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),
}
},
LoggingStartupPlan::Fallback { initial_error, diagnostic, settings } => {
initialize_planned_fallback_logging(initial_error, diagnostic, settings, runtime_identity)
},
};
}
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return 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(),
);
}
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
let composite = crate::load_raw_transaction_ingest_desk_composite(management);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
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 fallback_startup_plan(error),
};
let transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
if let std::result::Result::Err(error) = transport_profile {
return fallback_startup_plan(error);
}
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 fallback_startup_plan(error);
}
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 fallback_startup_plan(error),
};
return LoggingStartupPlan::Managed { active_profile_id: resolved.profile_id().to_owned(), settings: resolved.into_settings() };
}
fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPlan {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
return LoggingStartupPlan::Fallback { initial_error, diagnostic, settings: fallback_logging_settings() };
}
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);
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
}
fn initialize_planned_fallback_logging(
initial_error: ksp_core_lib::Error,
diagnostic: crate::CommandErrorDto,
settings: ksp_logging_lib::LoggingSettings,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<crate::LoggingStartup> {
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 Raw Transaction Ingest 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(),
"managed Logging configuration is unavailable; using transient trace-level development fallback"
);
return std::result::Result::Ok(LoggingStartup {
guard,
active_profile_id: std::option::Option::None,
fallback_active: true,
startup_diagnostic: std::option::Option::Some(diagnostic),
});
}
#[cfg(test)]
mod tests {
#[test]
fn development_fallback_keeps_trace_level_enabled() {
let settings = super::fallback_logging_settings();
assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Trace);
}
}

View File

@@ -0,0 +1,39 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs
// version: 6
//! Application-owned tracing targets and Config 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";
/// Composite component identifier for Transport.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
/// Composite-local optional Transport source for Helius WebSocket capabilities.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS: &str = "transport.helius";
/// Prefix used by optional same-network Transport capability sources.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX: &str = "transport.";
/// Composite-local optional Transport source for Yellowstone gRPC capabilities.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE: &str = "transport.yellowstone";
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "raw_transaction_ingest.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used while rebuilding Config-only route inventory.
pub(crate) const TRACING_DOMAIN_INVENTORY: &str = "raw_transaction_ingest.inventory";
/// Structured domain used by multi-route shared-Store and Worker lifecycle operations.
pub(crate) const TRACING_DOMAIN_ROUTE_RUNTIME: &str = "raw_transaction_ingest.route_runtime";
/// Structured domain used while revalidating future Start requests and reconstructing route resources.
pub(crate) const TRACING_DOMAIN_ROUTE_START: &str = "raw_transaction_ingest.route_start";
/// Structured domain used by the Raw Transaction Ingest Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "raw_transaction_ingest.shell";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Raw Transaction Ingest Desk.
pub(crate) const TRACING_TARGET: &str = "ksp-app-raw-transaction-ingest-desk";
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-raw-transaction-ingest-desk.frontend";
/// Owning target for main-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-raw-transaction-ingest-desk.frontend.main";
/// Owning target for splash-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-raw-transaction-ingest-desk.frontend.splash";

View File

@@ -0,0 +1,56 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_common.rs
// version: 1
//! Common Tauri DTOs shared by the Raw Transaction Ingest Desk shell.
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_raw_transaction_ingest_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 scaffold/runtime snapshot exposed to the Raw Transaction Ingest Desk shell.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts")]
pub(crate) struct ShellStatusDto {
/// Active composite profile selected by Config, when the composite resolves successfully.
pub(crate) active_composite_profile: std::option::Option<String>,
/// 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 Raw Transaction Ingest 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<CommandErrorDto>,
}
#[cfg(test)]
#[path = "../unit_tests/dto_common.rs"]
mod tests;

View File

@@ -0,0 +1,365 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
// version: 7
//! Safe route-identity, Config-composability and lifecycle DTOs for Raw Transaction Ingest Desk.
use ts_rs::TS; // rust-rules: trait-import
/// Stable logical V1 route identifiers owned by Raw Transaction Ingest Desk.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "kebab-case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteId.ts")]
pub(crate) enum RawIngestRouteId {
/// Yellowstone block subscription plus same-network HTTP block reconciliation.
YellowstoneHydrated,
/// Standard Solana logs subscription plus same-network HTTP hydration.
StandardLogsHydrated,
/// Standard Solana block subscription with direct RAW projection.
StandardBlockDirect,
/// Helius transaction subscription plus same-network HTTP hydration.
HeliusTransactionHydrated,
/// HTTP block polling and direct block material projection.
HttpBlockPolling,
}
impl crate::RawIngestRouteId {
/// Returns all V1 route identifiers in stable UI order.
#[must_use]
pub(crate) fn all() -> std::vec::Vec<Self> {
return vec![Self::YellowstoneHydrated, Self::StandardLogsHydrated, Self::StandardBlockDirect, Self::HeliusTransactionHydrated, Self::HttpBlockPolling];
}
/// Returns the application-owned route family used by safe UI projections.
#[must_use]
pub(crate) const fn family(self) -> crate::RawIngestRouteFamily {
return match self {
Self::YellowstoneHydrated => crate::RawIngestRouteFamily::Yellowstone,
Self::StandardLogsHydrated => crate::RawIngestRouteFamily::StandardLogs,
Self::StandardBlockDirect => crate::RawIngestRouteFamily::StandardBlock,
Self::HeliusTransactionHydrated => crate::RawIngestRouteFamily::HeliusTransaction,
Self::HttpBlockPolling => crate::RawIngestRouteFamily::HttpBlockPolling,
};
}
/// Returns the stable operator-facing route label without exposing physical endpoint metadata.
#[must_use]
pub(crate) const fn label(self) -> &'static str {
return match self {
Self::YellowstoneHydrated => "Yellowstone Block + HTTP block reconciliation",
Self::StandardLogsHydrated => "Standard Logs + HTTP hydration",
Self::StandardBlockDirect => "Standard Block direct",
Self::HeliusTransactionHydrated => "Helius Transaction + HTTP hydration",
Self::HttpBlockPolling => "HTTP Block Polling",
};
}
/// Returns the stable kebab-case route identifier used by safe logs and IPC.
#[must_use]
pub(crate) const fn as_str(self) -> &'static str {
return match self {
Self::YellowstoneHydrated => "yellowstone-hydrated",
Self::StandardLogsHydrated => "standard-logs-hydrated",
Self::StandardBlockDirect => "standard-block-direct",
Self::HeliusTransactionHydrated => "helius-transaction-hydrated",
Self::HttpBlockPolling => "http-block-polling",
};
}
}
/// Start-time commitments accepted by Raw Transaction Ingest Desk.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestCommitment.ts")]
pub(crate) enum RawIngestCommitment {
/// Confirmed commitment.
Confirmed,
/// Finalized commitment.
Finalized,
}
impl crate::RawIngestCommitment {
/// Returns the stable wire label.
#[must_use]
pub(crate) const fn as_str(self) -> &'static str {
return match self {
Self::Confirmed => "confirmed",
Self::Finalized => "finalized",
};
}
/// Maps the application-owned bounded commitment into the Transport contract.
#[must_use]
pub(crate) const fn into_transport(self) -> ksp_onchain_transport_lib::SolanaCommitment {
return match self {
Self::Confirmed => ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
Self::Finalized => ksp_onchain_transport_lib::SolanaCommitment::Finalized,
};
}
}
/// Safe request used to revalidate and reconstruct one future route Start without accepting physical resources from the frontend.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts")]
pub(crate) struct RawIngestRouteStartRequestDto {
/// Confirmed/finalized commitment selected for the future Worker.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation observed by the caller.
pub(crate) inventory_generation: u32,
/// Safe logical network-profile identifier selected from the current inventory.
pub(crate) profile_id: String,
/// Stable logical route identifier.
pub(crate) route_id: crate::RawIngestRouteId,
}
/// Safe targeted Stop request containing only the logical Worker route identity.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts")]
pub(crate) struct RawIngestRouteStopRequestDto {
/// Safe logical network-profile identifier that owns the Worker route.
pub(crate) profile_id: String,
/// Stable logical route identifier to stop.
pub(crate) route_id: crate::RawIngestRouteId,
}
const RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256;
impl crate::RawIngestRouteStartRequestDto {
/// Validates the only caller-controlled free-form logical identifier before Config or runtime reconstruction.
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
return validate_profile_id(self.profile_id.as_str());
}
}
impl crate::RawIngestRouteStopRequestDto {
/// Validates the only caller-controlled free-form logical identifier before runtime lookup.
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
return validate_profile_id(self.profile_id.as_str());
}
}
fn validate_profile_id(profile_id: &str) -> ksp_core_lib::Result<()> {
if profile_id.is_empty()
|| profile_id.len() > RAW_INGEST_PROFILE_ID_MAX_BYTES
|| profile_id.trim() != profile_id
|| profile_id.chars().any(char::is_control)
{
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_REQUEST_INVALID,
"Raw Transaction Ingest Desk route request contains an invalid logical profile identifier",
));
}
return std::result::Result::Ok(());
}
/// Safe acknowledgement that exact Start-time Transport resources were reconstructed and validated without Worker launch.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartValidationDto.ts")]
pub(crate) struct RawIngestRouteStartValidationDto {
/// Commitment validated by the exact Worker source constructor.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation validated against backend state.
pub(crate) inventory_generation: u32,
/// Logical network re-proven from Store and Transport Config.
pub(crate) network: String,
/// Safe logical network-profile identifier.
pub(crate) profile_id: String,
/// Exact logical route whose Transport resources were reconstructed.
pub(crate) route_id: crate::RawIngestRouteId,
}
/// Safe per-route runtime acknowledgement containing only logical identity and lifecycle state.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteRuntimeDto.ts")]
pub(crate) struct RawIngestRouteRuntimeDto {
/// Confirmed/finalized commitment bound to the active Worker.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation revalidated before Store opening.
pub(crate) inventory_generation: u32,
/// Logical network bound identically to Store and Worker.
pub(crate) network: String,
/// Safe logical network-profile identifier.
pub(crate) profile_id: String,
/// Exact logical route owned by the independent Worker.
pub(crate) route_id: crate::RawIngestRouteId,
/// Safe application-owned projection of current Worker lifecycle.
pub(crate) state: crate::RawIngestRouteState,
}
/// Stable application-owned route families used only for safe presentation and orchestration.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFamily.ts")]
pub(crate) enum RawIngestRouteFamily {
/// Yellowstone gRPC subscription route.
Yellowstone,
/// Standard Solana logs WebSocket route.
StandardLogs,
/// Standard Solana block WebSocket route.
StandardBlock,
/// Helius transaction WebSocket route.
HeliusTransaction,
/// HTTP block polling route.
HttpBlockPolling,
}
/// Safe reasons explaining why one logical route cannot currently be composed from Config.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteUnavailableReason.ts")]
pub(crate) enum RawIngestRouteUnavailableReason {
/// The selected composite or one of its typed Config components could not be resolved safely.
ProfileUnresolved,
/// Transport and Store do not prove one identical logical network.
NetworkMismatch,
/// No HTTP role can satisfy `getBlock` for same-network block reconciliation.
MissingHttpGetBlock,
/// No HTTP role can satisfy `getTransaction` for same-network hydration.
MissingHttpGetTransaction,
/// No HTTP role can satisfy the complete block-polling method set.
MissingHttpBlockScan,
/// No enabled standard WebSocket endpoint explicitly declares `Logs`.
MissingWsLogsCapability,
/// No enabled standard WebSocket endpoint explicitly declares `Block`.
MissingWsBlockCapability,
/// No enabled Helius WebSocket endpoint explicitly declares `HeliusTransaction`.
MissingHeliusTransactionCapability,
/// No enabled same-network Yellowstone gRPC endpoint is configured.
MissingYellowstoneGrpc,
/// A required Config secret is not resolved for the selected composite profile.
MissingRequiredSecret,
}
/// Stable Raw Transaction Ingest Desk route lifecycle states.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteState.ts")]
pub(crate) enum RawIngestRouteState {
/// Required Config composition is incomplete or incoherent.
Unavailable,
/// Config proves composability but no runtime Worker is active.
Configured,
/// Runtime resources are being revalidated and assembled.
Starting,
/// One Worker instance is active for the logical route.
Running,
/// Stop has been requested and bounded termination is pending.
Stopping,
/// The route Worker terminated normally and its handle was removed.
Stopped,
/// Start or runtime failed with a safe projected code.
Faulted,
}
impl crate::RawIngestRouteState {
/// Returns all lifecycle states in stable presentation order.
#[must_use]
pub(crate) fn all() -> std::vec::Vec<Self> {
return vec![Self::Unavailable, Self::Configured, Self::Starting, Self::Running, Self::Stopping, Self::Stopped, Self::Faulted];
}
}
/// Static route/state foundation retained as the bounded vocabulary behind the live Config inventory.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFoundationDto.ts")]
pub(crate) struct RawIngestRouteFoundationDto {
/// Five stable V1 logical route identifiers; this is not a composability claim.
pub(crate) route_ids: std::vec::Vec<crate::RawIngestRouteId>,
/// Stable lifecycle state vocabulary reserved for runtime projections.
pub(crate) states: std::vec::Vec<crate::RawIngestRouteState>,
}
impl crate::RawIngestRouteFoundationDto {
/// Builds the bounded route/state vocabulary without resolving endpoints, secrets or Worker resources.
#[must_use]
pub(crate) fn scaffold() -> Self {
return Self { route_ids: crate::RawIngestRouteId::all(), states: crate::RawIngestRouteState::all() };
}
}
/// Safe Config-only composability projection for one logical route.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteDto.ts")]
pub(crate) struct RawIngestRouteDto {
/// Stable route family used for presentation and bounded orchestration.
pub(crate) family: crate::RawIngestRouteFamily,
/// Stable operator-facing label with no physical resource identity.
pub(crate) label: String,
/// Logical network proven by Config when available.
pub(crate) network: std::option::Option<String>,
/// Safe reason explaining an unavailable route.
pub(crate) reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
/// Stable logical V1 route identifier.
pub(crate) route_id: crate::RawIngestRouteId,
/// Whether all Config requirements for the route are currently composable.
pub(crate) selectable: bool,
/// Config-level lifecycle projection; `Configured` is not a runtime health claim.
pub(crate) state: crate::RawIngestRouteState,
}
impl crate::RawIngestRouteDto {
/// Builds one route that is composable from Config without claiming runtime operability.
#[must_use]
pub(crate) fn configured(route_id: crate::RawIngestRouteId, network: &str) -> Self {
return Self {
family: route_id.family(),
label: route_id.label().to_owned(),
network: std::option::Option::Some(network.to_owned()),
reason: std::option::Option::None,
route_id,
selectable: true,
state: crate::RawIngestRouteState::Configured,
};
}
/// Builds one route that is not composable from the current Config profile.
#[must_use]
pub(crate) fn unavailable(route_id: crate::RawIngestRouteId, network: std::option::Option<&str>, reason: crate::RawIngestRouteUnavailableReason) -> Self {
return Self {
family: route_id.family(),
label: route_id.label().to_owned(),
network: network.map(str::to_owned),
reason: std::option::Option::Some(reason),
route_id,
selectable: false,
state: crate::RawIngestRouteState::Unavailable,
};
}
}
/// Safe route inventory for one logical Raw Transaction Ingest Desk composite profile.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestProfileInventoryDto.ts")]
pub(crate) struct RawIngestProfileInventoryDto {
/// Whether this profile is the composite's autonomous default.
pub(crate) is_default: bool,
/// Logical network proven from the resolved Config profile when available.
pub(crate) network: std::option::Option<String>,
/// Stable composite profile identifier.
pub(crate) profile_id: String,
/// Logical routes applicable to this network profile in stable V1 presentation order; provider-specific routes may be absent when their Config source is not declared.
pub(crate) routes: std::vec::Vec<crate::RawIngestRouteDto>,
}
/// Deterministic Config-only route inventory returned to the frontend.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts")]
pub(crate) struct RawIngestRouteInventoryDto {
/// Composite default profile identifier.
pub(crate) default_profile: String,
/// Monotonic application-owned generation incremented after each successful inventory rebuild.
pub(crate) generation: u32,
/// Safe inventories for every validated composite profile in source order.
pub(crate) profiles: std::vec::Vec<crate::RawIngestProfileInventoryDto>,
}
#[cfg(test)]
#[path = "../unit_tests/dto_route.rs"]
mod tests;

View File

@@ -0,0 +1,79 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
// version: 6
//! Stable Raw Transaction Ingest Desk application error codes.
/// Shared Raw Transaction Ingest Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "app_state_invalid");
/// Shared Raw Transaction Ingest 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("raw_transaction_ingest_desk", "app_state_lock_failed");
/// The Raw Transaction Ingest Desk Config composite is missing or misroutes a required component.
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_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("raw_transaction_ingest_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("raw_transaction_ingest_desk", "frontend_log_target_invalid");
/// Frontend logging payload exceeds the bounded technical IPC contract.
pub(crate) const ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_payload_invalid");
/// Raw Transaction Ingest Desk could not install managed Logging or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "logging_bootstrap_failed");
/// Config-only route inventory cannot be projected safely from the validated composite.
pub(crate) const ERROR_CODE_ROUTE_INVENTORY_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_inventory_invalid");
/// Exact Start-time Transport resources cannot be reconstructed safely.
pub(crate) const ERROR_CODE_ROUTE_START_PREPARATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_start_preparation_failed");
/// The selected logical route is already starting/active or shared Store lifecycle is temporarily exclusive.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_active");
/// No selected route Worker runtime is currently active.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_not_active");
/// A selected route cannot share the currently open Store because its network differs.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_network_mismatch");
/// Store or Worker startup failed after Start-time route revalidation.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_START_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_start_failed");
/// The opened Store did not prove Ready before Worker launch.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STORE_NOT_READY: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_store_not_ready");
/// The Store could not be reclaimed or explicitly closed after Worker termination.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_store_shutdown_failed");
/// The multi-route runtime reservation/handle state changed unexpectedly.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_state_invalid");
/// Application shutdown has closed admission for new route Worker starts.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutting_down");
/// Application shutdown could not join all route Workers and shared Store cleanup within the bounded deadline.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutdown_failed");
/// A Start/Stop IPC request contains an invalid logical identifier or unsupported shape.
pub(crate) const ERROR_CODE_ROUTE_REQUEST_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_request_invalid");
/// Start request references an inventory generation that is no longer current.
pub(crate) const ERROR_CODE_ROUTE_START_STALE_INVENTORY: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "stale_inventory");
/// Start request references a route that is no longer Configured/applicable.
pub(crate) const ERROR_CODE_ROUTE_START_UNAVAILABLE: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_start_unavailable");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "splash_origin_invalid");
/// 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("raw_transaction_ingest_desk", "splash_setting_invalid");
/// Tauri runtime assembly or execution failed.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_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("raw_transaction_ingest_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("raw_transaction_ingest_desk", "tauri_window_operation_failed");

View File

@@ -0,0 +1,241 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/frontend_logging.rs
// version: 2
//! KSP-owned bridge for technical log events emitted by Raw Transaction Ingest Desk frontend scripts.
use ts_rs::TS; // rust-rules: trait-import
/// Log payload sent by Raw Transaction Ingest Desk frontend scripts.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_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,
}
const FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192;
const FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16;
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
let shape = validate_payload_shape(&payload);
if let std::result::Result::Err(error) = shape {
return std::result::Result::Err(error);
}
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 validate_payload_shape(payload: &FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
if payload.level.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|| payload.target_id.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|| payload.message.len() > FRONTEND_LOG_MESSAGE_MAX_BYTES
|| payload.message.chars().any(char::is_control)
{
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID,
"Frontend log payload exceeds the bounded technical IPC contract",
));
}
return std::result::Result::Ok(());
}
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
let level = level.trim();
if level.eq_ignore_ascii_case("trace") {
return std::result::Result::Ok(FrontendLogLevel::Trace);
}
if level.eq_ignore_ascii_case("debug") {
return std::result::Result::Ok(FrontendLogLevel::Debug);
}
if level.eq_ignore_ascii_case("info") {
return std::result::Result::Ok(FrontendLogLevel::Info);
}
if level.eq_ignore_ascii_case("warn") {
return std::result::Result::Ok(FrontendLogLevel::Warn);
}
if level.eq_ignore_ascii_case("error") {
return std::result::Result::Ok(FrontendLogLevel::Error);
}
return 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> {
let target_id = target_id.trim();
if target_id.eq_ignore_ascii_case("frontend") {
return std::result::Result::Ok(FrontendLogTarget::Frontend);
}
if target_id.eq_ignore_ascii_case("main") {
return std::result::Result::Ok(FrontendLogTarget::Main);
}
if target_id.eq_ignore_ascii_case("splash") {
return std::result::Result::Ok(FrontendLogTarget::Splash);
}
return 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,201 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
// version: 8
//! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
mod app_state;
mod bootstrap;
mod constants;
mod dto_common;
mod dto_route;
mod errors;
mod frontend_logging;
mod logging_runtime;
mod route_inventory;
mod route_monitoring;
mod route_runtime;
mod route_start;
mod splash;
mod tauri;
mod tw_main;
mod tw_splash;
/// Runs the KSP Raw Transaction Ingest desktop application.
pub use self::tauri::run;
/// Shared Raw Transaction Ingest 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 Logging from the Raw Transaction Ingest Desk composite with a bounded fallback.
pub(crate) use self::bootstrap::initialize_logging;
/// Loads the Raw Transaction Ingest Desk composite selected by its registered logical file identifier.
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite;
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile.
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite_profile;
/// Resolves one required standard Config profile from the Raw Transaction Ingest Desk composite.
pub(crate) use self::bootstrap::required_composite_component_profile;
/// Composite-local identifier for the standard Logging component.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
/// Composite-local identifier for the standard Store component.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_STORE;
/// Composite-local identifier for the standard Transport component.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
/// Composite-local optional Transport source for Helius WebSocket capabilities.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS;
/// Prefix used by optional same-network Transport capability sources.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX;
/// Composite-local optional Transport source for Yellowstone gRPC capabilities.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE;
/// 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 while rebuilding Config-only route inventory.
pub(crate) use self::constants::TRACING_DOMAIN_INVENTORY;
/// Structured domain used by multi-route shared-Store and Worker lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_ROUTE_RUNTIME;
/// Structured domain used while revalidating future Start requests.
pub(crate) use self::constants::TRACING_DOMAIN_ROUTE_START;
/// Structured domain used by the Raw Transaction Ingest Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Raw Transaction Ingest 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;
/// Safe command error projection exposed to Tauri commands.
pub(crate) use self::dto_common::CommandErrorDto;
/// Safe scaffold/runtime snapshot exposed to the shell.
pub(crate) use self::dto_common::ShellStatusDto;
/// Bounded confirmed/finalized commitment accepted by future route Start requests.
pub(crate) use self::dto_route::RawIngestCommitment;
/// Safe route inventory for one logical composite profile.
pub(crate) use self::dto_route::RawIngestProfileInventoryDto;
/// Safe Config-only composability projection for one logical route.
pub(crate) use self::dto_route::RawIngestRouteDto;
/// Stable application-owned route family.
pub(crate) use self::dto_route::RawIngestRouteFamily;
/// Static route/state foundation retained as the bounded vocabulary behind the Config inventory.
pub(crate) use self::dto_route::RawIngestRouteFoundationDto;
/// Stable logical V1 route identifiers owned by the Desk.
pub(crate) use self::dto_route::RawIngestRouteId;
/// Complete deterministic Config-only route inventory.
pub(crate) use self::dto_route::RawIngestRouteInventoryDto;
/// Safe per-route runtime acknowledgement containing only logical identity and lifecycle.
pub(crate) use self::dto_route::RawIngestRouteRuntimeDto;
/// Safe future Start request containing only logical route selection and inventory generation.
pub(crate) use self::dto_route::RawIngestRouteStartRequestDto;
/// Safe acknowledgement that exact Start-time resources were reconstructed without Worker launch.
pub(crate) use self::dto_route::RawIngestRouteStartValidationDto;
/// Stable route lifecycle states reserved for runtime projections.
pub(crate) use self::dto_route::RawIngestRouteState;
/// Safe targeted Stop request containing only logical route identity.
pub(crate) use self::dto_route::RawIngestRouteStopRequestDto;
/// Safe reason explaining why one logical route is not composable from Config.
pub(crate) use self::dto_route::RawIngestRouteUnavailableReason;
/// Shared Raw Transaction Ingest Desk application state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Raw Transaction Ingest Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// Raw Transaction Ingest Desk Config composite is missing or invalid.
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 payload exceeds the bounded technical IPC contract.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID;
/// Frontend logging requested an unsupported target.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Managed and fallback Logging initialization failed.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Config-only route inventory cannot be projected safely.
pub(crate) use self::errors::ERROR_CODE_ROUTE_INVENTORY_INVALID;
/// Start/Stop IPC request contains an invalid logical shape.
pub(crate) use self::errors::ERROR_CODE_ROUTE_REQUEST_INVALID;
/// A Worker runtime already owns the same logical route in the shared application runtime.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_ACTIVE;
/// Selected route cannot share the currently open Store because its network differs.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH;
/// No selected route Worker runtime is currently active.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE;
/// Application shutdown could not complete all bounded route/Store cleanup.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED;
/// Application shutdown has closed admission for new route Worker starts.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN;
/// Store or Worker startup failed after Start-time route revalidation.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_START_FAILED;
/// The multi-route runtime reservation or handle state changed unexpectedly.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID;
/// The opened Store did not prove Ready before Worker launch.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_STORE_NOT_READY;
/// The Store could not be reclaimed or explicitly closed after Worker termination.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED;
/// Exact Start-time Transport resources cannot be reconstructed safely.
pub(crate) use self::errors::ERROR_CODE_ROUTE_START_PREPARATION_FAILED;
/// Start request references a stale inventory generation.
pub(crate) use self::errors::ERROR_CODE_ROUTE_START_STALE_INVENTORY;
/// Start request references a route that is no longer Configured/applicable.
pub(crate) use self::errors::ERROR_CODE_ROUTE_START_UNAVAILABLE;
/// Splash readiness originated from an invalid window.
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// Splash timing configuration is invalid.
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
/// Tauri runtime assembly or execution failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
/// A required Tauri window is missing.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
/// A Tauri window operation failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
/// Log payload sent by Raw Transaction Ingest 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 Raw Transaction Ingest Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Rebuilds the complete safe route inventory from validated Config without runtime I/O.
pub(crate) use self::route_inventory::build_route_inventory;
/// Rebuilds the route inventory from one already captured Config environment snapshot.
pub(crate) use self::route_inventory::build_route_inventory_with_environment;
/// Stable Tauri event carrying one complete latest-value route monitoring projection.
pub(crate) use self::route_monitoring::RAW_INGEST_ROUTE_STATUS_EVENT_NAME;
/// Frontend-safe bounded projection of one run-local continuity gap.
pub(crate) use self::route_monitoring::RawIngestRouteGapDto;
/// Frontend-safe latest-value monitoring projection of one independent route Worker.
pub(crate) use self::route_monitoring::RawIngestRouteMonitoringDto;
/// Projects one complete concrete Worker snapshot to the safe route monitoring contract.
pub(crate) use self::route_monitoring::project_route_monitoring;
/// Launch ownership for one independent route Worker sharing the application Store when eligible.
pub(crate) use self::route_runtime::RouteRuntimeLaunch;
/// Shared multi-route runtime state with one independent Worker per route and one same-network Store.
pub(crate) use self::route_runtime::RouteRuntimeState;
/// Opens Store and starts one exact route Worker after Start-time revalidation.
pub(crate) use self::route_runtime::start_route_runtime;
/// Fully revalidated Start preparation retaining Store settings and exact Worker resources.
pub(crate) use self::route_start::PreparedRouteStart;
/// Revalidates one Start request and retains its backend-only Store/Worker resources.
pub(crate) use self::route_start::prepare_route_start;
/// Revalidates one future Start request and reconstructs its exact Transport-owned Worker source contract without launch.
pub(crate) use self::route_start::validate_route_start;
/// 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;
/// Resolves the required main window or returns a typed error.
pub(crate) use self::tw_main::require_main_window;
/// Shows and focuses the main Raw Transaction Ingest 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-raw-transaction-ingest-desk/src/logging_runtime.rs
// version: 1
//! Raw Transaction Ingest Desk Logging runtime identity helpers.
/// Creates the stable runtime identity for this Raw Transaction Ingest 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-raw-transaction-ingest-desk/src/main.rs
// version: 1
//! Binary entry point for the KSP Raw Transaction Ingest 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 Raw Transaction Ingest 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_raw_transaction_ingest_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-raw-transaction-ingest-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_raw_transaction_ingest_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,459 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs
// version: 6
//! Config-only logical route inventory for Raw Transaction Ingest Desk.
struct CompositeProfileCatalog {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
#[derive(Default)]
struct RouteCapabilities {
has_get_transaction: bool,
has_http_block_scan: bool,
has_yellowstone: bool,
has_standard_logs: bool,
has_standard_block: bool,
has_helius_transaction: bool,
helius_source_declared: bool,
helius_source_missing_secret: bool,
yellowstone_has_get_block: bool,
yellowstone_source_missing_secret: bool,
}
impl RouteCapabilities {
fn absorb_transport(&mut self, transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) {
self.has_get_transaction |= http_role_supports_methods(transport.http_settings(), &["getTransaction"]);
self.has_http_block_scan |= http_role_supports_methods(transport.http_settings(), &["getSlot", "getBlocksWithLimit", "getBlock"]);
self.has_yellowstone |= has_yellowstone_grpc(transport, network);
self.has_standard_logs |= has_ws_capability(
transport,
network,
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
);
self.has_standard_block |= has_ws_capability(
transport,
network,
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
ksp_onchain_transport_lib::WsSubscriptionKind::Block,
);
self.has_helius_transaction |= has_ws_capability(
transport,
network,
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
);
}
}
/// Rebuilds the complete safe route inventory from validated Config without opening any network, Store or Worker resource.
pub(crate) fn build_route_inventory(management: &ksp_config_lib::ConfigManagement, generation: u32) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
let 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),
};
return build_route_inventory_with_environment(management, &environment, generation);
}
/// Rebuilds the complete safe route inventory from one already captured Config environment snapshot.
pub(crate) fn build_route_inventory_with_environment(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
generation: u32,
) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
let catalog = composite_profile_catalog(management);
let catalog = match catalog {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut profiles = std::vec::Vec::with_capacity(catalog.profile_ids.len());
for profile_id in &catalog.profile_ids {
profiles.push(build_profile_inventory(management, environment, profile_id.as_str(), profile_id == &catalog.default_profile));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_INVENTORY,
generation,
profile_count = profiles.len(),
"rebuilt Raw Transaction Ingest Desk network-centric Config-only route inventory"
);
return std::result::Result::Ok(crate::RawIngestRouteInventoryDto { default_profile: catalog.default_profile, generation, profiles });
}
fn composite_profile_catalog(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<CompositeProfileCatalog> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let document = management.engine().load_validated_document(&file_id);
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let root = document.value().as_object();
let root = match root {
std::option::Option::Some(value) => value,
std::option::Option::None => return inventory_invalid("validated Raw Transaction Ingest Desk composite root is not an object"),
};
let default_profile = root.get("default_profile").and_then(|value| return value.as_str());
let default_profile = match default_profile {
std::option::Option::Some(value) if !value.is_empty() => value.to_owned(),
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no readable default profile"),
};
let profile_values = root.get("profiles").and_then(|value| return value.as_array());
let profile_values = match profile_values {
std::option::Option::Some(value) if !value.is_empty() => value,
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no profile inventory"),
};
let mut profile_ids = std::vec::Vec::with_capacity(profile_values.len());
let mut seen = std::collections::BTreeSet::<String>::new();
for profile in profile_values {
let profile_id = profile.get("profile_id").and_then(|value| return value.as_str());
let profile_id = match profile_id {
std::option::Option::Some(value) if !value.is_empty() => value,
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite contains an unreadable profile identifier"),
};
if !seen.insert(profile_id.to_owned()) {
return inventory_invalid("validated Raw Transaction Ingest Desk composite contains duplicate profile identifiers");
}
profile_ids.push(profile_id.to_owned());
}
if !seen.contains(default_profile.as_str()) {
return inventory_invalid("validated Raw Transaction Ingest Desk composite default profile is absent from the profile inventory");
}
return std::result::Result::Ok(CompositeProfileCatalog { default_profile, profile_ids });
}
fn build_profile_inventory(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
profile_id: &str,
is_default: bool,
) -> crate::RawIngestProfileInventoryDto {
let composite = crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::Some(profile_id));
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
},
};
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
let store_profile = match store_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
},
};
let store = management.engine().resolve_store_config_profile(&store_profile, environment);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let reason = resolution_error_reason(&error);
return unavailable_profile(profile_id, is_default, std::option::Option::None, reason);
},
};
let network = store.settings().network().as_str().to_owned();
let base_transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
let base_transport_profile = match base_transport_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
);
},
};
let base_transport = management.engine().resolve_transport_config_profile(&base_transport_profile, environment);
let base_transport = match base_transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return unavailable_profile(profile_id, is_default, std::option::Option::Some(network.as_str()), resolution_error_reason(&error));
},
};
if !transport_matches_network(&base_transport, network.as_str()) {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
);
}
let mut capabilities = RouteCapabilities::default();
capabilities.absorb_transport(&base_transport, network.as_str());
for (component_id, component) in composite.components() {
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT {
continue;
}
if !component_id.starts_with(crate::COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX) {
continue;
}
if component_id != crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS && component_id != crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
);
}
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS {
capabilities.helius_source_declared = true;
}
if component.resolved().file_id().as_str() != ksp_config_lib::FILE_ID_STD_TRANSPORT {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
);
}
let resolved = management.engine().resolve_transport_config_profile(component.resolved(), environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING => {
mark_optional_source_missing_secret(&mut capabilities, component_id.as_str());
continue;
},
std::result::Result::Err(_) => {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
);
},
};
if !transport_matches_network(&resolved, network.as_str()) {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
);
}
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE {
capabilities.yellowstone_has_get_block = http_role_supports_methods(resolved.http_settings(), &["getBlock"]);
}
capabilities.absorb_transport(&resolved, network.as_str());
}
let routes = build_configured_profile_routes(&capabilities, network.as_str());
return crate::RawIngestProfileInventoryDto { is_default, network: std::option::Option::Some(network), profile_id: profile_id.to_owned(), routes };
}
fn mark_optional_source_missing_secret(capabilities: &mut RouteCapabilities, component_id: &str) {
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS {
capabilities.helius_source_missing_secret = true;
}
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE {
capabilities.yellowstone_source_missing_secret = true;
}
}
fn build_configured_profile_routes(capabilities: &RouteCapabilities, network: &str) -> std::vec::Vec<crate::RawIngestRouteDto> {
let yellowstone_reason = if capabilities.yellowstone_source_missing_secret {
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret
} else {
crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc
};
let helius_reason = if capabilities.helius_source_missing_secret {
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret
} else {
crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability
};
let mut routes = vec![
route_with_requirements(
crate::RawIngestRouteId::YellowstoneHydrated,
network,
&[
(capabilities.has_yellowstone, yellowstone_reason),
(capabilities.yellowstone_has_get_block, crate::RawIngestRouteUnavailableReason::MissingHttpGetBlock),
],
),
route_with_requirements(
crate::RawIngestRouteId::StandardLogsHydrated,
network,
&[
(capabilities.has_standard_logs, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
(capabilities.has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
),
route_with_requirements(
crate::RawIngestRouteId::StandardBlockDirect,
network,
&[(capabilities.has_standard_block, crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability)],
),
];
if capabilities.helius_source_declared {
routes.push(route_with_requirements(
crate::RawIngestRouteId::HeliusTransactionHydrated,
network,
&[
(capabilities.has_helius_transaction, helius_reason),
(capabilities.has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
));
}
routes.push(route_with_requirements(
crate::RawIngestRouteId::HttpBlockPolling,
network,
&[(capabilities.has_http_block_scan, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
));
return routes;
}
fn route_with_requirements(
route_id: crate::RawIngestRouteId,
network: &str,
requirements: &[(bool, crate::RawIngestRouteUnavailableReason)],
) -> crate::RawIngestRouteDto {
for (available, reason) in requirements.iter().copied() {
if !available {
return crate::RawIngestRouteDto::unavailable(route_id, std::option::Option::Some(network), reason);
}
}
return crate::RawIngestRouteDto::configured(route_id, network);
}
fn unavailable_profile(
profile_id: &str,
is_default: bool,
network: std::option::Option<&str>,
reason: crate::RawIngestRouteUnavailableReason,
) -> crate::RawIngestProfileInventoryDto {
let routes = crate::RawIngestRouteId::all()
.into_iter()
.map(|route_id| return crate::RawIngestRouteDto::unavailable(route_id, network, reason))
.collect::<std::vec::Vec<_>>();
return crate::RawIngestProfileInventoryDto { is_default, network: network.map(str::to_owned), profile_id: profile_id.to_owned(), routes };
}
fn resolution_error_reason(error: &ksp_core_lib::Error) -> crate::RawIngestRouteUnavailableReason {
if error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING {
return crate::RawIngestRouteUnavailableReason::MissingRequiredSecret;
}
return crate::RawIngestRouteUnavailableReason::ProfileUnresolved;
}
fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
let networks = enabled_transport_networks(transport);
return networks.len() == 1 && networks.contains(network);
}
fn enabled_transport_networks(transport: &ksp_config_lib::ResolvedTransportConfig) -> std::collections::BTreeSet<String> {
let mut networks = std::collections::BTreeSet::<String>::new();
for endpoint in transport.http_settings().endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
if let std::option::Option::Some(settings) = transport.ws_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
if let std::option::Option::Some(settings) = transport.grpc_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
return networks;
}
fn has_ws_capability(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
protocol: ksp_onchain_transport_lib::WsProtocolKind,
capability: ksp_onchain_transport_lib::WsSubscriptionKind,
) -> bool {
let settings = match transport.ws_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
for endpoint in settings.endpoints() {
if endpoint.enabled() && endpoint.cluster().as_str() == network && endpoint.protocol() == protocol && endpoint.supports_subscription(capability) {
return true;
}
}
return false;
}
fn has_yellowstone_grpc(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
let settings = match transport.grpc_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
return settings.endpoints().iter().any(|endpoint| return endpoint.enabled() && endpoint.cluster().as_str() == network);
}
fn http_role_supports_methods(settings: &ksp_onchain_transport_lib::HttpTransportSettings, methods: &[&'static str]) -> bool {
let mut request_kinds = std::vec::Vec::<&'static str>::with_capacity(methods.len());
for method_name in methods {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name);
let descriptor = match descriptor {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
request_kinds.push(descriptor.request_kind());
}
let mut role_names = std::collections::BTreeSet::<String>::new();
for endpoint in settings.endpoints() {
if !endpoint.enabled() {
continue;
}
for role in endpoint.roles() {
if role.enabled() {
role_names.insert(role.role().as_str().to_owned());
}
}
}
for role_name in role_names {
let mut complete = true;
for request_kind in &request_kinds {
if !http_role_supports_request_kind(settings, role_name.as_str(), request_kind) {
complete = false;
break;
}
}
if complete {
return true;
}
}
return false;
}
fn http_role_supports_request_kind(settings: &ksp_onchain_transport_lib::HttpTransportSettings, role_name: &str, request_kind: &str) -> bool {
for endpoint in settings.endpoints() {
if !endpoint.enabled() {
continue;
}
for role in endpoint.roles() {
if !role.enabled() || role.role().as_str() != role_name {
continue;
}
for capability in role.request_kinds() {
if capability.is_wildcard() || capability.as_str() == request_kind {
return true;
}
}
}
}
return false;
}
fn inventory_invalid(message: &'static str) -> ksp_core_lib::Result<CompositeProfileCatalog> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, message));
}
#[cfg(test)]
#[path = "../unit_tests/route_inventory.rs"]
mod tests;

View File

@@ -0,0 +1,352 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_monitoring.rs
// version: 1
//! Frontend-safe latest-value monitoring projection for Raw Transaction Ingest Desk route Workers.
use ts_rs::TS; // rust-rules: trait-import
/// Stable Tauri event carrying one complete latest-value route monitoring projection.
pub(crate) const RAW_INGEST_ROUTE_STATUS_EVENT_NAME: &str = "ksp-raw-ingest-route-status";
/// Frontend-safe bounded projection of one run-local continuity gap.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteGapDto.ts")]
pub(crate) struct RawIngestRouteGapDto {
/// Run-local gap identifier encoded as decimal text to avoid JavaScript integer truncation.
pub(crate) gap_id: String,
/// Inclusive first slot encoded as decimal text.
pub(crate) start_slot: String,
/// Inclusive last slot encoded as decimal text.
pub(crate) end_slot: String,
/// Stable source-neutral gap lifecycle code.
pub(crate) state: String,
/// Stable source-neutral reason code.
pub(crate) reason: String,
/// Stable source-neutral latest repair mechanism code, when any.
pub(crate) last_method: std::option::Option<String>,
}
/// Frontend-safe latest-value monitoring projection of one independent route Worker.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteMonitoringDto.ts")]
pub(crate) struct RawIngestRouteMonitoringDto {
/// Confirmed/finalized commitment bound to this Worker run.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation revalidated before this Worker started.
pub(crate) inventory_generation: u32,
/// Logical network shared by Worker and Store.
pub(crate) network: String,
/// Safe logical network-profile identifier.
pub(crate) profile_id: String,
/// Stable logical route identifier.
pub(crate) route_id: crate::RawIngestRouteId,
/// Monotone Worker snapshot sequence encoded as decimal text.
pub(crate) sequence: String,
/// Safe application-owned lifecycle projection.
pub(crate) state: crate::RawIngestRouteState,
/// Stable generic Worker health code.
pub(crate) health: String,
/// Stable generic Worker activity code.
pub(crate) activity: String,
/// Whether this latest Worker snapshot is terminal.
pub(crate) terminal: bool,
/// Stable terminal fault domain when lifecycle is faulted.
pub(crate) fault_domain: std::option::Option<String>,
/// Stable terminal fault code when lifecycle is faulted.
pub(crate) fault_code: std::option::Option<String>,
/// Configured bounded admission queue capacity.
pub(crate) admission_queue_capacity: u32,
/// Latest admission queue depth.
pub(crate) admission_queue_depth: u32,
/// Configured Store persistence concurrency.
pub(crate) persistence_concurrency: u32,
/// Latest number of in-flight Store persistence operations.
pub(crate) in_flight_persistence: u32,
/// Total admitted ingress entries encoded as decimal text.
pub(crate) admitted_total: String,
/// Total canonicalized ingress entries encoded as decimal text.
pub(crate) canonicalized_total: String,
/// Total successful Store persistence outcomes encoded as decimal text.
pub(crate) persisted_total: String,
/// Total newly inserted canonical RAW entities encoded as decimal text.
pub(crate) entity_inserted_total: String,
/// Total canonical RAW entities already present encoded as decimal text.
pub(crate) entity_already_present_total: String,
/// Total purge tombstones respected by persistence encoded as decimal text.
pub(crate) entity_skipped_purged_total: String,
/// Total newly inserted acquisition observations encoded as decimal text.
pub(crate) observation_inserted_total: String,
/// Total acquisition observations already present encoded as decimal text.
pub(crate) observation_already_present_total: String,
/// Total durable content conflicts encoded as decimal text.
pub(crate) content_conflict_total: String,
/// Total non-conflict Store failures encoded as decimal text.
pub(crate) store_failure_total: String,
/// Total source-task failures encoded as decimal text.
pub(crate) source_failure_total: String,
/// Total bounded-admission backpressure waits encoded as decimal text.
pub(crate) backpressure_wait_total: String,
/// Latest number of source signals awaiting hydration/admission processing.
pub(crate) hydration_pending: u32,
/// Run-local processing frontier encoded as decimal text when established.
pub(crate) processing_frontier_slot: std::option::Option<String>,
/// Oldest slot still owning pending source work encoded as decimal text when present.
pub(crate) oldest_pending_slot: std::option::Option<String>,
/// Whether continuity health policy has emitted a run-local projection.
pub(crate) continuity_policy_observed: bool,
/// Latest run-local continuity frontier encoded as decimal text when established.
pub(crate) continuity_frontier_slot: std::option::Option<String>,
/// Whether one or more continuity gaps are currently open.
pub(crate) continuity_has_open_gaps: bool,
/// Whether all failed-source continuity obligations have been reconciled.
pub(crate) failed_source_losses_reconciled: bool,
/// Whether current coverage proves the target can continue into future slots.
pub(crate) future_target_coverage: bool,
/// Stable source-neutral latest productive-source lifecycle code.
pub(crate) source_state: std::option::Option<String>,
/// Configured logical live-source count.
pub(crate) source_total: u32,
/// Latest Active source count.
pub(crate) source_active: u32,
/// Latest Reconnecting source count.
pub(crate) source_reconnecting: u32,
/// Latest Failed source count.
pub(crate) source_failed: u32,
/// Successful source reconnect count encoded as decimal text.
pub(crate) source_reconnect_total: String,
/// Replay-bearing reconnect attempt count encoded as decimal text.
pub(crate) source_replay_attempt_total: String,
/// Proven replay-retention continuity gap count encoded as decimal text.
pub(crate) source_continuity_gap_total: String,
/// Number of currently open continuity gaps.
pub(crate) open_gap_count: u32,
/// Number of continuity gaps currently under repair.
pub(crate) repairing_gap_count: u32,
/// Cumulative repaired-gap count encoded as decimal text.
pub(crate) repaired_gap_total: String,
/// Cumulative unresolved-gap count encoded as decimal text.
pub(crate) unresolved_gap_total: String,
/// Cumulative replay repair count encoded as decimal text.
pub(crate) replay_repair_total: String,
/// Cumulative redundant-coverage repair count encoded as decimal text.
pub(crate) redundant_coverage_repair_total: String,
/// Cumulative bounded HTTP scan repair count encoded as decimal text.
pub(crate) http_scan_repair_total: String,
/// Cumulative direct block-fetch repair count encoded as decimal text.
pub(crate) repair_block_fetch_total: String,
/// Cumulative transaction-hydration repair count encoded as decimal text.
pub(crate) repair_transaction_hydration_total: String,
/// Oldest currently open gap start slot encoded as decimal text when present.
pub(crate) oldest_open_gap_start_slot: std::option::Option<String>,
/// Bounded source-neutral details for current and recent continuity gaps.
pub(crate) gaps: std::vec::Vec<crate::RawIngestRouteGapDto>,
}
/// Projects one complete concrete Worker snapshot to the safe route monitoring contract.
pub(crate) fn project_route_monitoring(
runtime: &crate::RawIngestRouteRuntimeDto,
snapshot: &ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot,
) -> ksp_core_lib::Result<crate::RawIngestRouteMonitoringDto> {
let worker = snapshot.worker_snapshot();
let fault = worker.state().fault_code();
let admission_queue_capacity = usize_to_u32(snapshot.admission_queue_capacity(), "admission_queue_capacity");
let admission_queue_capacity = match admission_queue_capacity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let admission_queue_depth = usize_to_u32(snapshot.admission_queue_depth(), "admission_queue_depth");
let admission_queue_depth = match admission_queue_depth {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persistence_concurrency = usize_to_u32(snapshot.persistence_concurrency(), "persistence_concurrency");
let persistence_concurrency = match persistence_concurrency {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let in_flight_persistence = usize_to_u32(snapshot.in_flight_persistence(), "in_flight_persistence");
let in_flight_persistence = match in_flight_persistence {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let hydration_pending = usize_to_u32(snapshot.hydration_pending(), "hydration_pending");
let hydration_pending = match hydration_pending {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_total = usize_to_u32(snapshot.source_total(), "source_total");
let source_total = match source_total {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_active = usize_to_u32(snapshot.source_active(), "source_active");
let source_active = match source_active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_reconnecting = usize_to_u32(snapshot.source_reconnecting(), "source_reconnecting");
let source_reconnecting = match source_reconnecting {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_failed = usize_to_u32(snapshot.source_failed(), "source_failed");
let source_failed = match source_failed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let open_gap_count = usize_to_u32(snapshot.open_gap_count(), "open_gap_count");
let open_gap_count = match open_gap_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let repairing_gap_count = usize_to_u32(snapshot.repairing_gap_count(), "repairing_gap_count");
let repairing_gap_count = match repairing_gap_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let gaps = snapshot.gaps().iter().map(project_gap).collect::<std::vec::Vec<_>>();
return std::result::Result::Ok(crate::RawIngestRouteMonitoringDto {
commitment: runtime.commitment,
inventory_generation: runtime.inventory_generation,
network: runtime.network.clone(),
profile_id: runtime.profile_id.clone(),
route_id: runtime.route_id,
sequence: worker.sequence().value().to_string(),
state: project_worker_state(worker.state()),
health: worker.health().code().to_owned(),
activity: worker.activity().code().to_owned(),
terminal: worker.state().is_terminal(),
fault_domain: fault.map(|code| return code.domain().to_owned()),
fault_code: fault.map(|code| return code.code().to_owned()),
admission_queue_capacity,
admission_queue_depth,
persistence_concurrency,
in_flight_persistence,
admitted_total: snapshot.admitted_total().to_string(),
canonicalized_total: snapshot.canonicalized_total().to_string(),
persisted_total: snapshot.persisted_total().to_string(),
entity_inserted_total: snapshot.entity_inserted_total().to_string(),
entity_already_present_total: snapshot.entity_already_present_total().to_string(),
entity_skipped_purged_total: snapshot.entity_skipped_purged_total().to_string(),
observation_inserted_total: snapshot.observation_inserted_total().to_string(),
observation_already_present_total: snapshot.observation_already_present_total().to_string(),
content_conflict_total: snapshot.content_conflict_total().to_string(),
store_failure_total: snapshot.store_failure_total().to_string(),
source_failure_total: snapshot.source_failure_total().to_string(),
backpressure_wait_total: snapshot.backpressure_wait_total().to_string(),
hydration_pending,
processing_frontier_slot: slot_text(snapshot.processing_frontier_slot()),
oldest_pending_slot: slot_text(snapshot.oldest_pending_slot()),
continuity_policy_observed: snapshot.continuity_policy_observed(),
continuity_frontier_slot: slot_text(snapshot.continuity_frontier_slot()),
continuity_has_open_gaps: snapshot.continuity_has_open_gaps(),
failed_source_losses_reconciled: snapshot.failed_source_losses_reconciled(),
future_target_coverage: snapshot.future_target_coverage(),
source_state: snapshot.source_state().map(|state| return source_state_code(state).to_owned()),
source_total,
source_active,
source_reconnecting,
source_failed,
source_reconnect_total: snapshot.source_reconnect_total().to_string(),
source_replay_attempt_total: snapshot.source_replay_attempt_total().to_string(),
source_continuity_gap_total: snapshot.source_continuity_gap_total().to_string(),
open_gap_count,
repairing_gap_count,
repaired_gap_total: snapshot.repaired_gap_total().to_string(),
unresolved_gap_total: snapshot.unresolved_gap_total().to_string(),
replay_repair_total: snapshot.replay_repair_total().to_string(),
redundant_coverage_repair_total: snapshot.redundant_coverage_repair_total().to_string(),
http_scan_repair_total: snapshot.http_scan_repair_total().to_string(),
repair_block_fetch_total: snapshot.repair_block_fetch_total().to_string(),
repair_transaction_hydration_total: snapshot.repair_transaction_hydration_total().to_string(),
oldest_open_gap_start_slot: slot_text(snapshot.oldest_open_gap_start_slot()),
gaps,
});
}
fn project_gap(gap: &ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapSnapshot) -> crate::RawIngestRouteGapDto {
return crate::RawIngestRouteGapDto {
gap_id: gap.gap_id().value().to_string(),
start_slot: gap.start_slot().to_string(),
end_slot: gap.end_slot().to_string(),
state: gap_state_code(gap.state()).to_owned(),
reason: gap_reason_code(gap.reason()).to_owned(),
last_method: gap.last_method().map(|method| return repair_method_code(method).to_owned()),
};
}
fn project_worker_state(state: ksp_worker_api::WorkerState) -> crate::RawIngestRouteState {
return match state {
ksp_worker_api::WorkerState::Created | ksp_worker_api::WorkerState::Starting => crate::RawIngestRouteState::Starting,
ksp_worker_api::WorkerState::Running => crate::RawIngestRouteState::Running,
ksp_worker_api::WorkerState::Stopping => crate::RawIngestRouteState::Stopping,
ksp_worker_api::WorkerState::Stopped => crate::RawIngestRouteState::Stopped,
ksp_worker_api::WorkerState::Faulted(_) => crate::RawIngestRouteState::Faulted,
_ => crate::RawIngestRouteState::Faulted,
};
}
fn source_state_code(state: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState) -> &'static str {
return match state {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Active => "active",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Reconnecting => "reconnecting",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Closing => "closing",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Closed => "closed",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Failed => "failed",
};
}
fn gap_state_code(state: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState) -> &'static str {
return match state {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Pending => "pending",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repairing => "repairing",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repaired => "repaired",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Unresolved => "unresolved",
};
}
fn gap_reason_code(reason: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason) -> &'static str {
return match reason {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::HttpProducedBlockUnavailable => "http_produced_block_unavailable",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::KnownReferenceMissing => "known_reference_missing",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::SourceFailure => "source_failure",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::TransportOverflow => "transport_overflow",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::WebSocketReconnect => "websocket_reconnect",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::YellowstoneRetention => "yellowstone_retention",
};
}
fn repair_method_code(method: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod) -> &'static str {
return match method {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::Replay => "replay",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::RedundantCoverage => "redundant_coverage",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::HttpScan => "http_scan",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::BlockFetch => "block_fetch",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::TransactionHydration => "transaction_hydration",
};
}
fn slot_text(value: std::option::Option<u64>) -> std::option::Option<String> {
return value.map(|slot| return slot.to_string());
}
fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
let converted = u32::try_from(value);
return match converted {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Raw Transaction Ingest Desk cannot project a bounded Worker counter to the frontend",
)
.with_context("field", field)
.with_source(error),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/route_monitoring.rs"]
mod tests;

View File

@@ -0,0 +1,910 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
// version: 8
//! Multi-route Store and independent Worker lifecycle owned by Raw Transaction Ingest Desk.
const ROUTE_SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
const ROUTE_STOP_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
const STORE_RECLAIM_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(5);
const STORE_RECLAIM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
struct RouteRuntimeInner {
last_monitoring: std::vec::Vec<crate::RawIngestRouteMonitoringDto>,
last_terminals: std::vec::Vec<crate::RawIngestRouteRuntimeDto>,
network: std::option::Option<String>,
next_sequence: u64,
routes: std::vec::Vec<RouteRuntimeSlot>,
shutting_down: bool,
store: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>,
store_closing_token: std::option::Option<u64>,
store_opening_token: std::option::Option<u64>,
}
/// Shared multi-route runtime state. Each route owns an independent Worker while same-network routes share one Store.
pub(crate) struct RouteRuntimeState {
inner: std::sync::Mutex<RouteRuntimeInner>,
}
impl crate::RouteRuntimeState {
/// Creates an idle multi-route runtime state without Store or Worker ownership.
#[must_use]
pub(crate) fn new() -> Self {
return Self {
inner: std::sync::Mutex::new(RouteRuntimeInner {
last_monitoring: std::vec::Vec::new(),
last_terminals: std::vec::Vec::new(),
network: std::option::Option::None,
next_sequence: 0,
routes: std::vec::Vec::new(),
shutting_down: false,
store: std::option::Option::None,
store_closing_token: std::option::Option::None,
store_opening_token: std::option::Option::None,
}),
};
}
/// Rejects new route Start work after application shutdown has closed admission.
pub(crate) fn ensure_start_admission_open(&self) -> ksp_core_lib::Result<()> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.shutting_down {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
));
}
return std::result::Result::Ok(());
}
fn reserve(&self, prepared: &crate::PreparedRouteStart) -> ksp_core_lib::Result<RouteRuntimeReservation> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.shutting_down {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
));
}
if inner.store_closing_token.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
"Raw Transaction Ingest Desk Store is still closing after the previous route set",
));
}
if inner.routes.iter().any(|slot| return slot.matches_identity(prepared.profile_id.as_str(), prepared.route_id)) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
"Raw Transaction Ingest Desk already owns one Worker for the selected logical route",
));
}
if let std::option::Option::Some(network) = inner.network.as_deref()
&& network != prepared.network.as_str()
{
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH,
"Raw Transaction Ingest Desk cannot share one Store across different logical networks",
));
}
let needs_store_open = inner.store.is_none();
if needs_store_open && inner.store_opening_token.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
"Raw Transaction Ingest Desk same-network Store is still opening",
));
}
let sequence = match inner.next_sequence.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED,
"Raw Transaction Ingest Desk Worker sequence is exhausted",
));
},
};
let worker_id = ksp_worker_api::WorkerId::new(format!("raw-ingest-desk-{sequence}"));
let worker_id = match worker_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED, "Cannot create Raw Transaction Ingest Desk Worker identity")
.with_source(error),
);
},
};
let identity = RouteRuntimeIdentity {
commitment: prepared.commitment,
inventory_generation: prepared.inventory_generation,
network: prepared.network.clone(),
profile_id: prepared.profile_id.clone(),
route_id: prepared.route_id,
};
if inner.network.is_none() {
inner.last_monitoring.clear();
inner.last_terminals.clear();
inner.network = std::option::Option::Some(prepared.network.clone());
}
inner.last_monitoring.retain(|status| return !identity.matches_monitoring(status));
inner.last_terminals.retain(|terminal| return !identity.matches_dto(terminal));
inner.next_sequence = sequence;
if needs_store_open {
inner.store_opening_token = std::option::Option::Some(sequence);
}
inner.routes.push(RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: sequence });
return std::result::Result::Ok(RouteRuntimeReservation { identity, needs_store_open, token: sequence, worker_id });
}
fn install_store(&self, token: u64, store: std::sync::Arc<ksp_store_lib::Store>) -> ksp_core_lib::Result<()> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.store_opening_token != std::option::Option::Some(token) || inner.store.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID,
"Raw Transaction Ingest Desk shared Store reservation changed before installation",
));
}
inner.store = std::option::Option::Some(store);
inner.store_opening_token = std::option::Option::None;
return std::result::Result::Ok(());
}
fn shared_store(&self, expected_network: &str) -> ksp_core_lib::Result<std::sync::Arc<ksp_store_lib::Store>> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.network.as_deref() != std::option::Option::Some(expected_network) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH,
"Raw Transaction Ingest Desk shared Store network no longer matches the selected route",
));
}
return match &inner.store {
std::option::Option::Some(store) => std::result::Result::Ok(std::sync::Arc::clone(store)),
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID,
"Raw Transaction Ingest Desk shared Store is not installed",
)),
};
}
fn activate(
&self,
reservation: &RouteRuntimeReservation,
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let position = inner.routes.iter().position(|slot| return slot.starting_token() == std::option::Option::Some(reservation.token));
let position = match position {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID,
"Raw Transaction Ingest Desk route reservation changed before Worker activation",
));
},
};
let stop_requested = match &inner.routes[position] {
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested,
RouteRuntimeSlot::Active { .. } => false,
};
let should_stop = stop_requested || inner.shutting_down;
if should_stop {
let _accepted = handle.request_stop();
}
let state = if should_stop {
crate::RawIngestRouteState::Stopping
} else {
project_worker_state(handle.snapshot_source().current().worker_snapshot().state())
};
inner.routes[position] = RouteRuntimeSlot::Active { handle, identity: reservation.identity.clone(), token: reservation.token };
return std::result::Result::Ok(reservation.identity.dto(state));
}
fn rollback(&self, token: u64) -> ksp_core_lib::Result<std::option::Option<std::sync::Arc<ksp_store_lib::Store>>> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let cancelled_terminal = inner.routes.iter().find(|slot| return slot.token() == token).and_then(RouteRuntimeSlot::cancelled_start_terminal);
inner.routes.retain(|slot| return slot.token() != token);
if let std::option::Option::Some(terminal) = cancelled_terminal {
inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id);
inner.last_terminals.push(terminal);
}
if inner.store_opening_token == std::option::Option::Some(token) {
inner.store_opening_token = std::option::Option::None;
}
if inner.routes.is_empty() {
let store = inner.store.take();
if store.is_some() {
inner.store_closing_token = std::option::Option::Some(token);
} else {
inner.network = std::option::Option::None;
}
return std::result::Result::Ok(store);
}
return std::result::Result::Ok(std::option::Option::None);
}
fn finish(
&self,
token: u64,
terminal: crate::RawIngestRouteRuntimeDto,
monitoring: std::option::Option<crate::RawIngestRouteMonitoringDto>,
) -> ksp_core_lib::Result<std::option::Option<std::sync::Arc<ksp_store_lib::Store>>> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let before = inner.routes.len();
inner.routes.retain(|slot| return slot.token() != token);
if inner.routes.len() == before {
return std::result::Result::Ok(std::option::Option::None);
}
inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id);
inner.last_terminals.push(terminal);
if let std::option::Option::Some(status) = monitoring {
inner.last_monitoring.retain(|previous| return previous.profile_id != status.profile_id || previous.route_id != status.route_id);
inner.last_monitoring.push(status);
}
if inner.routes.is_empty() {
let store = inner.store.take();
if store.is_some() {
inner.store_closing_token = std::option::Option::Some(token);
} else {
inner.network = std::option::Option::None;
}
return std::result::Result::Ok(store);
}
return std::result::Result::Ok(std::option::Option::None);
}
fn complete_store_close(&self, token: u64) {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
if inner.store_closing_token == std::option::Option::Some(token) {
inner.store_closing_token = std::option::Option::None;
if inner.routes.is_empty() && inner.store.is_none() && inner.store_opening_token.is_none() {
inner.network = std::option::Option::None;
}
}
}
/// Returns complete latest-value monitoring projections for active and recent terminal routes in the current runtime session.
pub(crate) fn monitoring_statuses(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let mut values = inner.last_monitoring.clone();
for slot in &inner.routes {
if let RouteRuntimeSlot::Active { handle, identity, .. } = slot {
let snapshot = handle.snapshot_source().current();
let runtime = identity.dto(project_worker_state(snapshot.worker_snapshot().state()));
let projected = crate::project_route_monitoring(&runtime, &snapshot);
let projected = match projected {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
values.retain(|previous| return previous.profile_id != projected.profile_id || previous.route_id != projected.route_id);
values.push(projected);
}
}
return std::result::Result::Ok(values);
}
/// Closes route Start admission exactly once and requests cooperative Stop for all active or still-starting routes.
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.shutting_down {
return std::result::Result::Ok(false);
}
inner.shutting_down = true;
for slot in &mut inner.routes {
match slot {
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested = true,
RouteRuntimeSlot::Active { handle, .. } => {
let _accepted = handle.request_stop();
},
}
}
return std::result::Result::Ok(true);
}
/// Waits until all owned route Workers and the shared Store have completed bounded application-shutdown cleanup.
pub(crate) async fn shutdown_and_wait(&self) -> ksp_core_lib::Result<()> {
let begin = self.begin_shutdown();
if let std::result::Result::Err(error) = begin {
return std::result::Result::Err(error);
}
let started = std::time::Instant::now();
loop {
let complete = self.shutdown_complete();
let complete = match complete {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if complete {
return std::result::Result::Ok(());
}
if started.elapsed() >= ROUTE_SHUTDOWN_TIMEOUT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED,
"Raw Transaction Ingest Desk application shutdown did not release all route Workers and shared Store state before the bounded deadline",
));
}
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
}
}
fn shutdown_complete(&self) -> ksp_core_lib::Result<bool> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
return std::result::Result::Ok(
inner.routes.is_empty() && inner.store.is_none() && inner.store_opening_token.is_none() && inner.store_closing_token.is_none(),
);
}
/// Requests cooperative Stop for one exact logical route and waits for its Worker cleanup.
pub(crate) async fn stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let identity = request.validate_logical_identity();
if let std::result::Result::Err(error) = identity {
return std::result::Result::Err(error);
}
let target = self.stop_target(request);
let target = match target {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (handle, identity, token) = match target {
RouteStopTarget::Active { handle, identity, token } => (handle, identity, token),
RouteStopTarget::Starting { identity, token } => return self.wait_route_cleanup(token, &identity).await,
RouteStopTarget::Terminal(value) => return std::result::Result::Ok(value),
RouteStopTarget::TerminalClosing => return self.wait_terminal_store_cleanup(request).await,
};
let _first_stop_request = handle.request_stop();
if let std::result::Result::Err(error) = handle.wait_terminal().await {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID, "Cannot observe terminal Raw Transaction Ingest Desk Worker state")
.with_source(error),
);
}
return self.wait_route_cleanup(token, &identity).await;
}
fn stop_target(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<RouteStopTarget> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let position = inner.routes.iter().position(|slot| return slot.matches_identity(request.profile_id.as_str(), request.route_id));
if let std::option::Option::Some(position) = position {
return match &mut inner.routes[position] {
RouteRuntimeSlot::Active { handle, identity, token } => {
std::result::Result::Ok(RouteStopTarget::Active { handle: handle.clone(), identity: identity.clone(), token: *token })
},
RouteRuntimeSlot::Starting { identity, stop_requested, token } => {
*stop_requested = true;
std::result::Result::Ok(RouteStopTarget::Starting { identity: identity.clone(), token: *token })
},
};
}
let terminal = inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
return match terminal {
std::option::Option::Some(value) if inner.store_closing_token.is_none() => std::result::Result::Ok(RouteStopTarget::Terminal(value.clone())),
std::option::Option::Some(_) => std::result::Result::Ok(RouteStopTarget::TerminalClosing),
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
"Raw Transaction Ingest Desk has no active Worker for the selected logical route",
)),
};
}
async fn wait_route_cleanup(&self, token: u64, identity: &RouteRuntimeIdentity) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let started = std::time::Instant::now();
loop {
let cleanup = self.route_cleanup_complete(token, identity);
let cleanup = match cleanup {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(value) = cleanup {
return std::result::Result::Ok(value);
}
if started.elapsed() >= ROUTE_STOP_CLEANUP_TIMEOUT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Raw Transaction Ingest Desk terminal cleanup did not release the selected route",
));
}
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
}
}
async fn wait_terminal_store_cleanup(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let started = std::time::Instant::now();
loop {
let terminal = self.late_terminal_after_store_cleanup(request);
let terminal = match terminal {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(value) = terminal {
return std::result::Result::Ok(value);
}
if started.elapsed() >= ROUTE_STOP_CLEANUP_TIMEOUT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Raw Transaction Ingest Desk final shared Store cleanup did not complete for the selected terminal route",
));
}
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
}
}
fn late_terminal_after_store_cleanup(
&self,
request: &crate::RawIngestRouteStopRequestDto,
) -> ksp_core_lib::Result<std::option::Option<crate::RawIngestRouteRuntimeDto>> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let terminal = inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
let terminal = match terminal {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
"Raw Transaction Ingest Desk terminal route is no longer retained",
));
},
};
if inner.store_closing_token.is_some() {
return std::result::Result::Ok(std::option::Option::None);
}
return std::result::Result::Ok(std::option::Option::Some(terminal.clone()));
}
fn route_cleanup_complete(
&self,
token: u64,
identity: &RouteRuntimeIdentity,
) -> ksp_core_lib::Result<std::option::Option<crate::RawIngestRouteRuntimeDto>> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.routes.iter().any(|slot| return slot.token() == token) || inner.store_closing_token == std::option::Option::Some(token) {
return std::result::Result::Ok(std::option::Option::None);
}
let terminal = inner.last_terminals.iter().find(|value| return identity.matches_dto(value));
return std::result::Result::Ok(terminal.cloned());
}
}
/// Launch ownership returned only after shared Store readiness, Worker creation and route-handle installation have succeeded.
pub(crate) struct RouteRuntimeLaunch {
acknowledgement: crate::RawIngestRouteRuntimeDto,
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
runtime_state: std::sync::Arc<crate::RouteRuntimeState>,
token: u64,
}
impl crate::RouteRuntimeLaunch {
/// Returns the safe Start acknowledgement captured after the Worker handle was installed.
#[must_use]
pub(crate) fn acknowledgement(&self) -> crate::RawIngestRouteRuntimeDto {
return self.acknowledgement.clone();
}
/// Returns the complete safe logical route identity used to project Worker monitoring snapshots.
#[must_use]
pub(crate) fn monitoring_identity(&self) -> crate::RawIngestRouteRuntimeDto {
return self.acknowledgement.clone();
}
/// Returns an independent latest-value source for frontend-safe monitoring relay.
#[must_use]
pub(crate) fn monitoring_source(&self) -> ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshotSource {
return self.handle.snapshot_source();
}
/// Waits for this Worker terminal state, removes only its route and closes Store only after the final same-network route terminates.
pub(crate) async fn monitor(self) {
let terminal = self.handle.wait_terminal().await;
let snapshot = self.handle.snapshot_source().current();
let worker = snapshot.worker_snapshot();
let snapshot_fault = worker.state().fault_code();
let snapshot_fault_domain = snapshot_fault.map_or("none", |code| return code.domain());
let snapshot_fault_code = snapshot_fault.map_or("none", |code| return code.code());
let terminal_state = match terminal {
std::result::Result::Ok(state) => {
ksp_logging_lib::info!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
profile_id = self.acknowledgement.profile_id.as_str(),
network = self.acknowledgement.network.as_str(),
route_id = self.acknowledgement.route_id.as_str(),
commitment = self.acknowledgement.commitment.as_str(),
worker_state = state.code(),
worker_health = worker.health().code(),
worker_activity = worker.activity().code(),
fault_domain = snapshot_fault_domain,
fault_code = snapshot_fault_code,
source_failure_total = snapshot.source_failure_total(),
store_failure_total = snapshot.store_failure_total(),
content_conflict_total = snapshot.content_conflict_total(),
source_active = snapshot.source_active(),
source_reconnecting = snapshot.source_reconnecting(),
source_failed = snapshot.source_failed(),
open_gap_count = snapshot.open_gap_count(),
continuity_has_open_gaps = snapshot.continuity_has_open_gaps(),
future_target_coverage = snapshot.future_target_coverage(),
failed_source_losses_reconciled = snapshot.failed_source_losses_reconciled(),
"Raw Transaction Ingest Desk route Worker reached terminal state"
);
project_worker_state(state)
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
profile_id = self.acknowledgement.profile_id.as_str(),
network = self.acknowledgement.network.as_str(),
route_id = self.acknowledgement.route_id.as_str(),
commitment = self.acknowledgement.commitment.as_str(),
error_domain = error.code().domain(),
error_code = error.code().code(),
worker_health = worker.health().code(),
snapshot_fault_domain = snapshot_fault_domain,
snapshot_fault_code = snapshot_fault_code,
source_failure_total = snapshot.source_failure_total(),
store_failure_total = snapshot.store_failure_total(),
content_conflict_total = snapshot.content_conflict_total(),
"Raw Transaction Ingest Desk terminal Worker wait failed"
);
crate::RawIngestRouteState::Faulted
},
};
let mut terminal = self.acknowledgement;
terminal.state = terminal_state;
let monitoring = crate::project_route_monitoring(&terminal, &snapshot);
let monitoring = match monitoring {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk could not retain terminal route monitoring projection"
);
std::option::Option::None
},
};
let store = self.runtime_state.finish(self.token, terminal, monitoring);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk route cleanup state update failed"
);
return;
},
};
if let std::option::Option::Some(store) = store {
let close = close_store_arc(store).await;
if let std::result::Result::Err(error) = close {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk shared Store shutdown failed after final Worker"
);
}
self.runtime_state.complete_store_close(self.token);
}
}
}
/// Starts one exact route Worker and reuses the same ready Store for any already-active same-network route.
pub(crate) async fn start_route_runtime(
runtime_state: std::sync::Arc<crate::RouteRuntimeState>,
prepared: crate::PreparedRouteStart,
) -> ksp_core_lib::Result<crate::RouteRuntimeLaunch> {
let reservation = match runtime_state.reserve(&prepared) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = if reservation.needs_store_open {
let store = ksp_store_lib::Store::open(prepared.store_settings).await;
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let _rollback = runtime_state.rollback(reservation.token);
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED, "Cannot open Raw Transaction Ingest Desk shared Store runtime")
.with_source(error),
);
},
};
let health = store.health().await;
if health.state() != ksp_store_lib::StoreHealthState::Ready {
let close = store.close().await;
let _rollback = runtime_state.rollback(reservation.token);
if let std::result::Result::Err(error) = close {
return std::result::Result::Err(
ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Cannot close non-ready Raw Transaction Ingest Desk shared Store runtime",
)
.with_source(error),
);
}
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_NOT_READY,
"Raw Transaction Ingest Desk shared Store readiness probe did not prove Ready",
));
}
let store = std::sync::Arc::new(store);
if let std::result::Result::Err(error) = runtime_state.install_store(reservation.token, std::sync::Arc::clone(&store)) {
let close = close_store_arc(store).await;
let _rollback = runtime_state.rollback(reservation.token);
if let std::result::Result::Err(close_error) = close {
return std::result::Result::Err(close_error);
}
return std::result::Result::Err(error);
}
store
} else {
std::mem::drop(prepared.store_settings);
match runtime_state.shared_store(prepared.network.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let _rollback = runtime_state.rollback(reservation.token);
return std::result::Result::Err(error);
},
}
};
let network = store.runtime_snapshot().network().clone();
let settings = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSettings::with_defaults(network, reservation.worker_id.clone());
let handle = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestWorker::start_with_runtime_resources(
settings,
std::sync::Arc::clone(&store),
prepared.runtime_resources,
);
let handle = match handle {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let cleanup_store = runtime_state.rollback(reservation.token);
let cleanup_store = match cleanup_store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(rollback_error) => return std::result::Result::Err(rollback_error),
};
std::mem::drop(store);
if let std::option::Option::Some(value) = cleanup_store {
let close = close_store_arc(value).await;
runtime_state.complete_store_close(reservation.token);
if let std::result::Result::Err(close_error) = close {
return std::result::Result::Err(close_error);
}
}
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED, "Cannot start Raw Transaction Ingest Desk route Worker runtime")
.with_source(error),
);
},
};
let acknowledgement = runtime_state.activate(&reservation, handle.clone());
let acknowledgement = match acknowledgement {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let _stop_requested = handle.request_stop();
let _terminal = handle.wait_terminal().await;
let cleanup_store = runtime_state.rollback(reservation.token);
let cleanup_store = match cleanup_store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(rollback_error) => return std::result::Result::Err(rollback_error),
};
std::mem::drop(store);
if let std::option::Option::Some(value) = cleanup_store {
let close = close_store_arc(value).await;
runtime_state.complete_store_close(reservation.token);
if let std::result::Result::Err(close_error) = close {
return std::result::Result::Err(close_error);
}
}
return std::result::Result::Err(error);
},
};
std::mem::drop(store);
ksp_logging_lib::info!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
profile_id = reservation.identity.profile_id.as_str(),
network = reservation.identity.network.as_str(),
route_id = reservation.identity.route_id.as_str(),
commitment = reservation.identity.commitment.as_str(),
"Raw Transaction Ingest Desk independent route Worker handle installed on shared Store"
);
return std::result::Result::Ok(crate::RouteRuntimeLaunch { acknowledgement, handle, runtime_state, token: reservation.token });
}
#[derive(Clone)]
struct RouteRuntimeIdentity {
commitment: crate::RawIngestCommitment,
inventory_generation: u32,
network: String,
profile_id: String,
route_id: crate::RawIngestRouteId,
}
impl RouteRuntimeIdentity {
fn dto(&self, state: crate::RawIngestRouteState) -> crate::RawIngestRouteRuntimeDto {
return crate::RawIngestRouteRuntimeDto {
commitment: self.commitment,
inventory_generation: self.inventory_generation,
network: self.network.clone(),
profile_id: self.profile_id.clone(),
route_id: self.route_id,
state,
};
}
fn matches_dto(&self, value: &crate::RawIngestRouteRuntimeDto) -> bool {
return self.profile_id == value.profile_id && self.route_id == value.route_id;
}
fn matches_monitoring(&self, value: &crate::RawIngestRouteMonitoringDto) -> bool {
return self.profile_id == value.profile_id && self.route_id == value.route_id;
}
}
enum RouteStopTarget {
Active {
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
identity: RouteRuntimeIdentity,
token: u64,
},
Starting {
identity: RouteRuntimeIdentity,
token: u64,
},
Terminal(crate::RawIngestRouteRuntimeDto),
TerminalClosing,
}
struct RouteRuntimeReservation {
identity: RouteRuntimeIdentity,
needs_store_open: bool,
token: u64,
worker_id: ksp_worker_api::WorkerId,
}
enum RouteRuntimeSlot {
Starting {
identity: RouteRuntimeIdentity,
stop_requested: bool,
token: u64,
},
Active {
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
identity: RouteRuntimeIdentity,
token: u64,
},
}
impl RouteRuntimeSlot {
fn matches_identity(&self, profile_id: &str, route_id: crate::RawIngestRouteId) -> bool {
let identity = match self {
Self::Starting { identity, .. } | Self::Active { identity, .. } => identity,
};
return identity.profile_id == profile_id && identity.route_id == route_id;
}
fn starting_token(&self) -> std::option::Option<u64> {
return match self {
Self::Starting { token, .. } => std::option::Option::Some(*token),
Self::Active { .. } => std::option::Option::None,
};
}
fn cancelled_start_terminal(&self) -> std::option::Option<crate::RawIngestRouteRuntimeDto> {
return match self {
Self::Starting { identity, stop_requested: true, .. } => std::option::Option::Some(identity.dto(crate::RawIngestRouteState::Stopped)),
Self::Starting { stop_requested: false, .. } | Self::Active { .. } => std::option::Option::None,
};
}
fn token(&self) -> u64 {
return match self {
Self::Starting { token, .. } | Self::Active { token, .. } => *token,
};
}
}
async fn close_store_arc(store: std::sync::Arc<ksp_store_lib::Store>) -> ksp_core_lib::Result<()> {
let started = std::time::Instant::now();
let mut store = store;
loop {
match std::sync::Arc::try_unwrap(store) {
std::result::Result::Ok(value) => {
let close = value.close().await;
return match close {
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_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Cannot close Raw Transaction Ingest Desk shared Store runtime",
)
.with_source(error),
),
};
},
std::result::Result::Err(shared) => store = shared,
}
if started.elapsed() >= STORE_RECLAIM_TIMEOUT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Raw Transaction Ingest Desk shared Store remained referenced after final Worker",
));
}
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
}
}
fn project_worker_state(state: ksp_worker_api::WorkerState) -> crate::RawIngestRouteState {
return match state {
ksp_worker_api::WorkerState::Created | ksp_worker_api::WorkerState::Starting => crate::RawIngestRouteState::Starting,
ksp_worker_api::WorkerState::Running => crate::RawIngestRouteState::Running,
ksp_worker_api::WorkerState::Stopping => crate::RawIngestRouteState::Stopping,
ksp_worker_api::WorkerState::Stopped => crate::RawIngestRouteState::Stopped,
ksp_worker_api::WorkerState::Faulted(_) => crate::RawIngestRouteState::Faulted,
_ => crate::RawIngestRouteState::Faulted,
};
}
fn runtime_lock_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_LOCK_FAILED, "Raw Transaction Ingest Desk multi-route runtime state lock is poisoned");
}
#[cfg(test)]
#[path = "../unit_tests/route_runtime.rs"]
mod tests;

View File

@@ -0,0 +1,518 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
// version: 5
//! Start-time Config revalidation and exact Transport/Store resource reconstruction for route validation and runtime launch.
/// Fully revalidated Start preparation retaining backend-only Store settings and exact Worker runtime resources.
pub(crate) struct PreparedRouteStart {
/// Confirmed/finalized commitment validated by the exact route constructor.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation proven current while this preparation was built.
pub(crate) inventory_generation: u32,
/// Logical network proven identical across Store and Transport.
pub(crate) network: String,
/// Logical network-profile identifier selected by the operator.
pub(crate) profile_id: String,
/// Logical route selected by the operator.
pub(crate) route_id: crate::RawIngestRouteId,
/// Backend-neutral Store settings resolved from Config and retained only in Rust.
pub(crate) store_settings: ksp_store_lib::StoreSettings,
/// Exact single-route Worker runtime resources reconstructed from typed Transport contracts.
pub(crate) runtime_resources: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources,
}
/// Revalidates one Start request and retains the exact backend resources required for a later Worker launch.
pub(crate) fn prepare_route_start(
management: &ksp_config_lib::ConfigManagement,
current_generation: u32,
request: &crate::RawIngestRouteStartRequestDto,
) -> ksp_core_lib::Result<crate::PreparedRouteStart> {
let identity = request.validate_logical_identity();
if let std::result::Result::Err(error) = identity {
return std::result::Result::Err(error);
}
if current_generation == 0 || request.inventory_generation != current_generation {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_STALE_INVENTORY,
"Raw Transaction Ingest Desk Start request uses a stale route inventory generation",
));
}
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time Config environment", error),
};
let inventory = crate::build_route_inventory_with_environment(management, &environment, current_generation);
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot rebuild Start-time route inventory", error),
};
let profile = inventory.profiles.iter().find(|profile| return profile.profile_id == request.profile_id);
let profile = match profile {
std::option::Option::Some(value) => value,
std::option::Option::None => return route_unavailable_error(),
};
let route = profile.routes.iter().find(|route| return route.route_id == request.route_id);
let route = match route {
std::option::Option::Some(value) if value.selectable && value.state == crate::RawIngestRouteState::Configured => value,
std::option::Option::Some(_) | std::option::Option::None => return route_unavailable_error(),
};
let network = match route.network.as_deref() {
std::option::Option::Some(value) => value,
std::option::Option::None => return route_unavailable_error(),
};
let composite = crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::Some(request.profile_id.as_str()));
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time composite profile", error),
};
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
let store_profile = match store_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time Store profile", error),
};
let store = management.engine().resolve_store_config_profile(&store_profile, &environment);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time Store settings", error),
};
if store.settings().network().as_str() != network {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk Start-time Store network no longer matches the selected route",
));
}
let transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
let transport_profile = match transport_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time base Transport profile", error),
};
let transport = management.engine().resolve_transport_config_profile(&transport_profile, &environment);
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time base Transport settings", error),
};
if !transport_matches_network(&transport, network) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk Start-time Transport network no longer matches the selected route",
));
}
let commitment = request.commitment.into_transport();
let runtime_resources = match request.route_id {
crate::RawIngestRouteId::YellowstoneHydrated => prepare_yellowstone(management, &environment, &composite, network, commitment),
crate::RawIngestRouteId::StandardLogsHydrated => prepare_standard_logs(&transport, network, commitment),
crate::RawIngestRouteId::StandardBlockDirect => prepare_standard_block(&transport, network, commitment),
crate::RawIngestRouteId::HeliusTransactionHydrated => prepare_helius(management, &environment, &composite, &transport, network, commitment),
crate::RawIngestRouteId::HttpBlockPolling => prepare_http_polling(&transport, network, commitment),
};
let runtime_resources = match runtime_resources {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot reconstruct Start-time Transport resources", error),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_START,
profile_id = request.profile_id.as_str(),
network,
route_id = request.route_id.as_str(),
inventory_generation = request.inventory_generation,
commitment = request.commitment.as_str(),
"revalidated Start request and reconstructed exact Store/Transport-owned Worker launch resources"
);
return std::result::Result::Ok(crate::PreparedRouteStart {
commitment: request.commitment,
inventory_generation: request.inventory_generation,
network: network.to_owned(),
profile_id: request.profile_id.clone(),
route_id: request.route_id,
store_settings: store.into_settings(),
runtime_resources,
});
}
/// Revalidates one future Start request and destroys the reconstructed resources without opening Store or launching a Worker.
pub(crate) fn validate_route_start(
management: &ksp_config_lib::ConfigManagement,
current_generation: u32,
request: &crate::RawIngestRouteStartRequestDto,
) -> ksp_core_lib::Result<crate::RawIngestRouteStartValidationDto> {
let prepared = crate::prepare_route_start(management, current_generation, request);
let prepared = match prepared {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::RawIngestRouteStartValidationDto {
commitment: prepared.commitment,
inventory_generation: prepared.inventory_generation,
network: prepared.network,
profile_id: prepared.profile_id,
route_id: prepared.route_id,
});
}
fn prepare_standard_logs(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let endpoint =
single_ws_endpoint(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Logs);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let role = single_http_role(transport.http_settings(), network, &["getTransaction"]);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.http_settings().clone());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestStandardLogsSource::new(
endpoint,
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::AllWithVotes,
commitment,
pool,
role,
);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_standard_logs_source(source));
}
fn prepare_standard_block(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let endpoint =
single_ws_endpoint(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Block);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestStandardBlockSource::new(
endpoint,
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
commitment,
);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_standard_block_source(source));
}
fn prepare_helius(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
composite: &ksp_config_lib::ResolvedConfigComposite,
base_transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let transport = resolve_optional_transport(management, environment, composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS, network);
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let endpoint = single_ws_endpoint(
&transport,
network,
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let role = single_http_role(base_transport.http_settings(), network, &["getTransaction"]);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(base_transport.http_settings().clone());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHeliusTransactionSource::new(endpoint, filter, commitment, pool, role);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_helius_transaction_source(source));
}
fn prepare_yellowstone(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
composite: &ksp_config_lib::ResolvedConfigComposite,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let transport = resolve_optional_transport(management, environment, composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE, network);
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let endpoint = single_grpc_endpoint(&transport, network);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint);
let channel = match channel {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let filter_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("raw-ingest-blocks");
let filter_name = match filter_name {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut block_filter = ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new();
block_filter.set_include_transactions(std::option::Option::Some(false));
block_filter.set_include_accounts(std::option::Option::Some(false));
block_filter.set_include_entries(std::option::Option::Some(false));
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let inserted = request.insert_block_filter(filter_name, block_filter);
if let std::result::Result::Err(error) = inserted {
return std::result::Result::Err(error);
}
request.set_commitment(std::option::Option::Some(commitment));
let role = single_http_role(transport.http_settings(), network, &["getBlock"]);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.http_settings().clone());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestYellowstoneSource::new(channel, request, pool, role);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::new(source));
}
fn prepare_http_polling(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let role = single_http_role(transport.http_settings(), network, &["getSlot", "getBlocksWithLimit", "getBlock"]);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.http_settings().clone());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHttpBlockPollingSource::new(pool, role, commitment);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_http_block_polling_source(source));
}
fn resolve_optional_transport(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
composite: &ksp_config_lib::ResolvedConfigComposite,
component_id: &str,
network: &str,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedTransportConfig> {
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_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk selected route has no declared Transport source",
));
},
};
if component.resolved().file_id().as_str() != ksp_config_lib::FILE_ID_STD_TRANSPORT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk selected route references an unexpected Transport document",
));
}
let resolved = management.engine().resolve_transport_config_profile(component.resolved(), environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !transport_matches_network(&resolved, network) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk provider Transport network no longer matches the selected route",
));
}
return std::result::Result::Ok(resolved);
}
fn single_ws_endpoint(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
protocol: ksp_onchain_transport_lib::WsProtocolKind,
capability: ksp_onchain_transport_lib::WsSubscriptionKind,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsEndpointSettings> {
let settings = match transport.ws_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return ambiguous_resource_error("Start-time route requires one WebSocket endpoint but none is configured"),
};
let matching = settings
.endpoints()
.iter()
.filter(|endpoint| {
return endpoint.enabled()
&& endpoint.cluster().as_str() == network
&& endpoint.protocol() == protocol
&& endpoint.supports_subscription(capability);
})
.collect::<std::vec::Vec<_>>();
if matching.len() != 1 {
return ambiguous_resource_error("Start-time route requires exactly one matching WebSocket endpoint in its selected Transport profile");
}
return std::result::Result::Ok(matching[0].clone());
}
fn single_grpc_endpoint(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
let settings = match transport.grpc_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return ambiguous_resource_error("Start-time route requires one Yellowstone gRPC endpoint but none is configured"),
};
let matching = settings
.endpoints()
.iter()
.filter(|endpoint| return endpoint.enabled() && endpoint.cluster().as_str() == network)
.collect::<std::vec::Vec<_>>();
if matching.len() != 1 {
return ambiguous_resource_error("Start-time route requires exactly one matching Yellowstone gRPC endpoint in its selected Transport profile");
}
return std::result::Result::Ok(matching[0].clone());
}
fn single_http_role(
settings: &ksp_onchain_transport_lib::HttpTransportSettings,
network: &str,
methods: &[&'static str],
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpRoleName> {
let mut request_kinds = std::vec::Vec::<&'static str>::with_capacity(methods.len());
for method_name in methods {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name);
let descriptor = match descriptor {
std::option::Option::Some(value) => value,
std::option::Option::None => return ambiguous_resource_error("Start-time route references an unknown HTTP method requirement"),
};
request_kinds.push(descriptor.request_kind());
}
let mut roles = std::collections::BTreeSet::<String>::new();
for endpoint in settings.endpoints() {
if !endpoint.enabled() || endpoint.cluster().as_str() != network {
continue;
}
for role in endpoint.roles() {
if role.enabled() {
roles.insert(role.role().as_str().to_owned());
}
}
}
let matching = roles
.into_iter()
.filter(|role_name| {
return request_kinds.iter().all(|request_kind| return http_role_supports_request_kind(settings, network, role_name.as_str(), request_kind));
})
.collect::<std::vec::Vec<_>>();
if matching.len() != 1 {
return ambiguous_resource_error("Start-time route requires exactly one logical HTTP role satisfying its complete method set");
}
return std::result::Result::Ok(ksp_onchain_transport_lib::HttpRoleName::new(matching[0].clone()));
}
fn http_role_supports_request_kind(settings: &ksp_onchain_transport_lib::HttpTransportSettings, network: &str, role_name: &str, request_kind: &str) -> bool {
for endpoint in settings.endpoints() {
if !endpoint.enabled() || endpoint.cluster().as_str() != network {
continue;
}
for role in endpoint.roles() {
if !role.enabled() || role.role().as_str() != role_name {
continue;
}
if role.request_kinds().iter().any(|kind| return kind.is_wildcard() || kind.as_str() == request_kind) {
return true;
}
}
}
return false;
}
fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
let mut networks = std::collections::BTreeSet::<String>::new();
for endpoint in transport.http_settings().endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
if let std::option::Option::Some(settings) = transport.ws_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
if let std::option::Option::Some(settings) = transport.grpc_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
return networks.len() == 1 && networks.contains(network);
}
fn route_unavailable_error<T>() -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_UNAVAILABLE,
"Raw Transaction Ingest Desk selected route is no longer Configured in the current Start-time inventory",
));
}
fn ambiguous_resource_error<T>(message: &'static str) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED, message));
}
fn route_start_error<T>(message: &'static str, source: ksp_core_lib::Error) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED, message).with_source(source));
}
#[cfg(test)]
#[path = "../unit_tests/route_start.rs"]
mod tests;

View File

@@ -0,0 +1,214 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/splash.rs
// version: 1
//! Common splash settings and frontend event contracts for Raw Transaction Ingest 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 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_raw_transaction_ingest_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 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,318 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
// version: 9
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
/// Runs the Raw Transaction Ingest 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 = builder.manage(app_state);
builder = builder.plugin(tauri_plugin_tracing::Builder::new().build::<tauri::Wry>());
builder = configure_commands(builder);
builder = configure_window_events(builder);
builder = 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, "Raw Transaction Ingest Desk Tauri shell setup completed");
return std::result::Result::Ok(());
});
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 Raw Transaction Ingest 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),
),
};
}
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![
emit_frontend_log,
get_route_foundation,
get_route_inventory,
get_route_monitoring,
get_runtime_status,
splash_frontend_ready,
start_route,
stop_route,
validate_route_start
]);
}
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>();
let begin = state.begin_shutdown();
let begin = match begin {
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(),
"Raw Transaction Ingest Desk could not enter graceful shutdown; exiting after safe error projection"
);
app_handle.exit(1);
return;
},
};
if !begin {
return;
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
"Raw Transaction Ingest Desk main-window close requested; route Start admission closed and cooperative shutdown requested"
);
tauri::async_runtime::spawn(async move {
let state = app_handle.state::<crate::AppState>();
let shutdown = state.shutdown_routes().await;
match shutdown {
std::result::Result::Ok(()) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
"Raw Transaction Ingest Desk route and shared Store shutdown completed; exiting application"
);
app_handle.exit(0);
},
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(),
"Raw Transaction Ingest Desk bounded route shutdown failed; exiting application after bounded cleanup attempt"
);
app_handle.exit(1);
},
}
});
}
});
}
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(),
"Raw Transaction Ingest 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_route_foundation(state: tauri::State<'_, crate::AppState>) -> crate::RawIngestRouteFoundationDto {
return state.route_foundation();
}
#[tauri::command]
fn get_route_inventory(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RawIngestRouteInventoryDto, crate::CommandErrorDto> {
let result = state.route_inventory();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_route_inventory", crate::TRACING_DOMAIN_INVENTORY, &error)),
};
}
#[tauri::command]
fn get_route_monitoring(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>, crate::CommandErrorDto> {
let result = state.route_monitoring();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_route_monitoring", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &error)),
};
}
#[tauri::command]
fn get_runtime_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_runtime_status", crate::TRACING_DOMAIN_SHELL, &error)),
};
}
#[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)),
};
}
#[tauri::command]
async fn start_route(
app: tauri::AppHandle,
request: crate::RawIngestRouteStartRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::RawIngestRouteRuntimeDto, crate::CommandErrorDto> {
let launch = state.start_route(&request).await;
let launch = match launch {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(project_command_error("start_route", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &error));
},
};
let acknowledgement = launch.acknowledgement();
let monitoring_identity = launch.monitoring_identity();
let monitoring_source = launch.monitoring_source();
let monitoring_app = app.clone();
let monitoring_task = tauri::async_runtime::spawn(async move {
monitor_route_status(monitoring_app, monitoring_identity, monitoring_source).await;
});
std::mem::drop(monitoring_task);
let terminal_task = tauri::async_runtime::spawn(async move {
launch.monitor().await;
});
std::mem::drop(terminal_task);
return std::result::Result::Ok(acknowledgement);
}
const ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500;
async fn monitor_route_status(
app: tauri::AppHandle,
runtime: crate::RawIngestRouteRuntimeDto,
source: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshotSource,
) {
let mut snapshot = source.current();
loop {
let projected = crate::project_route_monitoring(&runtime, &snapshot);
match projected {
std::result::Result::Ok(status) => {
let main = app.get_webview_window("main");
if let std::option::Option::Some(window) = main {
let emitted = window.emit(crate::RAW_INGEST_ROUTE_STATUS_EVENT_NAME, status);
if emitted.is_err() {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
"Raw Transaction Ingest Desk could not emit latest-value route monitoring status"
);
}
}
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk could not project latest-value route monitoring status"
);
},
}
if snapshot.worker_snapshot().state().is_terminal() {
return;
}
let observed = snapshot.worker_snapshot().sequence();
snapshot = source.wait_for_change(observed).await;
if !snapshot.worker_snapshot().state().is_terminal() {
tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await;
snapshot = source.current();
}
}
}
#[tauri::command]
async fn stop_route(
request: crate::RawIngestRouteStopRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::RawIngestRouteRuntimeDto, crate::CommandErrorDto> {
let result = state.stop_route(&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("stop_route", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &error)),
};
}
#[tauri::command]
async fn validate_route_start(
request: crate::RawIngestRouteStartRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::RawIngestRouteStartValidationDto, crate::CommandErrorDto> {
let result = state.validate_route_start(&request);
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("validate_route_start", crate::TRACING_DOMAIN_ROUTE_START, &error)),
};
}

View File

@@ -0,0 +1,53 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tw_main.rs
// version: 1
//! Tauri-window helpers for the Raw Transaction Ingest Desk main window.
use tauri::Manager; // rust-rules: trait-import
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
pub(crate) 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, "Raw Transaction Ingest 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 Raw Transaction Ingest 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 Raw Transaction Ingest 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,173 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tw_splash.rs
// version: 1
//! Tauri-window lifecycle for the Raw Transaction Ingest Desk splash window.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
/// Crate-internal splash window label.
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
/// 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, "Raw Transaction Ingest 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 Raw Transaction Ingest 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 Raw Transaction Ingest 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 Raw Transaction Ingest Desk splash order")
.with_context("window_label", WINDOW_LABEL_SPLASH)
.with_context("action", action)
.with_source(error),
),
};
}

View File

@@ -0,0 +1,73 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Raw Transaction Ingest Desk",
"identifier": "com.sasedev.ksp-app-raw-transaction-ingest-desk",
"build": {
"beforeDevCommand": {
"script": "npm run dev",
"cwd": "."
},
"devUrl": "http://localhost:1440",
"beforeBuildCommand": {
"script": "npm run build",
"cwd": "."
},
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-raw-transaction-ingest-desk/dist"
},
"app": {
"windows": [
{
"label": "splash",
"url": "splash.html",
"title": "Chargement — KSP Raw Transaction Ingest Desk",
"width": 960,
"height": 637,
"resizable": false,
"decorations": false,
"transparent": true,
"center": true,
"alwaysOnTop": true
},
{
"label": "main",
"url": "main.html",
"title": "KSP Raw Transaction Ingest 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-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-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/schemas/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json",
"../../config/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

@@ -0,0 +1,98 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/dependency_boundary.rs
// version: 5
//! Dependency-boundary canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
#[test]
fn pre_008_manifest_adds_store_facade_and_worker_api_without_physical_backend_edge() {
let manifest = read_text(app_root().join("Cargo.toml").as_path());
for required in [
"ksp-config-lib",
"ksp-core-lib",
"ksp-logging-lib",
"ksp-onchain-transport-lib",
"ksp-store-lib",
"ksp-worker-api",
"ksp-worker-raw-transaction-ingest-lib",
"tauri-plugin-tracing",
] {
assert!(manifest.contains(required), "missing pre.008 dependency {required}");
}
for forbidden in ["ksp-store-api", "ksp-store-postgres-lib", "ksp-job-backfill-lib", "reqwest", "tokio-postgres", "tonic", "yellowstone-grpc"] {
assert!(!manifest.contains(forbidden), "pre.008 opens forbidden direct dependency {forbidden}");
}
}
#[test]
fn pre_007_route_start_reconstructs_exact_worker_sources_without_network_store_or_worker_launch() {
let source = read_text(app_root().join("src/route_start.rs").as_path());
for required in [
"HttpTransportPool::new",
"YellowstoneGrpcChannel::prepare",
"RawTransactionIngestYellowstoneSource::new",
"RawTransactionIngestStandardLogsSource::new",
"RawTransactionIngestStandardBlockSource::new",
"RawTransactionIngestHeliusTransactionSource::new",
"RawTransactionIngestHttpBlockPollingSource::new",
] {
assert!(source.contains(required), "missing exact resource constructor {required}");
}
for forbidden in [
"YellowstoneGrpcChannel::connect",
"SolanaStandardWsSession::connect",
"HeliusWsSession::connect",
"Store::open",
"RawTransactionIngestWorker::",
"start_with_runtime_resources",
"request_stop",
] {
assert!(!source.contains(forbidden), "pre.007 performs premature runtime operation {forbidden}");
}
}
#[test]
fn pre_008_route_runtime_owns_real_store_worker_start_stop_without_physical_backend_types() {
let source = read_text(app_root().join("src/route_runtime.rs").as_path());
for required in [
"Store::open",
"StoreHealthState::Ready",
"RawTransactionIngestWorker::start_with_runtime_resources",
"request_stop",
"wait_terminal",
".close().await",
"Arc::try_unwrap",
] {
assert!(source.contains(required), "missing mono-route runtime operation {required}");
}
for forbidden in ["tokio_postgres", "ksp_store_postgres_lib", "PostgresStore", "connection_uri", "endpoint_url"] {
assert!(!source.contains(forbidden), "pre.008 crosses physical or secret boundary {forbidden}");
}
}
#[test]
fn pre_010_monitoring_projects_existing_worker_snapshot_facade_without_new_backend_edges() {
let source = read_text(app_root().join("src/route_monitoring.rs").as_path());
for required in ["RawTransactionIngestSnapshot", "worker_snapshot()", "source_reconnect_total()", "source_replay_attempt_total()", "gaps()"] {
assert!(source.contains(required), "missing Worker snapshot facade marker {required}");
}
for forbidden in ["tokio_postgres", "ksp_store_postgres_lib", "reqwest", "tonic", "yellowstone_grpc_client"] {
assert!(!source.contains(forbidden), "monitoring opens forbidden backend edge {forbidden}");
}
}

View File

@@ -0,0 +1,343 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
// version: 13
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
#[test]
fn pre_005_vite_and_tauri_identity_use_reserved_ports_and_crate_local_hooks() {
let config = std::fs::read_to_string(app_root().join("tauri.conf.json"));
assert!(config.is_ok());
let config = match config {
std::result::Result::Ok(value) => serde_json::from_str::<serde_json::Value>(value.as_str()),
std::result::Result::Err(_) => return,
};
assert!(config.is_ok());
if let std::result::Result::Ok(config) = config {
assert_eq!(config["productName"], "KSP Raw Transaction Ingest Desk");
assert_eq!(config["identifier"], "com.sasedev.ksp-app-raw-transaction-ingest-desk");
assert_eq!(config["build"]["devUrl"], "http://localhost:1440");
}
let vite = read_text(app_root().join("vite.config.ts").as_path());
assert!(vite.contains("port: 1440"));
assert!(vite.contains("strictPort: true"));
assert!(vite.contains("port: 1441"));
}
#[test]
fn pre_005_rust_package_is_mixed_lib_bin_and_workspace_member() {
let manifest = read_text(app_root().join("Cargo.toml").as_path());
assert!(manifest.contains("name = \"ksp-app-raw-transaction-ingest-desk\""));
assert!(manifest.contains("name = \"ksp_app_raw_transaction_ingest_desk_lib\""));
assert!(manifest.contains("crate-type = [\"staticlib\", \"cdylib\", \"rlib\"]"));
let workspace = read_text(app_root().join("../../Cargo.toml").as_path());
assert!(workspace.contains("crates/ksp-app-raw-transaction-ingest-desk"));
}
#[test]
fn pre_005_package_dependencies_match_current_desk_baseline() {
let package = std::fs::read_to_string(app_root().join("package.json"));
assert!(package.is_ok());
let package = match package {
std::result::Result::Ok(value) => serde_json::from_str::<serde_json::Value>(value.as_str()),
std::result::Result::Err(_) => return,
};
assert!(package.is_ok());
if let std::result::Result::Ok(package) = package {
let dependencies = &package["dependencies"];
for dependency in
["@fltsci/tauri-plugin-tracing", "@fortawesome/fontawesome-free", "@tauri-apps/api", "bootstrap", "resize-observer-polyfill", "simplebar"]
{
assert!(dependencies.get(dependency).is_some(), "missing frontend dependency {dependency}");
}
assert!(dependencies.get("datatables.net-bs5").is_none());
let development = &package["devDependencies"];
for dependency in ["@tauri-apps/cli", "@types/bootstrap", "@types/node", "sass-embedded", "typescript", "vite"] {
assert!(development.get(dependency).is_some(), "missing frontend development dependency {dependency}");
}
}
}
#[test]
fn pre_005_packaging_contains_current_sixteen_config_resources() {
let config = std::fs::read_to_string(app_root().join("tauri.conf.json"));
assert!(config.is_ok());
let config = match config {
std::result::Result::Ok(value) => serde_json::from_str::<serde_json::Value>(value.as_str()),
std::result::Result::Err(_) => return,
};
assert!(config.is_ok());
if let std::result::Result::Ok(config) = config {
let resources = config["bundle"]["resources"].as_object();
assert!(resources.is_some());
if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 16);
assert!(resources.contains_key("../../config/composite.ksp-app-raw-transaction-ingest-desk.json"));
}
}
}
#[test]
fn pre_005_splash_assets_match_existing_ksp_desk_template_byte_exact() {
let reference = app_root().join("../ksp-app-backfill-desk");
for relative in ["icons/favicon.ico", "icons/favicon.png", "frontend/fonts/DOS_Amazigh.ttf", "frontend/imgs/logo.png", "frontend/imgs/splash.png"] {
let current = std::fs::read(app_root().join(relative));
let expected = std::fs::read(reference.join(relative));
assert!(current.is_ok());
assert!(expected.is_ok());
if let (std::result::Result::Ok(current), std::result::Result::Ok(expected)) = (current, expected) {
assert_eq!(current, expected, "binary template asset differs: {relative}");
}
}
}
#[test]
fn pre_006_route_inventory_is_backend_owned_and_keeps_exact_five_ids_and_seven_states() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for route in ["YellowstoneHydrated", "StandardLogsHydrated", "StandardBlockDirect", "HeliusTransactionHydrated", "HttpBlockPolling"] {
assert!(dto.contains(route));
}
for state in ["Unavailable", "Configured", "Starting", "Running", "Stopping", "Stopped", "Faulted"] {
assert!(dto.contains(state));
}
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("get_route_foundation"));
assert!(tauri.contains("get_route_inventory"));
for forbidden in ["start_worker", "stop_worker", "RawTransactionIngestWorker", "Store::open"] {
assert!(!tauri.contains(forbidden), "scaffold Tauri surface contains premature runtime marker {forbidden}");
}
}
#[test]
fn pre_005_fix_002_shell_reuses_common_light_template_and_bounds_route_foundation() {
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in [
"<header class=\"app-header\">",
"navbar h-100 py-0 bg-light text-dark",
"col-auto app-sidebar border-end bg-body-tertiary",
"col app-content app-scrollable h-100",
"card app-shell-card mx-auto shadow-sm border-0",
"id=\"routeFoundation\" class=\"row g-3 app-route-grid\"",
] {
assert!(html.contains(required), "missing common shell marker: {required}");
}
for forbidden in ["app-header bg-dark", "app-sidebar bg-dark text-light", "app-content flex-grow-1"] {
assert!(!html.contains(forbidden), "obsolete shell marker remains: {forbidden}");
}
let styles = read_text(app_root().join("frontend/sass/_app.scss").as_path());
for required in [
".app-scrollable {",
".app-shell-card {",
".app-route-grid {",
".app-route-grid > * {",
"min-width: 0;",
"max-width: 100%;",
"overflow: hidden;",
"overflow-wrap: anywhere;",
] {
assert!(styles.contains(required), "missing bounded layout marker: {required}");
}
}
#[test]
fn pre_006_frontend_projects_profile_network_generation_and_safe_route_reasons() {
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in ["id=\"routeProfile\"", "id=\"routeNetwork\"", "id=\"routeInventoryGeneration\"", "id=\"routeFoundation\""] {
assert!(html.contains(required), "missing route inventory UI marker: {required}");
}
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in ["get_route_inventory", "missing_ws_block_capability", "missing_required_secret", "Config route inventory"] {
assert!(main.contains(required), "missing route inventory frontend marker: {required}");
}
for forbidden in ["endpointUrl", "connectionUri", "apiKey", "sourceKey", "x-token"] {
assert!(!main.contains(forbidden), "frontend exposes forbidden physical marker: {forbidden}");
}
}
#[test]
fn pre_007_start_preflight_is_backend_owned_generation_bound_and_does_not_launch_worker() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for required in
["RawIngestCommitment", "RawIngestRouteStartRequestDto", "inventory_generation", "profile_id", "route_id", "RawIngestRouteStartValidationDto"]
{
assert!(dto.contains(required), "missing pre.007 Start DTO marker: {required}");
}
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("validate_route_start"));
for forbidden in ["start_worker", "stop_worker", "start_with_runtime_resources", "Store::open"] {
assert!(!tauri.contains(forbidden), "pre.007 Tauri surface contains premature runtime marker {forbidden}");
}
}
#[test]
fn pre_009_start_stop_controls_are_multi_route_targeted_and_backend_owned() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in ["start_route", "stop_route", "RawIngestRouteStopRequestDto", "launch.monitor().await"] {
assert!(tauri.contains(required), "missing pre.009 Tauri runtime marker {required}");
}
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in ["id=\"routeCommitment\"", "id=\"routeRuntimeState\"", "id=\"routeRuntimeIdentity\"", "Runtime multi-route / Store partagé"] {
assert!(html.contains(required), "missing pre.009 frontend runtime marker {required}");
}
assert!(!html.contains("id=\"stopRoute\""));
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in ["activeRuntimes", "targeted route Stop requested", "start_route", "stop_route", "RawIngestRouteStopRequestDto"] {
assert!(main.contains(required), "missing pre.009 frontend control marker {required}");
}
}
#[test]
fn pre_010_runtime_monitoring_is_backend_owned_event_driven_and_resynchronizable() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in [
"get_route_monitoring",
"monitor_route_status",
"window.emit(crate::RAW_INGEST_ROUTE_STATUS_EVENT_NAME, status)",
"source.wait_for_change(observed).await",
] {
assert!(tauri.contains(required), "missing pre.010 monitoring bridge marker {required}");
}
let state = read_text(app_root().join("src/app_state.rs").as_path());
assert!(state.contains("route_monitoring(&self)"));
assert!(state.contains("pre.010-runtime-monitoring"));
}
#[test]
fn pre_011_frontend_consumes_latest_value_monitoring_and_renders_full_route_supervision() {
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"listen<RawIngestRouteMonitoringDto>",
"ksp-raw-ingest-route-status",
"get_route_monitoring",
"routeMonitoring",
"syncRouteMonitoring",
"persistedTotal",
"sourceActive",
"openGapCount",
"repairTransactionHydrationTotal",
"route supervision selected",
"route monitoring resynchronization requested",
] {
assert!(main.contains(required), "missing pre.011 frontend monitoring marker {required}");
}
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in [
"id=\"refreshRouteMonitoring\"",
"id=\"routeMonitoringDetail\"",
"id=\"monitorPersistedTotal\"",
"id=\"monitorSourceCounts\"",
"id=\"monitorContinuityFrontier\"",
"id=\"monitorGapList\"",
"id=\"headerStore\"",
] {
assert!(html.contains(required), "missing pre.011 supervision UI marker {required}");
}
let styles = read_text(app_root().join("frontend/sass/_app.scss").as_path());
for required in [".app-route-monitoring-summary {", ".app-monitoring-detail {", ".app-monitoring-list dd", ".app-monitoring-gaps"] {
assert!(styles.contains(required), "missing pre.011 bounded supervision style {required}");
}
}
#[test]
fn pre_011_fix_001_monitoring_bridge_is_bounded_and_steady_updates_preserve_route_controls() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in [
"ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500",
"tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await",
"snapshot = source.current()",
] {
assert!(tauri.contains(required), "missing pre.011-fix.001 bounded relay marker {required}");
}
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"const routeCards = new Map<string, HTMLElement>()",
"renderRouteMonitoringCard",
"const lifecycleChanged = previous === null || previous.state !== status.state",
"if (lifecycleChanged || !renderRouteMonitoringCard(status))",
] {
assert!(main.contains(required), "missing pre.011-fix.001 stable-control marker {required}");
}
}
#[test]
fn pre_012_window_shutdown_and_start_stop_races_are_bounded_backend_owned() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in [
"configure_window_events(builder)",
"tauri::WindowEvent::CloseRequested",
"api.prevent_close()",
"state.begin_shutdown()",
"state.shutdown_routes().await",
"app_handle.exit(0)",
"app_handle.exit(1)",
] {
assert!(tauri.contains(required), "missing pre.012 bounded shutdown marker {required}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
for required in [
"shutting_down: bool",
"stop_requested: bool",
"ROUTE_SHUTDOWN_TIMEOUT",
"ensure_start_admission_open",
"shutdown_and_wait",
"RouteStopTarget::Starting",
"*stop_requested = true",
"handle.request_stop()",
] {
assert!(runtime.contains(required), "missing pre.012 route-race marker {required}");
}
let app_state = read_text(app_root().join("src/app_state.rs").as_path());
assert!(app_state.contains("let prepared = {"));
assert!(app_state.contains("let generation = self.inventory_generation.lock()"));
assert!(!app_state.contains("std::mem::drop(generation)"));
assert!(app_state.contains("self.route_runtime.ensure_start_admission_open()"));
}
#[test]
fn pre_012_fix_002_terminal_fault_logging_keeps_safe_route_identity_and_worker_cause_counters() {
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
let start = runtime.find("pub(crate) async fn monitor(self)");
assert!(start.is_some(), "missing RouteRuntimeLaunch::monitor");
let std::option::Option::Some(start) = start else {
return;
};
let remainder = &runtime[start..];
let end = remainder.find("/// Starts one exact route Worker");
assert!(end.is_some(), "missing route runtime monitor end marker");
let std::option::Option::Some(end) = end else {
return;
};
let monitor = &remainder[..end];
for required in [
"worker_health = worker.health().code()",
"fault_domain = snapshot_fault_domain",
"fault_code = snapshot_fault_code",
"source_failure_total = snapshot.source_failure_total()",
"store_failure_total = snapshot.store_failure_total()",
"content_conflict_total = snapshot.content_conflict_total()",
"open_gap_count = snapshot.open_gap_count()",
"future_target_coverage = snapshot.future_target_coverage()",
"failed_source_losses_reconciled = snapshot.failed_source_losses_reconciled()",
] {
assert!(monitor.contains(required), "missing pre.012-fix.002 terminal diagnostic marker {required}");
}
for forbidden in ["endpoint_url", "connection_uri", "api_key", "source_key", "worker_id", "signature", "canonical_bytes", "wire_bytes"] {
assert!(!monitor.contains(forbidden), "terminal diagnostic surface exposes forbidden marker {forbidden}");
}
}

View File

@@ -0,0 +1,225 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
// version: 9
//! Desktop security canaries for the Raw Transaction Ingest Desk scaffold.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
fn collect_typescript(root: &std::path::Path, files: &mut std::vec::Vec<std::path::PathBuf>) {
let entries = std::fs::read_dir(root);
if let std::result::Result::Ok(entries) = entries {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_typescript(path.as_path(), files);
} else if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("ts") {
files.push(path);
}
}
}
}
#[test]
fn pre_005_capability_surface_is_core_plus_tracing_only() {
let capability = read_text(app_root().join("capabilities/default.json").as_path());
assert!(capability.contains("\"core:default\""));
assert!(capability.contains("\"tracing:default\""));
for forbidden in ["dialog:", "fs:", "http:", "shell:"] {
assert!(!capability.contains(forbidden));
}
}
#[test]
fn pre_005_frontend_has_no_network_persistence_or_native_dialog_surface() {
let mut files = std::vec::Vec::new();
collect_typescript(app_root().join("frontend/ts").as_path(), &mut files);
assert!(!files.is_empty());
for path in files {
let source = read_text(path.as_path());
for forbidden in ["fetch(", "XMLHttpRequest", "localStorage", "sessionStorage", "window.alert(", "window.confirm(", "window.prompt("] {
assert!(!source.contains(forbidden), "{} contains forbidden frontend surface {forbidden}", path.display());
}
}
}
#[test]
fn pre_006_frontend_tracing_is_control_inventory_and_command_oriented_only() {
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
assert!(main.contains("frontend control clicked"));
assert!(main.contains("navigation tab clicked"));
assert!(main.contains("runtime status refresh requested"));
assert!(main.contains("Config route inventory refresh requested"));
assert!(main.contains("logical network selected"));
let invoke = read_text(app_root().join("frontend/ts/invoke.ts").as_path());
assert!(invoke.contains("Frontend IPC command requested"));
for forbidden in ["apiKey", "authorization", "connectionUri", "endpointUrl", "sourceKey", "rawTransaction", "transactionPayload", "x-token"] {
assert!(!main.contains(forbidden), "frontend contains forbidden sensitive marker {forbidden}");
}
}
#[test]
fn pre_006_route_inventory_dto_surface_excludes_endpoint_secret_and_physical_store_fields() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for required in ["RawIngestRouteInventoryDto", "RawIngestProfileInventoryDto", "RawIngestRouteUnavailableReason"] {
assert!(dto.contains(required));
}
for forbidden in ["endpoint_name", "endpoint_url", "api_key", "connection_uri", "source_key", "secret_metadata", "worker_handle"] {
assert!(!dto.contains(forbidden), "route DTO contains forbidden field marker {forbidden}");
}
}
#[test]
fn pre_007_start_request_and_acknowledgement_are_logical_only_and_resource_free() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for required in ["profile_id", "route_id", "inventory_generation", "commitment", "network"] {
assert!(dto.contains(required), "missing safe Start field marker {required}");
}
for forbidden in [
"endpoint_name",
"endpoint_url",
"api_key",
"connection_uri",
"source_key",
"secret_metadata",
"worker_handle",
"http_pool",
"ws_endpoint",
"yellowstone_channel",
] {
assert!(!dto.contains(forbidden), "Start DTO exposes forbidden physical marker {forbidden}");
}
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
assert!(route_start.contains("ERROR_CODE_ROUTE_START_STALE_INVENTORY"));
assert!(route_start.contains("build_route_inventory_with_environment"));
assert!(route_start.contains("resolve_store_config_profile"));
assert!(!route_start.contains("with_context(\"endpoint\""));
assert!(!route_start.contains("with_context(\"provider\""));
}
#[test]
fn pre_008_runtime_acknowledgement_and_frontend_tracing_remain_secret_and_handle_free() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
assert!(dto.contains("RawIngestRouteRuntimeDto"));
for forbidden in ["worker_id", "worker_handle", "endpoint_url", "connection_uri", "api_key", "source_key"] {
assert!(!dto.contains(forbidden), "runtime DTO exposes forbidden marker {forbidden}");
}
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in ["profileId", "routeId", "commitment", "state"] {
assert!(main.contains(required), "missing safe runtime trace marker {required}");
}
for forbidden in ["endpointUrl", "connectionUri", "workerId", "sourceKey", "x-token", "rawTransaction"] {
assert!(!main.contains(forbidden), "frontend runtime trace exposes forbidden marker {forbidden}");
}
}
#[test]
fn pre_010_monitoring_projection_is_source_neutral_payload_free_and_javascript_exact() {
let monitoring = read_text(app_root().join("src/route_monitoring.rs").as_path());
for required in [
"sequence: String",
"processing_frontier_slot: std::option::Option<String>",
"continuity_frontier_slot: std::option::Option<String>",
"fault_domain",
"fault_code",
"gaps: std::vec::Vec<crate::RawIngestRouteGapDto>",
] {
assert!(monitoring.contains(required), "missing pre.010 safe monitoring marker {required}");
}
for forbidden in ["endpoint_url", "connection_uri", "api_key", "source_key", "worker_id", "x-token"] {
assert!(!monitoring.contains(forbidden), "monitoring projection exposes forbidden physical marker {forbidden}");
}
let dto_start = monitoring.find("pub(crate) struct RawIngestRouteMonitoringDto");
assert!(dto_start.is_some(), "monitoring DTO declaration is missing");
let dto_start = match dto_start {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let dto_end = monitoring[dto_start..].find("/// Projects one complete concrete Worker snapshot");
assert!(dto_end.is_some(), "monitoring DTO declaration boundary is missing");
let dto_end = match dto_end {
std::option::Option::Some(value) => dto_start + value,
std::option::Option::None => return,
};
let dto_surface = &monitoring[dto_start..dto_end];
for forbidden in ["raw_transaction", "signature", "payload", "canonical_bytes", "wire_bytes", "transaction_bytes"] {
assert!(!dto_surface.contains(forbidden), "monitoring DTO exposes forbidden RAW marker {forbidden}");
}
}
#[test]
fn pre_011_frontend_monitoring_tracing_is_source_neutral_and_does_not_log_raw_material() {
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"route supervision selected",
"route monitoring latest value applied",
"route monitoring resynchronization requested",
"route monitoring refresh button clicked",
"commitment selection changed",
"routeId",
"sequence",
"statusCount",
] {
assert!(main.contains(required), "missing pre.011 safe tracing marker {required}");
}
for forbidden in [
"endpointUrl",
"connectionUri",
"apiKey",
"sourceKey",
"workerId",
"x-token",
"rawTransaction",
"transactionPayload",
"canonicalBytes",
"wireBytes",
"transactionBytes",
"signature",
] {
assert!(!main.contains(forbidden), "pre.011 frontend exposes forbidden marker {forbidden}");
}
assert!(!main.contains("frontendTrace(\"main\", \"Raw Transaction Ingest Desk route monitoring latest value applied\", status"));
assert!(!main.contains("frontendDebug(\"main\", \"Raw Transaction Ingest Desk route monitoring resynchronization completed\", statuses"));
}
#[test]
fn pre_012_hostile_ipc_is_shape_strict_bounded_and_safely_projected() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
assert!(dto.matches("deny_unknown_fields").count() >= 2);
assert!(dto.contains("RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256"));
assert!(dto.contains("validate_logical_identity"));
assert!(dto.contains("ERROR_CODE_ROUTE_REQUEST_INVALID"));
let frontend_logging = read_text(app_root().join("src/frontend_logging.rs").as_path());
for required in [
"deny_unknown_fields",
"FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192",
"FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16",
"payload.message.chars().any(char::is_control)",
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
"eq_ignore_ascii_case",
] {
assert!(frontend_logging.contains(required), "missing pre.012 frontend IPC guard {required}");
}
assert!(!frontend_logging.contains("to_ascii_lowercase()"));
let frontend = read_text(app_root().join("frontend/ts/frontend_log.ts").as_path());
assert!(frontend.contains("FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024"));
assert!(frontend.contains("boundMessage(message)"));
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("error_domain = error.code().domain()"));
assert!(tauri.contains("error_code = error.code().code()"));
assert!(!tauri.contains("error = ?error"));
}

View File

@@ -0,0 +1,14 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/public_api.rs
// version: 1
//! Public API checks for the KSP Raw Transaction Ingest desktop application library.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
#[test]
fn run_entry_point_is_available_from_crate_root() {
let run_entry_point: fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()> = ksp_app_raw_transaction_ingest_desk_lib::run;
assert_eq!(std::mem::size_of_val(&run_entry_point), std::mem::size_of::<fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()>>());
}

View File

@@ -0,0 +1,226 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
// version: 12
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
#[test]
fn pre_010_production_module_inventory_adds_only_route_monitoring_to_pre_009_surface() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
let expected = [
"mod app_state;",
"mod bootstrap;",
"mod constants;",
"mod dto_common;",
"mod dto_route;",
"mod errors;",
"mod frontend_logging;",
"mod logging_runtime;",
"mod route_inventory;",
"mod route_monitoring;",
"mod route_runtime;",
"mod route_start;",
"mod splash;",
"mod tauri;",
"mod tw_main;",
"mod tw_splash;",
];
assert_eq!(lib.matches("mod ").count(), expected.len());
for module in expected {
assert!(lib.contains(module));
}
}
#[test]
fn pre_008_public_surface_still_exposes_only_application_run_entry_point() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
let public_reexports = lib.lines().filter(|line| return line.starts_with("pub use ")).collect::<std::vec::Vec<_>>();
assert_eq!(public_reexports, vec!["pub use self::tauri::run;"]);
}
#[test]
fn pre_008_keeps_five_exact_source_reconstructions_separate_from_store_worker_lifecycle() {
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
for required in [
"RawTransactionIngestYellowstoneSource::new",
"RawTransactionIngestStandardLogsSource::new",
"RawTransactionIngestStandardBlockSource::new",
"RawTransactionIngestHeliusTransactionSource::new",
"RawTransactionIngestHttpBlockPollingSource::new",
"RawTransactionIngestRuntimeResources",
] {
assert!(route_start.contains(required), "missing pre.008 exact Worker resource marker {required}");
}
for forbidden in ["Store::open", "start_with_runtime_resources", "request_stop", "wait_terminal"] {
assert!(!route_start.contains(forbidden), "route preparation owns runtime lifecycle marker {forbidden}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
for required in ["Store::open", "start_with_runtime_resources", "request_stop", "wait_terminal", "StoreHealthState::Ready", "close_store_arc"] {
assert!(runtime.contains(required), "missing pre.008 lifecycle marker {required}");
}
}
#[test]
fn pre_007_start_revalidation_reuses_inventory_and_requires_profile_network_identity() {
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
for required in [
"current_generation == 0",
"request.inventory_generation != current_generation",
"build_route_inventory_with_environment",
"request.profile_id",
"resolve_store_config_profile",
"store.settings().network().as_str() != network",
"transport_matches_network",
] {
assert!(route_start.contains(required), "missing pre.007 revalidation marker {required}");
}
}
#[test]
fn pre_009_multi_route_runtime_keeps_independent_workers_on_one_same_network_store() {
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
for required in [
"routes: std::vec::Vec<RouteRuntimeSlot>",
"store: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>",
"ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH",
"already owns one Worker for the selected logical route",
"shared_store(prepared.network.as_str())",
"self.runtime_state.finish(self.token, terminal, monitoring)",
"Arc::try_unwrap",
] {
assert!(runtime.contains(required), "missing pre.009 shared-Store marker {required}");
}
assert!(!runtime.contains("RouteRuntimeSlot::Idle"));
}
#[test]
fn pre_009_targeted_stop_is_route_scoped_and_late_terminal_safe() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
assert!(dto.contains("RawIngestRouteStopRequestDto"));
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
assert!(runtime.contains("stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto)"));
assert!(runtime.contains("terminal.profile_id == request.profile_id && terminal.route_id == request.route_id"));
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("request: crate::RawIngestRouteStopRequestDto"));
}
#[test]
fn pre_009_yellowstone_uses_block_subscription_and_one_get_block_path_instead_of_per_transaction_hydration() {
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
let start = route_start.find("fn prepare_yellowstone(");
let end = route_start.find("fn prepare_http_polling(");
assert!(start.is_some());
assert!(end.is_some());
let yellowstone = match (start, end) {
(std::option::Option::Some(start), std::option::Option::Some(end)) if start < end => &route_start[start..end],
_ => "",
};
assert!(yellowstone.contains("insert_block_filter"));
assert!(yellowstone.contains("set_include_transactions(std::option::Option::Some(false))"));
assert!(yellowstone.contains("single_http_role(transport.http_settings(), network, &[\"getBlock\"])"));
assert!(!yellowstone.contains("insert_transaction_filter"));
let workspace = read_text(app_root().join("../ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs").as_path());
assert!(workspace.contains("RawTransactionIngestYellowstoneMode::BlockHydration"));
assert!(workspace.contains("fetch_yellowstone_block_ingresses"));
assert!(workspace.contains("get_block_observed"));
assert!(workspace.contains("RAW_TRANSACTION_INGEST_YELLOWSTONE_BLOCK_RECONCILIATION_ATTEMPTS"));
assert!(workspace.contains("stop_receiver.changed()"));
}
#[test]
fn pre_010_latest_value_monitoring_reuses_worker_snapshot_source_and_tauri_event_bridge() {
let monitoring = read_text(app_root().join("src/route_monitoring.rs").as_path());
for required in [
"RawIngestRouteMonitoringDto",
"RawIngestRouteGapDto",
"project_route_monitoring",
"continuity_frontier_slot",
"future_target_coverage",
"source_reconnect_total",
"source_replay_attempt_total",
"repaired_gap_total",
"backpressure_wait_total",
] {
assert!(monitoring.contains(required), "missing pre.010 monitoring marker {required}");
}
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in ["get_route_monitoring", "RAW_INGEST_ROUTE_STATUS_EVENT_NAME", "monitor_route_status", "source.wait_for_change(observed).await"] {
assert!(tauri.contains(required), "missing pre.010 event/resync marker {required}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
assert!(runtime.contains("monitoring_statuses"));
assert!(runtime.contains("last_monitoring"));
}
#[test]
fn pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
assert_eq!(lib.matches("mod ").count(), 16);
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"RawIngestRouteMonitoringDto",
"bindRouteMonitoring",
"renderMonitoringDetail",
"syncRouteMonitoring(\"startup\")",
"syncRouteMonitoring(\"start\")",
"syncRouteMonitoring(\"stop\")",
] {
assert!(main.contains(required), "missing pre.011 frontend completeness marker {required}");
}
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in ["Supervision route", "Pipeline / persistence", "Sources / continuité / repair", "Gaps courants / récents"] {
assert!(html.contains(required), "missing pre.011 operator supervision surface {required}");
}
}
#[test]
fn pre_011_fix_001_preserves_latest_value_contract_without_backend_module_growth() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
assert_eq!(lib.matches("mod ").count(), 16);
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("source.wait_for_change(observed).await"));
assert!(tauri.contains("ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500"));
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
assert!(main.contains("renderRouteMonitoringCard"));
assert!(main.contains("renderSelectedProfile"));
}
#[test]
fn pre_012_race_shutdown_and_ipc_hardening_adds_no_production_module_or_lower_layer_growth() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
assert_eq!(lib.matches("mod ").count(), 16);
for required in [
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
"ERROR_CODE_ROUTE_REQUEST_INVALID",
"ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN",
"ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED",
] {
assert!(lib.contains(required), "missing pre.012 crate-root error export {required}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
assert!(runtime.contains("stop_requested: bool"));
assert!(runtime.contains("RouteStopTarget::Starting"));
assert!(runtime.contains("cancelled_start_terminal"));
assert!(runtime.contains("inner.routes.is_empty()"));
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
assert!(route_start.contains("build_route_inventory_with_environment"));
assert!(route_start.contains("request.validate_logical_identity()"));
let worker_manifest = read_text(app_root().join("../ksp-worker-raw-transaction-ingest-lib/Cargo.toml").as_path());
assert!(!worker_manifest.contains("ksp-app-raw-transaction-ingest-desk"));
}

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,14 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_common.rs
// version: 1
#[test]
fn command_error_projection_excludes_arbitrary_context_and_source_values() {
let source = std::io::Error::other("secret-source");
let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failed"), "safe message")
.with_context("secret", "hidden")
.with_source(source);
let dto = crate::CommandErrorDto::from_error(&error);
assert_eq!(dto.domain, "test");
assert_eq!(dto.code, "failed");
assert_eq!(dto.message, "safe message");
}

View File

@@ -0,0 +1,124 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
// version: 5
#[test]
fn pre_005_route_ids_are_exact_and_stably_ordered() {
let foundation = crate::RawIngestRouteFoundationDto::scaffold();
assert_eq!(foundation.route_ids.len(), 5);
let value = serde_json::to_value(foundation.route_ids);
assert!(value.is_ok());
if let std::result::Result::Ok(value) = value {
assert_eq!(
value,
serde_json::json!(["yellowstone-hydrated", "standard-logs-hydrated", "standard-block-direct", "helius-transaction-hydrated", "http-block-polling"])
);
}
}
#[test]
fn pre_005_route_states_are_exact_without_runtime_claims() {
let foundation = crate::RawIngestRouteFoundationDto::scaffold();
assert_eq!(foundation.states.len(), 7);
let value = serde_json::to_value(foundation.states);
assert!(value.is_ok());
if let std::result::Result::Ok(value) = value {
assert_eq!(value, serde_json::json!(["unavailable", "configured", "starting", "running", "stopping", "stopped", "faulted"]));
}
}
#[test]
fn pre_006_unavailable_reason_codes_are_exact_and_safe() {
let values = [
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
crate::RawIngestRouteUnavailableReason::MissingHttpGetBlock,
crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction,
crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan,
crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability,
crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability,
crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability,
crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc,
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret,
];
let serialized = serde_json::to_value(values);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
assert_eq!(
serialized,
serde_json::json!([
"profile_unresolved",
"network_mismatch",
"missing_http_get_block",
"missing_http_get_transaction",
"missing_http_block_scan",
"missing_ws_logs_capability",
"missing_ws_block_capability",
"missing_helius_transaction_capability",
"missing_yellowstone_grpc",
"missing_required_secret"
])
);
}
}
#[test]
fn pre_008_runtime_acknowledgement_serializes_only_safe_logical_identity_and_state() {
let dto = crate::RawIngestRouteRuntimeDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 7,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::StandardBlockDirect,
state: crate::RawIngestRouteState::Running,
};
let value = serde_json::to_value(dto);
assert!(value.is_ok());
if let std::result::Result::Ok(value) = value {
assert_eq!(
value,
serde_json::json!({
"commitment": "confirmed",
"inventoryGeneration": 7,
"network": "mainnet",
"profileId": "mainnet",
"routeId": "standard-block-direct",
"state": "running"
})
);
let encoded = value.to_string();
for forbidden in ["endpoint", "provider", "connectionUri", "sourceKey", "workerId", "payload"] {
assert!(!encoded.contains(forbidden));
}
}
}
#[test]
fn pre_012_route_requests_reject_unknown_fields_and_bound_free_form_profile_identity() {
let start = serde_json::from_value::<crate::RawIngestRouteStartRequestDto>(serde_json::json!({
"commitment": "confirmed",
"inventoryGeneration": 1,
"profileId": "mainnet",
"routeId": "http-block-polling",
"endpointUrl": "https://forbidden.invalid"
}));
assert!(start.is_err());
let stop = serde_json::from_value::<crate::RawIngestRouteStopRequestDto>(serde_json::json!({
"profileId": "mainnet",
"routeId": "http-block-polling",
"workerId": "forbidden"
}));
assert!(stop.is_err());
let oversized = crate::RawIngestRouteStartRequestDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 1,
profile_id: "x".repeat(super::RAW_INGEST_PROFILE_ID_MAX_BYTES + 1),
route_id: crate::RawIngestRouteId::HttpBlockPolling,
};
let oversized = oversized.validate_logical_identity();
assert!(oversized.is_err());
if let std::result::Result::Err(error) = oversized {
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_REQUEST_INVALID);
}
let control = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet\n".to_owned(), route_id: crate::RawIngestRouteId::HttpBlockPolling };
assert!(control.validate_logical_identity().is_err());
}

View File

@@ -0,0 +1,63 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/frontend_logging.rs
// version: 2
#[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-raw-transaction-ingest-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);
}
#[test]
fn pre_012_frontend_log_ipc_rejects_unknown_fields_and_unbounded_or_control_messages() {
let unknown = serde_json::from_value::<crate::FrontendLogPayloadDto>(serde_json::json!({
"level": "info",
"targetId": "main",
"message": "safe",
"endpointUrl": "https://forbidden.invalid"
}));
assert!(unknown.is_err());
let oversized = crate::FrontendLogPayloadDto {
level: "info".to_owned(),
target_id: "main".to_owned(),
message: "x".repeat(super::FRONTEND_LOG_MESSAGE_MAX_BYTES + 1),
};
let result = super::validate_payload_shape(&oversized);
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID);
}
let nul = crate::FrontendLogPayloadDto { level: "info".to_owned(), target_id: "main".to_owned(), message: "safe\0hostile".to_owned() };
assert!(super::validate_payload_shape(&nul).is_err());
}

View File

@@ -0,0 +1,215 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs
// version: 5
#[test]
fn pre_006_route_requirement_projection_is_fail_closed_and_ordered() {
let route = super::route_with_requirements(
crate::RawIngestRouteId::StandardLogsHydrated,
"devnet",
&[
(false, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
(false, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
);
assert!(!route.selectable);
assert_eq!(route.state, crate::RawIngestRouteState::Unavailable);
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability));
assert_eq!(route.network.as_deref(), std::option::Option::Some("devnet"));
let configured = super::route_with_requirements(
crate::RawIngestRouteId::HttpBlockPolling,
"mainnet",
&[(true, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
);
assert!(configured.selectable);
assert_eq!(configured.state, crate::RawIngestRouteState::Configured);
assert_eq!(configured.reason, std::option::Option::None);
}
#[test]
fn pre_006_unavailable_profile_projects_all_five_routes_without_physical_material() {
let profile =
super::unavailable_profile("mainnet", false, std::option::Option::Some("mainnet"), crate::RawIngestRouteUnavailableReason::MissingRequiredSecret);
assert_eq!(profile.routes.len(), 5);
assert_eq!(profile.network.as_deref(), std::option::Option::Some("mainnet"));
for route in profile.routes {
assert!(!route.selectable);
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret));
}
}
#[test]
fn pre_006_fix_003_network_profiles_aggregate_same_network_capabilities_and_omit_undeclared_provider_routes() {
let management = committed_management();
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = ksp_config_lib::ConfigEnvironment::load();
assert!(environment.is_ok());
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let inventory = crate::build_route_inventory_with_environment(&management, &environment, 7);
assert!(inventory.is_ok());
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(inventory.generation, 7);
assert_eq!(inventory.default_profile, "devnet");
assert_eq!(
inventory.profiles.iter().map(|profile| return profile.profile_id.as_str()).collect::<std::vec::Vec<_>>(),
vec!["devnet", "mainnet", "testnet"]
);
let devnet = profile(&inventory, "devnet");
assert!(devnet.is_some());
if let std::option::Option::Some(devnet) = devnet {
assert!(devnet.is_default);
assert_eq!(devnet.network.as_deref(), std::option::Option::Some("devnet"));
assert_optional_secret_route(devnet, crate::RawIngestRouteId::YellowstoneHydrated);
assert_route(devnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
assert_route(devnet, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
assert_optional_secret_route(devnet, crate::RawIngestRouteId::HeliusTransactionHydrated);
assert_route(devnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
}
let mainnet = profile(&inventory, "mainnet");
assert!(mainnet.is_some());
if let std::option::Option::Some(mainnet) = mainnet {
assert_eq!(mainnet.network.as_deref(), std::option::Option::Some("mainnet"));
assert_optional_secret_route(mainnet, crate::RawIngestRouteId::YellowstoneHydrated);
assert_route(mainnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
assert_route(mainnet, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
assert_optional_secret_route(mainnet, crate::RawIngestRouteId::HeliusTransactionHydrated);
assert_route(mainnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
}
let testnet = profile(&inventory, "testnet");
assert!(testnet.is_some());
if let std::option::Option::Some(testnet) = testnet {
assert_eq!(testnet.network.as_deref(), std::option::Option::Some("testnet"));
assert_optional_secret_route(testnet, crate::RawIngestRouteId::YellowstoneHydrated);
assert_route(testnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
assert_route(testnet, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
assert_route_absent(testnet, crate::RawIngestRouteId::HeliusTransactionHydrated);
assert_eq!(testnet.routes.len(), 4);
assert_route(testnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
}
let serialized = serde_json::to_string(&inventory);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
for forbidden in [
"http://",
"https://",
"ws://",
"wss://",
"api-key",
"x-token",
"connection_uri",
"source_key",
"endpoint_name",
"publicnode_mainnet",
"helius_mainnet",
] {
assert!(!serialized.contains(forbidden), "safe network inventory leaked forbidden source marker {forbidden}");
}
}
}
#[test]
fn pre_006_fix_003_base_network_routes_remain_configured_independently_of_optional_sources() {
let management = committed_management();
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = ksp_config_lib::ConfigEnvironment::load();
assert!(environment.is_ok());
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let inventory = crate::build_route_inventory_with_environment(&management, &environment, 11);
assert!(inventory.is_ok());
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for profile_id in ["devnet", "mainnet", "testnet"] {
let selected = profile(&inventory, profile_id);
assert!(selected.is_some());
if let std::option::Option::Some(selected) = selected {
assert_route(selected, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
assert_route(selected, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
assert_route(selected, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
}
}
}
fn committed_management() -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let crates_root = manifest.parent();
let crates_root = match crates_root {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, "test manifest has no crates parent"));
},
};
let workspace = crates_root.parent();
let workspace = match workspace {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, "test crates root has no workspace parent"));
},
};
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
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::defaults();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry)));
}
fn profile<'a>(inventory: &'a crate::RawIngestRouteInventoryDto, profile_id: &str) -> std::option::Option<&'a crate::RawIngestProfileInventoryDto> {
return inventory.profiles.iter().find(|profile| return profile.profile_id == profile_id);
}
fn assert_optional_secret_route(profile: &crate::RawIngestProfileInventoryDto, route_id: crate::RawIngestRouteId) {
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
assert!(route.is_some());
if let std::option::Option::Some(route) = route {
if route.selectable {
assert_eq!(route.state, crate::RawIngestRouteState::Configured);
assert_eq!(route.reason, std::option::Option::None);
} else {
assert_eq!(route.state, crate::RawIngestRouteState::Unavailable);
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret));
}
}
}
fn assert_route_absent(profile: &crate::RawIngestProfileInventoryDto, route_id: crate::RawIngestRouteId) {
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
assert!(route.is_none());
}
fn assert_route(
profile: &crate::RawIngestProfileInventoryDto,
route_id: crate::RawIngestRouteId,
selectable: bool,
reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
) {
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
assert!(route.is_some());
if let std::option::Option::Some(route) = route {
assert_eq!(route.selectable, selectable);
assert_eq!(route.reason, reason);
assert_eq!(route.state, if selectable { crate::RawIngestRouteState::Configured } else { crate::RawIngestRouteState::Unavailable });
}
}

View File

@@ -0,0 +1,43 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_monitoring.rs
// version: 1
#[test]
fn pre_010_source_gap_and_repair_codes_are_exact_and_source_neutral() {
assert_eq!(super::source_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Active), "active");
assert_eq!(super::source_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Reconnecting), "reconnecting");
assert_eq!(super::gap_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repairing), "repairing");
assert_eq!(super::gap_reason_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::SourceFailure), "source_failure");
assert_eq!(
super::repair_method_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::TransactionHydration),
"transaction_hydration"
);
}
#[test]
fn pre_010_slot_projection_uses_decimal_text_for_javascript_safe_exactness() {
assert_eq!(super::slot_text(std::option::Option::Some(u64::MAX)), std::option::Option::Some(u64::MAX.to_string()));
assert_eq!(super::slot_text(std::option::Option::None), std::option::Option::None);
}
#[test]
fn pre_010_bounded_cardinality_projection_rejects_only_unrepresentable_values() {
let zero = super::usize_to_u32(0, "zero");
assert!(matches!(zero, std::result::Result::Ok(0)));
let maximum = usize::try_from(u32::MAX);
assert!(maximum.is_ok());
if let std::result::Result::Ok(maximum) = maximum {
let converted = super::usize_to_u32(maximum, "maximum");
assert!(matches!(converted, std::result::Result::Ok(u32::MAX)));
}
if usize::BITS > u32::BITS {
let overflow_value = usize::try_from(u64::from(u32::MAX) + 1);
assert!(overflow_value.is_ok());
if let std::result::Result::Ok(overflow_value) = overflow_value {
let overflow = super::usize_to_u32(overflow_value, "overflow");
assert!(overflow.is_err());
if let std::result::Result::Err(error) = overflow {
assert_eq!(error.code(), crate::ERROR_CODE_APP_STATE_INVALID);
}
}
}
}

View File

@@ -0,0 +1,143 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
// version: 5
#[test]
fn pre_009_multi_route_runtime_starts_without_workers_or_shared_store_material() {
let state = crate::RouteRuntimeState::new();
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(inner.routes.is_empty());
assert!(inner.last_monitoring.is_empty());
assert!(inner.store.is_none());
assert!(inner.network.is_none());
assert!(inner.store_opening_token.is_none());
assert!(inner.store_closing_token.is_none());
}
}
#[test]
fn pre_009_late_targeted_stop_returns_only_the_matching_terminal_route() {
let state = crate::RouteRuntimeState::new();
let terminal = crate::RawIngestRouteRuntimeDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 9,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::YellowstoneHydrated,
state: crate::RawIngestRouteState::Faulted,
};
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.last_terminals.push(terminal.clone());
}
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::YellowstoneHydrated };
let target = state.stop_target(&request);
assert!(target.is_ok());
assert!(matches!(&target, std::result::Result::Ok(super::RouteStopTarget::Terminal(_))));
if let std::result::Result::Ok(super::RouteStopTarget::Terminal(value)) = target {
assert_eq!(value.profile_id, terminal.profile_id);
assert_eq!(value.route_id, terminal.route_id);
assert_eq!(value.state, crate::RawIngestRouteState::Faulted);
}
}
#[test]
fn pre_009_late_targeted_stop_waits_while_final_shared_store_is_closing() {
let state = crate::RouteRuntimeState::new();
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.last_terminals.push(crate::RawIngestRouteRuntimeDto {
commitment: crate::RawIngestCommitment::Finalized,
inventory_generation: 11,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::HttpBlockPolling,
state: crate::RawIngestRouteState::Stopped,
});
inner.store_closing_token = std::option::Option::Some(77);
}
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::HttpBlockPolling };
let target = state.stop_target(&request);
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::TerminalClosing)));
}
#[test]
fn pre_012_stop_racing_start_marks_the_reservation_for_cooperative_stop() {
let state = crate::RouteRuntimeState::new();
let identity = super::RouteRuntimeIdentity {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 12,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::YellowstoneHydrated,
};
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.network = std::option::Option::Some("mainnet".to_owned());
inner.routes.push(super::RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: 12 });
}
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::YellowstoneHydrated };
let target = state.stop_target(&request);
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::Starting { token: 12, .. })));
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
}
let rollback = state.rollback(12);
assert!(matches!(rollback, std::result::Result::Ok(std::option::Option::None)));
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(inner.routes.is_empty());
assert!(matches!(inner.last_terminals.first(), std::option::Option::Some(value) if value.state == crate::RawIngestRouteState::Stopped));
}
}
#[test]
fn pre_012_application_shutdown_is_one_shot_and_marks_starting_routes_before_activation() {
let state = crate::RouteRuntimeState::new();
let identity = super::RouteRuntimeIdentity {
commitment: crate::RawIngestCommitment::Finalized,
inventory_generation: 13,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::HttpBlockPolling,
};
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.network = std::option::Option::Some("mainnet".to_owned());
inner.routes.push(super::RouteRuntimeSlot::Starting { identity, stop_requested: false, token: 13 });
}
let first = state.begin_shutdown();
assert!(matches!(first, std::result::Result::Ok(true)));
let second = state.begin_shutdown();
assert!(matches!(second, std::result::Result::Ok(false)));
let admission = state.ensure_start_admission_open();
assert!(admission.is_err());
if let std::result::Result::Err(error) = admission {
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN);
}
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(inner.shutting_down);
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
}
}
#[test]
fn pre_012_fix_001_start_route_future_is_send_for_tauri_command() {
fn assert_send_future<'a>(
state: &'a crate::AppState,
request: &'a crate::RawIngestRouteStartRequestDto,
) -> impl std::future::Future<Output = ksp_core_lib::Result<crate::RouteRuntimeLaunch>> + std::marker::Send + 'a {
return state.start_route(request);
}
let _assertion = assert_send_future;
}

View File

@@ -0,0 +1,97 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_start.rs
// version: 2
#[test]
fn pre_007_stale_generation_is_rejected_before_config_or_transport_reconstruction() {
let management = committed_management();
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let request = crate::RawIngestRouteStartRequestDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 3,
profile_id: "devnet".to_owned(),
route_id: crate::RawIngestRouteId::StandardBlockDirect,
};
let result = crate::validate_route_start(&management, 2, &request);
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_START_STALE_INVENTORY);
}
}
#[test]
fn pre_007_commitments_are_bounded_to_confirmed_and_finalized() {
assert_eq!(crate::RawIngestCommitment::Confirmed.into_transport(), ksp_onchain_transport_lib::SolanaCommitment::Confirmed);
assert_eq!(crate::RawIngestCommitment::Finalized.into_transport(), ksp_onchain_transport_lib::SolanaCommitment::Finalized);
assert_eq!(crate::RawIngestCommitment::Confirmed.as_str(), "confirmed");
assert_eq!(crate::RawIngestCommitment::Finalized.as_str(), "finalized");
}
#[test]
fn pre_007_public_standard_profiles_reconstruct_block_and_http_sources_without_network_io() {
let management = committed_management();
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let inventory = crate::build_route_inventory(&management, 1);
assert!(inventory.is_ok());
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for profile in inventory.profiles {
for route_id in [crate::RawIngestRouteId::StandardLogsHydrated, crate::RawIngestRouteId::StandardBlockDirect, crate::RawIngestRouteId::HttpBlockPolling]
{
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
assert!(route.is_some(), "missing base route for {}", profile.profile_id);
if let std::option::Option::Some(route) = route {
assert_eq!(route.state, crate::RawIngestRouteState::Configured);
let request = crate::RawIngestRouteStartRequestDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 1,
profile_id: profile.profile_id.clone(),
route_id,
};
let prepared = crate::validate_route_start(&management, 1, &request);
assert!(prepared.is_ok(), "failed Start-time reconstruction for {} / {}", profile.profile_id, route_id.as_str());
}
}
}
}
fn committed_management() -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let crates_root = manifest.parent();
let crates_root = match crates_root {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED, "test manifest has no crates parent"));
},
};
let workspace = crates_root.parent();
let workspace = match workspace {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"test crates root has no workspace parent",
));
},
};
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
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::defaults();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry)));
}

View File

@@ -0,0 +1,36 @@
// file: crates/ksp-app-raw-transaction-ingest-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-raw-transaction-ingest-desk/vite.config.ts
// version: 1
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-raw-transaction-ingest-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: 1440,
strictPort: true,
host: devHost || false,
ws: {
protocol: "ws",
host: devHost || "localhost",
port: 1441,
},
watch: {
ignored: ["**/src/**"],
},
},
});

View File

@@ -53,6 +53,7 @@
],
"resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-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",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
// version: 12
// version: 13
//! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`.
@@ -85,17 +85,21 @@ fn pre_002_package_is_mixed_lib_bin_and_frontend_is_scaffold_only() {
}
#[test]
fn v0_3_8_pre_006_packaging_contains_current_fifteen_config_resources() {
fn v0_3_8_pre_006_packaging_contains_current_sixteen_config_resources() {
let root = app_root();
let tauri = read_json(root.join("tauri.conf.json").as_path());
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some());
if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 15);
assert_eq!(resources.len(), 16);
assert_eq!(
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
);
assert_eq!(
resources.get("../../config/composite.ksp-app-raw-transaction-ingest-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-raw-transaction-ingest-desk.json"),
);
assert_eq!(
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"),

View File

@@ -53,6 +53,7 @@
],
"resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-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",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs
// version: 10
// version: 11
//! Desktop template and frontend contract tests for the Store Desk scaffold.
@@ -153,7 +153,8 @@ fn pre_006_composite_and_packaged_resources_cover_store_desk_and_the_complete_co
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_eq!(resources.len(), 16);
assert!(resources.contains_key("../../config/composite.ksp-app-raw-transaction-ingest-desk.json"));
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"));

View File

@@ -53,6 +53,7 @@
],
"resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-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",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 33
// version: 34
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -432,11 +432,15 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 15);
assert_eq!(resources.len(), 16);
assert_eq!(
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
);
assert_eq!(
resources.get("../../config/composite.ksp-app-raw-transaction-ingest-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-raw-transaction-ingest-desk.json"),
);
assert_eq!(
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"),

View File

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

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 12 -->
<!-- version: 14 -->
# ksp-config-lib
@@ -36,6 +36,7 @@ Le registre par défaut connaît :
```text
cfg.composite.ksp-app-backfill-desk -> config/composite.ksp-app-backfill-desk.json
cfg.composite.ksp-app-raw-transaction-ingest-desk -> config/composite.ksp-app-raw-transaction-ingest-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
@@ -103,6 +104,7 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
- [`../../config/std.store.json`](../../config/std.store.json) — targets Store PostgreSQL Devnet/Mainnet/Testnet et settings runtime bornés ;
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
- [`../../config/composite.ksp-app-backfill-desk.json`](../../config/composite.ksp-app-backfill-desk.json) — composition Logging/Transport/Store de Backfill Desk ;
- [`../../config/composite.ksp-app-raw-transaction-ingest-desk.json`](../../config/composite.ksp-app-raw-transaction-ingest-desk.json) — profils réseau-centriques Logging/Transport/Store du Raw Transaction Ingest Desk, capables dagréger plusieurs sources Transport same-network ;
- [`../../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 ;

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/USAGE.md -->
<!-- version: 15 -->
<!-- version: 19 -->
# Utilisation de ksp-config-lib
@@ -28,6 +28,7 @@ Les arguments compris par Config sont :
```text
--cfgpath=/path/to/config
--schemapath=/path/to/schemas
--filemap=cfg.composite.ksp-app-raw-transaction-ingest-desk=my-raw-ingest-desk.json
--filemap=cfg.composite.ksp-app-solprices-desk=my-solprices-desk.json
--filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json
--filemap=cfg.std.logging=my-logging.json
@@ -157,11 +158,13 @@ secret_metadata = metadata dont la valeur doit avoir une provenance KSP_SECRET_*
`protocol` n'est pas un nouveau `WsProtocolKind` et ne transforme pas PublicNode en protocole. Config résout les placeholders, vérifie la classe de sensibilité des metadata puis construit `YellowstoneGrpcTransportSettings`. Transport ne lit jamais l'environnement. Les valeurs de `secret_metadata` sont disponibles au runtime mais redacted dans `safe_value` et dans les représentations `Debug`.
Le provider affiche `solana-yellowstone-grpc.publicnode.com:443` ; le profil versionné `publicnode_mainnet` le représente sous la forme URL TLS requise par Transport, `https://solana-yellowstone-grpc.publicnode.com:443`, sans credential. Aucun hostname Testnet n'est versionné tant que sa valeur exacte n'est pas confirmée depuis une source opérateur/officielle.
Les profils versionnés `publicnode_mainnet` et `publicnode_testnet` représentent leurs endpoints Yellowstone sous la forme URL TLS requise par Transport et conservent le `x-token` dans `secret_metadata`. Ils restent des sources Transport network-scoped et ne définissent jamais lidentité du réseau applicatif. `publicnode_mainnet` expose en plus le RPC HTTP Mainnet PublicNode utilisé par les consumers qui doivent hydrater une source Yellowstone same-provider ; ce rôle HTTP ne recopie pas une limite numérique Solana-public qui ne fait pas partie du contrat PublicNode committé.
L'accesseur historique `into_transport_settings()` conserve volontairement son tuple `(HTTP, Option<WS>)`. Un consumer ayant besoin des trois backends utilise `into_all_transport_settings()` ou les accesseurs séparés afin de ne pas casser silencieusement les consumers V2.
Les protocoles WebSocket restent `kind = "solana_standard"` et `kind = "helius_laserstream"`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC.
Les protocoles WebSocket restent `kind = "solana_standard"` et `kind = "helius_laserstream"`. Chaque endpoint peut en plus déclarer `capabilities`, liste de descriptors `WsSubscriptionKind` (`account`, `block`, `logs`, `program`, `root`, `signature`, `slot`, `slots_updates`, `vote`, `helius_transaction`). L'absence de cette liste reste backward-compatible mais ne constitue aucune preuve de support ; une liste présente est mappée explicitement vers Transport puis validée contre le protocole. Config ne déduit jamais une capability depuis `provider`.
Les profils Solana standard publics versionnés déclarent `account/block/logs/program/root/signature/slot`. `block` reste une capability explicite, instable et validator/provider-gated : Config ne linfère jamais du protocol kind ni du provider, mais les endpoints publics committés lannoncent désormais explicitement pour rendre la route Standard Block composable. Le profil `testnet_public` fournit le socle HTTP + WS standard Testnet sans credential ; `publicnode_testnet` reste une source Yellowstone optionnelle séparée. Les profils `helius_devnet` et `helius_mainnet` déclarent les sept familles Helius déjà supportées par la façade (`account/logs/program/root/signature/slot/slots_updates`) plus `helius_transaction`, sans `block` ni `vote`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC.
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
@@ -289,7 +292,7 @@ let component = match composite.component("wallet") {
let wallet = engine.resolve_wallet_config_profile(component.resolved(), &environment);
```
La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite `cfg.composite.ksp-app-solprices-desk` référence `logging` et `offchain_transport`. Le composite `cfg.composite.ksp-app-wallet-desk` référence `logging`, `offchain_transport`, `transport` et `wallet`; chaque application valide ses frontières de composition au bootstrap.
La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite `cfg.composite.ksp-app-raw-transaction-ingest-desk` référence exactement `logging`, `transport` et `store`; ses profils couplent explicitement un profil Transport et un profil Store du même réseau logique (`devnet`, `mainnet` ou `testnet`) afin que l'application n'assemble jamais arbitrairement deux réseaux. Le composite `cfg.composite.ksp-app-solprices-desk` référence `logging` et `offchain_transport`. Le composite `cfg.composite.ksp-app-wallet-desk` référence `logging`, `offchain_transport`, `transport` et `wallet`; chaque application valide ses frontières de composition au bootstrap.
## 6. Management de `std.logging.json`
@@ -344,6 +347,8 @@ Elles n'altèrent jamais l'environnement hérité du processus. Une valeur proce
Sur Unix, un nouveau `.env` est créé avec des permissions privées `0600`; les permissions existantes sont préservées lors des remplacements atomiques.
Le composite `cfg.composite.ksp-app-raw-transaction-ingest-desk` expose des profils applicatifs `devnet`, `mainnet` et `testnet`. Un même profil peut référencer `transport`, `transport.helius` et `transport.yellowstone` tant que tous les profils Transport résolus appartiennent au même réseau que Store. Le provider identifie une source de capability, jamais un réseau logique.
## 8. `.env.example`
`/.env.example` est l'inventaire versionné. `/.env` reste local et ignoré.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 23
// version: 24
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -153,6 +153,8 @@ pub use self::registry::ConfigFileKind;
pub use self::registry::ConfigFileRegistry;
/// Default physical filename for the Backfill Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME;
/// Default physical filename for the Raw Transaction Ingest Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME;
/// Default physical filename for the SOL Prices Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME;
/// Default physical filename for the Store Desk composite configuration document.
@@ -183,6 +185,8 @@ pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
pub use self::registry::DEFAULT_STD_WALLET_SCHEMA_FILENAME;
/// Logical file identifier for the Backfill Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK;
/// Logical file identifier for the Raw Transaction Ingest Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK;
/// Logical file identifier for the SOL Prices Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK;
/// Logical file identifier for the Store Desk composite configuration document.

View File

@@ -1,10 +1,12 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 15
// version: 16
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
/// Default physical filename for the Backfill Desk composite configuration document.
pub const DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME: &str = "composite.ksp-app-backfill-desk.json";
/// Default physical filename for the Raw Transaction Ingest Desk composite configuration document.
pub const DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME: &str = "composite.ksp-app-raw-transaction-ingest-desk.json";
/// Default physical filename for the SOL Prices Desk composite configuration document.
pub const DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME: &str = "composite.ksp-app-solprices-desk.json";
/// Default physical filename for the Store Desk composite configuration document.
@@ -35,6 +37,8 @@ pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
pub const DEFAULT_STD_WALLET_SCHEMA_FILENAME: &str = "std.wallet.schema.json";
/// Logical file identifier for the Backfill Desk composite configuration document.
pub const FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK: &str = "cfg.composite.ksp-app-backfill-desk";
/// Logical file identifier for the Raw Transaction Ingest Desk composite configuration document.
pub const FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK: &str = "cfg.composite.ksp-app-raw-transaction-ingest-desk";
/// Logical file identifier for the SOL Prices Desk composite configuration document.
pub const FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK: &str = "cfg.composite.ksp-app-solprices-desk";
/// Logical file identifier for the Store Desk composite configuration document.
@@ -178,6 +182,16 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let raw_transaction_ingest_composite = ConfigFileDescriptor::new(
FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK,
ConfigFileKind::Config,
DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_COMPOSITE),
);
let raw_transaction_ingest_composite = match raw_transaction_ingest_composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let solprices_composite = ConfigFileDescriptor::new(
FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK,
ConfigFileKind::Config,
@@ -300,6 +314,7 @@ impl ConfigFileRegistry {
};
return crate::build_registry([
backfill_composite,
raw_transaction_ingest_composite,
solprices_composite,
store_desk_composite,
wallet_composite,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/transport.rs
// version: 5
// version: 6
/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime contracts.
#[derive(Clone, Eq, PartialEq)]
@@ -259,6 +259,8 @@ struct EffectiveWsEndpointSource {
kind: String,
url: String,
#[serde(default)]
capabilities: std::option::Option<std::vec::Vec<String>>,
#[serde(default)]
session: std::option::Option<EffectiveWsSessionOverrideSource>,
}
@@ -812,7 +814,7 @@ fn map_ws_endpoints(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
endpoints.push(ksp_onchain_transport_lib::WsEndpointSettings::new(
let mut endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
source.name,
source.enabled,
ksp_onchain_transport_lib::WsProviderName::new(source.provider),
@@ -820,7 +822,20 @@ fn map_ws_endpoints(
protocol,
url,
session,
));
);
if let std::option::Option::Some(capability_sources) = source.capabilities {
let mut capabilities = std::vec::Vec::<ksp_onchain_transport_lib::WsSubscriptionKind>::with_capacity(capability_sources.len());
for capability_source in capability_sources {
let capability = map_ws_subscription_capability(capability_source.as_str(), profile, endpoint_name.as_str());
let capability = match capability {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
capabilities.push(capability);
}
endpoint = endpoint.with_subscription_capabilities(capabilities);
}
endpoints.push(endpoint);
}
return std::result::Result::Ok(endpoints);
}
@@ -841,6 +856,30 @@ fn map_ws_protocol_kind(
};
}
fn map_ws_subscription_capability(
value: &str,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsSubscriptionKind> {
return match value {
"account" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Account),
"block" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Block),
"logs" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Logs),
"program" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Program),
"root" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Root),
"signature" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Signature),
"slot" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Slot),
"slots_updates" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates),
"vote" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Vote),
"helius_transaction" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction),
_ => std::result::Result::Err(
effective_error(profile, "effective WebSocket subscription capability is unsupported")
.with_context("endpoint_name", endpoint_name)
.with_context("ws_capability", value),
),
};
}
fn map_ws_session_settings(
defaults: &EffectiveWsSessionSource,
overrides: std::option::Option<&EffectiveWsSessionOverrideSource>,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/ownership.rs
// version: 9
// version: 10
//! Workspace ownership audits for KSP application configuration boundaries.
@@ -262,6 +262,7 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
};
for token in [
"\".env\"",
"\"composite.ksp-app-raw-transaction-ingest-desk.json\"",
"\"composite.ksp-app-solprices-desk.json\"",
"\"composite.ksp-app-wallet-desk.json\"",
"\"composite.schema.json\"",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 28
// version: 29
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport/Store adapters and management contracts.
@@ -80,23 +80,24 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 15);
assert_eq!(descriptors.len(), 16);
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE);
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE);
assert_eq!(descriptors[13].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[14].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
let schema_file_id = descriptors[8].schema_file_id();
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE);
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[13].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE);
assert_eq!(descriptors[14].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[15].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
let schema_file_id = descriptors[9].schema_file_id();
assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association");
if let std::option::Option::Some(schema_file_id) = schema_file_id {
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
@@ -167,6 +168,8 @@ fn composite_schema_and_provenance_contracts_are_available_from_crate_root() {
}
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK, "cfg.composite.ksp-app-backfill-desk");
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_RAW_TRANSACTION_INGEST_DESK, "cfg.composite.ksp-app-raw-transaction-ingest-desk");
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME, "composite.ksp-app-raw-transaction-ingest-desk.json");
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK, "cfg.composite.ksp-app-solprices-desk");
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME, "composite.ksp-app-solprices-desk.json");
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK, "cfg.composite.ksp-app-store-desk");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/composite.rs
// version: 4
// version: 6
const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json";
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
@@ -128,6 +128,82 @@ fn duplicate_component_ids_are_rejected_inside_one_composite_profile() {
cleanup_fixture(&fixture);
}
#[test]
fn v0_3_15_pre_006_fix_001_raw_ingest_composite_is_network_centric_and_keeps_all_transport_sources_coherent() {
let workspace = workspace_root();
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
assert!(bootstrap.is_ok(), "committed Config roots should be valid: {bootstrap:?}");
let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default Config registry should be valid: {registry:?}");
let file_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
assert!(file_id.is_ok(), "Raw Transaction Ingest Desk composite file_id should be valid: {file_id:?}");
let (bootstrap, registry, file_id) = match (bootstrap, registry, file_id) {
(std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) => (bootstrap, registry, file_id),
_ => return,
};
let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_HELIUS_API_KEY".to_owned(), "helius-composite-canary".to_owned());
process.insert("KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN".to_owned(), "orbitflare-composite-canary".to_owned());
process.insert("KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN".to_owned(), "publicnode-mainnet-composite-canary".to_owned());
process.insert("KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN".to_owned(), "publicnode-testnet-composite-canary".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
for (profile_id, expected_network, expected_transport_profiles) in [
("devnet", "devnet", vec!["devnet_public", "helius_devnet", "orbitflare_devnet"]),
("mainnet", "mainnet", vec!["mainnet_public", "helius_mainnet", "publicnode_mainnet"]),
("testnet", "testnet", vec!["testnet_public", "publicnode_testnet"]),
] {
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some(profile_id));
assert!(composite.is_ok(), "Raw Ingest network profile {profile_id} should resolve: {composite:?}");
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
assert_eq!(composite.profile_id(), profile_id);
let store_component = composite.component("store");
assert!(store_component.is_some(), "profile {profile_id} should contain Store");
let store_component = match store_component {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
assert_eq!(store_component.resolved().profile_id(), expected_network);
let store = engine.resolve_store_config_profile(store_component.resolved(), &environment);
assert!(store.is_ok(), "Store profile {expected_network} should map: {store:?}");
if let std::result::Result::Ok(store) = store {
assert_eq!(store.settings().network().as_str(), expected_network);
}
let transport_components = composite
.components()
.iter()
.filter(|(component_id, component)| {
return component_id.starts_with("transport") && component.resolved().file_id().as_str() == crate::FILE_ID_STD_TRANSPORT;
})
.map(|(_, component)| return component)
.collect::<std::vec::Vec<_>>();
assert_eq!(transport_components.len(), expected_transport_profiles.len());
let actual_profiles = transport_components.iter().map(|component| return component.resolved().profile_id()).collect::<std::vec::Vec<_>>();
for expected_transport_profile in expected_transport_profiles {
assert!(
actual_profiles.contains(&expected_transport_profile),
"network profile {profile_id} should include Transport source {expected_transport_profile}"
);
}
for component in transport_components {
let transport = engine.resolve_transport_config_profile(component.resolved(), &environment);
assert!(transport.is_ok(), "Transport source {} should map: {transport:?}", component.resolved().profile_id());
if let std::result::Result::Ok(transport) = transport {
assert!(transport.http_settings().endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network));
if let std::option::Option::Some(ws) = transport.ws_settings() {
assert!(ws.endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network));
}
if let std::option::Option::Some(grpc) = transport.grpc_settings() {
assert!(grpc.endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network));
}
}
}
}
}
fn fixture_example_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = workspace_root();
let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures");

View File

@@ -0,0 +1,91 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "secret_test"
},
{
"component_id": "transport.helius",
"file_id": "cfg.std.transport",
"profile_id": "helius_devnet"
},
{
"component_id": "transport.yellowstone",
"file_id": "cfg.std.transport",
"profile_id": "secret_test"
},
{
"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": "transport",
"file_id": "cfg.std.transport",
"profile_id": "secret_test"
},
{
"component_id": "transport.helius",
"file_id": "cfg.std.transport",
"profile_id": "helius_devnet"
},
{
"component_id": "transport.yellowstone",
"file_id": "cfg.std.transport",
"profile_id": "secret_test"
},
{
"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": "transport",
"file_id": "cfg.std.transport",
"profile_id": "secret_test"
},
{
"component_id": "transport.yellowstone",
"file_id": "cfg.std.transport",
"profile_id": "secret_test"
},
{
"component_id": "store",
"file_id": "cfg.std.store",
"profile_id": "testnet"
}
]
}
]
}

View File

@@ -70,7 +70,15 @@
"resubscribe": "never",
"notification_queue_capacity": 48,
"max_pending_requests": 24
}
},
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot"
]
},
{
"name": "fixture_helius_ws",
@@ -78,7 +86,17 @@
"provider": "helius",
"cluster": "mainnet",
"kind": "helius_laserstream",
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}"
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
}
]
},
@@ -119,7 +137,17 @@
"provider": "helius",
"cluster": "devnet",
"kind": "helius_laserstream",
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}"
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
}
]
}

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/transport.rs
// version: 13
// version: 18
#[test]
fn fixture_transport_profile_maps_complete_runtime_contract() {
@@ -51,6 +51,19 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
assert_eq!(endpoint.provider().as_str(), "fixture-provider");
assert_eq!(endpoint.cluster().as_str(), "fixture-cluster");
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
assert_eq!(
endpoint.subscription_capabilities(),
std::option::Option::Some(
&[
ksp_onchain_transport_lib::WsSubscriptionKind::Account,
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
ksp_onchain_transport_lib::WsSubscriptionKind::Program,
ksp_onchain_transport_lib::WsSubscriptionKind::Root,
ksp_onchain_transport_lib::WsSubscriptionKind::Signature,
ksp_onchain_transport_lib::WsSubscriptionKind::Slot,
][..],
)
);
assert_eq!(endpoint.url().as_str(), "wss://fallback.invalid");
assert_eq!(endpoint.session().command_timeout(), std::time::Duration::from_millis(4500));
assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(4000));
@@ -70,6 +83,21 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
assert_eq!(helius.provider().as_str(), "helius");
assert_eq!(helius.cluster().as_str(), "mainnet");
assert_eq!(helius.protocol(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream);
assert_eq!(
helius.subscription_capabilities(),
std::option::Option::Some(
&[
ksp_onchain_transport_lib::WsSubscriptionKind::Account,
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
ksp_onchain_transport_lib::WsSubscriptionKind::Program,
ksp_onchain_transport_lib::WsSubscriptionKind::Root,
ksp_onchain_transport_lib::WsSubscriptionKind::Signature,
ksp_onchain_transport_lib::WsSubscriptionKind::Slot,
ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates,
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
][..],
)
);
assert_eq!(helius.url().as_str(), "wss://mainnet.helius-rpc.com/?api-key=fixture-helius-key");
let _connect_future = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect(helius.clone());
}
@@ -117,6 +145,8 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.devnet.solana.com");
assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
}
}
if let std::result::Result::Ok(mainnet) = mainnet {
@@ -129,6 +159,8 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
assert!(ws.is_some(), "committed V3 Mainnet profile should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet.solana.com");
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
}
}
}
@@ -179,6 +211,33 @@ fn committed_v3_orbitflare_devnet_profile_maps_provider_neutral_yellowstone_grpc
}
}
#[test]
fn v0_3_15_pre_006_fix_001_testnet_public_is_keyless_standard_transport_without_yellowstone() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_transport_config(std::option::Option::Some("testnet_public"), &environment);
assert!(resolved.is_ok(), "committed keyless Testnet Transport profile should map without provider credentials: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "testnet_public");
assert_eq!(resolved.settings().endpoints().len(), 1);
assert_eq!(resolved.settings().endpoints()[0].cluster().as_str(), "testnet");
let ws = resolved.ws_settings();
assert!(ws.is_some());
if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints().len(), 1);
assert_eq!(ws.endpoints()[0].cluster().as_str(), "testnet");
assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
}
assert!(resolved.grpc_settings().is_none(), "keyless Testnet base profile must not conflate PublicNode Yellowstone with the network itself");
}
}
#[test]
fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_with_network_scoped_secret_x_tokens() {
let engine = committed_engine();
@@ -214,6 +273,27 @@ fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_with_n
std::result::Result::Err(_) => continue,
};
assert_eq!(resolved.profile_id(), profile_id);
if profile_id == "publicnode_mainnet" {
assert_eq!(resolved.settings().endpoints().len(), 2);
let publicnode = &resolved.settings().endpoints()[0];
assert_eq!(publicnode.name(), "publicnode_solana_mainnet_rpc");
assert_eq!(publicnode.provider().as_str(), "publicnode");
assert_eq!(publicnode.cluster().as_str(), "mainnet");
assert_eq!(publicnode.url().as_str(), "https://solana-rpc.publicnode.com");
assert_eq!(publicnode.roles().len(), 1);
let limits = publicnode.roles()[0].limits();
assert!(limits.requests_per_second().is_none());
assert!(limits.burst_capacity().is_none());
assert_eq!(limits.max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(8));
let solana_public = &resolved.settings().endpoints()[1];
assert_eq!(solana_public.name(), "solana_mainnet_public_repair");
assert_eq!(solana_public.provider().as_str(), "solana-public");
assert_eq!(solana_public.cluster().as_str(), "mainnet");
assert_eq!(solana_public.roles()[0].role().as_str(), "default");
let solana_limits = solana_public.roles()[0].limits();
assert_eq!(solana_limits.requests_per_second().map(std::num::NonZeroU32::get), std::option::Option::Some(5));
assert_eq!(solana_limits.burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(10));
}
assert!(resolved.ws_settings().is_some());
let grpc = resolved.grpc_settings();
assert!(grpc.is_some(), "PublicNode profile {profile_id} should expose Yellowstone gRPC settings");
@@ -529,6 +609,76 @@ fn helius_laserstream_mainnet_and_devnet_api_key_map_to_protocol_and_safe_redact
assert!(devnet_debug.contains(crate::REDACTED_CONFIG_VALUE));
}
#[test]
fn v0_3_15_pre_006_fix_002_committed_public_standard_profiles_declare_block_capability() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
for (profile_id, cluster) in [("devnet_public", "devnet"), ("mainnet_public", "mainnet"), ("testnet_public", "testnet")] {
let resolved = engine.load_resolved_transport_config(std::option::Option::Some(profile_id), &environment);
assert!(resolved.is_ok(), "committed public standard profile {profile_id} should resolve: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
let ws = resolved.ws_settings();
assert!(ws.is_some(), "committed public standard profile {profile_id} should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints().len(), 1);
let endpoint = &ws.endpoints()[0];
assert_eq!(endpoint.provider().as_str(), "solana-public");
assert_eq!(endpoint.cluster().as_str(), cluster);
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
}
}
}
#[test]
fn v0_3_15_pre_003_committed_helius_profiles_map_transaction_capability_without_block_or_vote() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let canary = "helius-pre-003-api-key-canary";
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_HELIUS_API_KEY".to_owned(), canary.to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
for (profile_id, cluster, expected_url) in [
("helius_devnet", "devnet", "wss://devnet.helius-rpc.com/?api-key=helius-pre-003-api-key-canary"),
("helius_mainnet", "mainnet", "wss://mainnet.helius-rpc.com/?api-key=helius-pre-003-api-key-canary"),
] {
let resolved = engine.load_resolved_transport_config(std::option::Option::Some(profile_id), &environment);
assert!(resolved.is_ok(), "committed Helius profile {profile_id} should resolve: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
assert_eq!(resolved.http_settings().endpoints()[0].cluster().as_str(), cluster);
let ws = resolved.ws_settings();
assert!(ws.is_some(), "committed Helius profile {profile_id} should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints().len(), 1);
let endpoint = &ws.endpoints()[0];
assert_eq!(endpoint.provider().as_str(), "helius");
assert_eq!(endpoint.cluster().as_str(), cluster);
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream);
assert_eq!(endpoint.url().as_str(), expected_url);
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction));
assert!(!endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
assert!(!endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Vote));
}
let debug = format!("{resolved:?}");
assert!(!debug.contains(canary));
}
}
#[test]
fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() {
let engine = fixture_engine();

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
<!-- version: 24 -->
<!-- version: 26 -->
# `ksp-onchain-transport-lib`
@@ -239,7 +239,9 @@ YellowstoneGrpcSubscribeSnapshot
Les sept unary standards retenus sont `SubscribeReplayInfo`, `Ping`, `GetLatestBlockhash`, `GetBlockHeight`, `GetSlot`, `IsBlockhashValid` et `GetVersion`. `Subscribe` couvre accounts, slots, transactions, transaction status, blocks, block metadata et entries, avec `commitment`, `accounts_data_slice`, `ping` et `from_slot`. `SubscribeDeshred` reste hors scope de la foundation standard.
Le stream bidirectionnel est borné : request/update queues, tailles inbound/outbound, half-close, close timeout et reconnect budget sont explicites. Après reconnect, KSP rejoue la dernière requête complète acceptée et avance prudemment `from_slot` selon le dernier slot observé et `SubscribeReplayInfo.first_available`. Les compteurs de gap et duplicate sont de lobservabilité ; ils ne constituent jamais une garantie exactly-once ou lossless.
Le stream bidirectionnel est borné : request/update queues, tailles inbound/outbound, half-close, close timeout et reconnect budget sont explicites. La queue d'updates Yellowstone reste bornée mais sa saturation locale n'est plus un fault : l'acteur attend asynchronement qu'une place soit rendue et cesse donc de poller le stream Tonic, ce qui propage la backpressure via gRPC/HTTP/2 sans drop silencieux ni queue non bornée. Le shutdown préempte cette attente. La queue de mutations de requête reste au contraire synchrone/fail-fast : un caller qui la sature reçoit toujours `grpc_backpressure_overflow`. Pendant un shutdown local explicite, un `Status` gRPC reçu après le half-close client est traité comme une terminaison `Closed` de cette fermeture déjà engagée ; le même `Status` reçu pendant une session active conserve son comportement normal de reconnect/fault et n'est pas masqué.
Après reconnect, KSP rejoue la dernière requête complète acceptée et avance prudemment `from_slot` selon le dernier slot observé et `SubscribeReplayInfo.first_available`. Les compteurs de gap et duplicate sont de lobservabilité ; ils ne constituent jamais une garantie exactly-once ou lossless.
Config Transport V3 peut mapper des `grpc_endpoints` vers ces settings sans inverser la dépendance. `protocol = solana_yellowstone` décrit le wire standard, tandis que `provider` reste un descripteur distinct. Les profils committés `publicnode_mainnet` et `publicnode_testnet` utilisent le standard sans façade PublicNode spécifique. Leur personal token est injecté comme metadata secrète `x-token` depuis Config ; Transport ne lit jamais les variables KSP.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
<!-- version: 25 -->
<!-- version: 27 -->
# Utilisation de `ksp-onchain-transport-lib`
@@ -82,7 +82,14 @@ let endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
ws_url,
ksp_onchain_transport_lib::WsSessionSettings::default(),
);
)
.with_subscription_capabilities(vec![ksp_onchain_transport_lib::WsSubscriptionKind::Logs]);
if !endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs) {
return Err(ksp_core_lib::Error::new(
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
"required WebSocket subscription capability is not declared",
));
}
let session = match ksp_onchain_transport_lib::WsSession::connect(endpoint).await {
Ok(value) => value,
Err(error) => return Err(error),
@@ -90,6 +97,8 @@ let session = match ksp_onchain_transport_lib::WsSession::connect(endpoint).awai
let snapshot = session.snapshot();
```
`with_subscription_capabilities(...)` porte une preuve explicite et typed des familles déclarées pour cet endpoint. Une déclaration vide, dupliquée ou incompatible avec `WsProtocolKind` est rejetée par `WsTransportSettings::validate()`. Un endpoint construit sans déclaration reste valide pour compatibilité, mais `subscription_capabilities()` retourne `None` et `supports_subscription(...)` retourne `false` : l'absence de déclaration n'est jamais une preuve implicite de support.
Deux appels `WsSession::connect` avec le même endpoint créent volontairement deux connexions physiques distinctes. Il n'existe encore aucun pool de sessions automatique.
Le socket brut et la primitive JSON-RPC générique ne sont pas publics. Le moteur générique de subscription typed reste `pub(crate)` ; il ne constitue donc pas une escape hatch provider-specific.
@@ -387,9 +396,11 @@ let snapshot = stream.snapshot();
let closed = stream.close().await;
```
`try_update()` remplace dynamiquement la requête complète tant que la session est `Active`. Une mutation pendant `Reconnecting` est refusée pour éviter une application ambiguë. Le snapshot expose reconnects, replay attempts, gaps, duplicates, dernier `from_slot` demandé et dernier slot observé, sans endpoint ni payload arbitraire.
`try_update()` remplace dynamiquement la requête complète tant que la session est `Active`. Une mutation pendant `Reconnecting` est refusée pour éviter une application ambiguë. Le snapshot expose reconnects, replay attempts, replay deliveries conservatrices, reprises dont la couverture reste non prouvée, gaps de rétention, duplicates, dernier `from_slot` demandé et dernier slot observé, sans endpoint ni payload arbitraire.
Le reconnect réutilise la dernière requête acceptée et peut avancer `from_slot`, mais le consumer doit traiter cette reprise comme best-effort. KSP ne promet ni exactly-once, ni replay historique complet, ni absence de fork/equivocation entre nœuds.
Une `replay_delivery_count` n'augmente que lorsque le premier flux post-reconnect redélivre exactement le slot de reprise demandé. Elle prouve une livraison au bord de replay, pas la complétude de l'intervalle. Dès que le premier update slot-bearing atteint ou dépasse cette borne, `replay_coverage_unproven_count` augmente aussi : Transport ne possède pas de preuve générique que tous les updates correspondant aux filtres ont été livrés entre la borne et la reprise live. Le compteur augmente également si un nouveau reconnect survient avant tout matériau slot-bearing. Cette valeur signifie explicitement « couverture non prouvée » ; elle ne prouve pas qu'un événement filtré existait ou a été perdu.
Le reconnect réutilise la dernière requête acceptée et peut avancer `from_slot`, mais le consumer doit traiter cette reprise comme best-effort. `replay_attempt_count`, `replay_delivery_count` et une preuve de coverage sont des notions distinctes. Transport ne publie actuellement aucun compteur `replay_covered` générique, car ni l'acceptation de `from_slot` ni une livraison ponctuelle ne démontrent à elles seules une couverture historique complète. KSP ne promet ni exactly-once, ni replay historique complet, ni absence de fork/equivocation entre nœuds.
## 5. Appels typés

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_stream.rs
// version: 3
// version: 7
use tonic_prost::prost::Message; // rust-rules: trait-import
@@ -34,11 +34,50 @@ pub struct YellowstoneGrpcSubscribeSnapshot {
continuity_gap_count: u64,
duplicate_update_count: u64,
replay_attempt_count: u64,
replay_delivery_count: u64,
replay_coverage_unproven_count: u64,
last_requested_from_slot: std::option::Option<u64>,
last_observed_slot: std::option::Option<u64>,
terminal_error_code: std::option::Option<ksp_core_lib::ErrorCode>,
}
/// Cloneable latest-value observer for one standard Yellowstone Subscribe session snapshot.
///
/// The observer exposes only the already-safe transport snapshot and keeps the internal Tokio watch channel private. Cloning the observer does not duplicate
/// the gRPC stream, request state or reconnect actor.
#[derive(Clone)]
pub struct YellowstoneGrpcSubscribeSnapshotSource {
receiver: tokio::sync::watch::Receiver<crate::YellowstoneGrpcSubscribeSnapshot>,
}
impl crate::YellowstoneGrpcSubscribeSnapshotSource {
fn new(receiver: tokio::sync::watch::Receiver<crate::YellowstoneGrpcSubscribeSnapshot>) -> Self {
return Self { receiver };
}
/// Returns the current safe reconnect/replay snapshot without waiting for another actor transition.
#[must_use]
pub fn current(&self) -> crate::YellowstoneGrpcSubscribeSnapshot {
return *self.receiver.borrow();
}
/// Waits for one newer safe reconnect/replay snapshot.
///
/// `None` means the owning Subscribe actor dropped the latest-value publisher and no further snapshot can arrive.
pub async fn wait_for_change(&mut self) -> std::option::Option<crate::YellowstoneGrpcSubscribeSnapshot> {
return match self.receiver.changed().await {
std::result::Result::Ok(()) => std::option::Option::Some(*self.receiver.borrow_and_update()),
std::result::Result::Err(_) => std::option::Option::None,
};
}
}
impl std::fmt::Debug for crate::YellowstoneGrpcSubscribeSnapshotSource {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("YellowstoneGrpcSubscribeSnapshotSource").field("current", &self.current()).finish();
}
}
impl YellowstoneGrpcSubscribeSnapshot {
const fn new(initial_from_slot: std::option::Option<u64>) -> Self {
return Self {
@@ -47,6 +86,8 @@ impl YellowstoneGrpcSubscribeSnapshot {
continuity_gap_count: 0,
duplicate_update_count: 0,
replay_attempt_count: 0,
replay_delivery_count: 0,
replay_coverage_unproven_count: 0,
last_requested_from_slot: initial_from_slot,
last_observed_slot: std::option::Option::None,
terminal_error_code: std::option::Option::None,
@@ -85,6 +126,25 @@ impl YellowstoneGrpcSubscribeSnapshot {
return self.replay_attempt_count;
}
/// Returns the number of replay-bearing reconnects that delivered the requested replay boundary slot again.
///
/// This is conservative delivery evidence only. It does not prove that every matching update in the replay interval was delivered and must never be
/// interpreted as `replay_covered`.
#[must_use]
pub const fn replay_delivery_count(self) -> u64 {
return self.replay_delivery_count;
}
/// Returns the number of successful replay-bearing reconnects whose target coverage remains unproven.
///
/// The counter advances when the first post-reconnect slot-bearing update reaches or passes the requested replay boundary, because generic Transport
/// cannot prove from that delivery alone that every matching update in the replay interval was delivered. It also advances when another reconnect starts
/// before any slot-bearing replay material arrives. This is an explicit lack of coverage proof, not proof that a filtered event actually existed or was lost.
#[must_use]
pub const fn replay_coverage_unproven_count(self) -> u64 {
return self.replay_coverage_unproven_count;
}
/// Returns the most recent effective `from_slot` sent by KSP, including any clamp to `SubscribeReplayInfo.first_available`.
#[must_use]
pub const fn last_requested_from_slot(self) -> std::option::Option<u64> {
@@ -154,6 +214,12 @@ impl SolanaYellowstoneGrpcSubscribeSession {
return *self.snapshot_rx.borrow();
}
/// Returns one cloneable latest-value observer for reconnect/replay snapshot transitions.
#[must_use]
pub fn snapshot_source(&self) -> crate::YellowstoneGrpcSubscribeSnapshotSource {
return crate::YellowstoneGrpcSubscribeSnapshotSource::new(self.snapshot_rx.clone());
}
/// Queues one complete standard Yellowstone request mutation without waiting for network dispatch.
///
/// The mutation is rejected synchronously when local validation fails, the encoded request exceeds the configured outbound bound, the bounded request
@@ -230,8 +296,9 @@ impl SolanaYellowstoneGrpcSubscribeSession {
/// Receives the next decoded standard Yellowstone update.
///
/// Transient remote stream loss is hidden while KSP performs a bounded reconnect. Terminal remote statuses after the reconnect budget, malformed updates
/// and overflow surface a safe KSP error. A normal close returns `Ok(None)`.
/// Transient remote stream loss is hidden while KSP performs a bounded reconnect. A full bounded update queue applies asynchronous backpressure to the
/// gRPC stream instead of dropping an update or terminating locally; terminal remote statuses after the reconnect budget and malformed updates still
/// surface a safe KSP error. A normal close returns `Ok(None)`.
pub async fn next_update(&mut self) -> ksp_core_lib::Result<std::option::Option<crate::YellowstoneSubscribeUpdate>> {
return match self.update_rx.recv().await {
std::option::Option::Some(std::result::Result::Ok(update)) => std::result::Result::Ok(std::option::Option::Some(update)),
@@ -463,17 +530,44 @@ enum UpdateIdentity {
}
struct ContinuityTracker {
pending_replay_from_slot: std::option::Option<u64>,
recent_order: std::collections::VecDeque<UpdateIdentity>,
recent_set: std::collections::HashSet<UpdateIdentity>,
}
impl ContinuityTracker {
fn new() -> Self {
return Self { recent_order: std::collections::VecDeque::new(), recent_set: std::collections::HashSet::new() };
return Self {
pending_replay_from_slot: std::option::Option::None,
recent_order: std::collections::VecDeque::new(),
recent_set: std::collections::HashSet::new(),
};
}
fn begin_replay(&mut self, from_slot: std::option::Option<u64>) {
self.pending_replay_from_slot = from_slot;
return;
}
fn abandon_pending_replay(&mut self, snapshot: &mut crate::YellowstoneGrpcSubscribeSnapshot) -> bool {
if self.pending_replay_from_slot.take().is_none() {
return false;
}
snapshot.replay_coverage_unproven_count = snapshot.replay_coverage_unproven_count.saturating_add(1);
return true;
}
fn observe(&mut self, update: &crate::YellowstoneSubscribeUpdate, snapshot: &mut crate::YellowstoneGrpcSubscribeSnapshot) {
if let std::option::Option::Some(slot) = update_slot(update) {
if let std::option::Option::Some(requested) = self.pending_replay_from_slot
&& slot >= requested
{
if slot == requested {
snapshot.replay_delivery_count = snapshot.replay_delivery_count.saturating_add(1);
}
snapshot.replay_coverage_unproven_count = snapshot.replay_coverage_unproven_count.saturating_add(1);
self.pending_replay_from_slot = std::option::Option::None;
}
snapshot.last_observed_slot = std::option::Option::Some(match snapshot.last_observed_slot {
std::option::Option::Some(previous) => std::cmp::max(previous, slot),
std::option::Option::None => slot,
@@ -542,23 +636,30 @@ async fn run_subscribe_actor(
}
tracker.observe(&update, &mut snapshot);
snapshot_tx.send_replace(snapshot);
match update_tx.try_send(std::result::Result::Ok(update)) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
endpoint_name = endpoint_name.as_str(),
provider = provider.as_str(),
cluster = cluster.as_str(),
"Yellowstone subscribe update queue overflowed"
);
fail_actor(crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW, &request_state, &mut snapshot, &snapshot_tx);
let delivery = tokio::select! {
biased;
shutdown_changed = shutdown_rx.changed() => {
let deadline = shutdown_deadline(&shutdown_rx, shutdown_changed);
snapshot.state = crate::YellowstoneGrpcSubscribeState::Closing;
snapshot_tx.send_replace(snapshot);
clear_request_sender(&request_state);
finish_client_half_close(
&endpoint_name,
&provider,
&cluster,
&mut incoming,
deadline,
&mut snapshot,
&snapshot_tx,
)
.await;
return;
},
std::result::Result::Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
}
result = update_tx.send(std::result::Result::Ok(update)) => result,
};
if delivery.is_err() {
close_actor(&request_state, &mut snapshot, &snapshot_tx);
return;
},
}
},
std::result::Result::Ok(std::option::Option::None) => {
@@ -577,6 +678,7 @@ async fn run_subscribe_actor(
&mut shutdown_rx,
&mut snapshot,
&snapshot_tx,
&mut tracker,
)
.await
{
@@ -619,6 +721,7 @@ async fn run_subscribe_actor(
&mut shutdown_rx,
&mut snapshot,
&snapshot_tx,
&mut tracker,
)
.await
{
@@ -658,8 +761,12 @@ async fn reconnect_subscribe_stream(
shutdown_rx: &mut tokio::sync::watch::Receiver<std::option::Option<tokio::time::Instant>>,
snapshot: &mut crate::YellowstoneGrpcSubscribeSnapshot,
snapshot_tx: &tokio::sync::watch::Sender<crate::YellowstoneGrpcSubscribeSnapshot>,
tracker: &mut ContinuityTracker,
) -> ReconnectOutcome {
clear_request_sender(request_state);
if tracker.abandon_pending_replay(snapshot) {
snapshot_tx.send_replace(*snapshot);
}
snapshot.state = crate::YellowstoneGrpcSubscribeState::Reconnecting;
snapshot.terminal_error_code = std::option::Option::None;
snapshot_tx.send_replace(*snapshot);
@@ -742,6 +849,7 @@ async fn reconnect_subscribe_stream(
return ReconnectOutcome::Exhausted(error);
}
snapshot.reconnect_count = snapshot.reconnect_count.saturating_add(1);
tracker.begin_replay(effective_from_slot);
snapshot.state = crate::YellowstoneGrpcSubscribeState::Active;
snapshot.terminal_error_code = std::option::Option::None;
snapshot_tx.send_replace(*snapshot);
@@ -904,16 +1012,16 @@ async fn finish_client_half_close(
},
std::result::Result::Ok(std::result::Result::Err(status)) => {
let code = status.code().to_string();
ksp_logging_lib::warn!(
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
endpoint_name,
provider = provider.as_str(),
cluster = cluster.as_str(),
grpc_code = code.as_str(),
"Yellowstone subscribe endpoint returned a status during graceful shutdown"
"Yellowstone subscribe endpoint returned a status after local graceful shutdown request; treating session as closed"
);
snapshot.state = crate::YellowstoneGrpcSubscribeState::Failed;
snapshot.terminal_error_code = std::option::Option::Some(crate::ERROR_CODE_GRPC_STATUS);
snapshot.state = crate::YellowstoneGrpcSubscribeState::Closed;
snapshot.terminal_error_code = std::option::Option::None;
snapshot_tx.send_replace(*snapshot);
return;
},

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
// version: 8
// version: 9
const MAX_GRPC_BLOCKHASH_TEXT_LENGTH_BYTES: usize = 128;
const MAX_GRPC_BLOCK_VECTOR_COUNT: usize = 65_536;
@@ -2529,13 +2529,34 @@ impl YellowstoneSubscribeEntryFilter {
}
}
/// Opaque deterministic identity material for one complete Yellowstone Subscribe request.
///
/// The encoded bytes remain private. `Hash` feeds those canonical bytes to a caller-provided hasher while `Debug` exposes only their length.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneSubscribeRequestIdentity {
bytes: std::vec::Vec<u8>,
}
impl std::hash::Hash for crate::YellowstoneSubscribeRequestIdentity {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write(&(self.bytes.len() as u64).to_be_bytes());
state.write(self.bytes.as_slice());
return;
}
}
impl std::fmt::Debug for crate::YellowstoneSubscribeRequestIdentity {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("YellowstoneSubscribeRequestIdentity").field("byte_len", &self.bytes.len()).finish();
}
}
/// Provider-neutral standard Yellowstone subscribe request owned by KSP.
///
/// The seven upstream maps are represented independently and retain named empty entries. An entirely empty map is the logical KSP representation of no active
/// filter in that family; protobuf map encoding does not distinguish an omitted map from an empty map. Filter-group names are globally unique across all seven
/// maps so the names echoed by `SubscribeUpdate.filters` remain unambiguous. `Debug` exposes only counts and common scalar options, never filter names or
/// future
/// filter payloads.
/// future filter payloads.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneSubscribeRequest {
accounts: std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, crate::YellowstoneSubscribeAccountFilter>,
@@ -2551,7 +2572,7 @@ pub struct YellowstoneSubscribeRequest {
from_slot: std::option::Option<u64>,
}
impl YellowstoneSubscribeRequest {
impl crate::YellowstoneSubscribeRequest {
/// Creates an empty subscribe request. Empty requests are valid because later bidi lifecycle code uses request mutations to clear filters or carry ping
/// state.
#[must_use]
@@ -2762,6 +2783,40 @@ impl YellowstoneSubscribeRequest {
return self.from_slot;
}
/// Builds one opaque deterministic identity for the complete logical Subscribe request.
///
/// The identity preserves filter-family separation, globally sorted filter names, exact filter wire payloads and common request options. Its Debug surface
/// exposes only the encoded byte length. Callers may hash the opaque value but cannot recover the underlying identity bytes through this API.
pub fn identity(&self) -> ksp_core_lib::Result<crate::YellowstoneSubscribeRequestIdentity> {
let validation = self.validate();
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let mut bytes = std::vec::Vec::new();
append_subscribe_identity_map(&mut bytes, b"accounts", &self.accounts, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"blocks", &self.blocks, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"blocks_meta", &self.blocks_meta, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"entry", &self.entry, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"slots", &self.slots, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"transactions", &self.transactions, |filter| return filter.to_wire());
append_subscribe_identity_map(&mut bytes, b"transactions_status", &self.transactions_status, |filter| return filter.to_wire());
let mut common = match self.to_wire() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
common.accounts.clear();
common.blocks.clear();
common.blocks_meta.clear();
common.entry.clear();
common.slots.clear();
common.transactions.clear();
common.transactions_status.clear();
let common = yellowstone_grpc_proto::prost::Message::encode_to_vec(&common);
append_subscribe_identity_component(&mut bytes, b"common");
append_subscribe_identity_component(&mut bytes, common.as_slice());
return std::result::Result::Ok(crate::YellowstoneSubscribeRequestIdentity { bytes });
}
/// Validates all deterministic common subscribe-request bounds before any network I/O.
pub fn validate(&self) -> ksp_core_lib::Result<()> {
if self.total_filter_count() > MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT {
@@ -3710,13 +3765,38 @@ fn commitment_to_wire(commitment: std::option::Option<crate::SolanaCommitment>)
});
}
impl std::default::Default for YellowstoneSubscribeRequest {
fn append_subscribe_identity_component(output: &mut std::vec::Vec<u8>, value: &[u8]) {
output.extend_from_slice(&(value.len() as u64).to_be_bytes());
output.extend_from_slice(value);
return;
}
fn append_subscribe_identity_map<V, W, F>(
output: &mut std::vec::Vec<u8>,
family: &[u8],
values: &std::collections::BTreeMap<crate::YellowstoneSubscribeFilterName, V>,
mut to_wire: F,
) where
W: yellowstone_grpc_proto::prost::Message,
F: FnMut(&V) -> W,
{
append_subscribe_identity_component(output, family);
output.extend_from_slice(&(values.len() as u64).to_be_bytes());
for (name, filter) in values {
append_subscribe_identity_component(output, name.as_str().as_bytes());
let wire = yellowstone_grpc_proto::prost::Message::encode_to_vec(&to_wire(filter));
append_subscribe_identity_component(output, wire.as_slice());
}
return;
}
impl std::default::Default for crate::YellowstoneSubscribeRequest {
fn default() -> Self {
return Self::new();
}
}
impl std::fmt::Debug for YellowstoneSubscribeRequest {
impl std::fmt::Debug for crate::YellowstoneSubscribeRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneSubscribeRequest")

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 45
// version: 48
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -143,6 +143,8 @@ pub use self::grpc_settings::YellowstoneGrpcTransportSettings;
pub use self::grpc_stream::SolanaYellowstoneGrpcSubscribeSession;
/// Safe Yellowstone reconnect/replay continuity snapshot.
pub use self::grpc_stream::YellowstoneGrpcSubscribeSnapshot;
/// Cloneable latest-value observer for one standard Yellowstone Subscribe session snapshot.
pub use self::grpc_stream::YellowstoneGrpcSubscribeSnapshotSource;
/// Safe Yellowstone bidirectional Subscribe lifecycle state.
pub use self::grpc_stream::YellowstoneGrpcSubscribeState;
/// One validated standard Yellowstone account predicate.
@@ -213,6 +215,8 @@ pub use self::grpc_subscribe::YellowstoneSubscribePingUpdate;
pub use self::grpc_subscribe::YellowstoneSubscribePongUpdate;
/// Provider-neutral standard Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribeRequest;
/// Opaque deterministic identity for one complete Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribeRequestIdentity;
/// Complete slot-family filter group for standard Yellowstone Subscribe.
pub use self::grpc_subscribe::YellowstoneSubscribeSlotFilter;
/// Complete transaction-family filter shared by transactions and transaction-status maps.
@@ -551,6 +555,8 @@ pub use self::ws_protocol_session::HeliusLaserStreamWsSession;
pub use self::ws_protocol_session::SolanaStandardWsSession;
/// Shareable compatibility handle for one explicitly created standard Solana physical WebSocket session.
pub use self::ws_session::WsSession;
/// Cloneable latest-value observer for one safe physical WebSocket session snapshot.
pub use self::ws_session::WsSessionSnapshotSource;
/// Open cluster or network descriptor used by WebSocket endpoint settings.
pub use self::ws_settings::WsClusterName;
/// Runtime settings for one named WebSocket endpoint.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
// version: 6
// version: 7
/// Typed facade for one standard Solana WebSocket physical session.
///
@@ -42,6 +42,12 @@ impl SolanaStandardWsSession {
return self.inner.snapshot();
}
/// Returns a cloneable latest-value observer for safe physical-session snapshots from the shared actor.
#[must_use]
pub fn snapshot_source(&self) -> crate::WsSessionSnapshotSource {
return self.inner.snapshot_source();
}
/// Returns the latest observable physical-session state.
#[must_use]
pub fn state(&self) -> crate::WsSessionState {
@@ -117,6 +123,12 @@ impl HeliusLaserStreamWsSession {
return self.inner.snapshot();
}
/// Returns a cloneable latest-value observer for safe physical-session snapshots from the shared actor.
#[must_use]
pub fn snapshot_source(&self) -> crate::WsSessionSnapshotSource {
return self.inner.snapshot_source();
}
/// Returns the latest observable physical-session state.
#[must_use]
pub fn state(&self) -> crate::WsSessionState {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/ws_session.rs
// version: 14
// version: 15
use futures_util::SinkExt; // rust-rules: trait-import
use futures_util::StreamExt; // rust-rules: trait-import
@@ -10,6 +10,43 @@ const HELIUS_WS_HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::f
type WsPhysicalStream = tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
/// Cloneable latest-value observer for one physical WebSocket session snapshot.
///
/// The observer exposes only the already-safe [`crate::WsSessionSnapshot`] projection and keeps the internal Tokio watch channel private. Cloning it does
/// not create another socket, actor, reconnect loop or subscription registry.
#[derive(Clone)]
pub struct WsSessionSnapshotSource {
receiver: tokio::sync::watch::Receiver<crate::WsSessionSnapshot>,
}
impl crate::WsSessionSnapshotSource {
fn new(receiver: tokio::sync::watch::Receiver<crate::WsSessionSnapshot>) -> Self {
return Self { receiver };
}
/// Returns the current safe physical-session snapshot without waiting for another actor transition.
#[must_use]
pub fn current(&self) -> crate::WsSessionSnapshot {
return (*self.receiver.borrow()).clone();
}
/// Waits for one newer safe physical-session snapshot.
///
/// `None` means the owning WebSocket actor dropped the latest-value publisher and no further snapshot can arrive.
pub async fn wait_for_change(&mut self) -> std::option::Option<crate::WsSessionSnapshot> {
return match self.receiver.changed().await {
std::result::Result::Ok(()) => std::option::Option::Some((*self.receiver.borrow_and_update()).clone()),
std::result::Result::Err(_) => std::option::Option::None,
};
}
}
impl std::fmt::Debug for crate::WsSessionSnapshotSource {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("WsSessionSnapshotSource").field("current", &self.current()).finish();
}
}
/// Shareable handle for one explicitly created physical WebSocket session.
///
/// The handle never exposes the sensitive endpoint URL or the underlying socket. All socket I/O is owned by one internal actor task and all caller
@@ -124,6 +161,12 @@ impl WsSession {
return self.snapshot_rx.borrow().clone();
}
/// Returns a cloneable latest-value observer for safe physical-session snapshots.
#[must_use]
pub fn snapshot_source(&self) -> crate::WsSessionSnapshotSource {
return crate::WsSessionSnapshotSource::new(self.snapshot_rx.clone());
}
/// Returns the latest observable physical-session state.
#[must_use]
pub fn state(&self) -> crate::WsSessionState {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/ws_settings.rs
// version: 4
// version: 6
const DEFAULT_WS_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const DEFAULT_WS_COMMAND_QUEUE_CAPACITY: usize = 128;
@@ -401,6 +401,7 @@ pub struct WsEndpointSettings {
provider: crate::WsProviderName,
cluster: crate::WsClusterName,
protocol: crate::WsProtocolKind,
subscription_capabilities: std::option::Option<std::vec::Vec<crate::WsSubscriptionKind>>,
url: crate::WsEndpointUrl,
session: crate::WsSessionSettings,
}
@@ -417,7 +418,42 @@ impl WsEndpointSettings {
url: crate::WsEndpointUrl,
session: crate::WsSessionSettings,
) -> Self {
return Self { name: name.into(), enabled, provider, cluster, protocol, url, session };
return Self {
name: name.into(),
enabled,
provider,
cluster,
protocol,
subscription_capabilities: std::option::Option::None,
url,
session,
};
}
/// Declares the exact WebSocket subscription families supported by this endpoint.
///
/// An endpoint created without this builder remains valid for backward compatibility, but its capabilities are considered undeclared and therefore
/// never constitute positive support evidence for a subscription family. Validation rejects an explicitly declared empty set, duplicates and
/// capabilities that cannot belong to the selected protocol surface.
#[must_use]
pub fn with_subscription_capabilities(mut self, capabilities: std::vec::Vec<crate::WsSubscriptionKind>) -> Self {
self.subscription_capabilities = std::option::Option::Some(capabilities);
return self;
}
/// Returns the explicitly declared WebSocket subscription capabilities, or `None` when the endpoint still uses the legacy undeclared form.
#[must_use]
pub fn subscription_capabilities(&self) -> std::option::Option<&[crate::WsSubscriptionKind]> {
return self.subscription_capabilities.as_deref();
}
/// Returns whether this endpoint explicitly declares support for one WebSocket subscription family.
///
/// Undeclared legacy capability state is fail-closed and therefore returns `false`.
#[must_use]
pub fn supports_subscription(&self, kind: crate::WsSubscriptionKind) -> bool {
return ws_protocol_supports_subscription(self.protocol, kind)
&& self.subscription_capabilities().is_some_and(|capabilities| return capabilities.contains(&kind));
}
/// Returns the logical endpoint name.
@@ -532,6 +568,32 @@ fn validate_ws_endpoint(endpoint: &crate::WsEndpointSettings, endpoint_index: us
if let std::result::Result::Err(error) = validate_ws_descriptor(endpoint.cluster().as_str(), cluster_field.as_str()) {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(capabilities) = endpoint.subscription_capabilities() {
if capabilities.is_empty() {
return ws_invalid_settings(
"explicit WebSocket subscription capabilities must not be empty",
format!("ws_endpoints[{endpoint_index}].subscription_capabilities").as_str(),
);
}
for (capability_index, capability) in capabilities.iter().copied().enumerate() {
let field = format!("ws_endpoints[{endpoint_index}].subscription_capabilities[{capability_index}]");
if !ws_protocol_supports_subscription(endpoint.protocol(), capability) {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket subscription capability is incompatible with endpoint protocol")
.with_context("field", field)
.with_context("subscription_kind", capability.as_str())
.with_context("protocol", endpoint.protocol().as_str()),
);
}
if capabilities[..capability_index].contains(&capability) {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket subscription capabilities must be unique")
.with_context("field", field)
.with_context("subscription_kind", capability.as_str()),
);
}
}
}
if let std::result::Result::Err(error) = endpoint.session().validate() {
return std::result::Result::Err(error);
}
@@ -547,6 +609,34 @@ fn validate_ws_endpoint(endpoint: &crate::WsEndpointSettings, endpoint_index: us
return std::result::Result::Ok(());
}
const fn ws_protocol_supports_subscription(protocol: crate::WsProtocolKind, kind: crate::WsSubscriptionKind) -> bool {
return match protocol {
crate::WsProtocolKind::SolanaStandard => matches!(
kind,
crate::WsSubscriptionKind::Account
| crate::WsSubscriptionKind::Block
| crate::WsSubscriptionKind::Logs
| crate::WsSubscriptionKind::Program
| crate::WsSubscriptionKind::Root
| crate::WsSubscriptionKind::Signature
| crate::WsSubscriptionKind::Slot
| crate::WsSubscriptionKind::SlotsUpdates
| crate::WsSubscriptionKind::Vote
),
crate::WsProtocolKind::HeliusLaserStream => matches!(
kind,
crate::WsSubscriptionKind::Account
| crate::WsSubscriptionKind::Logs
| crate::WsSubscriptionKind::Program
| crate::WsSubscriptionKind::Root
| crate::WsSubscriptionKind::Signature
| crate::WsSubscriptionKind::Slot
| crate::WsSubscriptionKind::SlotsUpdates
| crate::WsSubscriptionKind::HeliusTransaction
),
};
}
fn validate_ws_descriptor(value: &str, field: &str) -> ksp_core_lib::Result<()> {
if value.trim().is_empty() {
return ws_invalid_settings("WebSocket transport descriptor must not be empty", field);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 50
// version: 55
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -569,6 +569,17 @@ fn public_v0_2_7_pre_004_physical_websocket_session_contract_is_available_from_c
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_WS_SESSION_CLOSED.code(), "ws_session_closed");
}
#[test]
fn public_v0_3_14_pre_003_websocket_snapshot_source_is_available_from_crate_root() {
let _physical = ksp_onchain_transport_lib::WsSession::snapshot_source;
let _standard = ksp_onchain_transport_lib::SolanaStandardWsSession::snapshot_source;
let _helius = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::snapshot_source;
let _current = ksp_onchain_transport_lib::WsSessionSnapshotSource::current;
let _wait = ksp_onchain_transport_lib::WsSessionSnapshotSource::wait_for_change;
assert!(std::any::type_name::<ksp_onchain_transport_lib::WsSessionSnapshotSource>().ends_with("WsSessionSnapshotSource"));
return;
}
#[test]
fn public_v0_2_7_pre_005_bounded_websocket_close_contract_is_available_from_crate_root() {
let _close = ksp_onchain_transport_lib::WsSession::close;
@@ -1013,10 +1024,16 @@ fn public_v0_2_9_pre_010_yellowstone_reconnect_snapshot_is_available_from_crate_
let _snapshot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Reconnecting;
let _session_snapshot = ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession::snapshot;
let _snapshot_source = ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession::snapshot_source;
let _source_type = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshotSource>();
let _source_current = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshotSource::current;
let _source_wait = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshotSource::wait_for_change;
let _reconnect_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::reconnect_count;
let _gap_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::continuity_gap_count;
let _duplicate_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::duplicate_update_count;
let _replay_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::replay_attempt_count;
let _delivery_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::replay_delivery_count;
let _coverage_unproven_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::replay_coverage_unproven_count;
let _requested = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_requested_from_slot;
let _observed = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_observed_slot;
let _terminal = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::terminal_error_code;
@@ -1028,3 +1045,33 @@ fn public_v0_3_10_pre_004_observed_get_block_surface_is_available_from_crate_roo
let _ = method;
return;
}
#[test]
fn public_v0_3_13_pre_002_yellowstone_subscribe_identity_is_opaque_and_available_from_crate_root() {
let request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let identity = request.identity().expect("empty validated request identity must build");
let _identity_type = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeRequestIdentity>();
let debug = std::format!("{identity:?}");
assert!(debug.contains("YellowstoneSubscribeRequestIdentity"));
assert!(debug.contains("byte_len"));
assert!(!debug.contains("bytes:"));
return;
}
#[test]
fn public_v0_3_15_pre_002_websocket_endpoint_capability_contract_is_available_from_crate_root() {
let endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
"typed_public",
true,
ksp_onchain_transport_lib::WsProviderName::new("public"),
ksp_onchain_transport_lib::WsClusterName::new("devnet"),
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
ksp_onchain_transport_lib::WsEndpointUrl::parse("wss://api.devnet.solana.com").expect("public WebSocket URL fixture must parse"),
ksp_onchain_transport_lib::WsSessionSettings::default(),
)
.with_subscription_capabilities(std::vec![ksp_onchain_transport_lib::WsSubscriptionKind::Logs, ksp_onchain_transport_lib::WsSubscriptionKind::Block,]);
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
assert!(!endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction));
assert_eq!(endpoint.subscription_capabilities().map(|capabilities| return capabilities.len()), std::option::Option::Some(2));
}

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