0.7.48-pre

This commit is contained in:
2026-05-31 19:23:46 +02:00
parent 8b09e82b3b
commit abb810d544
20 changed files with 2864 additions and 348 deletions

View File

@@ -87,3 +87,7 @@
0.7.47-doc-matrix - Révision documentaire : ajout dune 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 lordre 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 quun DEX partiel hors scope, comme `fluxbeam`, reste diagnostiqué sans bloquer le checkpoint DB/event coverage.

View File

@@ -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 quun 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.

View File

@@ -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 liss depuis Carbon/fnzero/IDL.
À faire :
- utiliser le registre `0.7.47` comme source dindices, 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 dorigine 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 linvariant 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 dorigine.
### 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 quils 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 : sassurer 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 lobservation 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 à lanalyse `0.8.x`.
À faire : bases neuves, compteurs globaux et par DEX, diagnostics bloquants, samples danomalie, 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 :

View File

@@ -197,7 +197,8 @@
<div class="mb-3">
<label for="demoPipeline2ValidationProfileSelect" class="form-label">Validation profile</label>
<select id="demoPipeline2ValidationProfileSelect" class="form-select">
<option value="0.7.43_meteora_effective_surfaces" selected>0.7.43 — Meteora family event coverage</option>
<option value="0.7.48-pre_event_coverage_db_checkpoint" selected>0.7.48-pre — Event coverage DB checkpoint</option>
<option value="0.7.43_meteora_effective_surfaces">0.7.43 — Meteora family event coverage</option>
<option value="0.7.42_raydium_family_event_coverage">0.7.42 — Raydium family event coverage</option>
<option value="0.7.41_raydium_amm_v4_swap_decoder">0.7.41 — Raydium AMM v4 swap decoder</option>
<option value="0.7.40_raydium_effective_surfaces">0.7.40 — Raydium effective surfaces</option>

View File

@@ -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.
*/

View File

@@ -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.
*/

View File

@@ -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,

View File

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

View File

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

View File

@@ -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<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<std::string::String>,
/// 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<std::string::String>,
/// Source path inside the repository or registry source.
pub source_path: std::option::Option<std::string::String>,
/// 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<std::string::String>,
/// Optional discriminator byte length.
pub discriminator_len: std::option::Option<i64>,
/// Normalized event family.
pub event_family: std::option::Option<std::string::String>,
/// Expected DB target table or audit-only target.
pub expected_db_target: std::option::Option<std::string::String>,
/// 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<std::string::String>,
/// 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<std::string::String>,
/// Last local signature observed for this entry.
pub last_signature: std::option::Option<std::string::String>,
/// Notes preserving uncertainty, mapping decisions or DB gaps.
pub notes: std::option::Option<std::string::String>,
/// Creation timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
/// Update timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}
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<std::string::String>,
source_repo: &std::option::Option<std::string::String>,
source_path: &std::option::Option<std::string::String>,
entry_kind: &str,
entry_name: &str,
discriminator_hex: &std::option::Option<std::string::String>,
) -> 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<std::string::String>,
program_family: std::string::String,
surface_kind: std::string::String,
source_repo: std::option::Option<std::string::String>,
source_path: std::option::Option<std::string::String>,
entry_kind: std::string::String,
entry_name: std::string::String,
discriminator_hex: std::option::Option<std::string::String>,
discriminator_len: std::option::Option<i64>,
event_family: std::option::Option<std::string::String>,
expected_db_target: std::option::Option<std::string::String>,
proof_status: std::string::String,
local_event_kind: std::option::Option<std::string::String>,
observed_count: i64,
materialized_count: i64,
trade_count: i64,
first_signature: std::option::Option<std::string::String>,
last_signature: std::option::Option<std::string::String>,
notes: std::option::Option<std::string::String>,
) -> 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<std::string::String>,
expected_db_target: std::option::Option<std::string::String>,
local_event_kind: std::option::Option<std::string::String>,
) -> 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<crate::DexEventCoverageEntryEntity> for DexEventCoverageEntryDto {
type Error = crate::Error;
fn try_from(entity: crate::DexEventCoverageEntryEntity) -> Result<Self, Self::Error> {
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<u64, crate::Error> {
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<crate::DexEventCoverageSummaryEntity> for DexEventCoverageSummaryDto {
type Error = crate::Error;
fn try_from(entity: crate::DexEventCoverageSummaryEntity) -> Result<Self, Self::Error> {
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,
});
}
}

View File

@@ -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<crate::DexEventCoverageSummaryDto>,
/// 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<crate::LocalDexDiagnosticSummaryDto>,
/// Raydium surface diagnostics derived from the matrix and observed instructions.
pub raydium_surface_summaries:
std::vec::Vec<crate::LocalRaydiumSurfaceDiagnosticSummaryDto>,
pub raydium_surface_summaries: std::vec::Vec<crate::LocalRaydiumSurfaceDiagnosticSummaryDto>,
/// Diagnostics grouped by pair.
pub pair_summaries: std::vec::Vec<crate::LocalPairDiagnosticSummaryDto>,
/// Diagnostics grouped by pair materialization/actionability class.

View File

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

View File

@@ -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<std::string::String>,
/// 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<std::string::String>,
/// Source path inside the repository or registry source.
pub source_path: std::option::Option<std::string::String>,
/// 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<std::string::String>,
/// Optional discriminator byte length.
pub discriminator_len: std::option::Option<i64>,
/// Normalized event family.
pub event_family: std::option::Option<std::string::String>,
/// Expected DB target table or audit-only target.
pub expected_db_target: std::option::Option<std::string::String>,
/// 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<std::string::String>,
/// 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<std::string::String>,
/// Last local signature observed for this entry.
pub last_signature: std::option::Option<std::string::String>,
/// Notes preserving uncertainty, mapping decisions or DB gaps.
pub notes: std::option::Option<std::string::String>,
/// 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,
}

View File

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

View File

@@ -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<i64, crate::Error> {
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::<sqlx::Sqlite, i64>(
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<u64, crate::Error> {
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<std::vec::Vec<crate::DexEventCoverageEntryDto>, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::DexEventCoverageEntryEntity>(
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<std::vec::Vec<crate::DexEventCoverageSummaryDto>, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result =
sqlx::query_as::<sqlx::Sqlite, crate::DexEventCoverageSummaryEntity>(
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<u64, crate::Error> {
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<u64, crate::Error> {
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<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let now = chrono::Utc::now().to_rfc3339();
let decoder_code_filter: std::option::Option<String> = 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<crate::DexEventCoverageEntryEntity>,
) -> Result<std::vec::Vec<crate::DexEventCoverageEntryDto>, 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);
}
}

View File

@@ -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,

View File

@@ -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<std::string::String>,
/// 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<crate::DexEventCoverageSummaryDto>,
}
/// Service used to persist and refresh DEX event coverage rows.
#[derive(Debug, Clone)]
pub struct DexEventCoverageService {
database: std::sync::Arc<crate::Database>,
upstream_registry: crate::UpstreamRegistryService,
}
impl DexEventCoverageService {
/// Creates a new event coverage service.
pub fn new(database: std::sync::Arc<crate::Database>) -> 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<std::string::String>,
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
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<std::string::String>,
) -> Result<crate::DexEventCoverageSyncResult, crate::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: 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<std::string::String> {
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<std::string::String> {
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<std::string::String> {
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<crate::Database> {
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()
}));
}
}

View File

@@ -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.

View File

@@ -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<std::vec::Vec<crate::DexEventCoverageSummaryDto>, 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::DexEventCoverageSummaryDto>,
) -> 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,

View File

@@ -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<crate::DexEventCoverageSummaryDto>,
/// 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<crate::LocalPipelineValidationRunDto, crate::Error> {
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<crate::LocalPipelineValidationIssueDto>,
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();