diff --git a/CHANGELOG.md b/CHANGELOG.md
index ec604f3..9db3a8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -87,3 +87,7 @@
0.7.47-doc-matrix - Révision documentaire : ajout d’une matrice DEX dédiée, ajout explicite des sources Git/IDL à consulter, et redécoupage du plan `0.7.48+` en un DEX/version par tranche afin d’éviter les lots “tous events/tous decoders” trop larges.
0.7.47-doc-event-coverage - Ajout d'une matrice événementielle complémentaire `DEX_EVENT_COVERAGE_MATRIX.md` pour suivre, par DEX/version, les familles `swap`, `pool_create`, `liquidity`, `position`, `fee`, `reward`, `admin/config`, `mint`, `burn`, `transfer`, `account_create/close`, `wrap/unwrap`, `orderbook`, `vault`, `lock/unlock`, `launch` et `migration`; ajout de `DB_EVENT_MODEL_REVIEW.md` pour clarifier que `k_sol_dex_decoded_events` suffit à l'audit-only mais que des tables transversales sont nécessaires pour exploiter transfers, orderbook, vault, launch/migration et coverage upstream en requêtes métier.
+0.7.48-pre-event-coverage-sync - Raccordement de `k_sol_dex_event_coverage_entries` au registre upstream Git : ajout de `DexEventCoverageService`, sync des entrées registry vers SQLite, inférence conservatoire `event_family` / `expected_db_target`, mapping local limité aux events Raydium déjà connus, refresh des compteurs observés/matérialisés depuis `k_sol_dex_decoded_events` et tables non-trade existantes, sans modification des decoders ni de la matérialisation trade/candle.
+0.7.48-pre-event-coverage-fix-docs - Correction du refresh SQL `k_sol_dex_event_coverage_entries` pour éviter les requêtes dynamiques non compatibles avec `sqlx::query` 0.9 ; mise à jour documentaire README/ROADMAP pour acter `0.7.48-pre` comme checkpoint DB/reporting et réaligner la suite sur l’ordre Raydium avant Meteora (`0.7.48 raydium_cpmm`, `0.7.49 raydium_clmm`, puis Pump/Meteora).
+0.7.48-pre-event-coverage-report - Clôture du checkpoint `0.7.48-pre` : raccordement des summaries `k_sol_dex_event_coverage_entries` aux diagnostics locaux, ajout des compteurs agrégés de couverture au `LocalPipelineDiagnosticSummaryDto` et au `LocalPipelineValidationReportDto`, ajout du profil `0.7.48-pre_event_coverage_db_checkpoint`, exposition du profil dans Demo Pipeline 2, et maintien des invariants : aucun decoder DEX modifié, aucun trade/candle créé, aucun `program_id` promu sans corpus.
+0.7.48-pre-event-coverage-validation-scope - Correction du profil `0.7.48-pre_event_coverage_db_checkpoint` : le contrôle bloquant des trade candidates non matérialisés est maintenant borné aux DEX attendus de la tranche Raydium (`raydium_cpmm`, `raydium_clmm`, `raydium_amm_v4`) afin qu’un DEX partiel hors scope, comme `fluxbeam`, reste diagnostiqué sans bloquer le checkpoint DB/event coverage.
diff --git a/README.md b/README.md
index c718b52..1fd3d0f 100644
--- a/README.md
+++ b/README.md
@@ -438,3 +438,28 @@ Voir :
- `DEX_EVENT_COVERAGE_MATRIX.md` pour les familles d'events à couvrir ;
- `DB_EVENT_MODEL_REVIEW.md` pour les ajouts DB à envisager avant `0.7.48+`.
+
+## Note 0.7.48-pre — Event coverage DB checkpoint
+
+La micro-tranche `0.7.48-pre` introduit la persistance de couverture événementielle avant la reprise DEX par DEX.
+
+Ajouts côté `kb_lib` :
+
+- table `k_sol_dex_event_coverage_entries` ;
+- entity, DTO et requêtes dédiées ;
+- service `DexEventCoverageService` pour synchroniser les entrées du registre upstream Git vers SQLite ;
+- refresh des compteurs locaux depuis `k_sol_dex_decoded_events` et les tables déjà existantes de matérialisation non-trade / trade ;
+- exposition des summaries de coverage dans les diagnostics locaux ;
+- ajout du profil de validation `0.7.48-pre_event_coverage_db_checkpoint`, qui synchronise le registre upstream avant validation ;
+- le profil `0.7.48-pre` garde les invariants globaux de non-régression, mais borne le contrôle bloquant des trade candidates non matérialisés aux DEX Raydium attendus pour éviter qu’un DEX partiel hors scope bloque le checkpoint DB ;
+- sélection du profil `0.7.48-pre` dans Demo Pipeline 2.
+
+Cette tranche ne modifie pas les decoders DEX, ne crée aucun trade/candle, et ne promeut aucun `program_id` comme vérifié. Elle sert uniquement à objectiver la couverture : `listed`, `decoded/audit`, `observed`, `materialized`, `trade_count` et statut de preuve.
+
+La suite fonctionnelle reprend par Raydium avant Meteora :
+
+1. `0.7.48` — `raydium_cpmm` ;
+2. `0.7.49` — `raydium_clmm` ;
+3. `0.7.50` — `pump_swap` ;
+4. `0.7.51` — `pump_fun` ;
+5. `0.7.52+` — Meteora puis les autres DEX/surfaces.
diff --git a/ROADMAP.md b/ROADMAP.md
index be9ba3f..3f5e451 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1242,101 +1242,112 @@ Aucun de ces programmes ne doit être marqué `verified_by_corpus` uniquement pa
Objectif : éviter de limiter la matrice aux DEX/versions et imposer une couverture événementielle exhaustive avant la reprise DEX par DEX.
-À faire :
+Statut : implémenté en micro-tranche DB/reporting, sans modifier les decoders ni la matérialisation marché.
-- maintenir `DEX_EVENT_COVERAGE_MATRIX.md` en plus de `DEX_DECODER_MATRIX.md` ;
-- lister pour chaque DEX/version tous les events/instructions/logs connus depuis Carbon, fnzero, IDL, Pinax, HODL Warden, OpenBook, Phoenix et Vybe ;
+Fait :
+
+- maintien de `DEX_EVENT_COVERAGE_MATRIX.md` en plus de `DEX_DECODER_MATRIX.md` ;
+- ajout de `k_sol_dex_event_coverage_entries` dans `kb_lib/src/db/schema.rs` ;
+- ajout des entity/DTO/queries/re-exports associés ;
+- ajout de `DexEventCoverageService` pour synchroniser les entrées du registre upstream Git vers la table de coverage ;
+- refresh des compteurs `observed_count`, `materialized_count`, `trade_count`, `first_signature` et `last_signature` depuis les events décodés et les tables métier existantes ;
+- inférence conservatoire de `event_family`, `expected_db_target` et `local_event_kind`, sans promotion de `program_id` ni validation métier automatique ;
+- correction du refresh SQL pour rester compatible avec `sqlx::query` en SQL statique ;
+- exposition des summaries de coverage dans `LocalPipelineDiagnosticSummaryDto` et `LocalPipelineValidationReportDto` ;
+- ajout du profil de validation `0.7.48-pre_event_coverage_db_checkpoint`, avec synchronisation upstream préalable ;
+- contrôle bloquant des trade candidates non matérialisés borné aux DEX Raydium attendus dans ce profil, afin que les DEX partiels hors scope restent diagnostiqués sans bloquer le checkpoint DB/reporting ;
+- sélection du profil `0.7.48-pre` dans Demo Pipeline 2.
+
+Reste à faire dans les tranches DEX :
+
+- compléter la liste exhaustive des events/instructions/logs par DEX depuis Carbon, fnzero, IDL, Pinax, HODL Warden, OpenBook, Phoenix et Vybe ;
- inclure explicitement les familles non-trade : `burn`, `mint`, `transfer`, `account_create`, `account_close`, `wrap_sol`, `unwrap_sol`, `lock`, `unlock`, `vault_deposit`, `vault_withdraw`, `admin/config`, `fee`, `reward`, `launch`, `migration` ;
-- vérifier si la DB actuelle suffit ou si une table transversale doit être ajoutée ;
-- prioriser `k_sol_dex_event_coverage_entries`, puis `k_sol_token_transfer_events` et `k_sol_orderbook_events` ;
+- ajouter plus tard `k_sol_token_transfer_events` et `k_sol_orderbook_events` quand le besoin métier est prouvé par plusieurs DEX ;
- ne pas créer de trade/candle depuis ces nouveaux chemins sans validation économique et corpus.
-### 6.080. Version `0.7.48` — `meteora_damm_v2` séparé
-Objectif : reprendre `meteora_damm_v2` comme DEX effectif séparé après disponibilité du registre upstream Git.
+### 6.080. Version `0.7.48` — `raydium_cpmm` event coverage
+Objectif : reprendre `raydium_cpmm` en premier, avant Meteora, avec une couverture complète des events listés depuis Carbon/fnzero/IDL.
À faire :
-- utiliser le registre `0.7.47` comme source d’indices, pas comme preuve ;
-- vérifier `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` dans le corpus local avant de le marquer `verified_by_corpus` ;
-- consolider `create_pool`, swaps exploitables, configs dynamiques, fees/admin et events lifecycle ;
-- conserver les swaps sans payload montant/prix fiables comme `non_actionable_trade` ;
-- ne promouvoir aucun event depuis `instruction_audit` sans signature de validation.
+- utiliser `k_sol_dex_event_coverage_entries` comme ledger de couverture attendu/observé/matérialisé ;
+- lister tous les discriminants/instructions/events CPMM depuis les sources upstream ;
+- comparer avec les events déjà connus localement : swap, initialize, withdraw, collect_creator_fee et audits restants ;
+- conserver les swaps matérialisés uniquement si les montants et le sens économique restent validés ;
+- compléter les events non-trade CPMM en audit ou matérialisation existante uniquement avec corpus local ;
+- vérifier par SQL que les non-trades ne produisent aucun trade/candle.
-### 6.081. Version `0.7.49` — `meteora_dbc` séparé
-Objectif : séparer proprement bonding/launch, swap effectif, migration et attribution d’origine dans `meteora_dbc`.
+### 6.081. Version `0.7.49` — `raydium_clmm` event coverage
+Objectif : reprendre `raydium_clmm` après CPMM, avec couverture des swaps, positions, liquidité, rewards, fees, protocol fees et cas Token-2022.
À faire :
-- distinguer les events de bonding curve / launch des events de DEX effectif ;
-- vérifier swaps exploitables, migration, lifecycle, mint/burn éventuels et launch attribution ;
-- éviter toute candle artificielle sur events de bonding/launch non pricés ;
-- documenter les signatures/corpus avant toute promotion.
+- lister tous les events/instructions CLMM depuis Carbon/fnzero/IDL ;
+- consolider `swap`, `swap_v2`, open/close position, increase/decrease liquidity, reward/fee/admin ;
+- classer les events non observés en `upstream_git_mapped_unverified` ;
+- matérialiser uniquement les events prouvés par corpus ;
+- vérifier absence de faux trades/candles.
-### 6.082. Version `0.7.50` — `orca_whirlpools` séparé
-Objectif : revalider Orca Whirlpools par corpus dédié avant toute promotion au même niveau que Raydium/Meteora.
+### 6.082. Version `0.7.50` — `pump_swap` event coverage
+Objectif : compléter `pump_swap` au-delà de `buy/sell`.
-À faire :
+À faire : couvrir fees, cashback, volume accumulator, admin/config et autres events upstream disponibles, tout en maintenant l’invariant non-trade = zéro trade/candle.
-- revalider create_pool, swap, liquidité, positions, mints et montants fiables ;
-- traiter les swaps Orca partiels comme non-actionnables tant que les montants ne sont pas reconstruits ;
-- matérialiser uniquement les events prouvés ;
-- ajouter des diagnostics par event kind.
+### 6.083. Version `0.7.51` — `pump_fun` launch/bonding/migration
+Objectif : séparer launch/bonding de DEX effectif et valider migration vers PumpSwap ou autre surface tradable.
-### 6.083. Version `0.7.51` — `fluxbeam` séparé
-Objectif : vérifier FluxBeam comme DEX effectif distinct.
+À faire : traiter create, buy/sell bonding, update/config, mint/burn éventuels, migration/graduate et rattachement au pool tradable.
-À faire : constituer un corpus local, vérifier `program_id`, comptes, préfixes `data_json`, swaps, pools, liquidity et events non-trade prouvés.
+### 6.084. Version `0.7.52` — `meteora_dbc` séparé
+Objectif : reprendre Meteora après les tranches Raydium et Pump, en séparant bonding/launch, swap effectif, migration et attribution d’origine.
-### 6.084. Version `0.7.52` — `dexlab` / OpenBook relation
-Objectif : vérifier DexLab comme DEX effectif distinct sans le confondre avec OpenBook ou une couche de marché associée.
+À faire : vérifier swaps exploitables, migration, lifecycle, mint/burn éventuels, launch attribution, fees/admin, sans candle artificielle sur events non pricés.
-À faire : constituer un corpus local, vérifier `program_id`, comptes, préfixes `data_json`, swaps, pools et éventuels liens de market/pool.
+### 6.085. Version `0.7.53` — `meteora_dlmm` parité upstream finale
+Objectif : comparer la couverture locale DLMM déjà avancée avec toutes les sources Git/IDL et documenter ou fermer les audits résiduels.
-### 6.085. Version `0.7.53` — Lifinity / Phoenix / OpenBook / Stabble
-Objectif : traiter les DEX/orderbooks supplémentaires identifiés par le registre upstream Git.
+À faire : revalider swaps, liquidity, positions, lifecycle, fees/rewards/admin, et garder les discriminants non mappés en audit documenté.
-À faire : valider séparément `lifinity_amm_v2`, `phoenix_v1`, `openbook_v2`, `stabble_stable_swap` et `stabble_weighted_swap`, sans matérialiser de trade avant preuve de montants exploitables.
+### 6.086. Version `0.7.54` — `meteora_damm_v1` parité upstream finale
+Objectif : compléter la tranche DAMM v1 déjà engagée, résoudre les surfaces non observées et améliorer le rattachement pool/pair quand possible.
-### 6.086. Version `0.7.54` — BonkSwap / Boop / Moonshot / Heaven / Wavebreak / Vertigo / Virtuals / Pancake / OKX DEX
-Objectif : vérifier les surfaces de swap/launch hybrides ou candidates découvertes via registre et corpus.
+À faire : vérifier toutes les instructions upstream restantes, matérialiser uniquement les events prouvés et documenter les cas sans pool/pair local.
-À faire : séparer DEX effectif, launch surface, routeur/agrégateur et simple candidat ; ne promouvoir aucun `program_id` sans corpus local.
+### 6.087. Version `0.7.55` — `meteora_damm_v2` séparé
+Objectif : reprendre DAMM v2 comme DEX effectif séparé après disponibilité du ledger de coverage.
-### 6.087. Version `0.7.55` — Raydium surfaces complémentaires
-Objectif : traiter `raydium_launchpad`, `raydium_liquidity_locking`, `raydium_stable_swap` et éventuelles surfaces Raydium non couvertes par CPMM/CLMM/AMM v4.
+À faire : consolider create_pool, swaps exploitables, configs dynamiques, liquidity, fees/admin, lifecycle ; conserver les swaps sans payload montant/prix fiable comme `non_actionable_trade`.
-À faire : distinguer launch, lock, stable AMM et AMM legacy ; garder les events non prouvés en audit.
+### 6.088. Version `0.7.56` — `phoenix_v1` audit-only complet
+Objectif : finir tous les events Git disponibles en audit, sans activer de trade/candle.
-### 6.088. Version `0.7.56` — Aggregators, limit orders, perps et lending
-Objectif : intégrer les programmes utiles au routage, aux ordres, aux perps ou au lending sans les confondre avec les DEX effectifs.
+À faire : couvrir `Fill`, `FillSummary`, `Fee`, `Evict`, `ExpiredOrder` et autres logs/events disponibles ; préparer le futur modèle orderbook sans matérialisation marché par défaut.
-À faire : classifier `jupiter_*`, `kamino_*`, `drift_v2`, `marginfi_v2`, `dflow_aggregator_v4`, `zeta` comme contexte/routing/ordres tant qu’ils ne produisent pas directement une surface DEX matérialisable.
+### 6.089. Version `0.7.57` — `openbook_v2` audit-only complet
+Objectif : finir les layouts logs/events OpenBook v2 et définir les conditions futures de matérialisation orderbook/trade.
-### 6.089. Version `0.7.57` — Couverture événementielle DEX consolidée
-Objectif : s’assurer que chaque DEX effectif supporté expose les événements utiles au scoring et au risque sans polluer les trades/candles.
+À faire : vérifier fills, settle, consume events, open orders create/close, maker/taker, lots/decimals et sens économique avant toute promotion.
-À faire : vérifier par DEX `swap`, liquidité, lifecycle, fees, rewards, admin/config, burns/mints utiles, et matérialiser uniquement les événements prouvés.
+### 6.090. Version `0.7.58` — `orca_whirlpools` event coverage
+Objectif : reprendre Whirlpools depuis IDL/source avec corpus dédié.
-### 6.090. Version `0.7.58` — `kb_demo_app` Demo4 : DEX Screener et sources externes de découverte
-Objectif : utiliser des sources externes comme aides à la découverte de corpus sans les traiter comme vérité métier.
+À faire : swaps, pools, positions, liquidity, fees/rewards, tick arrays, mint/burn/Token-2022 si applicable.
-À faire : rechercher des paires par token mint, chain, DEX name, pool address ou program id lorsque disponible, comparer avec la base locale, copier les signatures/adresses candidates pour backfill, sans promotion automatique.
+### 6.091. Version `0.7.59` — Launch surfaces
+Objectif : traiter les surfaces de lancement après les DEX effectifs prioritaires.
-### 6.091. Version `0.7.59` — Démos spécialisées launch surfaces après DEX effectifs
-Objectif : préparer des vues spécialisées pour les launch surfaces après stabilisation des DEX effectifs.
+À faire : Raydium LaunchLab/Launchpad, PumpFun migration, Moonshot/Moonit, Boop, Heaven, Bags, LetsBonk, avec séparation stricte launch origin / pool origin / DEX effectif.
-À faire : couvrir `pump_fun`, `raydium_launchpad`, `believe`, `bags`, `moonshot` / `moonit`, `boop_fun`, `letsbonk` / `bonk_fun`, `heaven`, avec séparation stricte entre launch origin, pool origin, DEX effectif et migration.
+### 6.092. Version `0.7.60` — DEX historiques / candidats
+Objectif : valider les DEX ou surfaces candidates par corpus, sans promotion automatique depuis les sources externes.
-### 6.092. Version `0.7.60` — `kb_demo_app` Demo10 : watcher WebSocket live DEX
-Objectif : valider le passage du replay/backfill vers l’observation temps réel contrôlée.
+À faire : FluxBeam, DexLab, Lifinity, Stabble, BonkSwap, GooseFX, Obric, SolFi et autres entrées Vybe/registry.
-À faire : sélectionner endpoints WS/HTTP et DEX/program ids à souscrire, utiliser le pipeline existant, afficher compteurs live, erreurs, subscriptions actives et derniers objets persistés.
+### 6.093. Version `0.7.61` — Validation consolidée
+Objectif : rejouer une base neuve multi-DEX et valider les invariants du pipeline complet.
-### 6.093. Version `0.7.61` — Validation DEX v1 consolidée
-Objectif : rejouer tous les DEX effectifs supportés et valider les invariants du pipeline complet avant de revenir aux launch surfaces ou à l’analyse `0.8.x`.
-
-À faire : bases neuves, compteurs globaux et par DEX, diagnostics bloquants, samples d’anomalie, corpus documentés et matrice de support par DEX/variante/instruction/event.
+À faire : rapport coverage par DEX/event, zéro faux trade/candle, corpus documentés, matrices cohérentes, diagnostics bloquants à zéro.
### 6.091. Version `0.8.x` — Analyse et filtrage
Objectif : transformer les événements bruts en signaux exploitables.
@@ -1525,18 +1536,23 @@ Préconditions considérées acquises avant cette reprise :
Ordre de travail recommandé pour la suite :
-1. `0.7.44` : ledger de décodage/replay et skip sûr ;
+1. `0.7.44` : ledger de décodage/replay et skip sûr — acquis ;
2. `0.7.45` : `meteora_dlmm` — clos ;
-3. `0.7.46` : `meteora_damm_v1` ;
-4. `0.7.47` : `meteora_damm_v2` ;
-5. `0.7.48` : `meteora_dbc` ;
-6. `0.7.49` : `orca_whirlpools` ;
-7. `0.7.50` : `fluxbeam` ;
-8. `0.7.51` : `dexlab` ;
-9. `0.7.52` : `metaDAO` candidat DEX ;
-10. `0.7.53` : `printr` candidat DEX ;
-11. `0.7.54` : couverture événementielle DEX consolidée ;
-12. `0.7.55+` : sources externes de découverte, launch surfaces, watcher live et validation consolidée.
+3. `0.7.46` : `meteora_damm_v1` — clos côté corpus local ;
+4. `0.7.47` : Upstream Git Registry / DEX discovery preparation — acquis ;
+5. `0.7.48-pre` : event coverage + DB model checkpoint — clos après table, sync upstream, refresh counts, diagnostics et profil validation ;
+6. `0.7.48` : `raydium_cpmm` ;
+7. `0.7.49` : `raydium_clmm` ;
+8. `0.7.50` : `pump_swap` ;
+9. `0.7.51` : `pump_fun` ;
+10. `0.7.52` : `meteora_dbc` ;
+11. `0.7.53` : `meteora_dlmm` parité upstream finale ;
+12. `0.7.54` : `meteora_damm_v1` parité upstream finale ;
+13. `0.7.55` : `meteora_damm_v2` ;
+14. `0.7.56` : `phoenix_v1` audit-only complet ;
+15. `0.7.57` : `openbook_v2` audit-only complet ;
+16. `0.7.58` : `orca_whirlpools` ;
+17. `0.7.59+` : launch surfaces, DEX candidats/historiques et validation consolidée.
Garde-fous constants :
diff --git a/kb_demo_app/frontend/demo_pipeline2.html b/kb_demo_app/frontend/demo_pipeline2.html
index 5331c05..77521df 100644
--- a/kb_demo_app/frontend/demo_pipeline2.html
+++ b/kb_demo_app/frontend/demo_pipeline2.html
@@ -197,7 +197,8 @@
Validation profile
- 0.7.43 — Meteora family event coverage
+ 0.7.48-pre — Event coverage DB checkpoint
+ 0.7.43 — Meteora family event coverage
0.7.42 — Raydium family event coverage
0.7.41 — Raydium AMM v4 swap decoder
0.7.40 — Raydium effective surfaces
diff --git a/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineDiagnosticSummary.ts b/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineDiagnosticSummary.ts
index e58c966..a708db1 100644
--- a/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineDiagnosticSummary.ts
+++ b/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineDiagnosticSummary.ts
@@ -75,6 +75,26 @@ rewardEventCount: number,
* Total persisted pool administration events.
*/
poolAdminEventCount: number,
+/**
+ * Event coverage entries listed from upstream registry sources.
+ */
+eventCoverageListedEntryCount: number,
+/**
+ * Event coverage entries mapped to local decoder event kinds.
+ */
+eventCoverageDecodedEntryCount: number,
+/**
+ * Event coverage entries observed in local corpus.
+ */
+eventCoverageObservedEntryCount: number,
+/**
+ * Event coverage entries materialized into target tables.
+ */
+eventCoverageMaterializedEntryCount: number,
+/**
+ * Coverage rows whose mapped events are not locally observed yet.
+ */
+eventCoverageUpstreamGitMappedUnverifiedEntryCount: number,
/**
* Whether the local persisted pipeline has no blocking diagnostic issue.
*/
diff --git a/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineValidationReport.ts b/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineValidationReport.ts
index d59dee9..c756038 100644
--- a/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineValidationReport.ts
+++ b/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineValidationReport.ts
@@ -62,6 +62,26 @@ rewardEventCount: number,
* Total persisted pool administration events.
*/
poolAdminEventCount: number,
+/**
+ * Event coverage entries listed from upstream registry sources.
+ */
+eventCoverageListedEntryCount: number,
+/**
+ * Event coverage entries mapped to local decoder event kinds.
+ */
+eventCoverageDecodedEntryCount: number,
+/**
+ * Event coverage entries observed in local corpus.
+ */
+eventCoverageObservedEntryCount: number,
+/**
+ * Event coverage entries materialized into target tables.
+ */
+eventCoverageMaterializedEntryCount: number,
+/**
+ * Coverage rows whose mapped events are not locally observed yet.
+ */
+eventCoverageUpstreamGitMappedUnverifiedEntryCount: number,
/**
* Total known tokens.
*/
diff --git a/kb_demo_app/src/demo_pipeline2.rs b/kb_demo_app/src/demo_pipeline2.rs
index ae4948f..3a66b94 100644
--- a/kb_demo_app/src/demo_pipeline2.rs
+++ b/kb_demo_app/src/demo_pipeline2.rs
@@ -176,6 +176,21 @@ pub(crate) struct DemoPipeline2LocalPipelineValidationReport {
/// Total persisted pool administration events.
#[ts(type = "number")]
pub pool_admin_event_count: i64,
+ /// Event coverage entries listed from upstream registry sources.
+ #[ts(type = "number")]
+ pub event_coverage_listed_entry_count: u64,
+ /// Event coverage entries mapped to local decoder event kinds.
+ #[ts(type = "number")]
+ pub event_coverage_decoded_entry_count: u64,
+ /// Event coverage entries observed in local corpus.
+ #[ts(type = "number")]
+ pub event_coverage_observed_entry_count: u64,
+ /// Event coverage entries materialized into target tables.
+ #[ts(type = "number")]
+ pub event_coverage_materialized_entry_count: u64,
+ /// Coverage rows whose mapped events are not locally observed yet.
+ #[ts(type = "number")]
+ pub event_coverage_upstream_git_mapped_unverified_entry_count: u64,
/// Total known tokens.
#[ts(type = "number")]
pub token_count: i64,
@@ -327,6 +342,21 @@ pub(crate) struct DemoPipeline2LocalPipelineDiagnosticSummary {
/// Total persisted pool administration events.
#[ts(type = "number")]
pub pool_admin_event_count: i64,
+ /// Event coverage entries listed from upstream registry sources.
+ #[ts(type = "number")]
+ pub event_coverage_listed_entry_count: u64,
+ /// Event coverage entries mapped to local decoder event kinds.
+ #[ts(type = "number")]
+ pub event_coverage_decoded_entry_count: u64,
+ /// Event coverage entries observed in local corpus.
+ #[ts(type = "number")]
+ pub event_coverage_observed_entry_count: u64,
+ /// Event coverage entries materialized into target tables.
+ #[ts(type = "number")]
+ pub event_coverage_materialized_entry_count: u64,
+ /// Coverage rows whose mapped events are not locally observed yet.
+ #[ts(type = "number")]
+ pub event_coverage_upstream_git_mapped_unverified_entry_count: u64,
/// Whether the local persisted pipeline has no blocking diagnostic issue.
pub diagnostics_clean: bool,
/// Number of blocking diagnostic issues.
@@ -1279,7 +1309,7 @@ pub(crate) async fn demo_pipeline2_validate_local_pipeline(
let service = kb_lib::LocalPipelineValidationService::new(database.clone());
let profile_code = match request {
Some(request) => request.profile_code,
- None => "0.7.43_meteora_effective_surfaces".to_string(),
+ None => "0.7.48-pre_event_coverage_db_checkpoint".to_string(),
};
let run_result = match profile_code.as_str() {
"0.7.27" | "0.7.27_dexes_non_regression" => {
@@ -1335,6 +1365,9 @@ pub(crate) async fn demo_pipeline2_validate_local_pipeline(
"0.7.43" | "0.7.43_meteora_effective_surfaces" => {
service.validate_v0_7_43_current_database().await
},
+ "0.7.48-pre" | "0.7.48-pre_event_coverage_db_checkpoint" => {
+ service.validate_v0_7_48_pre_current_database().await
+ },
other => Err(kb_lib::Error::InvalidState(format!(
"unsupported local pipeline validation profile: {other}"
))),
@@ -1821,6 +1854,12 @@ fn demo_pipeline2_map_local_validation_report(
fee_event_count: report.fee_event_count,
reward_event_count: report.reward_event_count,
pool_admin_event_count: report.pool_admin_event_count,
+ event_coverage_listed_entry_count: report.event_coverage_listed_entry_count,
+ event_coverage_decoded_entry_count: report.event_coverage_decoded_entry_count,
+ event_coverage_observed_entry_count: report.event_coverage_observed_entry_count,
+ event_coverage_materialized_entry_count: report.event_coverage_materialized_entry_count,
+ event_coverage_upstream_git_mapped_unverified_entry_count: report
+ .event_coverage_upstream_git_mapped_unverified_entry_count,
token_count: report.token_count,
token_metadata_missing_count: report.token_metadata_missing_count,
tradable_token_metadata_missing_count: report.tradable_token_metadata_missing_count,
@@ -1973,6 +2012,12 @@ fn demo_pipeline2_map_local_diagnostics_summary(
fee_event_count: summary.fee_event_count,
reward_event_count: summary.reward_event_count,
pool_admin_event_count: summary.pool_admin_event_count,
+ event_coverage_listed_entry_count: summary.event_coverage_listed_entry_count,
+ event_coverage_decoded_entry_count: summary.event_coverage_decoded_entry_count,
+ event_coverage_observed_entry_count: summary.event_coverage_observed_entry_count,
+ event_coverage_materialized_entry_count: summary.event_coverage_materialized_entry_count,
+ event_coverage_upstream_git_mapped_unverified_entry_count: summary
+ .event_coverage_upstream_git_mapped_unverified_entry_count,
diagnostics_clean: summary.diagnostics_clean,
blocking_issue_count: summary.blocking_issue_count,
missing_trade_event_count: summary.missing_trade_event_count,
diff --git a/kb_lib/src/db.rs b/kb_lib/src/db.rs
index 127ee6b..206d6bb 100644
--- a/kb_lib/src/db.rs
+++ b/kb_lib/src/db.rs
@@ -24,6 +24,8 @@ pub use dtos::DbRuntimeEventDto;
pub use dtos::DexDecodeReplayLedgerDto;
pub use dtos::DexDecodedEventDto;
pub use dtos::DexDto;
+pub use dtos::DexEventCoverageEntryDto;
+pub use dtos::DexEventCoverageSummaryDto;
pub use dtos::FeeEventDto;
pub use dtos::KnownHttpEndpointDto;
pub use dtos::KnownWsEndpointDto;
@@ -91,6 +93,8 @@ pub use entities::DbRuntimeEventEntity;
pub use entities::DexDecodeReplayLedgerEntity;
pub use entities::DexDecodedEventEntity;
pub use entities::DexEntity;
+pub use entities::DexEventCoverageEntryEntity;
+pub use entities::DexEventCoverageSummaryEntity;
pub use entities::FeeEventEntity;
pub use entities::KnownHttpEndpointEntity;
pub use entities::KnownWsEndpointEntity;
@@ -152,6 +156,12 @@ pub use queries::query_dex_decoded_events_get_by_key;
pub use queries::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint;
pub use queries::query_dex_decoded_events_list_by_transaction_id;
pub use queries::query_dex_decoded_events_upsert;
+pub use queries::query_dex_event_coverage_entries_delete_by_decoder;
+pub use queries::query_dex_event_coverage_entries_list_by_decoder;
+pub use queries::query_dex_event_coverage_entries_list_summary_by_decoder;
+pub use queries::query_dex_event_coverage_entries_refresh_local_counts;
+pub use queries::query_dex_event_coverage_entries_refresh_local_counts_by_decoder;
+pub use queries::query_dex_event_coverage_entries_upsert;
pub use queries::query_dexs_get_by_code;
pub use queries::query_dexs_list;
pub use queries::query_dexs_upsert;
diff --git a/kb_lib/src/db/dtos.rs b/kb_lib/src/db/dtos.rs
index ee741a6..5bc53da 100644
--- a/kb_lib/src/db/dtos.rs
+++ b/kb_lib/src/db/dtos.rs
@@ -9,8 +9,9 @@ mod chain_transaction;
mod db_metadata;
mod db_runtime_event;
mod dex;
-mod dex_decoded_event;
mod dex_decode_replay_ledger;
+mod dex_decoded_event;
+mod dex_event_coverage_entry;
mod fee_event;
mod known_http_endpoint;
mod known_ws_endpoint;
@@ -76,8 +77,10 @@ pub use chain_transaction::ChainTransactionDto;
pub use db_metadata::DbMetadataDto;
pub use db_runtime_event::DbRuntimeEventDto;
pub use dex::DexDto;
-pub use dex_decoded_event::DexDecodedEventDto;
pub use dex_decode_replay_ledger::DexDecodeReplayLedgerDto;
+pub use dex_decoded_event::DexDecodedEventDto;
+pub use dex_event_coverage_entry::DexEventCoverageEntryDto;
+pub use dex_event_coverage_entry::DexEventCoverageSummaryDto;
pub use fee_event::FeeEventDto;
pub use known_http_endpoint::KnownHttpEndpointDto;
pub use known_ws_endpoint::KnownWsEndpointDto;
diff --git a/kb_lib/src/db/dtos/dex_event_coverage_entry.rs b/kb_lib/src/db/dtos/dex_event_coverage_entry.rs
new file mode 100644
index 0000000..349732e
--- /dev/null
+++ b/kb_lib/src/db/dtos/dex_event_coverage_entry.rs
@@ -0,0 +1,407 @@
+// file: kb_lib/src/db/dtos/dex_event_coverage_entry.rs
+
+//! Application-facing DTOs for DEX event coverage.
+
+/// Application-facing coverage row for one upstream/local event entry.
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct DexEventCoverageEntryDto {
+ /// Optional numeric primary key.
+ pub id: std::option::Option,
+ /// Stable unique key used for idempotent upserts.
+ pub coverage_key: std::string::String,
+ /// Internal decoder code.
+ pub decoder_code: std::string::String,
+ /// Optional Solana program id associated with the source entry.
+ pub program_id: std::option::Option,
+ /// Program family.
+ pub program_family: std::string::String,
+ /// Surface kind such as AMM, CLMM, launch, orderbook or aggregator.
+ pub surface_kind: std::string::String,
+ /// Source repository or registry source.
+ pub source_repo: std::option::Option,
+ /// Source path inside the repository or registry source.
+ pub source_path: std::option::Option,
+ /// Entry kind such as instruction, event, account, log or program_data.
+ pub entry_kind: std::string::String,
+ /// Source entry name.
+ pub entry_name: std::string::String,
+ /// Optional discriminator encoded as lowercase hexadecimal.
+ pub discriminator_hex: std::option::Option,
+ /// Optional discriminator byte length.
+ pub discriminator_len: std::option::Option,
+ /// Normalized event family.
+ pub event_family: std::option::Option,
+ /// Expected DB target table or audit-only target.
+ pub expected_db_target: std::option::Option,
+ /// Proof status for this entry.
+ pub proof_status: std::string::String,
+ /// Local event kind produced by the decoder, when any.
+ pub local_event_kind: std::option::Option,
+ /// Number of observed decoded/audit rows in the local corpus.
+ pub observed_count: i64,
+ /// Number of rows materialized into specialized business tables.
+ pub materialized_count: i64,
+ /// Number of trade rows produced from this entry.
+ pub trade_count: i64,
+ /// First local signature observed for this entry.
+ pub first_signature: std::option::Option,
+ /// Last local signature observed for this entry.
+ pub last_signature: std::option::Option,
+ /// Notes preserving uncertainty, mapping decisions or DB gaps.
+ pub notes: std::option::Option,
+ /// Creation timestamp.
+ pub created_at: chrono::DateTime,
+ /// Update timestamp.
+ pub updated_at: chrono::DateTime,
+}
+
+impl DexEventCoverageEntryDto {
+ /// Audit-only target for decoded events that should not leave `k_sol_dex_decoded_events`.
+ pub const DB_TARGET_DECODED_EVENTS_ONLY: &'static str = "k_sol_dex_decoded_events_only";
+ /// Target table for validated AMM/CLOB trade rows.
+ pub const DB_TARGET_TRADE_EVENTS: &'static str = "k_sol_trade_events";
+ /// Target table for validated liquidity add/remove rows.
+ pub const DB_TARGET_LIQUIDITY_EVENTS: &'static str = "k_sol_liquidity_events";
+ /// Target table for validated pool lifecycle and position lifecycle rows.
+ pub const DB_TARGET_POOL_LIFECYCLE_EVENTS: &'static str = "k_sol_pool_lifecycle_events";
+ /// Target table for validated fee rows.
+ pub const DB_TARGET_FEE_EVENTS: &'static str = "k_sol_fee_events";
+ /// Target table for validated reward rows.
+ pub const DB_TARGET_REWARD_EVENTS: &'static str = "k_sol_reward_events";
+ /// Target table for validated admin/config rows.
+ pub const DB_TARGET_POOL_ADMIN_EVENTS: &'static str = "k_sol_pool_admin_events";
+ /// Target table for validated token mint rows.
+ pub const DB_TARGET_TOKEN_MINT_EVENTS: &'static str = "k_sol_token_mint_events";
+ /// Target table for validated token burn rows.
+ pub const DB_TARGET_TOKEN_BURN_EVENTS: &'static str = "k_sol_token_burn_events";
+ /// Planned target table for transfer rows.
+ pub const DB_TARGET_TOKEN_TRANSFER_EVENTS: &'static str = "k_sol_token_transfer_events";
+ /// Planned target table for token account lifecycle rows.
+ pub const DB_TARGET_TOKEN_ACCOUNT_EVENTS: &'static str = "k_sol_token_account_events";
+ /// Planned target table for orderbook rows.
+ pub const DB_TARGET_ORDERBOOK_EVENTS: &'static str = "k_sol_orderbook_events";
+ /// Planned target table for vault rows.
+ pub const DB_TARGET_VAULT_EVENTS: &'static str = "k_sol_vault_events";
+ /// Planned target table for launch and migration rows.
+ pub const DB_TARGET_LAUNCH_EVENTS: &'static str = "k_sol_launch_events";
+ /// Planned target table for liquidity lock rows.
+ pub const DB_TARGET_LIQUIDITY_LOCK_EVENTS: &'static str = "k_sol_liquidity_lock_events";
+
+ /// Builds a stable coverage key from source identity fields.
+ pub fn build_coverage_key(
+ decoder_code: &str,
+ program_id: &std::option::Option,
+ source_repo: &std::option::Option,
+ source_path: &std::option::Option,
+ entry_kind: &str,
+ entry_name: &str,
+ discriminator_hex: &std::option::Option,
+ ) -> std::string::String {
+ return format!(
+ "{}|{}|{}|{}|{}|{}|{}",
+ decoder_code,
+ program_id.clone().unwrap_or_default(),
+ source_repo.clone().unwrap_or_default(),
+ source_path.clone().unwrap_or_default(),
+ entry_kind,
+ entry_name,
+ discriminator_hex.clone().unwrap_or_default()
+ );
+ }
+
+ /// Creates a new event coverage entry DTO.
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ decoder_code: std::string::String,
+ program_id: std::option::Option,
+ program_family: std::string::String,
+ surface_kind: std::string::String,
+ source_repo: std::option::Option,
+ source_path: std::option::Option,
+ entry_kind: std::string::String,
+ entry_name: std::string::String,
+ discriminator_hex: std::option::Option,
+ discriminator_len: std::option::Option,
+ event_family: std::option::Option,
+ expected_db_target: std::option::Option,
+ proof_status: std::string::String,
+ local_event_kind: std::option::Option,
+ observed_count: i64,
+ materialized_count: i64,
+ trade_count: i64,
+ first_signature: std::option::Option,
+ last_signature: std::option::Option,
+ notes: std::option::Option,
+ ) -> Self {
+ let now = chrono::Utc::now();
+ let coverage_key = Self::build_coverage_key(
+ decoder_code.as_str(),
+ &program_id,
+ &source_repo,
+ &source_path,
+ entry_kind.as_str(),
+ entry_name.as_str(),
+ &discriminator_hex,
+ );
+ return Self {
+ id: None,
+ coverage_key,
+ decoder_code,
+ program_id,
+ program_family,
+ surface_kind,
+ source_repo,
+ source_path,
+ entry_kind,
+ entry_name,
+ discriminator_hex,
+ discriminator_len,
+ event_family,
+ expected_db_target,
+ proof_status,
+ local_event_kind,
+ observed_count,
+ materialized_count,
+ trade_count,
+ first_signature,
+ last_signature,
+ notes,
+ created_at: now,
+ updated_at: now,
+ };
+ }
+
+ /// Creates a coverage row from an upstream registry entry without claiming local proof.
+ pub fn from_upstream_registry_entry(
+ entry: &crate::UpstreamRegistryEntryDto,
+ event_family: std::option::Option,
+ expected_db_target: std::option::Option,
+ local_event_kind: std::option::Option,
+ ) -> Self {
+ let discriminator_len = entry.discriminator_len.map(i64::from);
+ return Self::new(
+ entry.decoder_code.clone(),
+ entry.program_id.clone(),
+ entry.program_family.clone(),
+ entry.surface_kind.clone(),
+ entry.source_repo.clone(),
+ entry.source_path.clone(),
+ entry.entry_kind.clone(),
+ entry.entry_name.clone(),
+ entry.discriminator_hex.clone(),
+ discriminator_len,
+ event_family,
+ expected_db_target,
+ entry.proof_status.clone(),
+ local_event_kind,
+ 0,
+ 0,
+ 0,
+ None,
+ None,
+ Some(entry.notes.clone()),
+ );
+ }
+}
+
+impl TryFrom for DexEventCoverageEntryDto {
+ type Error = crate::Error;
+
+ fn try_from(entity: crate::DexEventCoverageEntryEntity) -> Result {
+ let created_at_result = chrono::DateTime::parse_from_rfc3339(&entity.created_at);
+ let created_at = match created_at_result {
+ Ok(created_at) => created_at.with_timezone(&chrono::Utc),
+ Err(error) => {
+ return Err(crate::Error::Db(format!(
+ "cannot parse dex event coverage created_at '{}': {}",
+ entity.created_at, error
+ )));
+ },
+ };
+ let updated_at_result = chrono::DateTime::parse_from_rfc3339(&entity.updated_at);
+ let updated_at = match updated_at_result {
+ Ok(updated_at) => updated_at.with_timezone(&chrono::Utc),
+ Err(error) => {
+ return Err(crate::Error::Db(format!(
+ "cannot parse dex event coverage updated_at '{}': {}",
+ entity.updated_at, error
+ )));
+ },
+ };
+ return Ok(Self {
+ id: Some(entity.id),
+ coverage_key: entity.coverage_key,
+ decoder_code: entity.decoder_code,
+ program_id: entity.program_id,
+ program_family: entity.program_family,
+ surface_kind: entity.surface_kind,
+ source_repo: entity.source_repo,
+ source_path: entity.source_path,
+ entry_kind: entity.entry_kind,
+ entry_name: entity.entry_name,
+ discriminator_hex: entity.discriminator_hex,
+ discriminator_len: entity.discriminator_len,
+ event_family: entity.event_family,
+ expected_db_target: entity.expected_db_target,
+ proof_status: entity.proof_status,
+ local_event_kind: entity.local_event_kind,
+ observed_count: entity.observed_count,
+ materialized_count: entity.materialized_count,
+ trade_count: entity.trade_count,
+ first_signature: entity.first_signature,
+ last_signature: entity.last_signature,
+ notes: entity.notes,
+ created_at,
+ updated_at,
+ });
+ }
+}
+
+/// Application-facing event coverage summary grouped by decoder.
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct DexEventCoverageSummaryDto {
+ /// Internal decoder code.
+ pub decoder_code: std::string::String,
+ /// Number of listed coverage entries.
+ pub listed_entry_count: u64,
+ /// Number of entries wired to a local event kind.
+ pub decoded_entry_count: u64,
+ /// Number of entries observed at least once in local corpus counts.
+ pub observed_entry_count: u64,
+ /// Number of entries materialized at least once.
+ pub materialized_entry_count: u64,
+ /// Sum of local observed counts.
+ pub total_observed_count: u64,
+ /// Sum of local materialized counts.
+ pub total_materialized_count: u64,
+ /// Sum of trade counts produced by these entries.
+ pub trade_count: u64,
+ /// Number of decoded entries that remain audit-only.
+ pub audit_only_entry_count: u64,
+ /// Number of entries without an expected DB target.
+ pub missing_db_target_entry_count: u64,
+ /// Number of entries still upstream Git unverified.
+ pub upstream_git_unverified_entry_count: u64,
+ /// Number of entries mapped but not locally observed.
+ pub upstream_git_mapped_unverified_entry_count: u64,
+ /// Number of entries observed in local corpus.
+ pub upstream_git_local_corpus_observed_entry_count: u64,
+ /// Number of entries materialized from local corpus.
+ pub upstream_git_local_corpus_materialized_entry_count: u64,
+}
+
+impl DexEventCoverageSummaryDto {
+ fn convert_count(value: i64, field_name: &str) -> Result {
+ let result = u64::try_from(value);
+ match result {
+ Ok(converted) => return Ok(converted),
+ Err(error) => {
+ return Err(crate::Error::Db(format!(
+ "cannot convert dex event coverage summary {} '{}' to u64: {}",
+ field_name, value, error
+ )));
+ },
+ }
+ }
+}
+
+impl TryFrom for DexEventCoverageSummaryDto {
+ type Error = crate::Error;
+
+ fn try_from(entity: crate::DexEventCoverageSummaryEntity) -> Result {
+ let listed_entry_count =
+ match Self::convert_count(entity.listed_entry_count, "listed_entry_count") {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let decoded_entry_count =
+ match Self::convert_count(entity.decoded_entry_count, "decoded_entry_count") {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let observed_entry_count =
+ match Self::convert_count(entity.observed_entry_count, "observed_entry_count") {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let materialized_entry_count = match Self::convert_count(
+ entity.materialized_entry_count,
+ "materialized_entry_count",
+ ) {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let total_observed_count =
+ match Self::convert_count(entity.total_observed_count, "total_observed_count") {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let total_materialized_count = match Self::convert_count(
+ entity.total_materialized_count,
+ "total_materialized_count",
+ ) {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let trade_count = match Self::convert_count(entity.trade_count, "trade_count") {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let audit_only_entry_count =
+ match Self::convert_count(entity.audit_only_entry_count, "audit_only_entry_count") {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let missing_db_target_entry_count = match Self::convert_count(
+ entity.missing_db_target_entry_count,
+ "missing_db_target_entry_count",
+ ) {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let upstream_git_unverified_entry_count = match Self::convert_count(
+ entity.upstream_git_unverified_entry_count,
+ "upstream_git_unverified_entry_count",
+ ) {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let upstream_git_mapped_unverified_entry_count = match Self::convert_count(
+ entity.upstream_git_mapped_unverified_entry_count,
+ "upstream_git_mapped_unverified_entry_count",
+ ) {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let upstream_git_local_corpus_observed_entry_count = match Self::convert_count(
+ entity.upstream_git_local_corpus_observed_entry_count,
+ "upstream_git_local_corpus_observed_entry_count",
+ ) {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ let upstream_git_local_corpus_materialized_entry_count = match Self::convert_count(
+ entity.upstream_git_local_corpus_materialized_entry_count,
+ "upstream_git_local_corpus_materialized_entry_count",
+ ) {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ };
+ return Ok(Self {
+ decoder_code: entity.decoder_code,
+ listed_entry_count,
+ decoded_entry_count,
+ observed_entry_count,
+ materialized_entry_count,
+ total_observed_count,
+ total_materialized_count,
+ trade_count,
+ audit_only_entry_count,
+ missing_db_target_entry_count,
+ upstream_git_unverified_entry_count,
+ upstream_git_mapped_unverified_entry_count,
+ upstream_git_local_corpus_observed_entry_count,
+ upstream_git_local_corpus_materialized_entry_count,
+ });
+ }
+}
diff --git a/kb_lib/src/db/dtos/local_pipeline_diagnostics.rs b/kb_lib/src/db/dtos/local_pipeline_diagnostics.rs
index 8880bf5..96528b7 100644
--- a/kb_lib/src/db/dtos/local_pipeline_diagnostics.rs
+++ b/kb_lib/src/db/dtos/local_pipeline_diagnostics.rs
@@ -33,6 +33,34 @@ pub struct LocalPipelineDiagnosticSummaryDto {
pub reward_event_count: i64,
/// Total persisted pool administration events.
pub pool_admin_event_count: i64,
+ /// Event coverage entries listed from upstream registry sources.
+ pub event_coverage_listed_entry_count: u64,
+ /// Event coverage entries that have a local decoder event kind mapping.
+ pub event_coverage_decoded_entry_count: u64,
+ /// Event coverage entries observed at least once in the local corpus.
+ pub event_coverage_observed_entry_count: u64,
+ /// Event coverage entries materialized at least once into a DB target.
+ pub event_coverage_materialized_entry_count: u64,
+ /// Sum of decoded-event observations across coverage entries.
+ pub event_coverage_total_observed_count: u64,
+ /// Sum of materialized rows across coverage entries.
+ pub event_coverage_total_materialized_count: u64,
+ /// Sum of trade rows linked to coverage entries.
+ pub event_coverage_trade_count: u64,
+ /// Coverage entries intentionally expected to remain audit-only.
+ pub event_coverage_audit_only_entry_count: u64,
+ /// Coverage entries whose DB target is still missing or undecided.
+ pub event_coverage_missing_db_target_entry_count: u64,
+ /// Coverage entries still marked as upstream Git unverified.
+ pub event_coverage_upstream_git_unverified_entry_count: u64,
+ /// Coverage entries mapped to local semantics but not observed locally yet.
+ pub event_coverage_upstream_git_mapped_unverified_entry_count: u64,
+ /// Coverage entries observed in local corpus but not necessarily materialized.
+ pub event_coverage_upstream_git_local_corpus_observed_entry_count: u64,
+ /// Coverage entries observed and materialized from local corpus.
+ pub event_coverage_upstream_git_local_corpus_materialized_entry_count: u64,
+ /// Event coverage summaries grouped by decoder.
+ pub event_coverage_summaries: std::vec::Vec,
/// Whether the local persisted pipeline has no blocking diagnostic issue.
pub diagnostics_clean: bool,
/// Number of blocking diagnostic issues.
@@ -123,8 +151,7 @@ pub struct LocalPipelineDiagnosticSummaryDto {
/// Diagnostics grouped by DEX.
pub dex_summaries: std::vec::Vec,
/// Raydium surface diagnostics derived from the matrix and observed instructions.
- pub raydium_surface_summaries:
- std::vec::Vec,
+ pub raydium_surface_summaries: std::vec::Vec,
/// Diagnostics grouped by pair.
pub pair_summaries: std::vec::Vec,
/// Diagnostics grouped by pair materialization/actionability class.
diff --git a/kb_lib/src/db/entities.rs b/kb_lib/src/db/entities.rs
index 9e89111..3c44045 100644
--- a/kb_lib/src/db/entities.rs
+++ b/kb_lib/src/db/entities.rs
@@ -11,8 +11,9 @@ mod chain_transaction;
mod db_metadata;
mod db_runtime_event;
mod dex;
-mod dex_decoded_event;
mod dex_decode_replay_ledger;
+mod dex_decoded_event;
+mod dex_event_coverage_entry;
mod fee_event;
mod known_http_endpoint;
mod known_ws_endpoint;
@@ -54,8 +55,10 @@ pub use chain_transaction::ChainTransactionEntity;
pub use db_metadata::DbMetadataEntity;
pub use db_runtime_event::DbRuntimeEventEntity;
pub use dex::DexEntity;
-pub use dex_decoded_event::DexDecodedEventEntity;
pub use dex_decode_replay_ledger::DexDecodeReplayLedgerEntity;
+pub use dex_decoded_event::DexDecodedEventEntity;
+pub use dex_event_coverage_entry::DexEventCoverageEntryEntity;
+pub use dex_event_coverage_entry::DexEventCoverageSummaryEntity;
pub use fee_event::FeeEventEntity;
pub use known_http_endpoint::KnownHttpEndpointEntity;
pub use known_ws_endpoint::KnownWsEndpointEntity;
diff --git a/kb_lib/src/db/entities/dex_event_coverage_entry.rs b/kb_lib/src/db/entities/dex_event_coverage_entry.rs
new file mode 100644
index 0000000..e181643
--- /dev/null
+++ b/kb_lib/src/db/entities/dex_event_coverage_entry.rs
@@ -0,0 +1,89 @@
+// file: kb_lib/src/db/entities/dex_event_coverage_entry.rs
+
+//! Database entities for DEX event coverage rows.
+
+/// Persisted coverage row for one listed upstream/local event entry.
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
+pub struct DexEventCoverageEntryEntity {
+ /// Internal row id.
+ pub id: i64,
+ /// Stable unique key used for idempotent upserts.
+ pub coverage_key: std::string::String,
+ /// Internal decoder code.
+ pub decoder_code: std::string::String,
+ /// Optional Solana program id associated with the source entry.
+ pub program_id: std::option::Option,
+ /// Program family.
+ pub program_family: std::string::String,
+ /// Surface kind such as AMM, CLMM, launch, orderbook or aggregator.
+ pub surface_kind: std::string::String,
+ /// Source repository or registry source.
+ pub source_repo: std::option::Option,
+ /// Source path inside the repository or registry source.
+ pub source_path: std::option::Option,
+ /// Entry kind such as instruction, event, account, log or program_data.
+ pub entry_kind: std::string::String,
+ /// Source entry name.
+ pub entry_name: std::string::String,
+ /// Optional discriminator encoded as lowercase hexadecimal.
+ pub discriminator_hex: std::option::Option,
+ /// Optional discriminator byte length.
+ pub discriminator_len: std::option::Option,
+ /// Normalized event family.
+ pub event_family: std::option::Option,
+ /// Expected DB target table or audit-only target.
+ pub expected_db_target: std::option::Option,
+ /// Proof status for this entry.
+ pub proof_status: std::string::String,
+ /// Local event kind produced by the decoder, when any.
+ pub local_event_kind: std::option::Option,
+ /// Number of observed decoded/audit rows in the local corpus.
+ pub observed_count: i64,
+ /// Number of rows materialized into specialized business tables.
+ pub materialized_count: i64,
+ /// Number of trade rows produced from this entry.
+ pub trade_count: i64,
+ /// First local signature observed for this entry.
+ pub first_signature: std::option::Option,
+ /// Last local signature observed for this entry.
+ pub last_signature: std::option::Option,
+ /// Notes preserving uncertainty, mapping decisions or DB gaps.
+ pub notes: std::option::Option,
+ /// Creation timestamp.
+ pub created_at: std::string::String,
+ /// Update timestamp.
+ pub updated_at: std::string::String,
+}
+
+/// Aggregated event coverage row grouped by decoder.
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
+pub struct DexEventCoverageSummaryEntity {
+ /// Internal decoder code.
+ pub decoder_code: std::string::String,
+ /// Number of listed coverage entries.
+ pub listed_entry_count: i64,
+ /// Number of entries wired to a local event kind.
+ pub decoded_entry_count: i64,
+ /// Number of entries observed at least once in local corpus counts.
+ pub observed_entry_count: i64,
+ /// Number of entries materialized at least once.
+ pub materialized_entry_count: i64,
+ /// Sum of local observed counts.
+ pub total_observed_count: i64,
+ /// Sum of local materialized counts.
+ pub total_materialized_count: i64,
+ /// Sum of trade counts produced by these entries.
+ pub trade_count: i64,
+ /// Number of decoded entries that remain audit-only.
+ pub audit_only_entry_count: i64,
+ /// Number of entries without an expected DB target.
+ pub missing_db_target_entry_count: i64,
+ /// Number of entries still upstream Git unverified.
+ pub upstream_git_unverified_entry_count: i64,
+ /// Number of entries mapped but not locally observed.
+ pub upstream_git_mapped_unverified_entry_count: i64,
+ /// Number of entries observed in local corpus.
+ pub upstream_git_local_corpus_observed_entry_count: i64,
+ /// Number of entries materialized from local corpus.
+ pub upstream_git_local_corpus_materialized_entry_count: i64,
+}
diff --git a/kb_lib/src/db/queries.rs b/kb_lib/src/db/queries.rs
index bba6750..0b7c646 100644
--- a/kb_lib/src/db/queries.rs
+++ b/kb_lib/src/db/queries.rs
@@ -11,6 +11,7 @@ mod db_runtime_event;
mod dex;
mod dex_decode_replay_ledger;
mod dex_decoded_event;
+mod dex_event_coverage_entry;
mod fee_event;
mod known_http_endpoint;
mod known_ws_endpoint;
@@ -76,6 +77,12 @@ pub use dex_decoded_event::query_dex_decoded_events_get_by_key;
pub use dex_decoded_event::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint;
pub use dex_decoded_event::query_dex_decoded_events_list_by_transaction_id;
pub use dex_decoded_event::query_dex_decoded_events_upsert;
+pub use dex_event_coverage_entry::query_dex_event_coverage_entries_delete_by_decoder;
+pub use dex_event_coverage_entry::query_dex_event_coverage_entries_list_by_decoder;
+pub use dex_event_coverage_entry::query_dex_event_coverage_entries_list_summary_by_decoder;
+pub use dex_event_coverage_entry::query_dex_event_coverage_entries_refresh_local_counts;
+pub use dex_event_coverage_entry::query_dex_event_coverage_entries_refresh_local_counts_by_decoder;
+pub use dex_event_coverage_entry::query_dex_event_coverage_entries_upsert;
pub use fee_event::query_fee_events_get_by_decoded_event_id;
pub use fee_event::query_fee_events_list_recent;
pub use fee_event::query_fee_events_upsert;
diff --git a/kb_lib/src/db/queries/dex_event_coverage_entry.rs b/kb_lib/src/db/queries/dex_event_coverage_entry.rs
new file mode 100644
index 0000000..19a423a
--- /dev/null
+++ b/kb_lib/src/db/queries/dex_event_coverage_entry.rs
@@ -0,0 +1,778 @@
+// file: kb_lib/src/db/queries/dex_event_coverage_entry.rs
+
+//! Queries for `k_sol_dex_event_coverage_entries`.
+
+/// Inserts or updates one DEX event coverage entry.
+pub async fn query_dex_event_coverage_entries_upsert(
+ database: &crate::Database,
+ dto: &crate::DexEventCoverageEntryDto,
+) -> Result {
+ match database.connection() {
+ crate::DatabaseConnection::Sqlite(pool) => {
+ let query_result = sqlx::query(
+ r#"
+INSERT INTO k_sol_dex_event_coverage_entries (
+ coverage_key,
+ decoder_code,
+ program_id,
+ program_family,
+ surface_kind,
+ source_repo,
+ source_path,
+ entry_kind,
+ entry_name,
+ discriminator_hex,
+ discriminator_len,
+ event_family,
+ expected_db_target,
+ proof_status,
+ local_event_kind,
+ observed_count,
+ materialized_count,
+ trade_count,
+ first_signature,
+ last_signature,
+ notes,
+ created_at,
+ updated_at
+)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ON CONFLICT(coverage_key) DO UPDATE SET
+ decoder_code = excluded.decoder_code,
+ program_id = excluded.program_id,
+ program_family = excluded.program_family,
+ surface_kind = excluded.surface_kind,
+ source_repo = excluded.source_repo,
+ source_path = excluded.source_path,
+ entry_kind = excluded.entry_kind,
+ entry_name = excluded.entry_name,
+ discriminator_hex = excluded.discriminator_hex,
+ discriminator_len = excluded.discriminator_len,
+ event_family = excluded.event_family,
+ expected_db_target = excluded.expected_db_target,
+ proof_status = excluded.proof_status,
+ local_event_kind = excluded.local_event_kind,
+ observed_count = excluded.observed_count,
+ materialized_count = excluded.materialized_count,
+ trade_count = excluded.trade_count,
+ first_signature = excluded.first_signature,
+ last_signature = excluded.last_signature,
+ notes = excluded.notes,
+ updated_at = excluded.updated_at
+ "#,
+ )
+ .bind(dto.coverage_key.clone())
+ .bind(dto.decoder_code.clone())
+ .bind(dto.program_id.clone())
+ .bind(dto.program_family.clone())
+ .bind(dto.surface_kind.clone())
+ .bind(dto.source_repo.clone())
+ .bind(dto.source_path.clone())
+ .bind(dto.entry_kind.clone())
+ .bind(dto.entry_name.clone())
+ .bind(dto.discriminator_hex.clone())
+ .bind(dto.discriminator_len)
+ .bind(dto.event_family.clone())
+ .bind(dto.expected_db_target.clone())
+ .bind(dto.proof_status.clone())
+ .bind(dto.local_event_kind.clone())
+ .bind(dto.observed_count)
+ .bind(dto.materialized_count)
+ .bind(dto.trade_count)
+ .bind(dto.first_signature.clone())
+ .bind(dto.last_signature.clone())
+ .bind(dto.notes.clone())
+ .bind(dto.created_at.to_rfc3339())
+ .bind(dto.updated_at.to_rfc3339())
+ .execute(pool)
+ .await;
+ if let Err(error) = query_result {
+ return Err(crate::Error::Db(format!(
+ "cannot upsert k_sol_dex_event_coverage_entries on sqlite: {}",
+ error
+ )));
+ }
+ let id_result = sqlx::query_scalar::(
+ r#"
+SELECT id
+FROM k_sol_dex_event_coverage_entries
+WHERE coverage_key = ?
+LIMIT 1
+ "#,
+ )
+ .bind(dto.coverage_key.clone())
+ .fetch_one(pool)
+ .await;
+ match id_result {
+ Ok(id) => return Ok(id),
+ Err(error) => {
+ return Err(crate::Error::Db(format!(
+ "cannot fetch k_sol_dex_event_coverage_entries id for coverage_key '{}' on sqlite: {}",
+ dto.coverage_key, error
+ )));
+ },
+ }
+ },
+ }
+}
+
+/// Deletes all event coverage rows for one decoder.
+pub async fn query_dex_event_coverage_entries_delete_by_decoder(
+ database: &crate::Database,
+ decoder_code: &str,
+) -> Result {
+ match database.connection() {
+ crate::DatabaseConnection::Sqlite(pool) => {
+ let query_result = sqlx::query(
+ r#"
+DELETE FROM k_sol_dex_event_coverage_entries
+WHERE decoder_code = ?
+ "#,
+ )
+ .bind(decoder_code.to_string())
+ .execute(pool)
+ .await;
+ match query_result {
+ Ok(query_result) => return Ok(query_result.rows_affected()),
+ Err(error) => {
+ return Err(crate::Error::Db(format!(
+ "cannot delete k_sol_dex_event_coverage_entries for decoder_code '{}' on sqlite: {}",
+ decoder_code, error
+ )));
+ },
+ }
+ },
+ }
+}
+
+/// Lists event coverage rows for one decoder.
+pub async fn query_dex_event_coverage_entries_list_by_decoder(
+ database: &crate::Database,
+ decoder_code: &str,
+) -> Result, crate::Error> {
+ match database.connection() {
+ crate::DatabaseConnection::Sqlite(pool) => {
+ let query_result = sqlx::query_as::(
+ r#"
+SELECT
+ id,
+ coverage_key,
+ decoder_code,
+ program_id,
+ program_family,
+ surface_kind,
+ source_repo,
+ source_path,
+ entry_kind,
+ entry_name,
+ discriminator_hex,
+ discriminator_len,
+ event_family,
+ expected_db_target,
+ proof_status,
+ local_event_kind,
+ observed_count,
+ materialized_count,
+ trade_count,
+ first_signature,
+ last_signature,
+ notes,
+ created_at,
+ updated_at
+FROM k_sol_dex_event_coverage_entries
+WHERE decoder_code = ?
+ORDER BY entry_kind ASC, entry_name ASC, discriminator_hex ASC, id ASC
+ "#,
+ )
+ .bind(decoder_code.to_string())
+ .fetch_all(pool)
+ .await;
+ let entities = match query_result {
+ Ok(entities) => entities,
+ Err(error) => {
+ return Err(crate::Error::Db(format!(
+ "cannot list k_sol_dex_event_coverage_entries for decoder_code '{}' on sqlite: {}",
+ decoder_code, error
+ )));
+ },
+ };
+ return dex_event_coverage_entry_entities_to_dtos(entities);
+ },
+ }
+}
+
+/// Lists event coverage summaries grouped by decoder.
+pub async fn query_dex_event_coverage_entries_list_summary_by_decoder(
+ database: &crate::Database,
+) -> Result, crate::Error> {
+ match database.connection() {
+ crate::DatabaseConnection::Sqlite(pool) => {
+ let query_result =
+ sqlx::query_as::(
+ r#"
+SELECT
+ decoder_code,
+ COUNT(*) AS listed_entry_count,
+ SUM(CASE WHEN local_event_kind IS NOT NULL AND local_event_kind <> '' THEN 1 ELSE 0 END) AS decoded_entry_count,
+ SUM(CASE WHEN observed_count > 0 THEN 1 ELSE 0 END) AS observed_entry_count,
+ SUM(CASE WHEN materialized_count > 0 THEN 1 ELSE 0 END) AS materialized_entry_count,
+ COALESCE(SUM(observed_count), 0) AS total_observed_count,
+ COALESCE(SUM(materialized_count), 0) AS total_materialized_count,
+ COALESCE(SUM(trade_count), 0) AS trade_count,
+ SUM(CASE WHEN expected_db_target = 'k_sol_dex_decoded_events_only' THEN 1 ELSE 0 END) AS audit_only_entry_count,
+ SUM(CASE WHEN expected_db_target IS NULL OR expected_db_target = '' THEN 1 ELSE 0 END) AS missing_db_target_entry_count,
+ SUM(CASE WHEN proof_status = 'upstream_git_unverified' THEN 1 ELSE 0 END) AS upstream_git_unverified_entry_count,
+ SUM(CASE WHEN proof_status = 'upstream_git_mapped_unverified' THEN 1 ELSE 0 END) AS upstream_git_mapped_unverified_entry_count,
+ SUM(CASE WHEN proof_status = 'upstream_git_local_corpus_observed' THEN 1 ELSE 0 END) AS upstream_git_local_corpus_observed_entry_count,
+ SUM(CASE WHEN proof_status = 'upstream_git_local_corpus_materialized' THEN 1 ELSE 0 END) AS upstream_git_local_corpus_materialized_entry_count
+FROM k_sol_dex_event_coverage_entries
+GROUP BY decoder_code
+ORDER BY decoder_code ASC
+ "#,
+ )
+ .fetch_all(pool)
+ .await;
+ let entities = match query_result {
+ Ok(entities) => entities,
+ Err(error) => {
+ return Err(crate::Error::Db(format!(
+ "cannot list k_sol_dex_event_coverage_entries summaries on sqlite: {}",
+ error
+ )));
+ },
+ };
+ let mut dtos = std::vec::Vec::new();
+ for entity in entities {
+ let dto_result = crate::DexEventCoverageSummaryDto::try_from(entity);
+ let dto = match dto_result {
+ Ok(dto) => dto,
+ Err(error) => return Err(error),
+ };
+ dtos.push(dto);
+ }
+ return Ok(dtos);
+ },
+ }
+}
+
+/// Refreshes local observed/materialized counts for every coverage row.
+pub async fn query_dex_event_coverage_entries_refresh_local_counts(
+ database: &crate::Database,
+) -> Result {
+ return query_dex_event_coverage_entries_refresh_local_counts_internal(database, None).await;
+}
+
+/// Refreshes local observed/materialized counts for one decoder coverage subset.
+pub async fn query_dex_event_coverage_entries_refresh_local_counts_by_decoder(
+ database: &crate::Database,
+ decoder_code: &str,
+) -> Result {
+ return query_dex_event_coverage_entries_refresh_local_counts_internal(
+ database,
+ Some(decoder_code),
+ )
+ .await;
+}
+
+async fn query_dex_event_coverage_entries_refresh_local_counts_internal(
+ database: &crate::Database,
+ decoder_code: std::option::Option<&str>,
+) -> Result {
+ match database.connection() {
+ crate::DatabaseConnection::Sqlite(pool) => {
+ let now = chrono::Utc::now().to_rfc3339();
+ let decoder_code_filter: std::option::Option = match decoder_code {
+ Some(value) => Some(value.to_string()),
+ None => None,
+ };
+ let update_result = sqlx::query(
+ r#"
+UPDATE k_sol_dex_event_coverage_entries
+SET
+ observed_count = (
+ SELECT COUNT(*)
+ FROM k_sol_dex_decoded_events de
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ ),
+ trade_count = (
+ SELECT COUNT(te.id)
+ FROM k_sol_dex_decoded_events de
+ JOIN k_sol_trade_events te ON te.decoded_event_id = de.id
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ ),
+ materialized_count = CASE
+ WHEN expected_db_target = 'k_sol_trade_events' THEN (
+ SELECT COUNT(te.id)
+ FROM k_sol_dex_decoded_events de
+ JOIN k_sol_trade_events te ON te.decoded_event_id = de.id
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ )
+ WHEN expected_db_target = 'k_sol_liquidity_events' THEN (
+ SELECT COUNT(le.id)
+ FROM k_sol_dex_decoded_events de
+ JOIN k_sol_liquidity_events le ON le.decoded_event_id = de.id
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ )
+ WHEN expected_db_target = 'k_sol_pool_lifecycle_events' THEN (
+ SELECT COUNT(pe.id)
+ FROM k_sol_dex_decoded_events de
+ JOIN k_sol_pool_lifecycle_events pe ON pe.decoded_event_id = de.id
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ )
+ WHEN expected_db_target = 'k_sol_fee_events' THEN (
+ SELECT COUNT(fe.id)
+ FROM k_sol_dex_decoded_events de
+ JOIN k_sol_fee_events fe ON fe.decoded_event_id = de.id
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ )
+ WHEN expected_db_target = 'k_sol_reward_events' THEN (
+ SELECT COUNT(re.id)
+ FROM k_sol_dex_decoded_events de
+ JOIN k_sol_reward_events re ON re.decoded_event_id = de.id
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ )
+ WHEN expected_db_target = 'k_sol_pool_admin_events' THEN (
+ SELECT COUNT(ae.id)
+ FROM k_sol_dex_decoded_events de
+ JOIN k_sol_pool_admin_events ae ON ae.decoded_event_id = de.id
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ )
+ ELSE materialized_count
+ END,
+ first_signature = (
+ SELECT tx.signature
+ FROM k_sol_dex_decoded_events de
+ JOIN k_sol_chain_transactions tx ON tx.id = de.transaction_id
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ ORDER BY COALESCE(tx.slot, 0) ASC, tx.id ASC
+ LIMIT 1
+ ),
+ last_signature = (
+ SELECT tx.signature
+ FROM k_sol_dex_decoded_events de
+ JOIN k_sol_chain_transactions tx ON tx.id = de.transaction_id
+ WHERE (
+ (k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
+ AND (
+ (
+ k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
+ AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
+ AND (
+ json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
+ OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
+ )
+ )
+ OR (
+ k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
+ AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
+ AND (
+ json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
+ OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
+ )
+ )
+ )
+ )
+ ORDER BY COALESCE(tx.slot, 0) DESC, tx.id DESC
+ LIMIT 1
+ ),
+ updated_at = ?
+WHERE (? IS NULL OR decoder_code = ?)
+ "#,
+ )
+ .bind(now.clone())
+ .bind(decoder_code_filter.clone())
+ .bind(decoder_code_filter.clone())
+ .execute(pool)
+ .await;
+ let refreshed_entry_count = match update_result {
+ Ok(result) => result.rows_affected(),
+ Err(error) => {
+ return Err(crate::Error::Db(format!(
+ "cannot refresh k_sol_dex_event_coverage_entries local counts on sqlite: {}",
+ error
+ )));
+ },
+ };
+ let proof_result = sqlx::query(
+ r#"
+UPDATE k_sol_dex_event_coverage_entries
+SET
+ proof_status = CASE
+ WHEN materialized_count > 0 OR trade_count > 0 THEN 'upstream_git_local_corpus_materialized'
+ WHEN observed_count > 0 THEN 'upstream_git_local_corpus_observed'
+ WHEN local_event_kind IS NOT NULL AND local_event_kind <> '' THEN 'upstream_git_mapped_unverified'
+ ELSE proof_status
+ END,
+ updated_at = ?
+WHERE (? IS NULL OR decoder_code = ?)
+ "#,
+ )
+ .bind(now)
+ .bind(decoder_code_filter.clone())
+ .bind(decoder_code_filter)
+ .execute(pool)
+ .await;
+ if let Err(error) = proof_result {
+ return Err(crate::Error::Db(format!(
+ "cannot refresh k_sol_dex_event_coverage_entries proof statuses on sqlite: {}",
+ error
+ )));
+ }
+ return Ok(refreshed_entry_count);
+ },
+ }
+}
+
+fn dex_event_coverage_entry_entities_to_dtos(
+ entities: std::vec::Vec,
+) -> Result, crate::Error> {
+ let mut dtos = std::vec::Vec::new();
+ for entity in entities {
+ let dto_result = crate::DexEventCoverageEntryDto::try_from(entity);
+ let dto = match dto_result {
+ Ok(dto) => dto,
+ Err(error) => return Err(error),
+ };
+ dtos.push(dto);
+ }
+ return Ok(dtos);
+}
+
+#[cfg(test)]
+mod tests {
+ async fn make_database() -> crate::Database {
+ let tempdir = tempfile::tempdir().expect("tempdir must succeed");
+ let database_path = tempdir.path().join("dex_event_coverage.sqlite3");
+ let config = crate::DatabaseConfig {
+ enabled: true,
+ backend: crate::DatabaseBackend::Sqlite,
+ sqlite: crate::SqliteDatabaseConfig {
+ path: database_path.to_string_lossy().to_string(),
+ create_if_missing: true,
+ busy_timeout_ms: 5000,
+ max_connections: 1,
+ auto_initialize_schema: true,
+ use_wal: true,
+ },
+ };
+ return crate::Database::connect_and_initialize(&config)
+ .await
+ .expect("database init must succeed");
+ }
+
+ #[tokio::test]
+ async fn dex_event_coverage_entry_roundtrip_and_summary_work() {
+ let database = make_database().await;
+ let upstream_service = crate::UpstreamRegistryService::new();
+ let request = crate::UpstreamRegistrySearchRequestDto {
+ decoder_code: Some("raydium-cpmm".to_string()),
+ program_id: None,
+ program_family: None,
+ surface_kind: None,
+ entry_kind: Some(crate::ENTRY_KIND_INSTRUCTION.to_string()),
+ proof_status: None,
+ limit: Some(1),
+ };
+ let upstream_result = upstream_service.search(&request);
+ assert_eq!(upstream_result.entries.len(), 1);
+ let upstream_entry = upstream_result.entries.first().expect("entry must exist");
+ let mut dto = crate::DexEventCoverageEntryDto::from_upstream_registry_entry(
+ upstream_entry,
+ Some("swap".to_string()),
+ Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string()),
+ Some("raydium_cpmm.swap".to_string()),
+ );
+ dto.proof_status = crate::PROOF_STATUS_UPSTREAM_GIT_LOCAL_CORPUS_OBSERVED.to_string();
+ dto.observed_count = 3;
+ let id = crate::query_dex_event_coverage_entries_upsert(&database, &dto)
+ .await
+ .expect("coverage upsert must succeed");
+ assert!(id > 0);
+ let rows =
+ crate::query_dex_event_coverage_entries_list_by_decoder(&database, "raydium-cpmm")
+ .await
+ .expect("coverage list must succeed");
+ assert_eq!(rows.len(), 1);
+ assert_eq!(rows[0].observed_count, 3);
+ let summaries = crate::query_dex_event_coverage_entries_list_summary_by_decoder(&database)
+ .await
+ .expect("coverage summary must succeed");
+ assert_eq!(summaries.len(), 1);
+ assert_eq!(summaries[0].decoder_code, "raydium-cpmm");
+ assert_eq!(summaries[0].listed_entry_count, 1);
+ assert_eq!(summaries[0].decoded_entry_count, 1);
+ assert_eq!(summaries[0].observed_entry_count, 1);
+ assert_eq!(summaries[0].total_observed_count, 3);
+ assert_eq!(summaries[0].audit_only_entry_count, 1);
+ }
+}
diff --git a/kb_lib/src/db/schema.rs b/kb_lib/src/db/schema.rs
index ea8c915..d061cb5 100644
--- a/kb_lib/src/db/schema.rs
+++ b/kb_lib/src/db/schema.rs
@@ -246,6 +246,26 @@ pub(crate) async fn ensure_schema(database: &crate::Database) -> Result<(), crat
if let Err(error) = result {
return Err(error);
}
+ let result = create_tbl_dex_event_coverage_entries(pool).await;
+ if let Err(error) = result {
+ return Err(error);
+ }
+ let result = create_uix_dex_event_coverage_entries_key(pool).await;
+ if let Err(error) = result {
+ return Err(error);
+ }
+ let result = create_idx_dex_event_coverage_entries_decoder(pool).await;
+ if let Err(error) = result {
+ return Err(error);
+ }
+ let result = create_idx_dex_event_coverage_entries_proof_status(pool).await;
+ if let Err(error) = result {
+ return Err(error);
+ }
+ let result = create_idx_dex_event_coverage_entries_event_family(pool).await;
+ if let Err(error) = result {
+ return Err(error);
+ }
let result = create_tbl_transaction_classifications(pool).await;
if let Err(error) = result {
return Err(error);
@@ -1462,9 +1482,7 @@ ON k_sol_dex_decoded_events (transaction_id, instruction_id, event_kind)
}
/// Creates `k_sol_dex_decode_replay_ledger`.
-async fn create_tbl_dex_decode_replay_ledger(
- pool: &sqlx::SqlitePool,
-) -> Result<(), crate::Error> {
+async fn create_tbl_dex_decode_replay_ledger(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_tbl_dex_decode_replay_ledger",
@@ -1535,6 +1553,105 @@ ON k_sol_dex_decode_replay_ledger (decode_status, certainty, force_replay_requir
.await;
}
+/// Creates `k_sol_dex_event_coverage_entries`.
+async fn create_tbl_dex_event_coverage_entries(
+ pool: &sqlx::SqlitePool,
+) -> Result<(), crate::Error> {
+ return execute_sqlite_schema_statement(
+ pool,
+ "create_tbl_dex_event_coverage_entries",
+ r#"
+CREATE TABLE IF NOT EXISTS k_sol_dex_event_coverage_entries (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ coverage_key TEXT NOT NULL,
+ decoder_code TEXT NOT NULL,
+ program_id TEXT NULL,
+ program_family TEXT NOT NULL,
+ surface_kind TEXT NOT NULL,
+ source_repo TEXT NULL,
+ source_path TEXT NULL,
+ entry_kind TEXT NOT NULL,
+ entry_name TEXT NOT NULL,
+ discriminator_hex TEXT NULL,
+ discriminator_len INTEGER NULL,
+ event_family TEXT NULL,
+ expected_db_target TEXT NULL,
+ proof_status TEXT NOT NULL,
+ local_event_kind TEXT NULL,
+ observed_count INTEGER NOT NULL DEFAULT 0,
+ materialized_count INTEGER NOT NULL DEFAULT 0,
+ trade_count INTEGER NOT NULL DEFAULT 0,
+ first_signature TEXT NULL,
+ last_signature TEXT NULL,
+ notes TEXT NULL,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+)
+ "#,
+ )
+ .await;
+}
+
+/// Creates unique index on `k_sol_dex_event_coverage_entries(coverage_key)`.
+async fn create_uix_dex_event_coverage_entries_key(
+ pool: &sqlx::SqlitePool,
+) -> Result<(), crate::Error> {
+ return execute_sqlite_schema_statement(
+ pool,
+ "create_uix_dex_event_coverage_entries_key",
+ r#"
+CREATE UNIQUE INDEX IF NOT EXISTS uix_dex_event_coverage_entries_key
+ON k_sol_dex_event_coverage_entries (coverage_key)
+ "#,
+ )
+ .await;
+}
+
+/// Creates index on `k_sol_dex_event_coverage_entries(decoder_code)`.
+async fn create_idx_dex_event_coverage_entries_decoder(
+ pool: &sqlx::SqlitePool,
+) -> Result<(), crate::Error> {
+ return execute_sqlite_schema_statement(
+ pool,
+ "create_idx_dex_event_coverage_entries_decoder",
+ r#"
+CREATE INDEX IF NOT EXISTS idx_dex_event_coverage_entries_decoder
+ON k_sol_dex_event_coverage_entries (decoder_code, entry_kind, entry_name)
+ "#,
+ )
+ .await;
+}
+
+/// Creates index on `k_sol_dex_event_coverage_entries(proof_status)`.
+async fn create_idx_dex_event_coverage_entries_proof_status(
+ pool: &sqlx::SqlitePool,
+) -> Result<(), crate::Error> {
+ return execute_sqlite_schema_statement(
+ pool,
+ "create_idx_dex_event_coverage_entries_proof_status",
+ r#"
+CREATE INDEX IF NOT EXISTS idx_dex_event_coverage_entries_proof_status
+ON k_sol_dex_event_coverage_entries (proof_status, decoder_code)
+ "#,
+ )
+ .await;
+}
+
+/// Creates index on `k_sol_dex_event_coverage_entries(event_family)`.
+async fn create_idx_dex_event_coverage_entries_event_family(
+ pool: &sqlx::SqlitePool,
+) -> Result<(), crate::Error> {
+ return execute_sqlite_schema_statement(
+ pool,
+ "create_idx_dex_event_coverage_entries_event_family",
+ r#"
+CREATE INDEX IF NOT EXISTS idx_dex_event_coverage_entries_event_family
+ON k_sol_dex_event_coverage_entries (event_family, expected_db_target)
+ "#,
+ )
+ .await;
+}
+
async fn create_tbl_launch_surfaces(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
diff --git a/kb_lib/src/dex_event_coverage.rs b/kb_lib/src/dex_event_coverage.rs
new file mode 100644
index 0000000..112a53f
--- /dev/null
+++ b/kb_lib/src/dex_event_coverage.rs
@@ -0,0 +1,473 @@
+// file: kb_lib/src/dex_event_coverage.rs
+
+//! Event coverage synchronization and reporting service.
+//!
+//! This service bridges the read-only upstream registry and the persisted
+//! coverage table. It does not decode transactions and never materializes
+//! trades, metrics or candles.
+
+/// Result of one event coverage synchronization pass.
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DexEventCoverageSyncResult {
+ /// Optional decoder filter used for this synchronization pass.
+ pub decoder_code: std::option::Option,
+ /// Number of upstream registry entries selected by the filter.
+ pub upstream_entry_count: usize,
+ /// Number of coverage rows upserted from the upstream registry.
+ pub upserted_entry_count: usize,
+ /// Number of coverage rows touched by the local observation refresh.
+ pub refreshed_entry_count: u64,
+ /// Aggregated coverage summaries after synchronization.
+ pub summaries: std::vec::Vec,
+}
+
+/// Service used to persist and refresh DEX event coverage rows.
+#[derive(Debug, Clone)]
+pub struct DexEventCoverageService {
+ database: std::sync::Arc,
+ upstream_registry: crate::UpstreamRegistryService,
+}
+
+impl DexEventCoverageService {
+ /// Creates a new event coverage service.
+ pub fn new(database: std::sync::Arc) -> Self {
+ return Self {
+ database,
+ upstream_registry: crate::UpstreamRegistryService::new(),
+ };
+ }
+
+ /// Synchronizes static upstream registry entries into SQLite coverage rows.
+ ///
+ /// The resulting rows are still discovery/audit metadata. A row can become
+ /// observed or materialized only through local corpus replay and explicit
+ /// count refreshes.
+ pub async fn sync_upstream_registry(
+ &self,
+ decoder_code: std::option::Option,
+ ) -> Result {
+ let request = crate::UpstreamRegistrySearchRequestDto {
+ decoder_code: decoder_code.clone(),
+ program_id: None,
+ program_family: None,
+ surface_kind: None,
+ entry_kind: None,
+ proof_status: None,
+ limit: None,
+ };
+ let search_result = self.upstream_registry.search(&request);
+ let mut upserted_entry_count = 0_usize;
+ for entry in &search_result.entries {
+ let coverage_entry = build_coverage_entry_from_upstream(entry);
+ let upsert_result = crate::query_dex_event_coverage_entries_upsert(
+ self.database.as_ref(),
+ &coverage_entry,
+ )
+ .await;
+ match upsert_result {
+ Ok(_) => upserted_entry_count += 1,
+ Err(error) => return Err(error),
+ }
+ }
+ let refreshed_entry_count = match &decoder_code {
+ Some(decoder_code) => {
+ let refresh_result =
+ crate::query_dex_event_coverage_entries_refresh_local_counts_by_decoder(
+ self.database.as_ref(),
+ decoder_code.as_str(),
+ )
+ .await;
+ match refresh_result {
+ Ok(refreshed_entry_count) => refreshed_entry_count,
+ Err(error) => return Err(error),
+ }
+ },
+ None => {
+ let refresh_result = crate::query_dex_event_coverage_entries_refresh_local_counts(
+ self.database.as_ref(),
+ )
+ .await;
+ match refresh_result {
+ Ok(refreshed_entry_count) => refreshed_entry_count,
+ Err(error) => return Err(error),
+ }
+ },
+ };
+ let summaries_result =
+ crate::query_dex_event_coverage_entries_list_summary_by_decoder(self.database.as_ref())
+ .await;
+ let summaries = match summaries_result {
+ Ok(summaries) => summaries,
+ Err(error) => return Err(error),
+ };
+ return Ok(crate::DexEventCoverageSyncResult {
+ decoder_code,
+ upstream_entry_count: search_result.entries.len(),
+ upserted_entry_count,
+ refreshed_entry_count,
+ summaries,
+ });
+ }
+
+ /// Refreshes observed, materialized and proof-status counters from local DB rows.
+ pub async fn refresh_local_counts(
+ &self,
+ decoder_code: std::option::Option,
+ ) -> Result {
+ let refreshed_entry_count = match &decoder_code {
+ Some(decoder_code) => {
+ let refresh_result =
+ crate::query_dex_event_coverage_entries_refresh_local_counts_by_decoder(
+ self.database.as_ref(),
+ decoder_code.as_str(),
+ )
+ .await;
+ match refresh_result {
+ Ok(refreshed_entry_count) => refreshed_entry_count,
+ Err(error) => return Err(error),
+ }
+ },
+ None => {
+ let refresh_result = crate::query_dex_event_coverage_entries_refresh_local_counts(
+ self.database.as_ref(),
+ )
+ .await;
+ match refresh_result {
+ Ok(refreshed_entry_count) => refreshed_entry_count,
+ Err(error) => return Err(error),
+ }
+ },
+ };
+ let summaries_result =
+ crate::query_dex_event_coverage_entries_list_summary_by_decoder(self.database.as_ref())
+ .await;
+ let summaries = match summaries_result {
+ Ok(summaries) => summaries,
+ Err(error) => return Err(error),
+ };
+ return Ok(crate::DexEventCoverageSyncResult {
+ decoder_code,
+ upstream_entry_count: 0,
+ upserted_entry_count: 0,
+ refreshed_entry_count,
+ summaries,
+ });
+ }
+}
+
+fn build_coverage_entry_from_upstream(
+ entry: &crate::UpstreamRegistryEntryDto,
+) -> crate::DexEventCoverageEntryDto {
+ let event_family = infer_event_family(entry.entry_name.as_str(), entry.entry_kind.as_str());
+ let expected_db_target =
+ infer_expected_db_target(event_family.as_deref(), entry.entry_kind.as_str());
+ let local_event_kind =
+ known_local_event_kind(entry.decoder_code.as_str(), entry.entry_name.as_str());
+ let mut coverage_entry = crate::DexEventCoverageEntryDto::from_upstream_registry_entry(
+ entry,
+ event_family,
+ expected_db_target,
+ local_event_kind.clone(),
+ );
+ if local_event_kind.is_some() && coverage_entry.observed_count == 0 {
+ coverage_entry.proof_status =
+ crate::PROOF_STATUS_UPSTREAM_GIT_MAPPED_UNVERIFIED.to_string();
+ }
+ return coverage_entry;
+}
+
+fn infer_expected_db_target(
+ event_family: std::option::Option<&str>,
+ entry_kind: &str,
+) -> std::option::Option {
+ if entry_kind == crate::ENTRY_KIND_PROGRAM || entry_kind == crate::ENTRY_KIND_ACCOUNT {
+ return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
+ }
+ let family = match event_family {
+ Some(family) => family,
+ None => {
+ return Some(
+ crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string(),
+ );
+ },
+ };
+ let target = match family {
+ "swap" => crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS,
+ "pool_create" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
+ "liquidity_add" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
+ "liquidity_remove" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
+ "position_open" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
+ "position_close" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
+ "fee" => crate::DexEventCoverageEntryDto::DB_TARGET_FEE_EVENTS,
+ "reward" => crate::DexEventCoverageEntryDto::DB_TARGET_REWARD_EVENTS,
+ "admin_config" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS,
+ "mint" => crate::DexEventCoverageEntryDto::DB_TARGET_TOKEN_MINT_EVENTS,
+ "burn" => crate::DexEventCoverageEntryDto::DB_TARGET_TOKEN_BURN_EVENTS,
+ "transfer" => crate::DexEventCoverageEntryDto::DB_TARGET_TOKEN_TRANSFER_EVENTS,
+ "account_create" => crate::DexEventCoverageEntryDto::DB_TARGET_TOKEN_ACCOUNT_EVENTS,
+ "account_close" => crate::DexEventCoverageEntryDto::DB_TARGET_TOKEN_ACCOUNT_EVENTS,
+ "wrap_sol" => crate::DexEventCoverageEntryDto::DB_TARGET_TOKEN_ACCOUNT_EVENTS,
+ "unwrap_sol" => crate::DexEventCoverageEntryDto::DB_TARGET_TOKEN_ACCOUNT_EVENTS,
+ "order_place" => crate::DexEventCoverageEntryDto::DB_TARGET_ORDERBOOK_EVENTS,
+ "order_cancel" => crate::DexEventCoverageEntryDto::DB_TARGET_ORDERBOOK_EVENTS,
+ "order_fill" => crate::DexEventCoverageEntryDto::DB_TARGET_ORDERBOOK_EVENTS,
+ "consume_events" => crate::DexEventCoverageEntryDto::DB_TARGET_ORDERBOOK_EVENTS,
+ "settle_funds" => crate::DexEventCoverageEntryDto::DB_TARGET_ORDERBOOK_EVENTS,
+ "vault_deposit" => crate::DexEventCoverageEntryDto::DB_TARGET_VAULT_EVENTS,
+ "vault_withdraw" => crate::DexEventCoverageEntryDto::DB_TARGET_VAULT_EVENTS,
+ "lock" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_LOCK_EVENTS,
+ "unlock" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_LOCK_EVENTS,
+ "launch" => crate::DexEventCoverageEntryDto::DB_TARGET_LAUNCH_EVENTS,
+ "migration" => crate::DexEventCoverageEntryDto::DB_TARGET_LAUNCH_EVENTS,
+ "stake" => crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY,
+ "unstake" => crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY,
+ _ => crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY,
+ };
+ return Some(target.to_string());
+}
+
+fn infer_event_family(
+ entry_name: &str,
+ entry_kind: &str,
+) -> std::option::Option {
+ if entry_kind == crate::ENTRY_KIND_PROGRAM {
+ return None;
+ }
+ let normalized = entry_name.to_ascii_lowercase();
+ if contains_any(normalized.as_str(), &["swap", "buy", "sell", "trade"]) {
+ return Some("swap".to_string());
+ }
+ if contains_any(normalized.as_str(), &["create_pool", "initialize_pool", "initialize2"])
+ || normalized == "initialize"
+ || normalized.starts_with("initialize_")
+ {
+ return Some("pool_create".to_string());
+ }
+ if contains_any(normalized.as_str(), &["add_liquidity", "increase_liquidity", "deposit"])
+ || normalized.contains("bootstrap_liquidity")
+ {
+ return Some("liquidity_add".to_string());
+ }
+ if contains_any(normalized.as_str(), &["remove_liquidity", "decrease_liquidity", "withdraw"])
+ && !normalized.contains("funds")
+ {
+ return Some("liquidity_remove".to_string());
+ }
+ if contains_any(
+ normalized.as_str(),
+ &["open_position", "initialize_position", "position_create"],
+ ) {
+ return Some("position_open".to_string());
+ }
+ if contains_any(normalized.as_str(), &["close_position", "position_close"])
+ || normalized.contains("close_position_if_empty")
+ {
+ return Some("position_close".to_string());
+ }
+ if contains_any(normalized.as_str(), &["fee", "collect", "claim_fee"])
+ && !normalized.contains("reward")
+ {
+ return Some("fee".to_string());
+ }
+ if normalized.contains("reward") {
+ return Some("reward".to_string());
+ }
+ if contains_any(
+ normalized.as_str(),
+ &["config", "admin", "authority", "permission", "pause", "status", "update_pool"],
+ ) {
+ return Some("admin_config".to_string());
+ }
+ if normalized.contains("mint") {
+ return Some("mint".to_string());
+ }
+ if normalized.contains("burn") {
+ return Some("burn".to_string());
+ }
+ if normalized.contains("transfer") {
+ return Some("transfer".to_string());
+ }
+ if contains_any(normalized.as_str(), &["create_ata", "init_account", "open_orders_create"]) {
+ return Some("account_create".to_string());
+ }
+ if contains_any(normalized.as_str(), &["close_account", "close_open_orders"])
+ || normalized.starts_with("close_")
+ {
+ return Some("account_close".to_string());
+ }
+ if normalized.contains("wrap_sol") {
+ return Some("wrap_sol".to_string());
+ }
+ if normalized.contains("unwrap_sol") {
+ return Some("unwrap_sol".to_string());
+ }
+ if normalized.contains("place_order") || normalized.contains("post_order") {
+ return Some("order_place".to_string());
+ }
+ if normalized.contains("cancel_order") || normalized.contains("cancel_all") {
+ return Some("order_cancel".to_string());
+ }
+ if normalized.contains("fill") {
+ return Some("order_fill".to_string());
+ }
+ if normalized.contains("consume_events") {
+ return Some("consume_events".to_string());
+ }
+ if normalized.contains("settle_funds") {
+ return Some("settle_funds".to_string());
+ }
+ if normalized.contains("vault") && normalized.contains("deposit") {
+ return Some("vault_deposit".to_string());
+ }
+ if normalized.contains("vault") && normalized.contains("withdraw") {
+ return Some("vault_withdraw".to_string());
+ }
+ if contains_any(normalized.as_str(), &["lock_liquidity", "create_lock", "lock"])
+ && !normalized.contains("unlock")
+ {
+ return Some("lock".to_string());
+ }
+ if normalized.contains("unlock") {
+ return Some("unlock".to_string());
+ }
+ if contains_any(normalized.as_str(), &["launch", "create_bonding", "bonding_curve"]) {
+ return Some("launch".to_string());
+ }
+ if contains_any(normalized.as_str(), &["migrate", "migration", "graduate"]) {
+ return Some("migration".to_string());
+ }
+ if normalized.contains("unstake") {
+ return Some("unstake".to_string());
+ }
+ if normalized.contains("stake") {
+ return Some("stake".to_string());
+ }
+ return Some("unknown".to_string());
+}
+
+fn contains_any(value: &str, needles: &[&str]) -> bool {
+ for needle in needles {
+ if value.contains(needle) {
+ return true;
+ }
+ }
+ return false;
+}
+
+fn known_local_event_kind(
+ decoder_code: &str,
+ entry_name: &str,
+) -> std::option::Option {
+ match (decoder_code, entry_name) {
+ ("raydium-cpmm", "swap_base_input") => {
+ return Some("raydium_cpmm.swap_base_input".to_string());
+ },
+ ("raydium-cpmm", "swap_base_output") => {
+ return Some("raydium_cpmm.swap_base_output".to_string());
+ },
+ ("raydium-cpmm", "collect_creator_fee") => {
+ return Some("raydium_cpmm.collect_creator_fee".to_string());
+ },
+ ("raydium-cpmm", "withdraw") => return Some("raydium_cpmm.withdraw".to_string()),
+ ("raydium-cpmm", "initialize") => return Some("raydium_cpmm.initialize".to_string()),
+ ("raydium-clmm", "swap") => return Some("raydium_clmm.swap".to_string()),
+ ("raydium-clmm", "swap_v2") => return Some("raydium_clmm.swap_v2".to_string()),
+ ("raydium-clmm", "increase_liquidity_v2") => {
+ return Some("raydium_clmm.increase_liquidity_v2".to_string());
+ },
+ ("raydium-clmm", "decrease_liquidity_v2") => {
+ return Some("raydium_clmm.decrease_liquidity_v2".to_string());
+ },
+ ("raydium-clmm", "open_position_with_token22_nft") => {
+ return Some("raydium_clmm.open_position_with_token22_nft".to_string());
+ },
+ ("raydium-clmm", "close_position") => {
+ return Some("raydium_clmm.close_position".to_string());
+ },
+ _ => return None,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ async fn make_database() -> std::sync::Arc {
+ let tempdir_result = tempfile::tempdir();
+ let tempdir = match tempdir_result {
+ Ok(tempdir) => tempdir,
+ Err(error) => panic!("tempdir must succeed: {}", error),
+ };
+ let database_path = tempdir.path().join("dex_event_coverage.sqlite3");
+ let config = crate::DatabaseConfig {
+ enabled: true,
+ backend: crate::DatabaseBackend::Sqlite,
+ sqlite: crate::SqliteDatabaseConfig {
+ path: database_path.to_string_lossy().to_string(),
+ create_if_missing: true,
+ busy_timeout_ms: 5000,
+ max_connections: 1,
+ auto_initialize_schema: true,
+ use_wal: true,
+ },
+ };
+ let database_result = crate::Database::connect_and_initialize(&config).await;
+ let database = match database_result {
+ Ok(database) => database,
+ Err(error) => panic!("database init must succeed: {}", error),
+ };
+ return std::sync::Arc::new(database);
+ }
+
+ #[test]
+ fn event_family_inference_covers_raydium_cpmm_core_entries() {
+ assert_eq!(
+ super::infer_event_family("swap_base_input", crate::ENTRY_KIND_INSTRUCTION),
+ Some("swap".to_string())
+ );
+ assert_eq!(
+ super::infer_event_family("initialize", crate::ENTRY_KIND_INSTRUCTION),
+ Some("pool_create".to_string())
+ );
+ assert_eq!(
+ super::infer_event_family("withdraw", crate::ENTRY_KIND_INSTRUCTION),
+ Some("liquidity_remove".to_string())
+ );
+ assert_eq!(
+ super::infer_event_family("collect_creator_fee", crate::ENTRY_KIND_INSTRUCTION),
+ Some("fee".to_string())
+ );
+ }
+
+ #[tokio::test]
+ async fn sync_upstream_registry_persists_raydium_cpmm_coverage_rows() {
+ let database = make_database().await;
+ let service = crate::DexEventCoverageService::new(database.clone());
+ let result = service.sync_upstream_registry(Some("raydium-cpmm".to_string())).await;
+ let result = match result {
+ Ok(result) => result,
+ Err(error) => panic!("coverage sync must succeed: {}", error),
+ };
+ assert!(result.upstream_entry_count > 0);
+ assert_eq!(result.upstream_entry_count, result.upserted_entry_count);
+ let rows_result = crate::query_dex_event_coverage_entries_list_by_decoder(
+ database.as_ref(),
+ "raydium-cpmm",
+ )
+ .await;
+ let rows = match rows_result {
+ Ok(rows) => rows,
+ Err(error) => panic!("coverage rows must load: {}", error),
+ };
+ assert_eq!(rows.len(), result.upstream_entry_count);
+ assert!(rows.iter().any(|row| return {
+ row.entry_name == "swap_base_input"
+ && row.event_family == Some("swap".to_string())
+ && row.local_event_kind == Some("raydium_cpmm.swap_base_input".to_string())
+ }));
+ assert!(rows.iter().any(|row| return {
+ row.entry_name == "deposit"
+ && row.event_family == Some("liquidity_add".to_string())
+ && row.local_event_kind.is_none()
+ }));
+ }
+}
diff --git a/kb_lib/src/lib.rs b/kb_lib/src/lib.rs
index 03df383..16c7dc8 100644
--- a/kb_lib/src/lib.rs
+++ b/kb_lib/src/lib.rs
@@ -37,6 +37,8 @@ mod dex_detect;
mod dex_detection_route;
/// Shared DEX event classification and decoded-payload enrichment helpers.
mod dex_event_classification;
+/// Event coverage synchronization and reporting.
+mod dex_event_coverage;
/// Shared DEX pool materialization helpers.
mod dex_pool_materialization;
/// Shared DEX support matrix.
@@ -477,6 +479,14 @@ pub use db::DexDecodedEventEntity;
pub use db::DexDto;
/// Persisted normalized DEX row.
pub use db::DexEntity;
+/// Application-facing DEX event coverage entry DTO.
+pub use db::DexEventCoverageEntryDto;
+/// Persisted DEX event coverage entry row.
+pub use db::DexEventCoverageEntryEntity;
+/// Application-facing DEX event coverage summary DTO.
+pub use db::DexEventCoverageSummaryDto;
+/// Aggregated DEX event coverage summary row.
+pub use db::DexEventCoverageSummaryEntity;
/// Normalized fee event persisted from useful non-trade DEX events.
pub use db::FeeEventDto;
/// Persisted fee event row.
@@ -733,6 +743,18 @@ pub use db::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint;
pub use db::query_dex_decoded_events_list_by_transaction_id;
/// Inserts or updates one decoded DEX event row.
pub use db::query_dex_decoded_events_upsert;
+/// Deletes DEX event coverage entries for one decoder.
+pub use db::query_dex_event_coverage_entries_delete_by_decoder;
+/// Lists DEX event coverage entries for one decoder.
+pub use db::query_dex_event_coverage_entries_list_by_decoder;
+/// Lists DEX event coverage summaries grouped by decoder.
+pub use db::query_dex_event_coverage_entries_list_summary_by_decoder;
+/// Refreshes local DEX event coverage counts for every decoder.
+pub use db::query_dex_event_coverage_entries_refresh_local_counts;
+/// Refreshes local DEX event coverage counts for one decoder.
+pub use db::query_dex_event_coverage_entries_refresh_local_counts_by_decoder;
+/// Inserts or updates one DEX event coverage entry.
+pub use db::query_dex_event_coverage_entries_upsert;
/// Reads one normalized DEX row by code.
pub use db::query_dexs_get_by_code;
/// Lists normalized DEX rows.
@@ -1196,6 +1218,10 @@ pub use dex_event_classification::is_dex_token_burn_event_kind;
pub use dex_event_classification::is_dex_token_mint_event_kind;
/// Returns true for swap-like DEX events.
pub use dex_event_classification::is_dex_trade_event_kind;
+/// Service that syncs upstream registry entries into event coverage rows.
+pub use dex_event_coverage::DexEventCoverageService;
+/// Result of one event coverage sync or refresh pass.
+pub use dex_event_coverage::DexEventCoverageSyncResult;
/// Static DEX support matrix entry.
pub use dex_support_matrix::DexSupportMatrixEntry;
/// Owned DEX support matrix entry DTO.
diff --git a/kb_lib/src/local_pipeline_diagnostics.rs b/kb_lib/src/local_pipeline_diagnostics.rs
index a306eda..ef3b118 100644
--- a/kb_lib/src/local_pipeline_diagnostics.rs
+++ b/kb_lib/src/local_pipeline_diagnostics.rs
@@ -64,6 +64,12 @@ impl LocalPipelineDiagnosticsService {
Ok(summaries) => summaries,
Err(error) => return Err(error),
};
+ let event_coverage_summaries_result =
+ load_event_coverage_summaries(self.database.as_ref()).await;
+ let event_coverage_summaries = match event_coverage_summaries_result {
+ Ok(summaries) => summaries,
+ Err(error) => return Err(error),
+ };
let blocking_issue_count = counters.actionable_missing_trade_event_count
+ counters.invalid_trade_event_count
+ counters.duplicate_decoded_event_trade_count
@@ -77,6 +83,7 @@ impl LocalPipelineDiagnosticsService {
raydium_surface_summaries,
decoded_event_summaries,
event_classification_summaries,
+ event_coverage_summaries,
));
}
@@ -145,6 +152,12 @@ impl LocalPipelineDiagnosticsService {
Ok(summaries) => summaries,
Err(error) => return Err(error),
};
+ let event_coverage_summaries_result =
+ load_event_coverage_summaries(self.database.as_ref()).await;
+ let event_coverage_summaries = match event_coverage_summaries_result {
+ Ok(summaries) => summaries,
+ Err(error) => return Err(error),
+ };
let missing_trade_event_reason_summaries_result =
crate::query_local_missing_trade_event_reason_list_summaries(self.database.as_ref())
.await;
@@ -248,6 +261,8 @@ impl LocalPipelineDiagnosticsService {
+ counters.duplicate_decoded_event_trade_count
+ counters.duplicate_candle_bucket_count;
let diagnostics_clean = blocking_issue_count == 0;
+ let event_coverage_aggregate =
+ aggregate_event_coverage_summaries(&event_coverage_summaries);
return Ok(crate::LocalPipelineDiagnosticSummaryDto {
transaction_count: counters.transaction_count,
ok_transaction_count: counters.ok_transaction_count,
@@ -264,6 +279,27 @@ impl LocalPipelineDiagnosticsService {
fee_event_count: counters.fee_event_count,
reward_event_count: counters.reward_event_count,
pool_admin_event_count: counters.pool_admin_event_count,
+ event_coverage_listed_entry_count: event_coverage_aggregate.listed_entry_count,
+ event_coverage_decoded_entry_count: event_coverage_aggregate.decoded_entry_count,
+ event_coverage_observed_entry_count: event_coverage_aggregate.observed_entry_count,
+ event_coverage_materialized_entry_count: event_coverage_aggregate
+ .materialized_entry_count,
+ event_coverage_total_observed_count: event_coverage_aggregate.total_observed_count,
+ event_coverage_total_materialized_count: event_coverage_aggregate
+ .total_materialized_count,
+ event_coverage_trade_count: event_coverage_aggregate.trade_count,
+ event_coverage_audit_only_entry_count: event_coverage_aggregate.audit_only_entry_count,
+ event_coverage_missing_db_target_entry_count: event_coverage_aggregate
+ .missing_db_target_entry_count,
+ event_coverage_upstream_git_unverified_entry_count: event_coverage_aggregate
+ .upstream_git_unverified_entry_count,
+ event_coverage_upstream_git_mapped_unverified_entry_count: event_coverage_aggregate
+ .upstream_git_mapped_unverified_entry_count,
+ event_coverage_upstream_git_local_corpus_observed_entry_count: event_coverage_aggregate
+ .upstream_git_local_corpus_observed_entry_count,
+ event_coverage_upstream_git_local_corpus_materialized_entry_count:
+ event_coverage_aggregate.upstream_git_local_corpus_materialized_entry_count,
+ event_coverage_summaries,
diagnostics_clean,
blocking_issue_count,
missing_trade_event_count: counters.missing_trade_event_count,
@@ -334,315 +370,387 @@ async fn query_lightweight_validation_counters(
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let transaction_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_chain_transactions", "transaction_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_chain_transactions",
+ "transaction_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let ok_transaction_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_chain_transactions WHERE err_json IS NULL", "ok_transaction_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_chain_transactions WHERE err_json IS NULL",
+ "ok_transaction_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let failed_transaction_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_chain_transactions WHERE err_json IS NOT NULL", "failed_transaction_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_chain_transactions WHERE err_json IS NOT NULL",
+ "failed_transaction_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let decoded_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events", "decoded_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_dex_decoded_events",
+ "decoded_event_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let decoded_trade_candidate_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE json_extract(payload_json, '$.tradeCandidate') = 1", "decoded_trade_candidate_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE json_extract(payload_json, '$.tradeCandidate') = 1", "decoded_trade_candidate_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let decoded_candle_candidate_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE json_extract(payload_json, '$.candleCandidate') = 1", "decoded_candle_candidate_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE json_extract(payload_json, '$.candleCandidate') = 1", "decoded_candle_candidate_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let decoded_non_trade_useful_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE COALESCE(json_extract(payload_json, '$.nonTradeUseful'), 0) = 1 OR COALESCE(json_extract(payload_json, '$.eventActionability'), '') = 'non_trade_useful'", "decoded_non_trade_useful_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE COALESCE(json_extract(payload_json, '$.nonTradeUseful'), 0) = 1 OR COALESCE(json_extract(payload_json, '$.eventActionability'), '') = 'non_trade_useful'", "decoded_non_trade_useful_event_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let decoded_non_actionable_trade_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE COALESCE(json_extract(payload_json, '$.eventActionability'), '') = 'non_actionable_trade' OR (COALESCE(json_extract(payload_json, '$.eventActionability'), '') = '' AND COALESCE(json_extract(payload_json, '$.eventCategory'), '') = 'trade' AND COALESCE(json_extract(payload_json, '$.tradeCandidate'), 0) = 0 AND COALESCE(json_extract(payload_json, '$.transactionFailed'), 0) = 0)", "decoded_non_actionable_trade_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE COALESCE(json_extract(payload_json, '$.eventActionability'), '') = 'non_actionable_trade' OR (COALESCE(json_extract(payload_json, '$.eventActionability'), '') = '' AND COALESCE(json_extract(payload_json, '$.eventCategory'), '') = 'trade' AND COALESCE(json_extract(payload_json, '$.tradeCandidate'), 0) = 0 AND COALESCE(json_extract(payload_json, '$.transactionFailed'), 0) = 0)", "decoded_non_actionable_trade_event_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let decoded_unknown_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE COALESCE(json_extract(payload_json, '$.eventCategory'), 'unknown') = 'unknown'", "decoded_unknown_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE COALESCE(json_extract(payload_json, '$.eventCategory'), 'unknown') = 'unknown'", "decoded_unknown_event_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let liquidity_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_liquidity_events", "liquidity_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_liquidity_events",
+ "liquidity_event_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let pool_lifecycle_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pool_lifecycle_events", "pool_lifecycle_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
- let fee_event_count =
- {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_fee_events", "fee_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_pool_lifecycle_events",
+ "pool_lifecycle_event_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
+ let fee_event_count = {
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_fee_events",
+ "fee_event_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let reward_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_reward_events", "reward_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_reward_events",
+ "reward_event_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let pool_admin_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pool_admin_events", "pool_admin_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_pool_admin_events",
+ "pool_admin_event_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let missing_trade_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id)", "missing_trade_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id)", "missing_trade_event_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let decoded_trade_candidate_without_trade_event_count = missing_trade_event_count;
let decoded_trade_candidate_without_trade_event_on_ok_transaction_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id) AND ct.err_json IS NULL AND dde.pool_account IS NOT NULL AND dde.token_a_mint IS NOT NULL AND dde.token_b_mint IS NOT NULL AND EXISTS (SELECT 1 FROM k_sol_pools p JOIN k_sol_pairs pair ON pair.pool_id = p.id WHERE p.address = dde.pool_account)", "decoded_trade_candidate_without_trade_event_on_ok_transaction_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id) AND ct.err_json IS NULL AND dde.pool_account IS NOT NULL AND dde.token_a_mint IS NOT NULL AND dde.token_b_mint IS NOT NULL AND EXISTS (SELECT 1 FROM k_sol_pools p JOIN k_sol_pairs pair ON pair.pool_id = p.id WHERE p.address = dde.pool_account)", "decoded_trade_candidate_without_trade_event_on_ok_transaction_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let decoded_trade_candidate_without_trade_event_on_failed_transaction_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id) AND ct.err_json IS NOT NULL", "decoded_trade_candidate_without_trade_event_on_failed_transaction_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id) AND ct.err_json IS NOT NULL", "decoded_trade_candidate_without_trade_event_on_failed_transaction_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let actionable_missing_trade_event_count =
decoded_trade_candidate_without_trade_event_on_ok_transaction_count;
let ignored_failed_transaction_trade_candidate_count =
decoded_trade_candidate_without_trade_event_on_failed_transaction_count;
let decoded_trade_candidate_without_amount_payload_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id) AND ((json_extract(dde.payload_json, '$.baseAmountRaw') IS NULL AND json_extract(dde.payload_json, '$.base_amount_raw') IS NULL) OR (json_extract(dde.payload_json, '$.quoteAmountRaw') IS NULL AND json_extract(dde.payload_json, '$.quote_amount_raw') IS NULL))", "decoded_trade_candidate_without_amount_payload_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id) AND ((json_extract(dde.payload_json, '$.baseAmountRaw') IS NULL AND json_extract(dde.payload_json, '$.base_amount_raw') IS NULL) OR (json_extract(dde.payload_json, '$.quoteAmountRaw') IS NULL AND json_extract(dde.payload_json, '$.quote_amount_raw') IS NULL))", "decoded_trade_candidate_without_amount_payload_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let trade_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_trade_events", "trade_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_trade_events",
+ "trade_event_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let invalid_trade_event_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_trade_events WHERE base_amount_raw IS NULL OR quote_amount_raw IS NULL OR price_quote_per_base IS NULL OR CAST(base_amount_raw AS INTEGER) <= 0 OR CAST(quote_amount_raw AS INTEGER) <= 0 OR price_quote_per_base <= 0", "invalid_trade_event_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_trade_events WHERE base_amount_raw IS NULL OR quote_amount_raw IS NULL OR price_quote_per_base IS NULL OR CAST(base_amount_raw AS INTEGER) <= 0 OR CAST(quote_amount_raw AS INTEGER) <= 0 OR price_quote_per_base <= 0", "invalid_trade_event_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let pair_candle_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pair_candles", "pair_candle_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(*) FROM k_sol_pair_candles",
+ "pair_candle_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let duplicate_decoded_event_trade_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM (SELECT decoded_event_id FROM k_sol_trade_events WHERE decoded_event_id IS NOT NULL GROUP BY decoded_event_id HAVING COUNT(*) > 1)", "duplicate_decoded_event_trade_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM (SELECT decoded_event_id FROM k_sol_trade_events WHERE decoded_event_id IS NOT NULL GROUP BY decoded_event_id HAVING COUNT(*) > 1)", "duplicate_decoded_event_trade_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let multi_trade_signature_pair_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM (SELECT signature, pair_id FROM k_sol_trade_events GROUP BY signature, pair_id HAVING COUNT(*) > 1)", "multi_trade_signature_pair_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM (SELECT signature, pair_id FROM k_sol_trade_events GROUP BY signature, pair_id HAVING COUNT(*) > 1)", "multi_trade_signature_pair_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let duplicate_candle_bucket_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM (SELECT pair_id, timeframe_seconds, bucket_start_unix FROM k_sol_pair_candles GROUP BY pair_id, timeframe_seconds, bucket_start_unix HAVING COUNT(*) > 1)", "duplicate_candle_bucket_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
- let token_count =
- {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_tokens", "token_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM (SELECT pair_id, timeframe_seconds, bucket_start_unix FROM k_sol_pair_candles GROUP BY pair_id, timeframe_seconds, bucket_start_unix HAVING COUNT(*) > 1)", "duplicate_candle_bucket_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
+ let token_count = {
+ let counter_result =
+ query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_tokens", "token_count")
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let token_metadata_missing_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_tokens WHERE symbol IS NULL OR TRIM(symbol) = '' OR name IS NULL OR TRIM(name) = ''", "token_metadata_missing_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_tokens WHERE symbol IS NULL OR TRIM(symbol) = '' OR name IS NULL OR TRIM(name) = ''", "token_metadata_missing_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let tradable_token_metadata_missing_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(DISTINCT token.id) FROM k_sol_tokens token JOIN (SELECT pair.base_token_id AS token_id FROM k_sol_pairs pair JOIN k_sol_trade_events te ON te.pair_id = pair.id UNION SELECT pair.quote_token_id AS token_id FROM k_sol_pairs pair JOIN k_sol_trade_events te ON te.pair_id = pair.id) tradable_pair_token ON tradable_pair_token.token_id = token.id WHERE token.symbol IS NULL OR TRIM(token.symbol) = '' OR token.name IS NULL OR TRIM(token.name) = ''", "tradable_token_metadata_missing_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(DISTINCT token.id) FROM k_sol_tokens token JOIN (SELECT pair.base_token_id AS token_id FROM k_sol_pairs pair JOIN k_sol_trade_events te ON te.pair_id = pair.id UNION SELECT pair.quote_token_id AS token_id FROM k_sol_pairs pair JOIN k_sol_trade_events te ON te.pair_id = pair.id) tradable_pair_token ON tradable_pair_token.token_id = token.id WHERE token.symbol IS NULL OR TRIM(token.symbol) = '' OR token.name IS NULL OR TRIM(token.name) = ''", "tradable_token_metadata_missing_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let quote_token_metadata_missing_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(DISTINCT quote_token.id) FROM k_sol_pairs pair JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE quote_token.symbol IS NULL OR TRIM(quote_token.symbol) = '' OR quote_token.name IS NULL OR TRIM(quote_token.name) = ''", "quote_token_metadata_missing_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(DISTINCT quote_token.id) FROM k_sol_pairs pair JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE quote_token.symbol IS NULL OR TRIM(quote_token.symbol) = '' OR quote_token.name IS NULL OR TRIM(quote_token.name) = ''", "quote_token_metadata_missing_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let pair_symbol_fallback_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens base_token ON base_token.id = pair.base_token_id JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE pair.symbol IS NULL OR TRIM(pair.symbol) = '' OR pair.symbol = base_token.mint || '/' || quote_token.mint OR instr(pair.symbol, base_token.mint) > 0 OR instr(pair.symbol, quote_token.mint) > 0", "pair_symbol_fallback_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens base_token ON base_token.id = pair.base_token_id JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE pair.symbol IS NULL OR TRIM(pair.symbol) = '' OR pair.symbol = base_token.mint || '/' || quote_token.mint OR instr(pair.symbol, base_token.mint) > 0 OR instr(pair.symbol, quote_token.mint) > 0", "pair_symbol_fallback_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let pair_symbol_resolved_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens base_token ON base_token.id = pair.base_token_id JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE pair.symbol IS NOT NULL AND TRIM(pair.symbol) != '' AND pair.symbol != base_token.mint || '/' || quote_token.mint AND instr(pair.symbol, base_token.mint) = 0 AND instr(pair.symbol, quote_token.mint) = 0", "pair_symbol_resolved_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens base_token ON base_token.id = pair.base_token_id JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE pair.symbol IS NOT NULL AND TRIM(pair.symbol) != '' AND pair.symbol != base_token.mint || '/' || quote_token.mint AND instr(pair.symbol, base_token.mint) = 0 AND instr(pair.symbol, quote_token.mint) = 0", "pair_symbol_resolved_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let wsol_quote_pair_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE quote_token.mint = 'So11111111111111111111111111111111111111112'", "wsol_quote_pair_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE quote_token.mint = 'So11111111111111111111111111111111111111112'", "wsol_quote_pair_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let stable_quote_pair_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE quote_token.mint IN ('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB', 'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD')", "stable_quote_pair_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
- let pool_count =
- {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pools", "pool_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
- let pair_count =
- {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs", "pair_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE quote_token.mint IN ('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB', 'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD')", "stable_quote_pair_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
+ let pool_count = {
+ let counter_result =
+ query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pools", "pool_count")
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
+ let pair_count = {
+ let counter_result =
+ query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs", "pair_count")
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let literal_pair_without_trade_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair WHERE NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.pair_id = pair.id)", "literal_pair_without_trade_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair WHERE NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.pair_id = pair.id)", "literal_pair_without_trade_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let literal_pair_without_candle_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair WHERE NOT EXISTS (SELECT 1 FROM k_sol_pair_candles pc WHERE pc.pair_id = pair.id)", "literal_pair_without_candle_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair WHERE NOT EXISTS (SELECT 1 FROM k_sol_pair_candles pc WHERE pc.pair_id = pair.id)", "literal_pair_without_candle_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let trade_materialized_pair_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(DISTINCT pair_id) FROM k_sol_trade_events", "trade_materialized_pair_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(DISTINCT pair_id) FROM k_sol_trade_events",
+ "trade_materialized_pair_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let candle_materialized_pair_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(DISTINCT pair_id) FROM k_sol_pair_candles", "candle_materialized_pair_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(DISTINCT pair_id) FROM k_sol_pair_candles",
+ "candle_materialized_pair_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let actionable_pair_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND ct.err_json IS NULL)", "actionable_pair_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND ct.err_json IS NULL)", "actionable_pair_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let candle_bucket_timeframe_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(DISTINCT timeframe_seconds) FROM k_sol_pair_candles", "candle_bucket_timeframe_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(
+ pool,
+ "SELECT COUNT(DISTINCT timeframe_seconds) FROM k_sol_pair_candles",
+ "candle_bucket_timeframe_count",
+ )
+ .await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let non_actionable_pair_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id)) AND NOT EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND ct.err_json IS NULL AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id))", "non_actionable_pair_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id)) AND NOT EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND ct.err_json IS NULL AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id))", "non_actionable_pair_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let blocking_pair_without_trade_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND ct.err_json IS NULL) AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.pair_id = pair.id)", "blocking_pair_without_trade_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND ct.err_json IS NULL) AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.pair_id = pair.id)", "blocking_pair_without_trade_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
let blocking_pair_without_candle_count = {
- let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.candleCandidate') = 1 AND ct.err_json IS NULL) AND NOT EXISTS (SELECT 1 FROM k_sol_pair_candles pc WHERE pc.pair_id = pair.id)", "blocking_pair_without_candle_count").await;
- match counter_result {
- Ok(value) => value,
- Err(error) => return Err(error),
- }
- };
+ let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.candleCandidate') = 1 AND ct.err_json IS NULL) AND NOT EXISTS (SELECT 1 FROM k_sol_pair_candles pc WHERE pc.pair_id = pair.id)", "blocking_pair_without_candle_count").await;
+ match counter_result {
+ Ok(value) => value,
+ Err(error) => return Err(error),
+ }
+ };
return Ok(crate::LocalPipelineDiagnosticCountersDto {
transaction_count,
ok_transaction_count,
@@ -714,6 +822,79 @@ async fn query_validation_i64(
}
}
+async fn load_event_coverage_summaries(
+ database: &crate::Database,
+) -> Result, crate::Error> {
+ let refresh_result =
+ crate::query_dex_event_coverage_entries_refresh_local_counts(database).await;
+ if let Err(error) = refresh_result {
+ return Err(error);
+ }
+ let summaries_result =
+ crate::query_dex_event_coverage_entries_list_summary_by_decoder(database).await;
+ match summaries_result {
+ Ok(summaries) => return Ok(summaries),
+ Err(error) => return Err(error),
+ }
+}
+
+#[derive(Debug, Clone)]
+struct EventCoverageDiagnosticAggregate {
+ listed_entry_count: u64,
+ decoded_entry_count: u64,
+ observed_entry_count: u64,
+ materialized_entry_count: u64,
+ total_observed_count: u64,
+ total_materialized_count: u64,
+ trade_count: u64,
+ audit_only_entry_count: u64,
+ missing_db_target_entry_count: u64,
+ upstream_git_unverified_entry_count: u64,
+ upstream_git_mapped_unverified_entry_count: u64,
+ upstream_git_local_corpus_observed_entry_count: u64,
+ upstream_git_local_corpus_materialized_entry_count: u64,
+}
+
+fn aggregate_event_coverage_summaries(
+ summaries: &[crate::DexEventCoverageSummaryDto],
+) -> EventCoverageDiagnosticAggregate {
+ let mut aggregate = EventCoverageDiagnosticAggregate {
+ listed_entry_count: 0,
+ decoded_entry_count: 0,
+ observed_entry_count: 0,
+ materialized_entry_count: 0,
+ total_observed_count: 0,
+ total_materialized_count: 0,
+ trade_count: 0,
+ audit_only_entry_count: 0,
+ missing_db_target_entry_count: 0,
+ upstream_git_unverified_entry_count: 0,
+ upstream_git_mapped_unverified_entry_count: 0,
+ upstream_git_local_corpus_observed_entry_count: 0,
+ upstream_git_local_corpus_materialized_entry_count: 0,
+ };
+ for summary in summaries {
+ aggregate.listed_entry_count += summary.listed_entry_count;
+ aggregate.decoded_entry_count += summary.decoded_entry_count;
+ aggregate.observed_entry_count += summary.observed_entry_count;
+ aggregate.materialized_entry_count += summary.materialized_entry_count;
+ aggregate.total_observed_count += summary.total_observed_count;
+ aggregate.total_materialized_count += summary.total_materialized_count;
+ aggregate.trade_count += summary.trade_count;
+ aggregate.audit_only_entry_count += summary.audit_only_entry_count;
+ aggregate.missing_db_target_entry_count += summary.missing_db_target_entry_count;
+ aggregate.upstream_git_unverified_entry_count +=
+ summary.upstream_git_unverified_entry_count;
+ aggregate.upstream_git_mapped_unverified_entry_count +=
+ summary.upstream_git_mapped_unverified_entry_count;
+ aggregate.upstream_git_local_corpus_observed_entry_count +=
+ summary.upstream_git_local_corpus_observed_entry_count;
+ aggregate.upstream_git_local_corpus_materialized_entry_count +=
+ summary.upstream_git_local_corpus_materialized_entry_count;
+ }
+ return aggregate;
+}
+
fn build_lightweight_diagnostic_summary(
counters: crate::LocalPipelineDiagnosticCountersDto,
diagnostics_clean: bool,
@@ -724,7 +905,9 @@ fn build_lightweight_diagnostic_summary(
event_classification_summaries: std::vec::Vec<
crate::LocalEventClassificationDiagnosticSummaryDto,
>,
+ event_coverage_summaries: std::vec::Vec,
) -> crate::LocalPipelineDiagnosticSummaryDto {
+ let event_coverage_aggregate = aggregate_event_coverage_summaries(&event_coverage_summaries);
return crate::LocalPipelineDiagnosticSummaryDto {
transaction_count: counters.transaction_count,
ok_transaction_count: counters.ok_transaction_count,
@@ -740,6 +923,25 @@ fn build_lightweight_diagnostic_summary(
fee_event_count: counters.fee_event_count,
reward_event_count: counters.reward_event_count,
pool_admin_event_count: counters.pool_admin_event_count,
+ event_coverage_listed_entry_count: event_coverage_aggregate.listed_entry_count,
+ event_coverage_decoded_entry_count: event_coverage_aggregate.decoded_entry_count,
+ event_coverage_observed_entry_count: event_coverage_aggregate.observed_entry_count,
+ event_coverage_materialized_entry_count: event_coverage_aggregate.materialized_entry_count,
+ event_coverage_total_observed_count: event_coverage_aggregate.total_observed_count,
+ event_coverage_total_materialized_count: event_coverage_aggregate.total_materialized_count,
+ event_coverage_trade_count: event_coverage_aggregate.trade_count,
+ event_coverage_audit_only_entry_count: event_coverage_aggregate.audit_only_entry_count,
+ event_coverage_missing_db_target_entry_count: event_coverage_aggregate
+ .missing_db_target_entry_count,
+ event_coverage_upstream_git_unverified_entry_count: event_coverage_aggregate
+ .upstream_git_unverified_entry_count,
+ event_coverage_upstream_git_mapped_unverified_entry_count: event_coverage_aggregate
+ .upstream_git_mapped_unverified_entry_count,
+ event_coverage_upstream_git_local_corpus_observed_entry_count: event_coverage_aggregate
+ .upstream_git_local_corpus_observed_entry_count,
+ event_coverage_upstream_git_local_corpus_materialized_entry_count: event_coverage_aggregate
+ .upstream_git_local_corpus_materialized_entry_count,
+ event_coverage_summaries,
diagnostics_clean,
blocking_issue_count,
missing_trade_event_count: counters.missing_trade_event_count,
diff --git a/kb_lib/src/local_pipeline_validation.rs b/kb_lib/src/local_pipeline_validation.rs
index 977ee01..706436d 100644
--- a/kb_lib/src/local_pipeline_validation.rs
+++ b/kb_lib/src/local_pipeline_validation.rs
@@ -368,6 +368,31 @@ impl LocalPipelineValidationConfig {
return config;
}
+ /// Builds the `0.7.48-pre` event coverage DB checkpoint validation config.
+ ///
+ /// This profile closes the DB/reporting checkpoint before `0.7.48` opens
+ /// the Raydium CPMM event-coverage tranche. It exposes coverage summaries,
+ /// keeps core trade/candle invariants, and scopes the successful
+ /// trade-candidate materialization check to the expected Raydium DEXes so
+ /// unrelated partial/candidate DEXes remain inspectable without blocking
+ /// the DB checkpoint.
+ pub fn v0_7_48_pre_event_coverage_db_checkpoint() -> Self {
+ let mut config = Self::v0_7_42_raydium_family_event_coverage();
+ config.profile_code = "0.7.48-pre_event_coverage_db_checkpoint".to_string();
+ config.expected_dex_codes = vec![
+ "raydium_cpmm".to_string(),
+ "raydium_clmm".to_string(),
+ "raydium_amm_v4".to_string(),
+ ];
+ config.require_all_expected_dexes = false;
+ config.allow_unexpected_dexes = true;
+ config.require_ok_trade_candidates_fully_materialized = false;
+ config.require_trade_events_per_dex = false;
+ config.require_candles_per_dex = false;
+ config.require_pair_trading_readiness_semantics = false;
+ return config;
+ }
+
/// Builds the legacy `0.7.39` launch-surface validation alias.
///
/// The implementation now delegates to the DEX-first profile so callers that
@@ -424,6 +449,34 @@ pub struct LocalPipelineValidationReportDto {
pub reward_event_count: i64,
/// Total persisted pool administration events.
pub pool_admin_event_count: i64,
+ /// Event coverage entries listed from upstream registry sources.
+ pub event_coverage_listed_entry_count: u64,
+ /// Event coverage entries wired to local decoder event kinds.
+ pub event_coverage_decoded_entry_count: u64,
+ /// Event coverage entries observed in local corpus.
+ pub event_coverage_observed_entry_count: u64,
+ /// Event coverage entries materialized into target tables.
+ pub event_coverage_materialized_entry_count: u64,
+ /// Sum of local observations across coverage entries.
+ pub event_coverage_total_observed_count: u64,
+ /// Sum of local materialized rows across coverage entries.
+ pub event_coverage_total_materialized_count: u64,
+ /// Sum of linked trades across coverage entries.
+ pub event_coverage_trade_count: u64,
+ /// Audit-only coverage entries.
+ pub event_coverage_audit_only_entry_count: u64,
+ /// Coverage entries without an expected DB target.
+ pub event_coverage_missing_db_target_entry_count: u64,
+ /// Coverage entries still upstream Git unverified.
+ pub event_coverage_upstream_git_unverified_entry_count: u64,
+ /// Coverage entries mapped but not observed locally yet.
+ pub event_coverage_upstream_git_mapped_unverified_entry_count: u64,
+ /// Coverage entries observed locally.
+ pub event_coverage_upstream_git_local_corpus_observed_entry_count: u64,
+ /// Coverage entries observed and materialized locally.
+ pub event_coverage_upstream_git_local_corpus_materialized_entry_count: u64,
+ /// Event coverage summaries grouped by decoder.
+ pub event_coverage_summaries: std::vec::Vec,
/// Total known tokens.
pub token_count: i64,
/// Total tokens missing symbol or name.
@@ -500,6 +553,7 @@ impl LocalPipelineValidationService {
crate::LocalPipelineDiagnosticsService::new(self.database.clone());
let summary_result = if config.profile_code == "0.7.42_raydium_family_event_coverage"
|| config.profile_code == "0.7.43_meteora_effective_surfaces"
+ || config.profile_code == "0.7.48-pre_event_coverage_db_checkpoint"
{
diagnostics_service.diagnose_for_validation().await
} else {
@@ -651,6 +705,20 @@ impl LocalPipelineValidationService {
let config = crate::LocalPipelineValidationConfig::v0_7_43_meteora_effective_surfaces();
return self.validate_current_database(&config).await;
}
+
+ /// Diagnoses the current database with the `0.7.48-pre` event coverage checkpoint profile.
+ pub async fn validate_v0_7_48_pre_current_database(
+ &self,
+ ) -> Result {
+ let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
+ let sync_result = coverage_service.sync_upstream_registry(None).await;
+ if let Err(error) = sync_result {
+ return Err(error);
+ }
+ let config =
+ crate::LocalPipelineValidationConfig::v0_7_48_pre_event_coverage_db_checkpoint();
+ return self.validate_current_database(&config).await;
+ }
}
/// Validates a diagnostics summary without performing database access.
@@ -762,6 +830,13 @@ pub fn validate_local_pipeline_diagnostics_summary(
}
}
}
+ if config.profile_code == "0.7.48-pre_event_coverage_db_checkpoint" {
+ validate_expected_dex_trade_candidates_materialized_for_checkpoint(
+ &mut issues,
+ summary,
+ &expected_dex_codes,
+ );
+ }
if config.require_dex_support_matrix_semantics {
validate_dex_support_matrix_semantics(&mut issues);
}
@@ -778,7 +853,8 @@ pub fn validate_local_pipeline_diagnostics_summary(
|| config.profile_code == "0.7.39_launch_surface_origin_baseline"
|| config.profile_code == "0.7.40_raydium_effective_surfaces"
|| config.profile_code == "0.7.41_raydium_amm_v4_swap_decoder"
- || config.profile_code == "0.7.43_meteora_effective_surfaces";
+ || config.profile_code == "0.7.43_meteora_effective_surfaces"
+ || config.profile_code == "0.7.48-pre_event_coverage_db_checkpoint";
if config.require_all_expected_dexes || missing_expected_dex_is_warning {
for expected_dex_code in &expected_dex_codes {
if !observed_dex_codes.contains(expected_dex_code) {
@@ -853,6 +929,25 @@ pub fn validate_local_pipeline_diagnostics_summary(
fee_event_count: summary.fee_event_count,
reward_event_count: summary.reward_event_count,
pool_admin_event_count: summary.pool_admin_event_count,
+ event_coverage_listed_entry_count: summary.event_coverage_listed_entry_count,
+ event_coverage_decoded_entry_count: summary.event_coverage_decoded_entry_count,
+ event_coverage_observed_entry_count: summary.event_coverage_observed_entry_count,
+ event_coverage_materialized_entry_count: summary.event_coverage_materialized_entry_count,
+ event_coverage_total_observed_count: summary.event_coverage_total_observed_count,
+ event_coverage_total_materialized_count: summary.event_coverage_total_materialized_count,
+ event_coverage_trade_count: summary.event_coverage_trade_count,
+ event_coverage_audit_only_entry_count: summary.event_coverage_audit_only_entry_count,
+ event_coverage_missing_db_target_entry_count: summary
+ .event_coverage_missing_db_target_entry_count,
+ event_coverage_upstream_git_unverified_entry_count: summary
+ .event_coverage_upstream_git_unverified_entry_count,
+ event_coverage_upstream_git_mapped_unverified_entry_count: summary
+ .event_coverage_upstream_git_mapped_unverified_entry_count,
+ event_coverage_upstream_git_local_corpus_observed_entry_count: summary
+ .event_coverage_upstream_git_local_corpus_observed_entry_count,
+ event_coverage_upstream_git_local_corpus_materialized_entry_count: summary
+ .event_coverage_upstream_git_local_corpus_materialized_entry_count,
+ event_coverage_summaries: summary.event_coverage_summaries.clone(),
token_count: summary.token_count,
token_metadata_missing_count: summary.token_metadata_missing_count,
tradable_token_metadata_missing_count: summary.tradable_token_metadata_missing_count,
@@ -1104,6 +1199,48 @@ fn collect_observed_dex_codes(
return observed_dex_codes;
}
+fn validate_expected_dex_trade_candidates_materialized_for_checkpoint(
+ issues: &mut std::vec::Vec,
+ summary: &crate::LocalPipelineDiagnosticSummaryDto,
+ expected_dex_codes: &[std::string::String],
+) {
+ for decoded_summary in &summary.decoded_event_summaries {
+ if !expected_dex_codes.contains(&decoded_summary.protocol_name) {
+ continue;
+ }
+ let actionability = match &decoded_summary.event_actionability {
+ Some(value) => value.as_str(),
+ None => "",
+ };
+ let is_trade_candidate = match decoded_summary.trade_candidate {
+ Some(value) => value,
+ None => false,
+ };
+ if actionability != "trade_candidate" && !is_trade_candidate {
+ continue;
+ }
+ let missing_trade_event_count =
+ decoded_summary.event_count - decoded_summary.trade_event_count;
+ if missing_trade_event_count <= 0 {
+ continue;
+ }
+ issues.push(crate::LocalPipelineValidationIssueDto {
+ code: "expected_dex_trade_candidates_without_trade_events".to_string(),
+ message: format!(
+ "expected DEX '{}' event '{}' has {} trade candidate(s) without persisted trade event",
+ decoded_summary.protocol_name,
+ decoded_summary.event_kind,
+ missing_trade_event_count
+ ),
+ subject: Some(format!(
+ "{}:{}",
+ decoded_summary.protocol_name, decoded_summary.event_kind
+ )),
+ blocking: true,
+ });
+ }
+}
+
#[cfg(test)]
mod tests {
fn make_clean_summary() -> crate::LocalPipelineDiagnosticSummaryDto {
@@ -1122,6 +1259,20 @@ mod tests {
fee_event_count: 0,
reward_event_count: 0,
pool_admin_event_count: 0,
+ event_coverage_listed_entry_count: 0,
+ event_coverage_decoded_entry_count: 0,
+ event_coverage_observed_entry_count: 0,
+ event_coverage_materialized_entry_count: 0,
+ event_coverage_total_observed_count: 0,
+ event_coverage_total_materialized_count: 0,
+ event_coverage_trade_count: 0,
+ event_coverage_audit_only_entry_count: 0,
+ event_coverage_missing_db_target_entry_count: 0,
+ event_coverage_upstream_git_unverified_entry_count: 0,
+ event_coverage_upstream_git_mapped_unverified_entry_count: 0,
+ event_coverage_upstream_git_local_corpus_observed_entry_count: 0,
+ event_coverage_upstream_git_local_corpus_materialized_entry_count: 0,
+ event_coverage_summaries: vec![],
diagnostics_clean: true,
blocking_issue_count: 0,
missing_trade_event_count: 6,
@@ -1658,6 +1809,98 @@ mod tests {
assert_eq!(report.fee_event_count, 2);
}
+ #[test]
+ fn validation_accepts_0_7_48_pre_event_coverage_checkpoint() {
+ let mut summary = make_0_7_28_summary_with_meteora();
+ summary.event_coverage_listed_entry_count = 4;
+ summary.event_coverage_decoded_entry_count = 3;
+ summary.event_coverage_observed_entry_count = 2;
+ summary.event_coverage_materialized_entry_count = 1;
+ summary.event_coverage_total_observed_count = 8;
+ summary.event_coverage_total_materialized_count = 5;
+ summary.event_coverage_trade_count = 4;
+ summary.event_coverage_audit_only_entry_count = 1;
+ summary.event_coverage_upstream_git_mapped_unverified_entry_count = 1;
+ summary.event_coverage_upstream_git_local_corpus_observed_entry_count = 1;
+ summary.event_coverage_upstream_git_local_corpus_materialized_entry_count = 1;
+ summary.event_coverage_summaries.push(crate::DexEventCoverageSummaryDto {
+ decoder_code: "raydium-cpmm".to_string(),
+ listed_entry_count: 4,
+ decoded_entry_count: 3,
+ observed_entry_count: 2,
+ materialized_entry_count: 1,
+ total_observed_count: 8,
+ total_materialized_count: 5,
+ trade_count: 4,
+ audit_only_entry_count: 1,
+ missing_db_target_entry_count: 0,
+ upstream_git_unverified_entry_count: 0,
+ upstream_git_mapped_unverified_entry_count: 1,
+ upstream_git_local_corpus_observed_entry_count: 1,
+ upstream_git_local_corpus_materialized_entry_count: 1,
+ });
+ let config =
+ crate::LocalPipelineValidationConfig::v0_7_48_pre_event_coverage_db_checkpoint();
+ let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
+ assert!(report.validation_passed);
+ assert_eq!(report.validation_profile_code, "0.7.48-pre_event_coverage_db_checkpoint");
+ assert_eq!(report.event_coverage_listed_entry_count, 4);
+ assert_eq!(report.event_coverage_summaries.len(), 1);
+ assert!(report.expected_dex_codes.contains(&"raydium_cpmm".to_string()));
+ }
+
+ #[test]
+ fn validation_accepts_0_7_48_pre_with_unrelated_partial_dex_gap() {
+ let mut summary = make_0_7_28_summary_with_meteora();
+ summary.actionable_missing_trade_event_count = 2;
+ summary.decoded_trade_candidate_without_trade_event_on_ok_transaction_count = 2;
+ summary.decoded_trade_candidate_without_trade_event_count = 2;
+ summary.decoded_trade_candidate_without_amount_payload_count = 2;
+ summary
+ .decoded_event_summaries
+ .push(crate::LocalDecodedEventDiagnosticSummaryDto {
+ protocol_name: "fluxbeam".to_string(),
+ event_kind: "fluxbeam.swap".to_string(),
+ event_category: Some("trade".to_string()),
+ event_lifecycle_kind: Some("trade_swap".to_string()),
+ event_actionability: Some("trade_candidate".to_string()),
+ non_trade_useful: Some(false),
+ trade_candidate: Some(true),
+ candle_candidate: Some(true),
+ event_count: 2,
+ trade_event_count: 0,
+ });
+ let config =
+ crate::LocalPipelineValidationConfig::v0_7_48_pre_event_coverage_db_checkpoint();
+ let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
+ assert!(report.validation_passed);
+ assert_eq!(report.blocking_issue_count, 0);
+ }
+
+ #[test]
+ fn validation_rejects_0_7_48_pre_expected_dex_trade_gap() {
+ let mut summary = make_0_7_28_summary_with_meteora();
+ summary
+ .decoded_event_summaries
+ .push(crate::LocalDecodedEventDiagnosticSummaryDto {
+ protocol_name: "raydium_cpmm".to_string(),
+ event_kind: "raydium_cpmm.swap_base_input".to_string(),
+ event_category: Some("trade".to_string()),
+ event_lifecycle_kind: Some("trade_swap".to_string()),
+ event_actionability: Some("trade_candidate".to_string()),
+ non_trade_useful: Some(false),
+ trade_candidate: Some(true),
+ candle_candidate: Some(true),
+ event_count: 7,
+ trade_event_count: 6,
+ });
+ let config =
+ crate::LocalPipelineValidationConfig::v0_7_48_pre_event_coverage_db_checkpoint();
+ let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
+ assert!(!report.validation_passed);
+ assert_eq!(report.issues[0].code, "expected_dex_trade_candidates_without_trade_events");
+ }
+
#[test]
fn validation_rejects_0_7_33_pair_trading_readiness_mismatch() {
let mut summary = make_0_7_28_summary_with_meteora();