This commit is contained in:
2026-06-01 19:05:46 +02:00
parent abb810d544
commit 27e25d5bf4
59 changed files with 5727 additions and 1706 deletions

View File

@@ -80,14 +80,14 @@
0.7.46-demo3 - Correction ciblée de Demo3 pour la découverte/backfill : ajout dun décodage léger des instructions `meteora_damm_v1` connues par discriminant upstream Git dans `onchain_dex_pair_discovery`, classification instruction-scoped prioritaire pour éviter quun `Swap` soit classé `add_liquidity` à cause de logs mixtes de transaction, filtrage `target_event` strict sur les surfaces explicites, conservation des transactions mixtes quand un target explicite est demandé malgré `excludeSwaps`, et ajout des cibles UI `create_lock_escrow` / `lock_liquidity`. 0.7.46-demo3 - Correction ciblée de Demo3 pour la découverte/backfill : ajout dun décodage léger des instructions `meteora_damm_v1` connues par discriminant upstream Git dans `onchain_dex_pair_discovery`, classification instruction-scoped prioritaire pour éviter quun `Swap` soit classé `add_liquidity` à cause de logs mixtes de transaction, filtrage `target_event` strict sur les surfaces explicites, conservation des transactions mixtes quand un target explicite est demandé malgré `excludeSwaps`, et ajout des cibles UI `create_lock_escrow` / `lock_liquidity`.
0.7.46-demo3-paged - Amélioration Demo3 discovery : pagination `getSignaturesForAddress` via `before_signature` / `until_signature`, scan de plusieurs adresses source dans une seule requête, déduplication des signatures multi-pool, compteur de pages, curseurs `next_before` par adresse et ordre de traitement `newest_first` / `oldest_first` pour constituer un corpus depuis les premières signatures dun pool sans promouvoir de nouveau `program_id`. 0.7.46-demo3-paged - Amélioration Demo3 discovery : pagination `getSignaturesForAddress` via `before_signature` / `until_signature`, scan de plusieurs adresses source dans une seule requête, déduplication des signatures multi-pool, compteur de pages, curseurs `next_before` par adresse et ordre de traitement `newest_first` / `oldest_first` pour constituer un corpus depuis les premières signatures dun pool sans promouvoir de nouveau `program_id`.
0.7.46-final - Renommage documentaire et payload des anciens statuts/fonctions liés au dépôt source vers une terminologie générique `upstream_git_*` : `proofStatus` utilise désormais `upstream_git_local_corpus_observed`, `upstream_git_mapped_unverified` et `upstream_git_layout_unverified`; les payloads DAMM v1 utilisent `upstreamInstructionName`; la documentation prépare `0.7.47` comme Upstream Git Registry / DEX discovery preparation au lieu dune tranche DAMM v2 immédiate. 0.7.46-final - Renommage documentaire et payload des anciens statuts/fonctions liés au dépôt source vers une terminologie générique `upstream_git_*` : `proofStatus` utilise désormais `upstream_git_local_corpus_observed`, `upstream_git_mapped_unverified` et `upstream_git_layout_unverified`; les payloads DAMM v1 utilisent `upstreamInstructionName`; la documentation prépare `0.7.47` comme Upstream Git Registry / DEX discovery preparation au lieu dune tranche DAMM v2 immédiate.
0.7.47 - Upstream Git Registry / DEX discovery preparation : ajout dun registre générique `upstream_git` pour indexer `program_id`, discriminants dinstructions/events, noms dentrées et familles de programmes depuis Carbon et sources Git/IDL externes ; extension Demo3 aux targets multi-surfaces, orderbook, burn/mint/transfer/wrap/unwrap/stake ; ajout de groupes de signatures réussies/échouées pour alimenter Demo2 ; maintien strict de linvariant : aucune entrée upstream Git ne produit trade/candle sans decoder spécialisé et corpus local.
0.7.47 - Upstream Git Registry / DEX discovery preparation : ajout dun registre générique `upstream_git` pour indexer `program_id`, discriminants dinstructions/events, noms dentrées et familles de programmes depuis Carbon et sources Git/IDL externes ; extension Demo3 aux targets multi-surfaces, orderbook, burn/mint/transfer/wrap/unwrap/stake ; ajout de groupes de signatures réussies/échouées pour alimenter Demo2 ; maintien strict de linvariant : aucune entrée upstream Git ne produit trade/candle sans decoder spécialisé et corpus local. 0.7.47-openbook-v2-audit - Ajout dun decoder local `openbook_v2` audit-only : instructions `place_order`, `cancel_order_by_client_order_id`, `consume_events`, `settle_funds`, `close_open_orders_account`; cleanup du fallback `upstream_git.instruction_match`; extraction audit des `Program return` et `Program data`; mapping des logs `FillLog`, `OpenOrdersPositionLog`, `TotalOrderFillEvent`, `SettleFundsLog`; aucune matérialisation trade/candle.
0.7.47-openbook-v2-audit - Ajout dun decoder local `openbook_v2` audit-only : instructions `place_order`, `cancel_order_by_client_order_id`, `consume_events`, `settle_funds`, `close_open_orders_account`; cleanup du fallback `upstream_git.instruction_match`; extraction audit des `Program return` et `Program data`; mapping des logs `FillLog`, `OpenOrdersPositionLog`, `TotalOrderFillEvent`, `SettleFundsLog`; aucune matérialisation trade/candle. 0.7.47-phoenix-v1-audit - Ajout dun decoder local `phoenix_v1` audit-only : `order_place`, `order_cancel`, `funds_withdraw`, `log`; parsing strict des instructions log `0x0f`; décodage audit du header Phoenix log et des events `Reduce`, `Place`, `TimeInForce`; correction du mapping `PlaceMultiplePostOnlyOrders` tag `0x10`; aucune matérialisation trade/candle.
0.7.47-phoenix-v1-audit - Ajout dun decoder local `phoenix_v1` audit-only : `order_place`, `order_cancel`, `funds_withdraw`, `log`; parsing strict des instructions log `0x0f`; décodage audit du header Phoenix log et des events `Reduce`, `Place`, `TimeInForce`; correction du mapping `PlaceMultiplePostOnlyOrders` tag `0x10`; aucune matérialisation trade/candle. 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-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.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-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-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-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-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-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.
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-raydium-cpmm-program-data - Complément Raydium CPMM : auto-sync conservatoire de `k_sol_dex_event_coverage_entries` lors des diagnostics/backfills si la table de coverage est vide, décodage des events CPMM `Program data` `swap_event` et `lp_change_event` sans sélecteur Anchor self-CPI, conservation explicite de `swap_event` en audit-only pour éviter tout doublon de trade/candle, matérialisation liquidity conservatoire de `lp_change_event` via `changeType`, et maintien des invariants failed transaction / non-trade / upstream Git.
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. 0.7.48 - Raydium CPMM event coverage clôturé : décodage spécialisé des instructions/events CPMM Carbon/Raydium/fnzero, auto-sync coverage, index technique `k_sol_instruction_observations`, recherche Demo3 par instruction/discriminant, matérialisation validée des swaps, lifecycle, fees, admin/config, deposit/withdraw et `lp_change_event`, conservation de `swap_event` et de linstruction inconnue `40f4bc78a7e9690a` en audit-only, et maintien des entrées `close_permission_pda` / `update_pool_status` en `upstream_git_mapped_unverified` faute de corpus local.

View File

@@ -8,7 +8,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.7.47" version = "0.7.48"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"
@@ -89,3 +89,4 @@ manual_unwrap_or_default = "allow"
manual_find = "allow" manual_find = "allow"
explicit_counter_loop = "allow" explicit_counter_loop = "allow"
get_first = "allow" get_first = "allow"
implicit_saturating_sub = "allow"

View File

@@ -289,3 +289,20 @@ Objectif :
- materialized events, - materialized events,
- missing DB target, - missing DB target,
- trade_count invariant. - trade_count invariant.
## Note `0.7.48` — Raydium CPMM sans nouvelle table DB
La tranche `0.7.48` confirme que `k_sol_dex_decoded_events` suffit pour continuer la couverture exhaustive CPMM en audit-only.
Aucune nouvelle table n'est ajoutée pour CPMM :
- les swaps exploitables restent dans `k_sol_trade_events` via les chemins existants ;
- `deposit` / `withdraw` utilisent les tables non-trade existantes seulement si le corpus et le rattachement pool/pair sont fiables ;
- les fees/admin/config/permission restent non-trade et ne peuvent pas produire candles ;
- les transfers SPL, account lifecycle, wrap/unwrap SOL, vault et launch/migration restent des familles transversales futures, à promouvoir seulement si plusieurs DEX en justifient le besoin.
## Note 0.7.48 final — Instruction observations et CPMM
La tranche `raydium_cpmm` ajoute `k_sol_instruction_observations` comme table technique dindex local, non comme table métier. Elle sert à chercher les instructions observées par `decoder_code`, `instruction_name` et `discriminator_hex`, puis à relier ces observations au corpus backfillé/rejoué.
La matérialisation métier reste limitée aux tables existantes : `k_sol_trade_events`, `k_sol_liquidity_events`, `k_sol_pool_lifecycle_events`, `k_sol_fee_events` et `k_sol_pool_admin_events`. Les opérations SPL Token / Token-2022 visibles dans Solscan (`burn`, `transfer`, `transferChecked`, `closeAccount`) ne sont pas promues en tables métier dans `0.7.48`; elles justifient seulement une future table transversale si plusieurs DEX le nécessitent.

View File

@@ -43,7 +43,7 @@ Cette matrice complète `kb_lib/src/dex_support_matrix.rs`. Elle documente **ce
| 13 | `meteora_vault` | `to_verify` | Présent comme indice upstream / compte associé. | Corpus direct obligatoire ; decoder séparé si events vault réels ; aucune promotion via DAMM indirect. | | 13 | `meteora_vault` | `to_verify` | Présent comme indice upstream / compte associé. | Corpus direct obligatoire ; decoder séparé si events vault réels ; aucune promotion via DAMM indirect. |
| 14 | `fluxbeam` | `partial/to_verify` | Decoder initial existant ; Demo3 peut produire des candidats. | Vérifier source/IDL ; compléter swap, pool, liquidity, fees/admin ; matérialisation uniquement après corpus. | | 14 | `fluxbeam` | `partial/to_verify` | Decoder initial existant ; Demo3 peut produire des candidats. | Vérifier source/IDL ; compléter swap, pool, liquidity, fees/admin ; matérialisation uniquement après corpus. |
| 15 | `dexlab` | `partial/to_verify` | Decoder initial historique ; ancienne entrée beta supprimée. | Reconfirmer program id/source ; décoder events disponibles ; distinguer DexLab natif et liens OpenBook/market. | | 15 | `dexlab` | `partial/to_verify` | Decoder initial historique ; ancienne entrée beta supprimée. | Reconfirmer program id/source ; décoder events disponibles ; distinguer DexLab natif et liens OpenBook/market. |
| 16 | `lifinity_amm_v2` | `to_verify` | Program id listé par sources externes/Vybe ; pas de corpus concluant. | Trouver IDL/source ; Demo3 par program/market ; audit-only dabord. | | 16 | `lifinity_v2` | `to_verify` | Program id listé par sources externes/Vybe ; pas de corpus concluant. | Trouver IDL/source ; Demo3 par program/market ; audit-only dabord. |
| 17 | `stabble_stable_swap` / `stabble_weighted_swap` | `to_verify` | Program ids/indices via sources externes ; candidats Demo3 observables. | Source/IDL + corpus + decoder audit-only ; déterminer surface AMM et montants exploitables. | | 17 | `stabble_stable_swap` / `stabble_weighted_swap` | `to_verify` | Program ids/indices via sources externes ; candidats Demo3 observables. | Source/IDL + corpus + decoder audit-only ; déterminer surface AMM et montants exploitables. |
| 18 | `bonkswap` | `to_verify` | Program id/Carbon/Vybe selon registre ; swaps candidats possibles. | Vérifier program id, source et corpus ; décoder tous events ; pas de trade sans montants. | | 18 | `bonkswap` | `to_verify` | Program id/Carbon/Vybe selon registre ; swaps candidats possibles. | Vérifier program id, source et corpus ; décoder tous events ; pas de trade sans montants. |
| 19 | `boop` / `boop_fun` | `to_verify / launch` | Entrée de découverte. | Séparer launch surface et swap effectif ; corpus + source obligatoire. | | 19 | `boop` / `boop_fun` | `to_verify / launch` | Entrée de découverte. | Séparer launch surface et swap effectif ; corpus + source obligatoire. |
@@ -202,3 +202,24 @@ Un event peut devenir `materialized` uniquement si :
| `aquifer` | `to_verify` | `unknown` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source | | `aquifer` | `to_verify` | `unknown` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source |
| `humidifi` | `to_verify` | `unknown` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source | | `humidifi` | `to_verify` | `unknown` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source |
| `solfi_v2` | `to_verify` | `AMM` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source | | `solfi_v2` | `to_verify` | `AMM` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source |
## Note `0.7.48` — Raydium CPMM
`raydium_cpmm` reste `supported`, mais sa couverture est maintenant explicitée au niveau entry/event coverage.
Entrées CPMM couvertes localement depuis Carbon/fnzero/IDL :
- swaps : `swap_base_input`, `swap_base_output` ;
- events Anchor self-CPI audit-only : `lp_change_event`, `swap_event` ;
- pool/lifecycle : `initialize`, `initialize_with_permission` ;
- liquidity : `deposit`, `withdraw` ;
- fees : `collect_creator_fee`, `collect_fund_fee`, `collect_protocol_fee` ;
- admin/config/permission : `create_amm_config`, `update_amm_config`, `update_pool_status`, `create_permission_pda`, `close_permission_pda`.
`create_amm_config` est traité comme admin/config, pas comme pool creation. `swap_event` est conservé comme audit-only pour ne pas doubler les trades matérialisés depuis les instructions `swap_base_input` / `swap_base_output`.
## Note `0.7.48 final` — Raydium CPMM
`raydium_cpmm` est considéré `supported` et clôturable pour la tranche `0.7.48`. Les entrées matérialisées couvrent swaps (`swap_base_input`, `swap_base_output`), liquidity (`deposit`, `withdraw`, `lp_change_event`), lifecycle (`initialize`, `initialize_with_permission`), fees (`collect_creator_fee`, `collect_fund_fee`, `collect_protocol_fee`) et admin/config (`create_amm_config`, `create_permission_pda`, `update_amm_config`).
`swap_event` reste audit-only pour éviter tout doublon de trade/candle. `close_permission_pda` et `update_pool_status` restent connus upstream mais non observés localement.

View File

@@ -1,232 +1,71 @@
# DEX Event Coverage Matrix — `khadhroony-bobobot` `0.7.47-1FE5` # DEX Event Coverage Matrix — `khadhroony-bobobot` `0.7.48`
Cette matrice complète `DEX_DECODER_MATRIX.md`. Cette matrice complète `DEX_DECODER_MATRIX.md` avec une lecture par familles d'événements. Elle ne remplace pas la preuve locale : une entrée Git/IDL reste un indice tant qu'elle n'est pas observée dans le corpus local puis validée par replay et SQL.
La matrice précédente répondait à la question : **quel DEX/version est couvert ?** ## Règles de statut
Cette matrice répond à la question : **quels events/instructions doivent être décodés, audit-only ou matérialisés ?** | Statut | Sens |
## Principe
Lobjectif nest plus seulement de décoder les swaps. Lobjectif est de décoder le maximum dévénements disponibles dans les sources Git/IDL et dans le corpus local, parce que certains événements non-trade peuvent influencer une décision de trading :
- perte ou ajout de liquidité ;
- burn de tokens ou de LP ;
- mint anormal ;
- lock/unlock de liquidité ;
- close account / fermeture de position ;
- admin/config changes ;
- fees/rewards ;
- migration launch → DEX ;
- market/orderbook activity ;
- vault deposit/withdraw ;
- changement de market cap ou de supply dérivée.
Un event peut donc être important même sil ne produit jamais de `trade_event` ou de candle.
## Règle de couverture
Pour chaque DEX/version, on doit viser trois niveaux :
| Niveau | Description |
|---|---| |---|---|
| `listed` | Levent/instruction existe dans une source Git/IDL/Carbon/Vybe/autre. | | `decoded` | Un decoder local produit un event spécialisé ou un event audit-only classé. |
| `decoded_audit` | Le code local reconnaît levent et le persiste dans `k_sol_dex_decoded_events` avec payload structuré ou audit-only. | | `materialized` | L'event alimente une table métier existante validée par corpus. |
| `materialized` | Levent alimente une table métier spécialisée : trade, liquidity, lifecycle, fee, reward, admin, mint, burn, orderbook, vault, launch/migration, etc. | | `audit-only` | L'event reste dans `k_sol_dex_decoded_events` et ne produit jamais trade/candle. |
| `upstream_git_mapped_unverified` | L'entrée est connue depuis Carbon/fnzero/IDL, mais non observée localement. |
| `not_applicable` | La famille n'existe pas pour ce DEX/version ou appartient à un autre programme. |
Ne pas sauter directement de `listed` à `materialized`. ## `0.7.48` — `raydium_cpmm`
## Univers minimal devents à suivre Sources inventoriées : Carbon `carbon-raydium-cpmm-decoder`, fnzero `solana-streamer` / `sol-parser-sdk` IDL `raydium_cpmm.json`.
Cette liste doit devenir la grille commune pour toutes les tranches `0.7.48+`. | Famille | Entrées Raydium CPMM | Statut `0.7.48` | Cible DB | Justification / règle |
|---|---|---|---|---|
| `swap` | `swap_base_input`, `swap_base_output`, `swap_event` | `materialized` pour `swap_base_*`; `swap_event` audit-only | `k_sol_trade_events` seulement pour `swap_base_*`; `k_sol_dex_decoded_events_only` pour `swap_event` | `swap_event` est décodé mais ne produit jamais trade/candle afin d'éviter les doublons. |
| `pool_create` | `initialize`, `initialize_with_permission` | `materialized` | `k_sol_pool_lifecycle_events` | `initialize_with_permission` est lifecycle-only et ne crée plus d'admin row. |
| `add_liquidity` | `deposit`, `lp_change_event(changeType=0)` | `materialized` | `k_sol_liquidity_events` | `deposit` et `lp_change_event(changeType=0)` matérialisent liquidity, sans trade/candle. |
| `remove_liquidity` | `withdraw`, `lp_change_event(changeType=1)` | `materialized` | `k_sol_liquidity_events` | `withdraw` et `lp_change_event(changeType=1)` matérialisent liquidity, sans trade/candle. |
| `position_open` | `-` | `not_applicable` | `-` | CPMM n'a pas de position CLMM. |
| `position_close` | `-` | `not_applicable` | `-` | CPMM n'a pas de position CLMM. |
| `fee` | `collect_creator_fee`, `collect_fund_fee`, `collect_protocol_fee` | `materialized` | `k_sol_fee_events` | Les trois familles de fee CPMM observées sont matérialisées avec `trade_count=0`. |
| `reward` | `-` | `not_applicable` | `-` | Aucune entrée reward CPMM dans Carbon/fnzero IDL inventoriée pour cette tranche. |
| `admin/config` | `create_amm_config`, `update_amm_config`, `create_permission_pda`, `update_pool_status`, `close_permission_pda` | `materialized` pour les entrées observées ; `upstream_git_mapped_unverified` pour `update_pool_status` / `close_permission_pda` | `k_sol_pool_admin_events` ou decoded-only selon corpus | `create_amm_config`, `create_permission_pda` et `update_amm_config` sont matérialisés ; les deux autres restent non observés localement. |
| `mint` | `-` direct | `not_applicable` | `-` | Mint LP implicite possible dans les instructions, mais pas d'instruction CPMM `mint` dédiée. |
| `burn` | `-` direct | `not_applicable` | `-` | Burn LP implicite possible dans `withdraw`, mais pas d'instruction CPMM `burn` dédiée. |
| `transfer` | SPL Token inner transfers | `audit-only` indirect | `k_sol_dex_decoded_events` pour cette tranche | Pas de table `k_sol_token_transfer_events` ajoutée en `0.7.48`. |
| `account_create` | comptes système/ATA indirects | `audit-only` indirect | decoded-only | Hors programme CPMM direct. |
| `account_close` | comptes système/ATA indirects | `audit-only` indirect | decoded-only | Hors programme CPMM direct. |
| `wrap_sol` | user/router side | `not_applicable` | `-` | Préparation WSOL hors CPMM. |
| `unwrap_sol` | user/router side | `not_applicable` | `-` | Cleanup WSOL hors CPMM. |
| `order_place` | `-` | `not_applicable` | `-` | CPMM est AMM, pas orderbook. |
| `order_cancel` | `-` | `not_applicable` | `-` | CPMM est AMM, pas orderbook. |
| `order_fill` | `-` | `not_applicable` | `-` | CPMM est AMM, pas orderbook. |
| `consume_events` | `-` | `not_applicable` | `-` | CPMM est AMM, pas orderbook. |
| `settle_funds` | `-` | `not_applicable` | `-` | CPMM est AMM, pas orderbook. |
| `vault_deposit` | `-` | `not_applicable` | `-` | Les vaults CPMM sont des comptes de pool, pas une surface vault séparée. |
| `vault_withdraw` | `-` | `not_applicable` | `-` | Les vaults CPMM sont des comptes de pool, pas une surface vault séparée. |
| `lock` | `-` | `not_applicable` | `-` | Raydium liquidity locking est une surface séparée. |
| `unlock` | `-` | `not_applicable` | `-` | Raydium liquidity locking est une surface séparée. |
| `launch` | `-` | `not_applicable` | `-` | Raydium LaunchLab/Launchpad est séparé de CPMM. |
| `migration` | `-` | `not_applicable` | `-` | Les migrations launch → pool relèvent des launch surfaces. |
| `stake` | `-` | `not_applicable` | `-` | Hors CPMM. |
| `unstake` | `-` | `not_applicable` | `-` | Hors CPMM. |
| `unknown/unmapped audit` | `raydium_cpmm.instruction_audit` | `audit-only` | `k_sol_dex_decoded_events_only` | Toute instruction observée mais non mappée reste audit-only et ne produit jamais trade/candle. |
| Famille | Exemples | Impact possible | Table actuelle / cible | ## Validation attendue
|---|---|---|---|
| `swap/trade` | swap, buy, sell, route swap, exact in/out | Prix, volume, candles | `k_sol_trade_events`, `k_sol_pair_metrics`, `k_sol_pair_candles` |
| `pool_create` | initialize, create_pool, initialize_market | Découverte pool/pair | `k_sol_pool_lifecycle_events`, `k_sol_pools`, `k_sol_pairs` |
| `liquidity_add` | add_liquidity, deposit liquidity, bootstrap | Profondeur, risque, market cap indirect | `k_sol_liquidity_events` |
| `liquidity_remove` | remove_liquidity, withdraw liquidity | Rug/liquidity drain | `k_sol_liquidity_events` |
| `position_open` | open_position, init_position | CLMM/DLMM state | `k_sol_pool_lifecycle_events` ou future `k_sol_position_events` |
| `position_close` | close_position, close_position_if_empty | Sortie de LP, risque | `k_sol_pool_lifecycle_events` ou future `k_sol_position_events` |
| `fee` | claim_fee, collect_protocol_fee, collect_creator_fee | Rentabilité, activité pool | `k_sol_fee_events` |
| `reward` | claim_reward, fund_reward, update_reward | Incitations, farming | `k_sol_reward_events` |
| `admin/config` | set_config, update_fee, pause, whitelist, authority change | Risque protocole/pool | `k_sol_pool_admin_events` |
| `mint` | token mint, LP mint, position NFT mint | Supply, launch, LP | `k_sol_token_mint_events` + future token activity |
| `burn` | token burn, LP burn, position NFT burn | Supply, LP burn, risque/réassurance | `k_sol_token_burn_events` + future token activity |
| `transfer` | SPL transfer, Token-2022 transfer, routed transfer | Flux wallet/vault, whale movement | future `k_sol_token_transfer_events` |
| `account_create` | ATA create, token account init | Préparation trade/wallet/vault | future `k_sol_token_account_events` |
| `account_close` | close token account, close open orders | Sortie position/wallet cleanup | future `k_sol_token_account_events` ou `k_sol_orderbook_events` |
| `wrap/unwrap` | wrap SOL, unwrap SOL, close WSOL ATA | Routing, PnL, trade prep | future token account/activity |
| `order_place` | place_order, post_only, IOC | Orderbook pressure | future `k_sol_orderbook_events` |
| `order_cancel` | cancel_order, cancel_all, reduce order | Changement intention | future `k_sol_orderbook_events` |
| `order_fill` | FillLog, fill event, trade event | Trade réel orderbook | future `k_sol_orderbook_events`; trade seulement après validation économique |
| `settle_funds` | settle_funds, withdraw funds | Finalisation CLOB | future `k_sol_orderbook_events` |
| `consume_events` | crank/event queue processing | Orderbook fill/out | future `k_sol_orderbook_events` |
| `vault_deposit` | deposit into vault | TVL/risque | future `k_sol_vault_events` |
| `vault_withdraw` | withdraw from vault | TVL drain | future `k_sol_vault_events` |
| `lock` | lock_liquidity, create_lock_escrow | LP lock/risk | `k_sol_pool_lifecycle_events` ou future lock table |
| `unlock` | unlock, release escrow | Risque de retrait LP | future lock/lifecycle |
| `launch` | create bonding curve, launch pool | Origine token | future `k_sol_launch_events` |
| `migration` | migrate to DEX, migrate liquidity | Passage launch → tradable | future `k_sol_launch_events`, pool origins |
| `stake/unstake` | stake LP/token, unstake | Incentives/withdraw risk | future staking/reward events |
| `oracle/price` | oracle update, price account update | Pricing/risk | future oracle/context events |
| `unknown` | unmapped discriminator | Dette de décodage | `k_sol_dex_decoded_events` audit-only |
## Matrice de couverture par DEX/version - `k_sol_dex_event_coverage_entries.decoder_code = 'raydium_cpmm'` contient toutes les entrées Carbon/fnzero synchronisées.
- `upstream_git.instruction_match` ne doit plus apparaître pour une instruction CPMM remplacée par un decoder local spécialisé.
- Les familles non-trade CPMM doivent rester `trade_count = 0`.
- Les transactions failed CPMM doivent rester décodées/auditables mais non matérialisées en trade/candle.
Légende :
- `M` = matérialisé déjà ou historiquement validé ; ## Note `0.7.48-part2-fix2` — CPMM official instruction parity
- `A` = audit-only local ;
- `P` = partiel / doit être complété ;
- `L` = listé upstream, non validé localement ;
- `-` = non applicable connu ;
- `?` = à vérifier.
| DEX/version | swap | pool create | liq add/remove | position | fee/reward | admin/config | mint/burn | transfer/account | orderbook | vault | launch/migration | état immédiat | La couverture `raydium_cpmm` est alignée avec les instructions exposées par le programme officiel Raydium CP-Swap et par Carbon : `create_amm_config`, `update_amm_config`, `update_pool_status`, `collect_protocol_fee`, `collect_fund_fee`, `collect_creator_fee`, `create_permission_pda`, `close_permission_pda`, `initialize`, `initialize_with_permission`, `deposit`, `withdraw`, `swap_base_input`, `swap_base_output`, `lp_change_event` et `swap_event`.
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|
| `raydium_cpmm` | M | P | P | - | P | P | ? | ? | - | - | ? | Reprendre en `0.7.48`, comparer tous events Carbon/IDL/fnzero. |
| `raydium_clmm` | M | P | M/P | M/P | P | P | P | ? | - | - | ? | Reprendre en `0.7.49`, compléter positions/rewards/fees/admin. |
| `pump_swap` | M | P | P | - | P | P | ? | P | - | - | P | Reprendre en `0.7.50`, couvrir buy/sell/cashback/fee/volume/admin. |
| `pump_fun` | P | P | - | - | ? | P | P | P | - | - | M/P | Reprendre en `0.7.51`, launch/bonding/migration/buy/sell/create/update. |
| `meteora_dbc` | P | P | P | - | P | P | P | ? | - | P | M/P | Reprendre en `0.7.52`, séparer bonding, swap, migration, config, fees. |
| `meteora_dlmm` | M | M | M | M | M/P | P | ? | ? | - | ? | ? | Reprendre en `0.7.53` pour exhaustive upstream + audits résiduels. |
| `meteora_damm_v1` | M/P | M | M/P | - | M/P | P | ? | ? | - | P | ? | Reprendre en `0.7.54`, vérifier toutes surfaces upstream non observées. |
| `meteora_damm_v2` | P | P | L/P | - | L/P | L/P | ? | ? | - | P | ? | Reprendre en `0.7.55`, decoder tous events Carbon/source. |
| `phoenix_v1` | A | A/P | - | - | A/P | A/P | - | A/P | A | - | - | Continuer audit des events Git, pas de trade/candle. |
| `openbook_v2` | A | A/P | - | - | A/P | A/P | - | A/P | A | - | - | Audit-only avancé, matérialisation orderbook future. |
| `orca_whirlpools` | P | P | L/P | L/P | L/P | L/P | P | ? | - | - | ? | Reprendre en `0.7.58`, IDL complet + corpus dédié. |
| `raydium_launchlab` | - | P | ? | - | ? | P | P | P | - | - | P | Launch/migration après DEX effectifs. |
| `bonkswap` | L | L | L | - | L | L | ? | ? | - | - | L | Vérifier source/corpus. |
| `moonshot` | L/P | P | ? | - | ? | P | P | P | - | - | P | Séparer launch, buy/sell, migration. |
| `heaven` | L/P | L/P | L/P | - | L/P | L/P | P | P | - | ? | P | Vérifier AMM vs launch. |
| `goosefx_v1` | L/P | L/P | L/P | ? | ? | ? | ? | ? | ? | ? | ? | Vybe/Demo3 candidates ; source nécessaire. |
| `obric_v2` | L/P | L/P | ? | ? | ? | ? | ? | ? | ? | ? | ? | Bon candidat après sources. |
| `solfi_v2` | L/P | L/P | ? | ? | ? | ? | ? | ? | ? | ? | ? | Bon candidat après sources. |
## Points critiques manquants dans lancienne matrice `lp_change_event` est maintenant classé `event_family=liquidity` dans la table coverage, parce que l'event couvre à la fois dépôt et retrait. La matérialisation reste déterminée par `changeType` dans le payload décodé : `0` = add/deposit, `1` = remove/withdraw.
### `burn`
`burn` doit être une famille de première classe, pas seulement une sous-note. Il peut signaler : ## Note `0.7.48 final` — Raydium CPMM clôturable
- burn de LP tokens ; Validation finale locale : `deposit` = `11/11` liquidity, `withdraw` = `14/14` liquidity, `lp_change_event` = `25/25` liquidity, fees = `26/26` fee, admin/config observés = `23/23` admin, lifecycle = `9/9`, `swap_event` = audit-only avec `0` trade, et trades matérialisés uniquement depuis `swap_base_input` / `swap_base_output`.
- burn de supply token ;
- fermeture ou destruction indirecte de position ;
- réduction du risque de dump si le burn est réel et vérifié ;
- au contraire, faux signal si le burn ne concerne pas la bonne mint ou si le compte propriétaire est ambigu.
Action : ajouter `burn` à toutes les checklists DEX et aux diagnostics de couverture. `close_permission_pda` et `update_pool_status` restent `upstream_git_mapped_unverified` faute de corpus local. Les filtres Solscan `instruction=9c5420764587467b` et `instruction=82576c062ee0757b` n'ont pas fourni de signatures exploitables dans l'horizon testé.
### `transfer`
Les transfers ne sont pas des trades par défaut, mais ils sont nécessaires pour :
- repérer vault movements ;
- détecter migration / liquidity routing ;
- comprendre des orderbook settle/fill ;
- analyser whale movement ou sortie de pool.
Action : prévoir une table dédiée plutôt que tout stocker uniquement dans `payload_json`.
### `account close/create`
Les close/create ATA sont utiles pour détecter :
- fin de route WSOL ;
- sortie de position ;
- cleanup après swap ;
- close open-orders account ;
- activité de bots.
Action : famille dédiée `token_account` / `account_lifecycle`.
## Checklist exhaustive par DEX
Pour chaque DEX/version, la tranche doit remplir un tableau événementiel :
| Colonne | Description |
|---|---|
| `source_repo` | Git/IDL/source utilisée. |
| `source_path` | Chemin exact du fichier source. |
| `decoder_code` | Code interne/upstream. |
| `program_id` | Program id ou `to_verify`. |
| `entry_kind` | `instruction`, `event`, `account`, `log`, `program_data`. |
| `entry_name` | Nom source exact. |
| `discriminator_hex` | Discriminator ou tag. |
| `discriminator_len` | Longueur en octets. |
| `event_family` | Famille commune : swap, burn, admin, order_fill, etc. |
| `local_event_kind` | Event local produit. |
| `local_status` | `not_implemented`, `audit_only`, `decoded`, `materialized`. |
| `db_target` | Table cible ou `k_sol_dex_decoded_events_only`. |
| `proof_status` | Statut upstream/local. |
| `observed_count` | Count local après replay. |
| `materialized_count` | Count table métier. |
| `trade_count` | Count trades générés, doit être 0 sauf swap validé. |
| `notes` | Ambiguïtés, layout, corpus, reste à faire. |
## Requêtes SQL génériques de couverture
### Couverture decoded events par programme
```sql
SELECT
protocol_name,
event_kind,
program_id,
json_extract(payload_json, '$.upstreamEntryName') AS upstream_entry_name,
json_extract(payload_json, '$.upstreamDiscriminatorHex') AS upstream_discriminator_hex,
COUNT(*) AS n
FROM k_sol_dex_decoded_events
GROUP BY
protocol_name,
event_kind,
program_id,
upstream_entry_name,
upstream_discriminator_hex
ORDER BY protocol_name, n DESC;
```
### Sécurité trades pour audit-only
```sql
SELECT
de.protocol_name,
de.event_kind,
COUNT(te.id) AS trade_count
FROM k_sol_dex_decoded_events de
LEFT JOIN k_sol_trade_events te
ON te.decoded_event_id = de.id
WHERE de.event_kind LIKE '%_audit'
OR de.protocol_name IN ('upstream_git', 'phoenix_v1', 'openbook_v2')
GROUP BY de.protocol_name, de.event_kind
ORDER BY trade_count DESC;
```
### Vérifier burn/mint présents dans decoded payloads
```sql
SELECT
protocol_name,
event_kind,
json_extract(payload_json, '$.eventLifecycleKind') AS lifecycle,
json_extract(payload_json, '$.eventActionability') AS actionability,
COUNT(*) AS n
FROM k_sol_dex_decoded_events
WHERE event_kind LIKE '%burn%'
OR event_kind LIKE '%mint%'
OR lifecycle IN ('burn', 'mint')
GROUP BY protocol_name, event_kind, lifecycle, actionability
ORDER BY n DESC;
```
## Décision
À partir de maintenant, une tranche DEX nest pas complète si elle ne liste que les swaps. Elle doit explicitement indiquer :
- events source listés ;
- events décodés audit-only ;
- events matérialisés ;
- events volontairement non implémentés ;
- events non observés localement ;
- trous de DB éventuels.

View File

@@ -118,75 +118,75 @@ Les entrées de registre doivent être exposées à `kb_demo_app` via une comman
DEX / AMM / CLMM / orderbook : DEX / AMM / CLMM / orderbook :
```text ```text
meteora-damm-v2 meteora_damm_v2
meteora-dbc meteora_dbc
meteora-dlmm meteora_dlmm
meteora-vault meteora_vault
raydium-amm-v4 raydium_amm_v4
raydium-clmm raydium_clmm
raydium-cpmm raydium_cpmm
raydium-launchpad raydium_launchpad
raydium-liquidity-locking raydium_liquidity_locking
raydium-stable-swap raydium_stable_swap
orca-whirlpool orca_whirlpools
fluxbeam fluxbeam
lifinity-amm-v2 lifinity_v2
phoenix-v1 phoenix_v1
openbook-v2 openbook_v2
stabble-stable-swap stabble_stable_swap
stabble-weighted-swap stabble_weighted_swap
bonkswap bonkswap
boop boop
moonshot moonshot
heaven heaven
okx-dex okx_dex
pancake-swap pancake_swap
vertigo vertigo
virtuals virtuals
wavebreak wavebreak
onchain-labs-dex-v1 onchain_labs_dex_v1
onchain-labs-dex-v2 onchain_labs_dex_v2
``` ```
Agrégateurs / ordres / perps / lending : Agrégateurs / ordres / perps / lending :
```text ```text
jupiter-swap jupiter_swap
jupiter-dca jupiter_dca
jupiter-limit-order jupiter_limit_order
jupiter-limit-order-2 jupiter_limit_order_2
jupiter-perpetuals jupiter_perpetuals
jupiter-lend jupiter_lend
kamino-lending kamino_lending
kamino-vault kamino_vault
kamino-farms kamino_farms
kamino-limit-order kamino_limit_order
drift-v2 drift_v2
marginfi-v2 marginfi_v2
dflow-aggregator-v4 dflow_aggregator_v4
zeta zeta
``` ```
Contexte transactionnel non DEX : Contexte transactionnel non DEX :
```text ```text
system-program system_program
token-program token_program
token-2022 token_2022
associated-token-account associated_token_account
address-lookup-table address_lookup_table
memo-program memo_program
stake-program stake_program
mpl-token-metadata mpl_token_metadata
mpl-core mpl_core
bubblegum bubblegum
name-service name_service
marinade-finance marinade_finance
solayer-restaking-program solayer_restaking_program
swig swig
sharky sharky
circle-message-transmitter-v2 circle_message_transmitter_v2
circle-token-messenger-v2 circle_token_messenger_v2
``` ```
## Règles de validation ## Règles de validation

View File

@@ -0,0 +1,327 @@
# Prompt de reprise — khadhroony-bobobot `0.7.49` / Raydium CLMM event coverage
Reprise du projet `khadhroony-bobobot` après clôture fonctionnelle de `0.7.48 raydium_cpmm`.
## Archive de départ
Utiliser la dernière archive complète du workspace intégrant les deltas validés jusqu'à :
```text
0.7.48-raydium-cpmm-final
```
Docs à fournir aussi :
```text
README.md
ROADMAP.md
CHANGELOG.md
DEX_DECODER_MATRIX.md
DEX_EVENT_COVERAGE_MATRIX.md
DB_EVENT_MODEL_REVIEW.md
RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md
SQL_VALIDATION_RAYDIUM_CPMM_0_7_48.sql
```
## État validé avant reprise
`0.7.48` a clôturé la tranche `raydium_cpmm` :
```text
k_sol_dex_event_coverage_entries synchronisée en snake_case local
k_sol_instruction_observations ajoutée comme table technique d'index instruction/discriminator
Demo3 enrichie avec recherche instruction/discriminator
Solscan instruction=<discriminator> utilisé comme accélérateur de recherche de signatures
raydium_cpmm Program data décodé pour lp_change_event / swap_event
raydium_cpmm deposit / withdraw / lp_change_event matérialisés liquidity
raydium_cpmm initialize / initialize_with_permission matérialisés lifecycle-only
raydium_cpmm collect_*_fee matérialisés fee
raydium_cpmm create_amm_config / create_permission_pda / update_amm_config matérialisés admin/config
raydium_cpmm swap_event conservé audit-only
close_permission_pda et update_pool_status conservés upstream_git_mapped_unverified faute de corpus local
instruction inconnue 40f4bc78a7e9690a conservée raydium_cpmm.instruction_audit
```
Validation locale finale observée :
```text
cargo test -p kb_lib: ok, 386 passed
cargo clippy -p kb_lib --all-targets -- -D warnings: ok
replay local: 1124 replayed, 561 trades, 50 liquidity, 9 lifecycle, 2224 candle upserts
```
Couverture finale `raydium_cpmm` :
```text
lp_change_event 25/25 liquidity, 0 trade
swap_event 529 decoded-only, 0 trade
deposit 11/11 liquidity, 0 trade
withdraw 14/14 liquidity, 0 trade
initialize 5/5 lifecycle, 0 admin, 0 trade
initialize_with_permission 4/4 lifecycle, 0 admin, 0 trade
collect_creator_fee 4/4 fee, 0 trade
collect_fund_fee 7/7 fee, 0 trade
collect_protocol_fee 15/15 fee, 0 trade
create_amm_config 6/6 admin, 0 trade
create_permission_pda 4/4 admin, 0 trade
update_amm_config 13/13 admin, 0 trade
swap_base_input 750 decoded, 482 trades
swap_base_output 25 decoded, 17 trades
close_permission_pda upstream_git_mapped_unverified
update_pool_status upstream_git_mapped_unverified
```
Invariants maintenus :
```text
non-trade event = jamais trade/candle
failed transaction = audit-only
upstream Git/IDL/Solscan = indice, pas preuve métier
program_id upstream non promu sans corpus local
chaque decoder spécialisé remplace le fallback upstream_git.instruction_match
side effects SPL Token / Token-2022 restent transversaux, pas raydium_cpmm.* directs
pas de nouvelle table métier transversale sans preuve multi-DEX
```
## Décision de reprise
Commencer `0.7.49` par `raydium_clmm`, avant Pump/Meteora.
Ordre courant :
```text
0.7.49 raydium_clmm
0.7.50 pump_swap
0.7.51 pump_fun
0.7.52 meteora_dbc
0.7.53 meteora_dlmm upstream parity
0.7.54 meteora_damm_v1 upstream parity
0.7.55 meteora_damm_v2
0.7.56 phoenix_v1 audit-only completion
0.7.57 openbook_v2 audit-only completion
0.7.58 orca_whirlpools
0.7.59+ launch surfaces, candidats/historiques, validation consolidée
```
## Sources Git/IDL à utiliser systématiquement
- https://github.com/sevenlabs-hq/carbon/tree/main/decoders
- https://github.com/0xfnzero/solana-streamer
- https://github.com/0xfnzero/sol-parser-sdk/tree/main/idl
- https://github.com/pinax-network/substreams-solana-idls/tree/main/src
- https://github.com/hodlwarden/solana-tx-parser/tree/main/src
- https://github.com/openbook-dex/openbook-v2
- https://github.com/all-in-one-blockchain/phoenix-onchain-mm
- https://docs.vybenetwork.com/docs/available-dexs-amms
Pour `0.7.49 raydium_clmm`, utiliser aussi explicitement :
```text
https://solscan.io/account/CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK#programIdl
```
et les filtres Solscan de type :
```text
https://solscan.io/account/CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK?instruction=<DISCRIMINATOR>&hide_spam=true&hide_failed=true&show_related=false&sort=desc
```
Solscan doit servir à trouver vite des signatures à backfiller, jamais comme preuve métier finale.
## Objectif `0.7.49` — `raydium_clmm`
Objectif : reprendre `raydium_clmm` comme deuxième tranche Raydium/version après CPMM.
À faire :
1. lire le code local `raydium_clmm` et les matérialisations existantes ;
2. lister toutes les instructions/events CLMM depuis Carbon/fnzero/IDL/Raydium/Solscan Program IDL ;
3. synchroniser/remplir `k_sol_dex_event_coverage_entries` pour `raydium_clmm` ;
4. utiliser `k_sol_instruction_observations` pour inspecter les discriminants réellement observés localement ;
5. ajouter à Demo3 les filtres instruction/discriminant CLMM si un binding/UI manque encore ;
6. chercher des signatures ciblées via Solscan `instruction=<discriminator>` ;
7. backfiller les signatures utiles dans Demo Pipeline 2 ;
8. rejouer localement `forceDexDecode=yes` ;
9. comparer listed/decoded/observed/materialized/trade_count via SQL coverage ;
10. compléter le decoder spécialisé `raydium_clmm` seulement pour les events confirmables ;
11. remplacer/nettoyer le fallback `upstream_git.instruction_match` quand un decoder local spécialisé couvre l'entrée ;
12. garder les events connus mais non observés en `upstream_git_mapped_unverified` ;
13. garder les events observés mais non matérialisés en audit-only/decoded ;
14. ne matérialiser que les non-trades prouvés par corpus et compatibles avec les tables existantes ;
15. ne pas modifier les règles trade/candle sauf bug de faux positif prouvé.
## Familles à couvrir explicitement
Ne pas se limiter aux swaps.
Inclure dans l'audit coverage CLMM :
```text
swap
pool_create
add_liquidity
remove_liquidity
position_open
position_close
fee
reward
admin/config
mint
burn
transfer
account_create
account_close
wrap_sol
unwrap_sol
order_place
order_cancel
order_fill
consume_events
settle_funds
vault_deposit
vault_withdraw
lock
unlock
launch
migration
stake
unstake
unknown/unmapped audit
```
Pour `raydium_clmm`, certaines familles sont probablement non applicables ou seulement observées comme side effects SPL Token/Token-2022. Elles doivent être explicitement justifiées dans la coverage matrix.
## Points d'attention hérités de CPMM
- `decoder_code` local doit rester en `snake_case` : `raydium_clmm`, pas `raydium-clmm`.
- Les slugs/chemins upstream peuvent garder les tirets : `raydium-clmm-decoder`.
- Les events side effects SPL Token (`burn`, `transfer`, `transferChecked`, `closeAccount`) ne doivent pas devenir `raydium_clmm.*` sans preuve qu'ils sont des instructions directes du programme CLMM.
- `k_sol_instruction_observations` est technique et peut être enrichie ; ne pas la confondre avec une table métier.
- `initialize_*` / création de pool doit rester lifecycle-only si c'est bien une création, pas admin.
- Les positions CLMM sont potentiellement des tables/catégories existantes ou à auditer : ne pas forcer liquidity simple si l'event représente une position NFT/tick.
- Les rewards/fees CLMM peuvent nécessiter un mapping plus fin que CPMM.
## Requêtes SQL utiles
Coverage CLMM :
```sql
SELECT
entry_name,
entry_kind,
event_family,
expected_db_target,
proof_status,
observed_count,
materialized_count,
trade_count
FROM k_sol_dex_event_coverage_entries
WHERE decoder_code = 'raydium_clmm'
ORDER BY entry_kind, entry_name, discriminator_hex;
```
Instruction observations CLMM :
```sql
SELECT
instruction_name,
discriminator_hex,
COUNT(*) AS observed_count,
COUNT(DISTINCT signature) AS tx_count
FROM k_sol_instruction_observations
WHERE decoder_code = 'raydium_clmm'
GROUP BY instruction_name, discriminator_hex
ORDER BY observed_count DESC;
```
Non-trade safety :
```sql
SELECT
de.event_kind,
COUNT(*) AS decoded_count,
COUNT(le.id) AS liquidity_count,
COUNT(fe.id) AS fee_count,
COUNT(pa.id) AS admin_count,
COUNT(ple.id) AS lifecycle_count,
COUNT(te.id) AS trade_count
FROM k_sol_dex_decoded_events de
LEFT JOIN k_sol_liquidity_events le
ON le.decoded_event_id = de.id
LEFT JOIN k_sol_fee_events fe
ON fe.decoded_event_id = de.id
LEFT JOIN k_sol_pool_admin_events pa
ON pa.decoded_event_id = de.id
LEFT JOIN k_sol_pool_lifecycle_events ple
ON ple.decoded_event_id = de.id
LEFT JOIN k_sol_trade_events te
ON te.decoded_event_id = de.id
WHERE de.protocol_name = 'raydium_clmm'
GROUP BY de.event_kind
ORDER BY de.event_kind;
```
Fallback upstream CLMM :
```sql
SELECT
json_extract(payload_json, '$.upstreamDecoderCode') AS upstream_decoder_code,
json_extract(payload_json, '$.entryName') AS entry_name,
json_extract(payload_json, '$.discriminatorHex') AS discriminator_hex,
COUNT(*) AS fallback_count
FROM k_sol_dex_decoded_events
WHERE protocol_name = 'upstream_git'
AND event_kind = 'upstream_git.instruction_match'
AND json_extract(payload_json, '$.upstreamDecoderCode') = 'raydium_clmm'
GROUP BY upstream_decoder_code, entry_name, discriminator_hex
ORDER BY fallback_count DESC, entry_name;
```
Failed transaction safety :
```sql
SELECT
de.event_kind,
COUNT(*) AS decoded_count,
COUNT(te.id) AS trade_count
FROM k_sol_dex_decoded_events de
JOIN k_sol_chain_transactions tx
ON tx.id = de.transaction_id
LEFT JOIN k_sol_trade_events te
ON te.decoded_event_id = de.id
WHERE de.protocol_name = 'raydium_clmm'
AND tx.err_json IS NOT NULL
AND tx.err_json <> ''
GROUP BY de.event_kind
ORDER BY trade_count DESC, decoded_count DESC;
```
## Contraintes de code
Conserver les règles du workspace :
```text
Rust 2024
pas de mod.rs
fichiers Rust avec // file: ...
pas de anyhow
pas de thiserror
pas de ? / unwrap / expect dans kb_lib applicatif
match / if let Err / let Err = ... else
rustdoc sur API publique
re-exports db.rs puis lib.rs si DB modifiée
```
## Livrables attendus pour `0.7.49`
1. delta archive avec uniquement les fichiers ajoutés/modifiés ;
2. mise à jour `README.md`, `ROADMAP.md`, `CHANGELOG.md` si la tranche avance ;
3. rapport de couverture `raydium_clmm` ;
4. SQL de validation ;
5. tests verts :
```bash
cargo fmt
cargo test -p kb_lib
cargo clippy -p kb_lib --all-targets -- -D warnings
```

View File

@@ -0,0 +1,204 @@
# Rapport `0.7.48` — Raydium CPMM event coverage
## Scope
Tranche : `0.7.48`.
Decoder local : `raydium_cpmm`.
Programme : `CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C`.
Objectif : couvrir Raydium CPMM au-delà des swaps, avec preuve locale par backfill/replay SQL, sans modifier les règles trade/candle existantes et sans promouvoir de table métier transversale non nécessaire.
## Sources utilisées
Sources principales :
- Carbon `carbon-raydium-cpmm-decoder` ;
- fnzero `solana-streamer` / `sol-parser-sdk` IDL `raydium_cpmm.json` ;
- Raydium CP-Swap officiel ;
- Solscan page programme / Program IDL comme accélérateur de recherche de signatures, notamment via `instruction=<discriminator>`.
Règle retenue : Git/IDL/Solscan sont des indices de recherche. La preuve métier reste le corpus local backfillé puis rejoué.
## Entrées CPMM inventoriées
| Kind | Entrée | Discriminant | État final `0.7.48` |
|---|---|---|---|
| instruction | `close_permission_pda` | `9c5420764587467b` | connu upstream, non observé localement |
| instruction | `collect_creator_fee` | `1416567bc61cdb84` | décodé + matérialisé fee |
| instruction | `collect_fund_fee` | `a78a4e95dfc2067e` | décodé + matérialisé fee |
| instruction | `collect_protocol_fee` | `8888fcddc2427e59` | décodé + matérialisé fee |
| instruction | `create_amm_config` | `8934edd4d7756c68` | décodé + matérialisé admin/config |
| instruction | `create_permission_pda` | `878802d889a9b5ca` | décodé + matérialisé admin/config |
| instruction | `deposit` | `f223c68952e1f2b6` | décodé + matérialisé liquidity add |
| instruction | `initialize` | `afaf6d1f0d989bed` | décodé + matérialisé lifecycle |
| instruction | `initialize_with_permission` | `3f37fe4131b25979` | décodé + matérialisé lifecycle only |
| event | `lp_change_event` | `79a3cdc939da753c` | décodé + matérialisé liquidity bidirectionnelle |
| instruction | `swap_base_input` | `8fbe5adac41e33de` | décodé + trade matérialisé quand transaction OK |
| instruction | `swap_base_output` | `37d96256a34ab4ad` | décodé + trade matérialisé quand transaction OK |
| event | `swap_event` | `40c6cde8260871e2` | décodé audit-only, pas trade/candle |
| instruction | `update_amm_config` | `313cae889a1c74c8` | décodé + matérialisé admin/config |
| instruction | `update_pool_status` | `82576c062ee0757b` | connu upstream, non observé localement |
| instruction | `withdraw` | `b712469c946da122` | décodé + matérialisé liquidity remove |
## Décodage ajouté ou stabilisé
Le decoder spécialisé `raydium_cpmm` couvre maintenant :
- les swaps instruction-scoped `swap_base_input` / `swap_base_output` ;
- les events `Program data:` / Anchor CPI `swap_event` et `lp_change_event` ;
- les instructions lifecycle `initialize` / `initialize_with_permission` ;
- les instructions liquidity `deposit` / `withdraw` ;
- les fees `collect_creator_fee`, `collect_fund_fee`, `collect_protocol_fee` ;
- les instructions admin/config `create_amm_config`, `create_permission_pda`, `update_amm_config` ;
- les instructions connues mais non observées `close_permission_pda`, `update_pool_status` restent listées sans promotion métier.
Le fallback `upstream_git.instruction_match` ne doit plus apparaître pour les instructions CPMM couvertes localement.
## Matérialisation finale validée
Rejeu local validé après backfills ciblés Solscan + Demo Pipeline 2 :
| Event kind | Decoded | Table métier | Materialized | Trade count | Statut |
|---|---:|---|---:|---:|---|
| `raydium_cpmm.collect_creator_fee` | 4 | `k_sol_fee_events` | 4 | 0 | fee materialized |
| `raydium_cpmm.collect_fund_fee` | 7 | `k_sol_fee_events` | 7 | 0 | fee materialized |
| `raydium_cpmm.collect_protocol_fee` | 15 | `k_sol_fee_events` | 15 | 0 | fee materialized |
| `raydium_cpmm.create_amm_config` | 6 | `k_sol_pool_admin_events` | 6 | 0 | admin materialized |
| `raydium_cpmm.create_permission_pda` | 4 | `k_sol_pool_admin_events` | 4 | 0 | admin materialized |
| `raydium_cpmm.deposit` | 11 | `k_sol_liquidity_events` | 11 | 0 | liquidity add materialized |
| `raydium_cpmm.initialize` | 5 | `k_sol_pool_lifecycle_events` | 5 | 0 | lifecycle materialized |
| `raydium_cpmm.initialize_with_permission` | 4 | `k_sol_pool_lifecycle_events` | 4 | 0 | lifecycle only |
| `raydium_cpmm.lp_change_event` | 25 | `k_sol_liquidity_events` | 25 | 0 | liquidity materialized, `changeType` bidirectionnel |
| `raydium_cpmm.swap_base_input` | 750 | `k_sol_trade_events` | 482 | 482 | trade materialized for OK/actionable tx |
| `raydium_cpmm.swap_base_output` | 25 | `k_sol_trade_events` | 17 | 17 | trade materialized for OK/actionable tx |
| `raydium_cpmm.swap_event` | 529 | `k_sol_dex_decoded_events_only` | 0 | 0 | audit-only, no duplicate trade |
| `raydium_cpmm.update_amm_config` | 13 | `k_sol_pool_admin_events` | 13 | 0 | admin materialized |
| `raydium_cpmm.withdraw` | 14 | `k_sol_liquidity_events` | 14 | 0 | liquidity remove materialized |
| `raydium_cpmm.instruction_audit` | 3 | `k_sol_dex_decoded_events_only` | 0 | 0 | unknown audit-only |
Replay final observé :
```text
1124 replayed
561 trades
50 liquidity
9 lifecycle
2224 candle upserts
```
Le total liquidity correspond à :
```text
deposit 11
withdraw 14
lp_change_event 25
-------------------
total 50
```
## `lp_change_event`
`lp_change_event` est un event bidirectionnel :
- `changeType = 0` : add/deposit liquidity ;
- `changeType = 1` : remove/withdraw liquidity.
La coverage statique utilise donc `event_family = liquidity`, pas `liquidity_add`. La matérialisation résout le sens au niveau payload. Les events qui ne contiennent pas directement les mints sont enrichis via le contexte pool/pair local ou via le sibling `deposit` / `withdraw` déjà matérialisé dans la même transaction/replay.
Validation finale :
```text
changeType 0 -> 11 decoded / 11 liquidity / 0 trade
changeType 1 -> 14 decoded / 14 liquidity / 0 trade
```
## `initialize_with_permission`
`initialize_with_permission` est traité comme pool lifecycle only.
Validation finale :
```text
raydium_cpmm.initialize 5 decoded / 5 lifecycle / 0 admin / 0 trade
raydium_cpmm.initialize_with_permission 4 decoded / 4 lifecycle / 0 admin / 0 trade
```
Le cleanup de matérialisation supprime les anciennes lignes admin dérivées si elles existent déjà dans la base.
## Entrées non observées
Solscan avec filtre `instruction=` n'a pas retourné de transaction locale utile pour :
- `close_permission_pda` / `9c5420764587467b` ;
- `update_pool_status` / `82576c062ee0757b`.
Ces entrées restent donc :
```text
upstream_git_mapped_unverified
```
Absence Solscan ne signifie pas absence on-chain absolue, surtout si l'index UI ne couvre pas tout l'historique. Cela suffit cependant pour ne pas les promouvoir sans corpus local.
## Instruction audit inconnue
Le discriminator suivant a été observé localement :
```text
40f4bc78a7e9690a
```
Il produit actuellement `raydium_cpmm.instruction_audit` avec `tradeCandidate=false` / `candleCandidate=false`. Il n'est pas nommé dans la tranche `0.7.48`, faute de preuve upstream/corpus suffisante.
## Recherche Solscan retenue
La page programme Solscan et l'onglet Program IDL sont utiles pour accélérer la recherche :
```text
https://solscan.io/account/CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C#programIdl
```
Le filtre `instruction=<discriminator>` est documenté comme méthode pratique de découverte de signatures à backfiller localement, par exemple :
```text
https://solscan.io/account/CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C?instruction=f223c68952e1f2b6&instruction=b712469c946da122&hide_spam=true&hide_failed=true&show_related=false&sort=desc
```
Solscan reste une aide de recherche, pas une source de vérité métier.
## Table technique ajoutée
`k_sol_instruction_observations` est ajoutée comme table technique d'index local. Elle permet de retrouver les signatures observées par `decoder_code`, `instruction_name` et `discriminator_hex`, sans créer de nouvelle table métier.
Exemple :
```sql
SELECT
instruction_name,
discriminator_hex,
COUNT(*) AS observed_count,
COUNT(DISTINCT signature) AS tx_count
FROM k_sol_instruction_observations
WHERE decoder_code = 'raydium_cpmm'
GROUP BY instruction_name, discriminator_hex
ORDER BY observed_count DESC;
```
## Invariants validés
- `swap_event` ne produit aucun trade/candle.
- `deposit`, `withdraw`, `lp_change_event`, fees, admin/config et lifecycle gardent `trade_count=0`.
- Les transactions failed restent non matérialisées en trade/candle.
- Les side effects SPL Token / Token-2022 (`burn`, `transfer`, `transferChecked`, `closeAccount`) restent hors decoder métier CPMM direct et devront passer par une future table transversale si plusieurs DEX le justifient.
- Aucun program id n'est promu sans corpus local.
## État de clôture
`0.7.48 raydium_cpmm` est clôturable avec deux entrées connues mais non observées :
```text
close_permission_pda
update_pool_status
```
La prochaine tranche fonctionnelle est `0.7.49 raydium_clmm`.

View File

@@ -223,7 +223,6 @@ Chaque DEX ou variante de DEX doit avoir sa propre étape de validation. Les fam
- `pump_fun` ; - `pump_fun` ;
- `raydium_launchlab` ; - `raydium_launchlab` ;
- `raydium_launchpad` ;
- `letsbonk` / `bonk_fun` ; - `letsbonk` / `bonk_fun` ;
- `bags` ; - `bags` ;
- `moonshot` ; - `moonshot` ;
@@ -351,9 +350,12 @@ La priorité immédiate après le point de reprise `0.7.43-E5C` est :
3. `0.7.45` : reprise séparée de `meteora_dlmm` — clôturée côté corpus DLMM actuel ; 3. `0.7.45` : reprise séparée de `meteora_dlmm` — clôturée côté corpus DLMM actuel ;
4. `0.7.46` : reprise séparée de `meteora_damm_v1` — clôturée côté corpus DAMM v1 local ; 4. `0.7.46` : reprise séparée de `meteora_damm_v1` — clôturée côté corpus DAMM v1 local ;
5. `0.7.47` : Upstream Git Registry / DEX discovery preparation — registre générique des `program_id`, discriminants, instructions et events issus de dépôts Git externes, tous non vérifiés par défaut ; 5. `0.7.47` : Upstream Git Registry / DEX discovery preparation — registre générique des `program_id`, discriminants, instructions et events issus de dépôts Git externes, tous non vérifiés par défaut ;
6. `0.7.48` : reprise séparée de `meteora_damm_v2` ; 6. `0.7.48-pre` : event coverage + DB model checkpoint — table coverage, sync upstream, refresh counts, diagnostics et profil validation ;
7. `0.7.49` : reprise séparée de `meteora_dbc` ; 7. `0.7.48` : reprise séparée de `raydium_cpmm` ;
8. `0.7.50+` : validation progressive des autres DEX/surfaces issus du registre upstream Git : Orca, FluxBeam, DexLab, Lifinity, Phoenix, OpenBook, Stabble, BonkSwap, Boop, Moonshot, Heaven, Wavebreak, Vertigo, Virtuals, Pancake Swap, OKX DEX, Raydium Launchpad/Stable/Locking, puis launch surfaces. 8. `0.7.49` : reprise séparée de `raydium_clmm` ;
9. `0.7.50` : reprise séparée de `pump_swap` ;
10. `0.7.51` : reprise séparée de `pump_fun` ;
11. `0.7.52+` : Meteora puis validation progressive des autres DEX/surfaces issus du registre upstream Git : Orca, FluxBeam, DexLab, Lifinity, Phoenix, OpenBook, Stabble, BonkSwap, Boop, Moonshot, Heaven, Wavebreak, Vertigo, Virtuals, Pancake Swap, OKX DEX, Raydium LaunchLab/Stable/Locking, puis launch surfaces.
Garde-fous constants : Garde-fous constants :
@@ -463,3 +465,37 @@ La suite fonctionnelle reprend par Raydium avant Meteora :
3. `0.7.50``pump_swap` ; 3. `0.7.50``pump_swap` ;
4. `0.7.51``pump_fun` ; 4. `0.7.51``pump_fun` ;
5. `0.7.52+` — Meteora puis les autres DEX/surfaces. 5. `0.7.52+` — Meteora puis les autres DEX/surfaces.
## Note 0.7.48 — Raydium CPMM event coverage
La tranche `0.7.48` reprend `raydium_cpmm` avant Meteora, en s'appuyant sur la table `k_sol_dex_event_coverage_entries` ajoutée en `0.7.48-pre`.
Le scope CPMM couvre désormais les entrées Carbon/fnzero/IDL suivantes : `close_permission_pda`, `collect_creator_fee`, `collect_fund_fee`, `collect_protocol_fee`, `create_amm_config`, `create_permission_pda`, `deposit`, `initialize`, `initialize_with_permission`, `lp_change_event`, `swap_base_input`, `swap_base_output`, `swap_event`, `update_amm_config`, `update_pool_status` et `withdraw`.
Le decoder local spécialisé remplace le fallback `upstream_git.instruction_match` pour les instructions CPMM couvertes localement. Les events Anchor self-CPI `lp_change_event` et `swap_event` sont décodés comme preuve audit/coverage, mais ne produisent pas directement `trade_events`, metrics ou candles. Les swaps matérialisables restent les chemins instruction-scoped `swap_base_input` et `swap_base_output` lorsque les montants et le sens économique sont exploitables.
Aucune nouvelle table DB n'est ajoutée en `0.7.48`. Les transfers, token account lifecycle, vaults, orderbook, launch/migration et locking restent documentés comme familles transversales futures, à promouvoir seulement après preuve multi-DEX.
Voir aussi :
- `DEX_EVENT_COVERAGE_MATRIX.md` pour la couverture par familles ;
- `RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md` pour le rapport de tranche ;
- `SQL_VALIDATION_RAYDIUM_CPMM_0_7_48.sql` pour les requêtes de validation.
Complément `0.7.48-raydium-cpmm-program-data` : la coverage DB est maintenant synchronisée automatiquement quand un refresh de coverage est demandé sur une base neuve et qu'aucune ligne n'existe encore. Les backfills token/pool/signature déclenchent aussi un refresh best-effort de la coverage afin d'éviter un état incohérent où `k_sol_dex_decoded_events` est rempli mais `k_sol_dex_event_coverage_entries` reste vide.
Le decoder CPMM lit désormais les events `Program data:` émis par le programme Raydium CPMM. Les layouts `lp_change_event` et `swap_event` sont décodés depuis le payload event direct, en plus du chemin Anchor self-CPI `e445a52e51cb9a1d + event_discriminator`. `swap_event` reste `tradeCandidate=false` / `candleCandidate=false` : les seuls trades CPMM matérialisables restent `swap_base_input` et `swap_base_output`. `lp_change_event` peut alimenter la matérialisation liquidity si le corpus fournit un pool/pair fiable et si `changeType` distingue dépôt/retrait.
### Note 0.7.48-part2-fix2 — Raydium CPMM coverage finalization
La tranche CPMM reconnaît désormais tous les discriminants instruction-level listés par Carbon / Raydium CP-Swap côté classificateur local. `lp_change_event` est traité comme famille bidirectionnelle `liquidity`, avec sens add/remove résolu par `changeType`, et le refresh coverage est confirmé après replay local sans validation séparée.
## Note 0.7.48 final — Raydium CPMM clôturable
La tranche `0.7.48` clôture la couverture Raydium CPMM sur corpus local. Les entrées `deposit`, `withdraw` et `lp_change_event` matérialisent `k_sol_liquidity_events` sans créer de trade/candle ; `initialize` et `initialize_with_permission` matérialisent seulement `k_sol_pool_lifecycle_events` ; les fees matérialisent `k_sol_fee_events` ; les entrées admin/config observées matérialisent `k_sol_pool_admin_events`.
La table technique `k_sol_instruction_observations` est ajoutée pour indexer localement les instructions observées par `decoder_code`, `instruction_name` et `discriminator_hex`. Demo3 peut rechercher par instruction/discriminant, ce qui reproduit localement lusage pratique du filtre Solscan `instruction=<discriminator>`.
État final CPMM observé : `561` trades, `50` liquidity events, `9` lifecycle events, `25/25` `lp_change_event` matérialisés, `swap_event` audit-only à `0` trade, et deux entrées connues mais non observées (`close_permission_pda`, `update_pool_status`) conservées en `upstream_git_mapped_unverified`.

View File

@@ -851,7 +851,6 @@ Matrice cible initiale :
| `raydium_cpmm` | AMM | supporté | conserver trades/candles | | `raydium_cpmm` | AMM | supporté | conserver trades/candles |
| `raydium_clmm` | CLMM | supporté | conserver trades/candles | | `raydium_clmm` | CLMM | supporté | conserver trades/candles |
| `raydium_launchlab` | launch surface | planifié, program id local connu | ajouter decoder/materialization dédiée | | `raydium_launchlab` | launch surface | planifié, program id local connu | ajouter decoder/materialization dédiée |
| `raydium_launchpad` | launch surface | à vérifier | ne pas inventer de program id |
| `raydium_amm_v4` | AMM legacy | partiel | corpus dédié après autres Raydium | | `raydium_amm_v4` | AMM legacy | partiel | corpus dédié après autres Raydium |
| `raydium_router` | router | partiel | ne pas matérialiser en trade direct avant preuve | | `raydium_router` | router | partiel | ne pas matérialiser en trade direct avant preuve |
| `raydium_stable_swap` | AMM legacy | planifié | traiter seulement si corpus pertinent | | `raydium_stable_swap` | AMM legacy | planifié | traiter seulement si corpus pertinent |
@@ -1231,9 +1230,9 @@ Objectif : accélérer la découverte multi-DEX en indexant les `program_id`, di
Familles prioritaires à indexer en premier : Familles prioritaires à indexer en premier :
- DEX / AMM / CLMM / orderbook : `meteora-damm-v2`, `meteora-dbc`, `meteora-dlmm`, `meteora-vault`, `raydium-amm-v4`, `raydium-clmm`, `raydium-cpmm`, `raydium-launchpad`, `raydium-liquidity-locking`, `raydium-stable-swap`, `orca-whirlpool`, `fluxbeam`, `lifinity-amm-v2`, `phoenix-v1`, `openbook-v2`, `stabble-stable-swap`, `stabble-weighted-swap`, `bonkswap`, `boop`, `moonshot`, `heaven`, `okx-dex`, `pancake-swap`, `vertigo`, `virtuals`, `wavebreak`, `onchain-labs-dex-v1`, `onchain-labs-dex-v2` ; - DEX / AMM / CLMM / orderbook : `meteora_damm_v2`, `meteora_dbc`, `meteora_dlmm`, `meteora_vault`, `raydium_amm_v4`, `raydium_clmm`, `raydium_cpmm`, `raydium_launchlab`, `raydium_liquidity_locking`, `raydium_stable_swap`, `orca_whirlpools`, `fluxbeam`, `lifinity_v2`, `phoenix_v1`, `openbook_v2`, `stabble_stable_swap`, `stabble_weighted_swap`, `bonkswap`, `boop`, `moonshot`, `heaven`, `okx_dex`, `pancake_swap`, `vertigo`, `virtuals`, `wavebreak`, `onchain_labs_dex_v1`, `onchain_labs_dex_v2` ;
- agrégateurs / ordres / perps / lending utiles au routage ou à lanalyse : `jupiter-swap`, `jupiter-dca`, `jupiter-limit-order`, `jupiter-limit-order-2`, `jupiter-perpetuals`, `jupiter-lend`, `kamino-lending`, `kamino-vault`, `kamino-farms`, `kamino-limit-order`, `drift-v2`, `marginfi-v2`, `dflow-aggregator-v4`, `zeta` ; - agrégateurs / ordres / perps / lending utiles au routage ou à lanalyse : `jupiter_swap`, `jupiter_dca`, `jupiter_limit_order`, `jupiter_limit_order_2`, `jupiter_perpetuals`, `jupiter_lend`, `kamino_lending`, `kamino_vault`, `kamino_farms`, `kamino_limit_order`, `drift_v2`, `marginfi_v2`, `dflow_aggregator_v4`, `zeta` ;
- contexte transactionnel non DEX : `system-program`, `token-program`, `token-2022`, `associated-token-account`, `address-lookup-table`, `memo-program`, `stake-program`, `mpl-token-metadata`, `mpl-core`, `bubblegum`, `name-service`, `marinade-finance`, `solayer-restaking-program`, `swig`, `sharky`, `circle-message-transmitter-v2`, `circle-token-messenger-v2`. - contexte transactionnel non DEX : `system_program`, `token_program`, `token_2022`, `associated_token_account`, `address_lookup_table`, `memo_program`, `stake_program`, `mpl_token_metadata`, `mpl_core`, `bubblegum`, `name_service`, `marinade_finance`, `solayer_restaking_program`, `swig`, `sharky`, `circle_message_transmitter_v2`, `circle_token_messenger_v2`.
Aucun de ces programmes ne doit être marqué `verified_by_corpus` uniquement parce quil existe dans un dépôt Git externe. Aucun de ces programmes ne doit être marqué `verified_by_corpus` uniquement parce quil existe dans un dépôt Git externe.
@@ -1581,3 +1580,47 @@ Demo3 discovery now supports multiple source addresses, `before` / `until` pagin
La tranche DAMM v1 doit couvrir les instructions/events listés par upstream Git decoder source `meteora-pools-decoder`. Les surfaces non observées localement sont volontairement persistées avec `proofStatus=upstream_git_mapped_unverified`; elles restent à valider par signatures réelles, replay et requêtes SQL. La tranche DAMM v1 doit couvrir les instructions/events listés par upstream Git decoder source `meteora-pools-decoder`. Les surfaces non observées localement sont volontairement persistées avec `proofStatus=upstream_git_mapped_unverified`; elles restent à valider par signatures réelles, replay et requêtes SQL.
Après backfills ciblés, les surfaces `swap` et `add_balance_liquidity` sont confirmées par corpus local et ne doivent plus rester en `upstream_git_mapped_unverified`. Les deux `remove_liquidity` non matérialisés en table liquidity sont expliqués par labsence de `pool_id/pair_id` local pour leurs pools, pas par un échec de décodage. Après backfills ciblés, les surfaces `swap` et `add_balance_liquidity` sont confirmées par corpus local et ne doivent plus rester en `upstream_git_mapped_unverified`. Les deux `remove_liquidity` non matérialisés en table liquidity sont expliqués par labsence de `pool_id/pair_id` local pour leurs pools, pas par un échec de décodage.
### 6.080B. Clôture `0.7.48` — `raydium_cpmm` event coverage
Statut : delta préparé.
Fait :
- inventaire des entrées `raydium_cpmm` depuis Carbon et fnzero/IDL ;
- mapping local spécialisé des instructions CPMM non-swap connues ;
- ajout du décodage Anchor self-CPI audit-only `lp_change_event` et `swap_event` ;
- mise à jour de `known_local_event_kind` pour que la coverage DB reflète la couverture locale CPMM ;
- conservation de `swap_event` comme audit-only pour éviter un doublon avec les trades matérialisés depuis `swap_base_input` / `swap_base_output` ;
- maintien des règles : non-trade = zéro trade/candle, failed transaction = audit-only, upstream Git/IDL = indice et non preuve métier.
Reste à exécuter localement après application du delta :
```bash
cargo fmt
cargo test -p kb_lib
cargo clippy -p kb_lib --all-targets -- -D warnings
```
Puis relancer la validation SQL `SQL_VALIDATION_RAYDIUM_CPMM_0_7_48.sql` sur la base de corpus CPMM.
Complément appliqué après constitution du corpus CPMM :
- auto-sync conservatoire de `k_sol_dex_event_coverage_entries` lors d'un refresh coverage si la base neuve ne contient encore aucune entrée coverage ;
- refresh coverage best-effort après backfill token, pool ou signature ;
- décodage des events `Program data:` CPMM sans sélecteur Anchor self-CPI pour `swap_event` et `lp_change_event` ;
- maintien de `swap_event` en decoded/audit-only pour ne jamais doubler les `trade_events` issus des instructions swap ;
- matérialisation liquidity possible pour `lp_change_event` lorsque le pool/pair local est connu, avec `changeType=0` comme add/deposit et `changeType=1` comme remove/withdraw ;
- aucun changement de règle trade/candle pour les swaps existants.
### Note 0.7.48-part2-fix2 — Raydium CPMM coverage finalization
La tranche CPMM reconnaît désormais tous les discriminants instruction-level listés par Carbon / Raydium CP-Swap côté classificateur local. `lp_change_event` est traité comme famille bidirectionnelle `liquidity`, avec sens add/remove résolu par `changeType`, et le refresh coverage est confirmé après replay local sans validation séparée.
### Note 0.7.48 final — Raydium CPMM
`0.7.48` est clôturable côté `raydium_cpmm`. Le decoder couvre les instructions/events CPMM listés par Carbon/fnzero/Raydium CP-Swap, avec matérialisation locale validée pour trades, liquidity, lifecycle, fees et admin/config. `swap_event` reste audit-only pour éviter les doublons avec `swap_base_input` / `swap_base_output`. Les side effects SPL Token / Token-2022 observés via Solscan (`burn`, `transfer`, `transferChecked`, `closeAccount`) restent hors decoder CPMM direct et alimenteront une réflexion transversale future.
La suite reprend en `0.7.49` par `raydium_clmm`, en utilisant les sources Git/IDL habituelles et la page Solscan Program IDL `https://solscan.io/account/CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK#programIdl` pour accélérer la découverte de signatures par discriminant.

View File

@@ -0,0 +1,231 @@
-- file: SQL_VALIDATION_RAYDIUM_CPMM_0_7_48.sql
-- 1. Coverage rows must exist after diagnostics/validation/backfill refresh.
SELECT *
FROM k_sol_dex_event_coverage_entries
WHERE decoder_code = 'raydium_cpmm'
ORDER BY entry_kind, entry_name, discriminator_hex;
-- 2. Aggregated coverage summary for Raydium CPMM.
SELECT
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,
upstream_git_mapped_unverified_entry_count,
upstream_git_local_corpus_observed_entry_count,
upstream_git_local_corpus_materialized_entry_count
FROM (
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 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
)
WHERE decoder_code = 'raydium_cpmm';
-- 3. Decoded event distribution.
SELECT
de.protocol_name,
de.event_kind,
COUNT(*) AS decoded_count,
COUNT(DISTINCT de.transaction_id) AS transaction_count
FROM k_sol_dex_decoded_events de
WHERE de.protocol_name = 'raydium_cpmm'
GROUP BY de.protocol_name, de.event_kind
ORDER BY decoded_count DESC, de.event_kind;
-- 4. Audit/non-trade safety: no audit/non-trade CPMM event may produce trades.
SELECT
de.protocol_name,
de.event_kind,
COUNT(te.id) AS trade_count
FROM k_sol_dex_decoded_events de
LEFT JOIN k_sol_trade_events te
ON te.decoded_event_id = de.id
WHERE de.protocol_name = 'raydium_cpmm'
AND (
de.event_kind LIKE '%audit%'
OR de.event_kind IN (
'raydium_cpmm.lp_change_event',
'raydium_cpmm.swap_event',
'raydium_cpmm.collect_creator_fee',
'raydium_cpmm.collect_fund_fee',
'raydium_cpmm.collect_protocol_fee',
'raydium_cpmm.create_amm_config',
'raydium_cpmm.update_amm_config',
'raydium_cpmm.update_pool_status',
'raydium_cpmm.create_permission_pda',
'raydium_cpmm.close_permission_pda'
)
OR json_extract(de.payload_json, '$.eventActionability') IN ('non_trade_useful', 'informational', 'non_actionable_trade')
)
GROUP BY de.protocol_name, de.event_kind
ORDER BY trade_count DESC, de.event_kind;
-- 5. Failed transactions must not materialize trades.
SELECT
de.event_kind,
COUNT(*) AS decoded_count,
COUNT(te.id) AS trade_count
FROM k_sol_dex_decoded_events de
JOIN k_sol_chain_transactions tx
ON tx.id = de.transaction_id
LEFT JOIN k_sol_trade_events te
ON te.decoded_event_id = de.id
WHERE de.protocol_name = 'raydium_cpmm'
AND tx.err_json IS NOT NULL
AND tx.err_json <> ''
GROUP BY de.event_kind
ORDER BY trade_count DESC, decoded_count DESC;
-- 6. Local specialized decoder must replace upstream fallback for CPMM entries.
SELECT
json_extract(payload_json, '$.upstreamDecoderCode') AS upstream_decoder_code,
json_extract(payload_json, '$.entryName') AS entry_name,
json_extract(payload_json, '$.discriminatorHex') AS discriminator_hex,
COUNT(*) AS fallback_count
FROM k_sol_dex_decoded_events
WHERE protocol_name = 'upstream_git'
AND event_kind = 'upstream_git.instruction_match'
AND json_extract(payload_json, '$.upstreamDecoderCode') = 'raydium_cpmm'
GROUP BY upstream_decoder_code, entry_name, discriminator_hex
ORDER BY fallback_count DESC, entry_name;
-- 7. Program data events are decoded but must remain non-trade/non-candle candidates.
SELECT
event_kind,
COUNT(*) AS decoded_count,
SUM(CASE WHEN json_extract(payload_json, '$.tradeCandidate') = 1 THEN 1 ELSE 0 END) AS trade_candidate_count,
SUM(CASE WHEN json_extract(payload_json, '$.candleCandidate') = 1 THEN 1 ELSE 0 END) AS candle_candidate_count,
SUM(CASE WHEN json_extract(payload_json, '$.eventActionability') IN ('non_trade_useful', 'non_actionable_trade') THEN 1 ELSE 0 END) AS safe_actionability_count
FROM k_sol_dex_decoded_events
WHERE protocol_name = 'raydium_cpmm'
AND event_kind IN ('raydium_cpmm.lp_change_event', 'raydium_cpmm.swap_event')
GROUP BY event_kind
ORDER BY event_kind;
-- 8. Liquidity materialization for lp_change_event, if pool/pair exists in local corpus.
SELECT
de.event_kind,
json_extract(de.payload_json, '$.changeType') AS change_type,
COUNT(de.id) AS decoded_count,
COUNT(le.id) AS liquidity_materialized_count,
COUNT(te.id) AS trade_count
FROM k_sol_dex_decoded_events de
LEFT JOIN k_sol_liquidity_events le
ON le.decoded_event_id = de.id
LEFT JOIN k_sol_trade_events te
ON te.decoded_event_id = de.id
WHERE de.protocol_name = 'raydium_cpmm'
AND de.event_kind = 'raydium_cpmm.lp_change_event'
GROUP BY de.event_kind, change_type
ORDER BY change_type;
-- 9. Replay coverage hook check: capture this value before and after local replay.
SELECT MAX(updated_at) AS latest_coverage_refresh_at
FROM k_sol_dex_event_coverage_entries
WHERE decoder_code = 'raydium_cpmm';
-- 10. lp_change_event is a bidirectional liquidity event in coverage.
SELECT
entry_name,
event_family,
expected_db_target,
proof_status,
observed_count,
materialized_count,
trade_count
FROM k_sol_dex_event_coverage_entries
WHERE decoder_code = 'raydium_cpmm'
AND entry_name = 'lp_change_event';
-- 0.7.48 final — materialization safety summary.
SELECT
de.event_kind,
COUNT(*) AS decoded_count,
COUNT(le.id) AS liquidity_count,
COUNT(fe.id) AS fee_count,
COUNT(pa.id) AS admin_count,
COUNT(ple.id) AS lifecycle_count,
COUNT(te.id) AS trade_count
FROM k_sol_dex_decoded_events de
LEFT JOIN k_sol_liquidity_events le
ON le.decoded_event_id = de.id
LEFT JOIN k_sol_fee_events fe
ON fe.decoded_event_id = de.id
LEFT JOIN k_sol_pool_admin_events pa
ON pa.decoded_event_id = de.id
LEFT JOIN k_sol_pool_lifecycle_events ple
ON ple.decoded_event_id = de.id
LEFT JOIN k_sol_trade_events te
ON te.decoded_event_id = de.id
WHERE de.protocol_name = 'raydium_cpmm'
GROUP BY de.event_kind
ORDER BY de.event_kind;
-- 0.7.48 final — lp_change_event add/remove split.
SELECT
de.event_kind,
json_extract(de.payload_json, '$.changeType') AS change_type,
COUNT(*) AS decoded_count,
COUNT(le.id) AS liquidity_count,
COUNT(te.id) AS trade_count
FROM k_sol_dex_decoded_events de
LEFT JOIN k_sol_liquidity_events le
ON le.decoded_event_id = de.id
LEFT JOIN k_sol_trade_events te
ON te.decoded_event_id = de.id
WHERE de.protocol_name = 'raydium_cpmm'
AND de.event_kind = 'raydium_cpmm.lp_change_event'
GROUP BY de.event_kind, change_type
ORDER BY change_type;
-- 0.7.48 final — initialize_with_permission must remain lifecycle-only.
SELECT
de.event_kind,
COUNT(DISTINCT de.id) AS decoded_count,
COUNT(DISTINCT ple.id) AS lifecycle_count,
COUNT(DISTINCT pa.id) AS admin_count,
COUNT(DISTINCT te.id) AS trade_count
FROM k_sol_dex_decoded_events de
LEFT JOIN k_sol_pool_lifecycle_events ple
ON ple.decoded_event_id = de.id
LEFT JOIN k_sol_pool_admin_events pa
ON pa.decoded_event_id = de.id
LEFT JOIN k_sol_trade_events te
ON te.decoded_event_id = de.id
WHERE de.protocol_name = 'raydium_cpmm'
AND de.event_kind IN (
'raydium_cpmm.initialize',
'raydium_cpmm.initialize_with_permission'
)
GROUP BY de.event_kind
ORDER BY de.event_kind;
-- 0.7.48 final — instruction observation index.
SELECT
instruction_name,
discriminator_hex,
COUNT(*) AS observed_count,
COUNT(DISTINCT signature) AS tx_count
FROM k_sol_instruction_observations
WHERE decoder_code = 'raydium_cpmm'
GROUP BY instruction_name, discriminator_hex
ORDER BY observed_count DESC;

View File

@@ -218,6 +218,16 @@
<div class="form-text">Leave all unchecked for generic discovery. Check several surfaces to scan once and keep candidates matching any selected target.</div> <div class="form-text">Leave all unchecked for generic discovery. Check several surfaces to scan once and keep candidates matching any selected target.</div>
<div class="form-text">Use this to find corpus signatures for non-swap decoders without promoting unverified events. Leave all unchecked to request target='any'.</div> <div class="form-text">Use this to find corpus signatures for non-swap decoders without promoting unverified events. Leave all unchecked to request target='any'.</div>
</div> </div>
<div class="col-6">
<label for="demo3TargetInstructionNameInput" class="form-label">Target instruction name</label>
<input id="demo3TargetInstructionNameInput" type="text" class="form-control font-monospace" placeholder="withdraw, raydium_cpmm.deposit" />
<div class="form-text">Optional exact instruction-name filter. Accepts comma/space separated names.</div>
</div>
<div class="col-6">
<label for="demo3TargetDiscriminatorHexInput" class="form-label">Target discriminator hex</label>
<input id="demo3TargetDiscriminatorHexInput" type="text" class="form-control font-monospace" placeholder="b712469c946da122, f223c68952e1f2b6" />
<div class="form-text">Optional first 8 bytes of instruction data, matching the Solscan instruction filter.</div>
</div>
<div class="col-6"> <div class="col-6">
<label for="demo3HttpRoleInput" class="form-label">HTTP role</label> <label for="demo3HttpRoleInput" class="form-label">HTTP role</label>
<input id="demo3HttpRoleInput" type="text" class="form-control" value="history_backfill" /> <input id="demo3HttpRoleInput" type="text" class="form-control" value="history_backfill" />
@@ -335,6 +345,7 @@
<th>Kind</th> <th>Kind</th>
<th>Confidence</th> <th>Confidence</th>
<th>Data prefix</th> <th>Data prefix</th>
<th>Discriminator</th>
<th>Verified pool</th> <th>Verified pool</th>
<th>Token A</th> <th>Token A</th>
<th>Token B</th> <th>Token B</th>
@@ -346,7 +357,7 @@
</thead> </thead>
<tbody id="demo3OnchainCandidateTableBody"> <tbody id="demo3OnchainCandidateTableBody">
<tr> <tr>
<td colspan="12" class="text-body-secondary">No on-chain candidate.</td> <td colspan="13" class="text-body-secondary">No on-chain candidate.</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -416,7 +416,7 @@
Aucun jeu de candles chargé. Aucun jeu de candles chargé.
</div> </div>
</div> </div>
<div id="demoPipeline2Chart" class="w-100 border rounded bg-body" style="height: 560px;"></div> <div id="demoPipeline2Chart" class="w-100 border rounded bg-body" style="height: 680px; min-height: 680px;"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -44,6 +44,14 @@ scanOrder: string | null,
* Optional target event family used to find non-swap signatures. * Optional target event family used to find non-swap signatures.
*/ */
targetEvent: string | null, targetEvent: string | null,
/**
* Optional instruction name filter, e.g. `withdraw` or `raydium_cpmm.withdraw`.
*/
targetInstructionName: string | null,
/**
* Optional instruction discriminator filter as 16-char lower hex, comma-separated when needed.
*/
targetDiscriminatorHex: string | null,
/** /**
* Whether transactions containing swap-like logs should be skipped. * Whether transactions containing swap-like logs should be skipped.
*/ */

View File

@@ -54,6 +54,10 @@ instructionName: string | null,
* Prefix of the raw base58 instruction data, useful for audit grouping. * Prefix of the raw base58 instruction data, useful for audit grouping.
*/ */
instructionDataPrefix: string | null, instructionDataPrefix: string | null,
/**
* First eight instruction-data bytes as lower hex.
*/
instructionDiscriminatorHex: string | null,
/** /**
* Candidate pool address. * Candidate pool address.
*/ */

View File

@@ -187,6 +187,20 @@ function validateOnchainRequest(request: Demo3OnchainDexDiscoveryRequest): void
} }
throw new Error("Program id filter must be a valid Solana program id, or empty when using a preset that resolves it."); throw new Error("Program id filter must be a valid Solana program id, or empty when using a preset that resolves it.");
} }
validateDiscriminatorFilter(request.targetDiscriminatorHex);
}
function validateDiscriminatorFilter(value: string | null): void {
if (value === null || value.trim() === "") {
return;
}
const tokens = value.split(/[\s,]+/).map((token) => token.trim()).filter((token) => token !== "");
for (const token of tokens) {
const normalized = token.startsWith("0x") || token.startsWith("0X") ? token.slice(2) : token;
if (!/^[0-9a-fA-F]{16}$/.test(normalized)) {
throw new Error(`Target discriminator '${token}' must be exactly 8 bytes / 16 hex characters.`);
}
}
} }
function numberValueOrNull(value: string): number | null { function numberValueOrNull(value: string): number | null {
@@ -320,6 +334,8 @@ function readOnchainRequest(): Demo3OnchainDexDiscoveryRequest {
maxPages: intValue("demo3MaxPagesInput", 1), maxPages: intValue("demo3MaxPagesInput", 1),
scanOrder: valueOrNull(byId<HTMLSelectElement>("demo3ScanOrderSelect").value), scanOrder: valueOrNull(byId<HTMLSelectElement>("demo3ScanOrderSelect").value),
targetEvent: readTargetEventFilter(), targetEvent: readTargetEventFilter(),
targetInstructionName: valueOrNull(byId<HTMLInputElement>("demo3TargetInstructionNameInput").value),
targetDiscriminatorHex: valueOrNull(byId<HTMLInputElement>("demo3TargetDiscriminatorHexInput").value),
excludeSwaps: byId<HTMLInputElement>("demo3ExcludeSwapsInput").checked, excludeSwaps: byId<HTMLInputElement>("demo3ExcludeSwapsInput").checked,
includeFailed: byId<HTMLInputElement>("demo3IncludeFailedInput").checked, includeFailed: byId<HTMLInputElement>("demo3IncludeFailedInput").checked,
httpRole: byId<HTMLInputElement>("demo3HttpRoleInput").value.trim() || "history_backfill", httpRole: byId<HTMLInputElement>("demo3HttpRoleInput").value.trim() || "history_backfill",
@@ -375,8 +391,10 @@ function renderOnchainResult(result: Demo3OnchainDexDiscoveryResult): void {
byId<HTMLElement>("demo3SummaryRejectedCandidateCount").textContent = String(result.targetRejectedCandidateCount); byId<HTMLElement>("demo3SummaryRejectedCandidateCount").textContent = String(result.targetRejectedCandidateCount);
byId<HTMLElement>("demo3SummaryCandidateCount").textContent = String(result.candidateCount); byId<HTMLElement>("demo3SummaryCandidateCount").textContent = String(result.candidateCount);
const targetEvent = targetEventLabel(result.request.targetEvent); const targetEvent = targetEventLabel(result.request.targetEvent);
const targetInstruction = result.request.targetInstructionName ?? "any";
const targetDiscriminator = result.request.targetDiscriminatorHex ?? "any";
const sourceText = result.resolvedSignatureAddresses.length === 0 ? result.resolvedSignatureAddress : result.resolvedSignatureAddresses.join(","); const sourceText = result.resolvedSignatureAddresses.length === 0 ? result.resolvedSignatureAddress : result.resolvedSignatureAddresses.join(",");
byId<HTMLElement>("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / program=${result.resolvedProgramId} / source=${result.resolvedSignatureSource}:${sourceText} / target=${targetEvent} / order=${result.request.scanOrder ?? "newest_first"}`; byId<HTMLElement>("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / program=${result.resolvedProgramId} / source=${result.resolvedSignatureSource}:${sourceText} / target=${targetEvent} / instr=${targetInstruction} / disc=${targetDiscriminator} / order=${result.request.scanOrder ?? "newest_first"}`;
byId<HTMLElement>("demo3UniqueSignatureText").textContent = result.uniqueBackfillSignatures.length === 0 ? "-" : result.uniqueBackfillSignatures.join(", "); byId<HTMLElement>("demo3UniqueSignatureText").textContent = result.uniqueBackfillSignatures.length === 0 ? "-" : result.uniqueBackfillSignatures.join(", ");
byId<HTMLElement>("demo3NextBeforeText").textContent = result.nextBeforeByAddress.length === 0 ? "-" : result.nextBeforeByAddress.map((cursor) => `${cursor.address}:${cursor.nextBeforeSignature ?? "-"}`).join(" | "); byId<HTMLElement>("demo3NextBeforeText").textContent = result.nextBeforeByAddress.length === 0 ? "-" : result.nextBeforeByAddress.map((cursor) => `${cursor.address}:${cursor.nextBeforeSignature ?? "-"}`).join(" | ");
renderRejectedSummary(result); renderRejectedSummary(result);
@@ -402,7 +420,7 @@ function renderRejectedSummary(result: Demo3OnchainDexDiscoveryResult): void {
function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): void { function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): void {
const body = byId<HTMLTableSectionElement>("demo3OnchainCandidateTableBody"); const body = byId<HTMLTableSectionElement>("demo3OnchainCandidateTableBody");
if (candidates.length === 0) { if (candidates.length === 0) {
body.innerHTML = '<tr><td colspan="12" class="text-body-secondary">No on-chain candidate.</td></tr>'; body.innerHTML = '<tr><td colspan="13" class="text-body-secondary">No on-chain candidate.</td></tr>';
return; return;
} }
body.innerHTML = candidates.map((candidate) => { body.innerHTML = candidates.map((candidate) => {
@@ -428,6 +446,7 @@ function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): vo
<td><span class="badge text-bg-info">${escapeHtml(candidate.candidateKind)}</span></td> <td><span class="badge text-bg-info">${escapeHtml(candidate.candidateKind)}</span></td>
<td><span class="badge text-bg-${candidate.confidence === "high" ? "success" : candidate.confidence === "medium" ? "warning" : "secondary"}">${escapeHtml(candidate.confidence)}</span></td> <td><span class="badge text-bg-${candidate.confidence === "high" ? "success" : candidate.confidence === "medium" ? "warning" : "secondary"}">${escapeHtml(candidate.confidence)}</span></td>
<td class="font-monospace" title="${escapeHtml(candidate.instructionDataPrefix ?? "")}">${escapeHtml(shortText(candidate.instructionDataPrefix, 14))}</td> <td class="font-monospace" title="${escapeHtml(candidate.instructionDataPrefix ?? "")}">${escapeHtml(shortText(candidate.instructionDataPrefix, 14))}</td>
<td class="font-monospace" title="${escapeHtml(candidate.instructionDiscriminatorHex ?? "")}">${escapeHtml(shortText(candidate.instructionDiscriminatorHex, 16))}</td>
<td class="font-monospace" title="${escapeHtml(verifiedPool ?? "")}">${escapeHtml(shortText(verifiedPool, 14))}</td> <td class="font-monospace" title="${escapeHtml(verifiedPool ?? "")}">${escapeHtml(shortText(verifiedPool, 14))}</td>
<td class="font-monospace" title="${escapeHtml(candidate.tokenAMint ?? "")}">${escapeHtml(shortText(candidate.tokenAMint, 14))}</td> <td class="font-monospace" title="${escapeHtml(candidate.tokenAMint ?? "")}">${escapeHtml(shortText(candidate.tokenAMint, 14))}</td>
<td class="font-monospace" title="${escapeHtml(candidate.tokenBMint ?? "")}">${escapeHtml(shortText(candidate.tokenBMint, 14))}</td> <td class="font-monospace" title="${escapeHtml(candidate.tokenBMint ?? "")}">${escapeHtml(shortText(candidate.tokenBMint, 14))}</td>
@@ -469,7 +488,7 @@ async function discoverOnchain(): Promise<void> {
return; return;
} }
setStatus("running", "text-bg-warning"); setStatus("running", "text-bg-warning");
appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' source='${request.signatureSource ?? "program_id"}:${request.sourceAddresses.join(",")}' target='${targetEventLabel(request.targetEvent)}' pages='${request.maxPages}' order='${request.scanOrder ?? "newest_first"}' before='${request.beforeSignature ?? ""}' until='${request.untilSignature ?? ""}' excludeSwaps='${request.excludeSwaps}' role='${request.httpRole}'`); appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' source='${request.signatureSource ?? "program_id"}:${request.sourceAddresses.join(",")}' target='${targetEventLabel(request.targetEvent)}' instruction='${request.targetInstructionName ?? ""}' discriminator='${request.targetDiscriminatorHex ?? ""}' pages='${request.maxPages}' order='${request.scanOrder ?? "newest_first"}' before='${request.beforeSignature ?? ""}' until='${request.untilSignature ?? ""}' excludeSwaps='${request.excludeSwaps}' role='${request.httpRole}'`);
try { try {
const payload = await invoke<Demo3OnchainDexDiscoveryPayload>("demo3_discover_onchain_dex_pairs", { request }); const payload = await invoke<Demo3OnchainDexDiscoveryPayload>("demo3_discover_onchain_dex_pairs", { request });
lastResultJson = payload.resultJson; lastResultJson = payload.resultJson;

View File

@@ -186,6 +186,32 @@ function renderCatalogTextareas(
pairsTextarea.value = JSON.stringify(catalog.pairs, null, 2); pairsTextarea.value = JSON.stringify(catalog.pairs, null, 2);
} }
function toChartNumber(value: number | string | null | undefined): number {
if (value === null || value === undefined) {
return 0;
}
if (typeof value === "number") {
return Number.isFinite(value) ? value : 0;
}
const parsed = Number.parseFloat(value);
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
return 0;
}
return parsed;
}
function calculateVisibleWindowStart(totalCandles: number): number {
if (totalCandles <= 90) {
return 0;
}
return Math.max(0, ((totalCandles - 90) / totalCandles) * 100);
}
function parseCandlesJson(raw: string): PairCandle[] { function parseCandlesJson(raw: string): PairCandle[] {
if (raw.trim() === "") { if (raw.trim() === "") {
return []; return [];
@@ -239,16 +265,18 @@ function renderCandlesChart(
); );
const ohlcData = sorted.map((candle) => [ const ohlcData = sorted.map((candle) => [
candle.open_price_quote_per_base, toChartNumber(candle.open_price_quote_per_base),
candle.close_price_quote_per_base, toChartNumber(candle.close_price_quote_per_base),
candle.low_price_quote_per_base, toChartNumber(candle.low_price_quote_per_base),
candle.high_price_quote_per_base, toChartNumber(candle.high_price_quote_per_base),
]); ]);
const volumeData = sorted.map((candle) => const volumeData = sorted.map((candle) =>
parseVolume(candle.quote_volume_raw, candle.trade_count), parseVolume(candle.quote_volume_raw, candle.trade_count),
); );
const zoomStart = calculateVisibleWindowStart(sorted.length);
chartMeta.textContent = chartMeta.textContent =
`Pair #${pairId.toString()} • timeframe ${timeframeSeconds.toString()}s • ${sorted.length} candles`; `Pair #${pairId.toString()} • timeframe ${timeframeSeconds.toString()}s • ${sorted.length} candles`;
@@ -256,7 +284,8 @@ function renderCandlesChart(
animation: false, animation: false,
legend: { legend: {
data: ["OHLC", "Volume"], data: ["OHLC", "Volume"],
top: 0, top: 4,
left: 16,
}, },
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
@@ -268,8 +297,8 @@ function renderCandlesChart(
link: [{ xAxisIndex: "all" }], link: [{ xAxisIndex: "all" }],
}, },
grid: [ grid: [
{ left: 60, right: 24, top: 40, height: "58%" }, { left: 76, right: 32, top: 52, height: "58%" },
{ left: 60, right: 24, top: "74%", height: "16%" }, { left: 76, right: 32, top: "77%", height: "12%" },
], ],
xAxis: [ xAxis: [
{ {
@@ -277,7 +306,9 @@ function renderCandlesChart(
data: categoryData, data: categoryData,
boundaryGap: true, boundaryGap: true,
axisLine: { onZero: false }, axisLine: { onZero: false },
splitLine: { show: false }, axisTick: { alignWithLabel: true },
splitLine: { show: true },
axisLabel: { hideOverlap: true, margin: 14 },
min: "dataMin", min: "dataMin",
max: "dataMax", max: "dataMax",
}, },
@@ -287,9 +318,9 @@ function renderCandlesChart(
data: categoryData, data: categoryData,
boundaryGap: true, boundaryGap: true,
axisLine: { onZero: false }, axisLine: { onZero: false },
axisTick: { show: false }, axisTick: { alignWithLabel: true },
splitLine: { show: false }, splitLine: { show: false },
axisLabel: { show: false }, axisLabel: { hideOverlap: true, margin: 10 },
min: "dataMin", min: "dataMin",
max: "dataMax", max: "dataMax",
}, },
@@ -297,27 +328,33 @@ function renderCandlesChart(
yAxis: [ yAxis: [
{ {
scale: true, scale: true,
splitNumber: 5,
splitArea: { show: false }, splitArea: { show: false },
axisLabel: { margin: 12 },
}, },
{ {
gridIndex: 1, gridIndex: 1,
scale: true, scale: true,
splitNumber: 2, splitNumber: 2,
axisLabel: { margin: 12 },
}, },
], ],
dataZoom: [ dataZoom: [
{ {
type: "inside", type: "inside",
xAxisIndex: [0, 1], xAxisIndex: [0, 1],
start: 0, filterMode: "none",
start: zoomStart,
end: 100, end: 100,
}, },
{ {
show: true, show: true,
type: "slider", type: "slider",
xAxisIndex: [0, 1], xAxisIndex: [0, 1],
bottom: 6, filterMode: "none",
start: 0, bottom: 8,
height: 22,
start: zoomStart,
end: 100, end: 100,
}, },
], ],
@@ -326,11 +363,12 @@ function renderCandlesChart(
name: "OHLC", name: "OHLC",
type: "candlestick", type: "candlestick",
data: ohlcData, data: ohlcData,
xAxisIndex: 0,
yAxisIndex: 0,
barMinWidth: 4,
barMaxWidth: 16,
itemStyle: { itemStyle: {
color: "#16a34a", borderWidth: 1.4,
color0: "#dc2626",
borderColor: "#15803d",
borderColor0: "#b91c1c",
}, },
}, },
{ {
@@ -339,9 +377,13 @@ function renderCandlesChart(
xAxisIndex: 1, xAxisIndex: 1,
yAxisIndex: 1, yAxisIndex: 1,
data: volumeData, data: volumeData,
barMinWidth: 2,
barMaxWidth: 10,
}, },
], ],
}, true); }, true);
window.setTimeout(() => chart.resize(), 0);
} }
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
@@ -468,6 +510,10 @@ document.addEventListener("DOMContentLoaded", async () => {
const chart = echarts.init(safeChartElement); const chart = echarts.init(safeChartElement);
setEmptyChart(chart, safeChartMeta, "Aucune candle disponible."); setEmptyChart(chart, safeChartMeta, "Aucune candle disponible.");
window.addEventListener("resize", () => chart.resize()); window.addEventListener("resize", () => chart.resize());
const chartCollapse = document.querySelector<HTMLDivElement>("#demoPipeline2ChartCollapse");
chartCollapse?.addEventListener("shown.bs.collapse", () => {
window.setTimeout(() => chart.resize(), 0);
});
clearLogButton.addEventListener("click", () => { clearLogButton.addEventListener("click", () => {
logTextarea.value = ""; logTextarea.value = "";

View File

@@ -1,7 +1,7 @@
{ {
"name": "kb-demo-app", "name": "kb-demo-app",
"private": true, "private": true,
"version": "0.7.47", "version": "0.7.48",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -287,7 +287,10 @@ pub(crate) async fn demo3_search_local_dex_corpus(
/// Search request for the static upstream registry exposed through Demo3. /// Search request for the static upstream registry exposed through Demo3.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/Demo3UpstreamRegistrySearchRequest.ts")] #[ts(
export,
export_to = "../frontend/ts/bindings/Demo3UpstreamRegistrySearchRequest.ts"
)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct Demo3UpstreamRegistrySearchRequest { pub(crate) struct Demo3UpstreamRegistrySearchRequest {
/// Optional decoder-code filter. /// Optional decoder-code filter.
@@ -423,10 +426,7 @@ pub(crate) fn demo3_search_upstream_registry(
return Err(format!("cannot serialize upstream registry result: {}", error)); return Err(format!("cannot serialize upstream registry result: {}", error));
}, },
}; };
return Ok(Demo3UpstreamRegistryPayload { return Ok(Demo3UpstreamRegistryPayload { result_json, result: ui_result });
result_json,
result: ui_result,
});
} }
fn to_lib_upstream_registry_request( fn to_lib_upstream_registry_request(
@@ -491,8 +491,7 @@ fn from_lib_upstream_registry_summary(
account_entry_count: summary.account_entry_count, account_entry_count: summary.account_entry_count,
upstream_git_unverified_count: summary.upstream_git_unverified_count, upstream_git_unverified_count: summary.upstream_git_unverified_count,
upstream_git_mapped_unverified_count: summary.upstream_git_mapped_unverified_count, upstream_git_mapped_unverified_count: summary.upstream_git_mapped_unverified_count,
upstream_git_local_corpus_observed_count: summary upstream_git_local_corpus_observed_count: summary.upstream_git_local_corpus_observed_count,
.upstream_git_local_corpus_observed_count,
upstream_git_local_corpus_materialized_count: summary upstream_git_local_corpus_materialized_count: summary
.upstream_git_local_corpus_materialized_count, .upstream_git_local_corpus_materialized_count,
upstream_git_layout_unverified_count: summary.upstream_git_layout_unverified_count, upstream_git_layout_unverified_count: summary.upstream_git_layout_unverified_count,
@@ -685,6 +684,12 @@ pub(crate) struct Demo3OnchainDexDiscoveryRequest {
pub scan_order: std::option::Option<std::string::String>, pub scan_order: std::option::Option<std::string::String>,
/// Optional target event family used to find non-swap signatures. /// Optional target event family used to find non-swap signatures.
pub target_event: std::option::Option<std::string::String>, pub target_event: std::option::Option<std::string::String>,
/// Optional instruction name filter, e.g. `withdraw` or `raydium_cpmm.withdraw`.
#[serde(default)]
pub target_instruction_name: std::option::Option<std::string::String>,
/// Optional instruction discriminator filter as 16-char lower hex, comma-separated when needed.
#[serde(default)]
pub target_discriminator_hex: std::option::Option<std::string::String>,
/// Whether transactions containing swap-like logs should be skipped. /// Whether transactions containing swap-like logs should be skipped.
pub exclude_swaps: bool, pub exclude_swaps: bool,
/// Whether failed transactions should be returned as candidates. /// Whether failed transactions should be returned as candidates.
@@ -846,6 +851,8 @@ pub(crate) struct Demo3OnchainDexPairCandidate {
pub instruction_name: std::option::Option<std::string::String>, pub instruction_name: std::option::Option<std::string::String>,
/// Prefix of the raw base58 instruction data, useful for audit grouping. /// Prefix of the raw base58 instruction data, useful for audit grouping.
pub instruction_data_prefix: std::option::Option<std::string::String>, pub instruction_data_prefix: std::option::Option<std::string::String>,
/// First eight instruction-data bytes as lower hex.
pub instruction_discriminator_hex: std::option::Option<std::string::String>,
/// Candidate pool address. /// Candidate pool address.
pub pool_address: std::option::Option<std::string::String>, pub pool_address: std::option::Option<std::string::String>,
/// Candidate token A mint. /// Candidate token A mint.
@@ -966,6 +973,8 @@ fn to_lib_onchain_request(
max_pages: request.max_pages, max_pages: request.max_pages,
scan_order: normalize_optional_text(request.scan_order.clone()), scan_order: normalize_optional_text(request.scan_order.clone()),
target_event: normalize_optional_text(request.target_event.clone()), target_event: normalize_optional_text(request.target_event.clone()),
target_instruction_name: normalize_optional_text(request.target_instruction_name.clone()),
target_discriminator_hex: normalize_optional_text(request.target_discriminator_hex.clone()),
exclude_swaps: request.exclude_swaps, exclude_swaps: request.exclude_swaps,
include_failed: request.include_failed, include_failed: request.include_failed,
http_role: request.http_role.trim().to_string(), http_role: request.http_role.trim().to_string(),
@@ -994,6 +1003,8 @@ fn from_lib_onchain_result(
max_pages: result.request.max_pages, max_pages: result.request.max_pages,
scan_order: result.request.scan_order, scan_order: result.request.scan_order,
target_event: result.request.target_event, target_event: result.request.target_event,
target_instruction_name: result.request.target_instruction_name,
target_discriminator_hex: result.request.target_discriminator_hex,
exclude_swaps: result.request.exclude_swaps, exclude_swaps: result.request.exclude_swaps,
include_failed: result.request.include_failed, include_failed: result.request.include_failed,
http_role: result.request.http_role, http_role: result.request.http_role,
@@ -1074,6 +1085,7 @@ fn from_lib_onchain_candidate(
inner_instruction_index: candidate.inner_instruction_index, inner_instruction_index: candidate.inner_instruction_index,
instruction_name: candidate.instruction_name, instruction_name: candidate.instruction_name,
instruction_data_prefix: candidate.instruction_data_prefix, instruction_data_prefix: candidate.instruction_data_prefix,
instruction_discriminator_hex: candidate.instruction_discriminator_hex,
pool_address: candidate.pool_address, pool_address: candidate.pool_address,
token_a_mint: candidate.token_a_mint, token_a_mint: candidate.token_a_mint,
token_b_mint: candidate.token_b_mint, token_b_mint: candidate.token_b_mint,

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "kb-demo-app", "productName": "kb-demo-app",
"version": "0.7.47", "version": "0.7.48",
"identifier": "com.sasedev.kb-demo-app", "identifier": "com.sasedev.kb-demo-app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -288,7 +288,7 @@ pub const RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID: &str = "5quBtoiQqxF9Jv6KYKctB59NT3
pub const BONKSWAP_PROGRAM_ID: &str = "BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p"; pub const BONKSWAP_PROGRAM_ID: &str = "BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p";
/// Boop program id extracted from upstream Git decoder source. /// Boop program id extracted from upstream Git decoder source.
pub const BOOP_PROGRAM_ID: &str = "boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4"; pub const BOOP_FUN_PROGRAM_ID: &str = "boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4";
/// DFlow Aggregator v4 program id extracted from upstream Git decoder source. /// DFlow Aggregator v4 program id extracted from upstream Git decoder source.
pub const DFLOW_AGGREGATOR_V4_PROGRAM_ID: &str = "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"; pub const DFLOW_AGGREGATOR_V4_PROGRAM_ID: &str = "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH";

View File

@@ -27,6 +27,7 @@ pub use dtos::DexDto;
pub use dtos::DexEventCoverageEntryDto; pub use dtos::DexEventCoverageEntryDto;
pub use dtos::DexEventCoverageSummaryDto; pub use dtos::DexEventCoverageSummaryDto;
pub use dtos::FeeEventDto; pub use dtos::FeeEventDto;
pub use dtos::InstructionObservationDto;
pub use dtos::KnownHttpEndpointDto; pub use dtos::KnownHttpEndpointDto;
pub use dtos::KnownWsEndpointDto; pub use dtos::KnownWsEndpointDto;
pub use dtos::LaunchAttributionDto; pub use dtos::LaunchAttributionDto;
@@ -96,6 +97,7 @@ pub use entities::DexEntity;
pub use entities::DexEventCoverageEntryEntity; pub use entities::DexEventCoverageEntryEntity;
pub use entities::DexEventCoverageSummaryEntity; pub use entities::DexEventCoverageSummaryEntity;
pub use entities::FeeEventEntity; pub use entities::FeeEventEntity;
pub use entities::InstructionObservationEntity;
pub use entities::KnownHttpEndpointEntity; pub use entities::KnownHttpEndpointEntity;
pub use entities::KnownWsEndpointEntity; pub use entities::KnownWsEndpointEntity;
pub use entities::LaunchAttributionEntity; pub use entities::LaunchAttributionEntity;
@@ -168,6 +170,8 @@ pub use queries::query_dexs_upsert;
pub use queries::query_fee_events_get_by_decoded_event_id; pub use queries::query_fee_events_get_by_decoded_event_id;
pub use queries::query_fee_events_list_recent; pub use queries::query_fee_events_list_recent;
pub use queries::query_fee_events_upsert; pub use queries::query_fee_events_upsert;
pub use queries::query_instruction_observations_list_by_filter;
pub use queries::query_instruction_observations_upsert;
pub use queries::query_known_http_endpoints_get; pub use queries::query_known_http_endpoints_get;
pub use queries::query_known_http_endpoints_list; pub use queries::query_known_http_endpoints_list;
pub use queries::query_known_http_endpoints_upsert; pub use queries::query_known_http_endpoints_upsert;

View File

@@ -15,6 +15,7 @@ mod dex_event_coverage_entry;
mod fee_event; mod fee_event;
mod known_http_endpoint; mod known_http_endpoint;
mod known_ws_endpoint; mod known_ws_endpoint;
mod instruction_observation;
mod launch_attribution; mod launch_attribution;
mod launch_surface; mod launch_surface;
mod launch_surface_key; mod launch_surface_key;
@@ -84,6 +85,7 @@ pub use dex_event_coverage_entry::DexEventCoverageSummaryDto;
pub use fee_event::FeeEventDto; pub use fee_event::FeeEventDto;
pub use known_http_endpoint::KnownHttpEndpointDto; pub use known_http_endpoint::KnownHttpEndpointDto;
pub use known_ws_endpoint::KnownWsEndpointDto; pub use known_ws_endpoint::KnownWsEndpointDto;
pub use instruction_observation::InstructionObservationDto;
pub use launch_attribution::LaunchAttributionDto; pub use launch_attribution::LaunchAttributionDto;
pub use launch_surface::LaunchSurfaceDto; pub use launch_surface::LaunchSurfaceDto;
pub use launch_surface_key::LaunchSurfaceKeyDto; pub use launch_surface_key::LaunchSurfaceKeyDto;

View File

@@ -0,0 +1,155 @@
// file: kb_lib/src/db/dtos/instruction_observation.rs
//! Instruction observation DTOs.
/// Persisted technical observation for one Solana instruction.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct InstructionObservationDto {
/// Optional numeric primary key.
pub id: std::option::Option<i64>,
/// Stable observation key.
pub observation_key: std::string::String,
/// Parent transaction id.
pub transaction_id: i64,
/// Parent transaction signature.
pub signature: std::string::String,
/// Optional Solana slot.
pub slot: std::option::Option<i64>,
/// Optional block time.
pub block_time: std::option::Option<i64>,
/// Whether the parent transaction failed.
pub failed: bool,
/// Instruction row id.
pub instruction_id: i64,
/// Optional parent instruction id.
pub parent_instruction_id: std::option::Option<i64>,
/// Outer instruction index.
pub instruction_index: i64,
/// Optional inner instruction index.
pub inner_instruction_index: std::option::Option<i64>,
/// Instruction program id.
pub program_id: std::string::String,
/// Local decoder code when resolved.
pub decoder_code: std::option::Option<std::string::String>,
/// First eight instruction-data bytes as lower-hex.
pub discriminator_hex: std::option::Option<std::string::String>,
/// Known local instruction name when resolved.
pub instruction_name: std::option::Option<std::string::String>,
/// Serialized accounts JSON.
pub accounts_json: std::string::String,
/// Optional serialized data JSON.
pub data_json: std::option::Option<std::string::String>,
/// Optional decoded pool account from local decoded events.
pub pool_account: std::option::Option<std::string::String>,
/// Optional decoded event kind attached to this instruction.
pub decoded_event_kind: std::option::Option<std::string::String>,
/// Optional decoded event id attached to this instruction.
pub decoded_event_id: std::option::Option<i64>,
/// First observation timestamp.
pub observed_at: chrono::DateTime<chrono::Utc>,
/// Last refresh timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}
impl InstructionObservationDto {
/// Creates a new instruction observation DTO.
#[allow(clippy::too_many_arguments)]
pub fn new(
observation_key: std::string::String,
transaction_id: i64,
signature: std::string::String,
slot: std::option::Option<i64>,
block_time: std::option::Option<i64>,
failed: bool,
instruction_id: i64,
parent_instruction_id: std::option::Option<i64>,
instruction_index: i64,
inner_instruction_index: std::option::Option<i64>,
program_id: std::string::String,
decoder_code: std::option::Option<std::string::String>,
discriminator_hex: std::option::Option<std::string::String>,
instruction_name: std::option::Option<std::string::String>,
accounts_json: std::string::String,
data_json: std::option::Option<std::string::String>,
pool_account: std::option::Option<std::string::String>,
decoded_event_kind: std::option::Option<std::string::String>,
decoded_event_id: std::option::Option<i64>,
) -> Self {
let now = chrono::Utc::now();
return Self {
id: None,
observation_key,
transaction_id,
signature,
slot,
block_time,
failed,
instruction_id,
parent_instruction_id,
instruction_index,
inner_instruction_index,
program_id,
decoder_code,
discriminator_hex,
instruction_name,
accounts_json,
data_json,
pool_account,
decoded_event_kind,
decoded_event_id,
observed_at: now,
updated_at: now,
};
}
}
impl TryFrom<crate::InstructionObservationEntity> for InstructionObservationDto {
type Error = crate::Error;
fn try_from(entity: crate::InstructionObservationEntity) -> Result<Self, Self::Error> {
let observed_at_result = chrono::DateTime::parse_from_rfc3339(entity.observed_at.as_str());
let observed_at = match observed_at_result {
Ok(observed_at) => observed_at.with_timezone(&chrono::Utc),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot parse instruction observation observed_at '{}': {}",
entity.observed_at, error
)));
},
};
let updated_at_result = chrono::DateTime::parse_from_rfc3339(entity.updated_at.as_str());
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 instruction observation updated_at '{}': {}",
entity.updated_at, error
)));
},
};
return Ok(Self {
id: Some(entity.id),
observation_key: entity.observation_key,
transaction_id: entity.transaction_id,
signature: entity.signature,
slot: entity.slot,
block_time: entity.block_time,
failed: entity.failed != 0,
instruction_id: entity.instruction_id,
parent_instruction_id: entity.parent_instruction_id,
instruction_index: entity.instruction_index,
inner_instruction_index: entity.inner_instruction_index,
program_id: entity.program_id,
decoder_code: entity.decoder_code,
discriminator_hex: entity.discriminator_hex,
instruction_name: entity.instruction_name,
accounts_json: entity.accounts_json,
data_json: entity.data_json,
pool_account: entity.pool_account,
decoded_event_kind: entity.decoded_event_kind,
decoded_event_id: entity.decoded_event_id,
observed_at,
updated_at,
});
}
}

View File

@@ -17,6 +17,7 @@ mod dex_event_coverage_entry;
mod fee_event; mod fee_event;
mod known_http_endpoint; mod known_http_endpoint;
mod known_ws_endpoint; mod known_ws_endpoint;
mod instruction_observation;
mod launch_attribution; mod launch_attribution;
mod launch_surface; mod launch_surface;
mod launch_surface_key; mod launch_surface_key;
@@ -62,6 +63,7 @@ pub use dex_event_coverage_entry::DexEventCoverageSummaryEntity;
pub use fee_event::FeeEventEntity; pub use fee_event::FeeEventEntity;
pub use known_http_endpoint::KnownHttpEndpointEntity; pub use known_http_endpoint::KnownHttpEndpointEntity;
pub use known_ws_endpoint::KnownWsEndpointEntity; pub use known_ws_endpoint::KnownWsEndpointEntity;
pub use instruction_observation::InstructionObservationEntity;
pub use launch_attribution::LaunchAttributionEntity; pub use launch_attribution::LaunchAttributionEntity;
pub use launch_surface::LaunchSurfaceEntity; pub use launch_surface::LaunchSurfaceEntity;
pub use launch_surface_key::LaunchSurfaceKeyEntity; pub use launch_surface_key::LaunchSurfaceKeyEntity;

View File

@@ -0,0 +1,52 @@
// file: kb_lib/src/db/entities/instruction_observation.rs
//! Instruction observation entity.
/// Persisted technical observation for one Solana instruction.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
pub struct InstructionObservationEntity {
/// Internal row id.
pub id: i64,
/// Stable observation key.
pub observation_key: std::string::String,
/// Parent transaction id.
pub transaction_id: i64,
/// Parent transaction signature.
pub signature: std::string::String,
/// Optional Solana slot.
pub slot: std::option::Option<i64>,
/// Optional block time.
pub block_time: std::option::Option<i64>,
/// Transaction failed flag.
pub failed: i64,
/// Instruction row id.
pub instruction_id: i64,
/// Optional parent instruction id.
pub parent_instruction_id: std::option::Option<i64>,
/// Outer instruction index.
pub instruction_index: i64,
/// Optional inner instruction index.
pub inner_instruction_index: std::option::Option<i64>,
/// Instruction program id.
pub program_id: std::string::String,
/// Local decoder code when resolved.
pub decoder_code: std::option::Option<std::string::String>,
/// First eight instruction-data bytes as lower-hex.
pub discriminator_hex: std::option::Option<std::string::String>,
/// Known local instruction name when resolved.
pub instruction_name: std::option::Option<std::string::String>,
/// Accounts JSON.
pub accounts_json: std::string::String,
/// Optional data JSON.
pub data_json: std::option::Option<std::string::String>,
/// Optional pool account.
pub pool_account: std::option::Option<std::string::String>,
/// Optional decoded event kind.
pub decoded_event_kind: std::option::Option<std::string::String>,
/// Optional decoded event id.
pub decoded_event_id: std::option::Option<i64>,
/// First observation timestamp.
pub observed_at: std::string::String,
/// Last refresh timestamp.
pub updated_at: std::string::String,
}

View File

@@ -15,6 +15,7 @@ mod dex_event_coverage_entry;
mod fee_event; mod fee_event;
mod known_http_endpoint; mod known_http_endpoint;
mod known_ws_endpoint; mod known_ws_endpoint;
mod instruction_observation;
mod launch_attribution; mod launch_attribution;
mod launch_surface; mod launch_surface;
mod launch_surface_key; mod launch_surface_key;
@@ -92,6 +93,8 @@ pub use known_http_endpoint::query_known_http_endpoints_upsert;
pub use known_ws_endpoint::query_known_ws_endpoints_get; pub use known_ws_endpoint::query_known_ws_endpoints_get;
pub use known_ws_endpoint::query_known_ws_endpoints_list; pub use known_ws_endpoint::query_known_ws_endpoints_list;
pub use known_ws_endpoint::query_known_ws_endpoints_upsert; pub use known_ws_endpoint::query_known_ws_endpoints_upsert;
pub use instruction_observation::query_instruction_observations_list_by_filter;
pub use instruction_observation::query_instruction_observations_upsert;
pub use launch_attribution::query_launch_attributions_get_by_decoded_event_id; pub use launch_attribution::query_launch_attributions_get_by_decoded_event_id;
pub use launch_attribution::query_launch_attributions_list_by_pool_id; pub use launch_attribution::query_launch_attributions_list_by_pool_id;
pub use launch_attribution::query_launch_attributions_upsert; pub use launch_attribution::query_launch_attributions_upsert;

View File

@@ -442,7 +442,7 @@ mod tests {
0, 0,
None, None,
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()), Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
Some("raydium-amm-v4".to_string()), Some("raydium_amm_v4".to_string()),
Some(1), Some(1),
r#"["Account0","Pool111","Lp111","TokenA111","TokenB111"]"#.to_string(), r#"["Account0","Pool111","Lp111","TokenA111","TokenB111"]"#.to_string(),
None, None,
@@ -529,7 +529,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()), Some("meteora_dlmm".to_string()),
Some(1), Some(1),
r#"["ParentAccount","Pool111"]"#.to_string(), r#"["ParentAccount","Pool111"]"#.to_string(),
None, None,
@@ -548,7 +548,7 @@ mod tests {
0, 0,
Some(0), Some(0),
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()), Some("meteora_dlmm".to_string()),
Some(2), Some(2),
r#"["ChildAccount","Pool111"]"#.to_string(), r#"["ChildAccount","Pool111"]"#.to_string(),
None, None,

View File

@@ -735,7 +735,7 @@ mod tests {
let database = make_database().await; let database = make_database().await;
let upstream_service = crate::UpstreamRegistryService::new(); let upstream_service = crate::UpstreamRegistryService::new();
let request = crate::UpstreamRegistrySearchRequestDto { let request = crate::UpstreamRegistrySearchRequestDto {
decoder_code: Some("raydium-cpmm".to_string()), decoder_code: Some("raydium_cpmm".to_string()),
program_id: None, program_id: None,
program_family: None, program_family: None,
surface_kind: None, surface_kind: None,
@@ -759,7 +759,7 @@ mod tests {
.expect("coverage upsert must succeed"); .expect("coverage upsert must succeed");
assert!(id > 0); assert!(id > 0);
let rows = let rows =
crate::query_dex_event_coverage_entries_list_by_decoder(&database, "raydium-cpmm") crate::query_dex_event_coverage_entries_list_by_decoder(&database, "raydium_cpmm")
.await .await
.expect("coverage list must succeed"); .expect("coverage list must succeed");
assert_eq!(rows.len(), 1); assert_eq!(rows.len(), 1);
@@ -768,7 +768,7 @@ mod tests {
.await .await
.expect("coverage summary must succeed"); .expect("coverage summary must succeed");
assert_eq!(summaries.len(), 1); assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].decoder_code, "raydium-cpmm"); assert_eq!(summaries[0].decoder_code, "raydium_cpmm");
assert_eq!(summaries[0].listed_entry_count, 1); assert_eq!(summaries[0].listed_entry_count, 1);
assert_eq!(summaries[0].decoded_entry_count, 1); assert_eq!(summaries[0].decoded_entry_count, 1);
assert_eq!(summaries[0].observed_entry_count, 1); assert_eq!(summaries[0].observed_entry_count, 1);

View File

@@ -0,0 +1,173 @@
// file: kb_lib/src/db/queries/instruction_observation.rs
//! Queries for `k_sol_instruction_observations`.
/// Upserts one instruction observation row.
pub async fn query_instruction_observations_upsert(
database: &crate::Database,
dto: &crate::InstructionObservationDto,
) -> Result<i64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query(
r#"
INSERT INTO k_sol_instruction_observations (
observation_key,
transaction_id,
signature,
slot,
block_time,
failed,
instruction_id,
parent_instruction_id,
instruction_index,
inner_instruction_index,
program_id,
decoder_code,
discriminator_hex,
instruction_name,
accounts_json,
data_json,
pool_account,
decoded_event_kind,
decoded_event_id,
observed_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(observation_key) DO UPDATE SET
transaction_id = excluded.transaction_id,
signature = excluded.signature,
slot = excluded.slot,
block_time = excluded.block_time,
failed = excluded.failed,
instruction_id = excluded.instruction_id,
parent_instruction_id = excluded.parent_instruction_id,
instruction_index = excluded.instruction_index,
inner_instruction_index = excluded.inner_instruction_index,
program_id = excluded.program_id,
decoder_code = excluded.decoder_code,
discriminator_hex = excluded.discriminator_hex,
instruction_name = excluded.instruction_name,
accounts_json = excluded.accounts_json,
data_json = excluded.data_json,
pool_account = excluded.pool_account,
decoded_event_kind = excluded.decoded_event_kind,
decoded_event_id = excluded.decoded_event_id,
updated_at = excluded.updated_at
"#,
)
.bind(dto.observation_key.clone())
.bind(dto.transaction_id)
.bind(dto.signature.clone())
.bind(dto.slot)
.bind(dto.block_time)
.bind(if dto.failed { 1_i64 } else { 0_i64 })
.bind(dto.instruction_id)
.bind(dto.parent_instruction_id)
.bind(dto.instruction_index)
.bind(dto.inner_instruction_index)
.bind(dto.program_id.clone())
.bind(dto.decoder_code.clone())
.bind(dto.discriminator_hex.clone())
.bind(dto.instruction_name.clone())
.bind(dto.accounts_json.clone())
.bind(dto.data_json.clone())
.bind(dto.pool_account.clone())
.bind(dto.decoded_event_kind.clone())
.bind(dto.decoded_event_id)
.bind(dto.observed_at.to_rfc3339())
.bind(dto.updated_at.to_rfc3339())
.execute(pool)
.await;
let query_result = match query_result {
Ok(query_result) => query_result,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot upsert k_sol_instruction_observations on sqlite: {}",
error
)));
},
};
return Ok(query_result.last_insert_rowid());
},
}
}
/// Lists instruction observations by optional decoder/discriminator/instruction filters.
pub async fn query_instruction_observations_list_by_filter(
database: &crate::Database,
decoder_code: std::option::Option<&str>,
discriminator_hex: std::option::Option<&str>,
instruction_name: std::option::Option<&str>,
limit: u32,
) -> Result<std::vec::Vec<crate::InstructionObservationDto>, crate::Error> {
if limit == 0 {
return Ok(std::vec::Vec::new());
}
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::InstructionObservationEntity>(
r#"
SELECT
id,
observation_key,
transaction_id,
signature,
slot,
block_time,
failed,
instruction_id,
parent_instruction_id,
instruction_index,
inner_instruction_index,
program_id,
decoder_code,
discriminator_hex,
instruction_name,
accounts_json,
data_json,
pool_account,
decoded_event_kind,
decoded_event_id,
observed_at,
updated_at
FROM k_sol_instruction_observations
WHERE (? IS NULL OR decoder_code = ?)
AND (? IS NULL OR discriminator_hex = ?)
AND (? IS NULL OR instruction_name = ?)
ORDER BY slot DESC, transaction_id DESC, instruction_id ASC
LIMIT ?
"#,
)
.bind(decoder_code.map(str::to_string))
.bind(decoder_code.map(str::to_string))
.bind(discriminator_hex.map(str::to_string))
.bind(discriminator_hex.map(str::to_string))
.bind(instruction_name.map(str::to_string))
.bind(instruction_name.map(str::to_string))
.bind(i64::from(limit))
.fetch_all(pool)
.await;
let entities = match query_result {
Ok(entities) => entities,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list k_sol_instruction_observations on sqlite: {}",
error
)));
},
};
let mut dtos = std::vec::Vec::new();
for entity in entities {
let dto_result = crate::InstructionObservationDto::try_from(entity);
let dto = match dto_result {
Ok(dto) => dto,
Err(error) => return Err(error),
};
dtos.push(dto);
}
return Ok(dtos);
},
}
}

View File

@@ -230,6 +230,26 @@ pub(crate) async fn ensure_schema(database: &crate::Database) -> Result<(), crat
if let Err(error) = result { if let Err(error) = result {
return Err(error); return Err(error);
} }
let result = create_tbl_instruction_observations(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_uix_instruction_observations_key(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_instruction_observations_decoder_discriminator(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_instruction_observations_signature(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_instruction_observations_instruction_name(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_tbl_dex_decode_replay_ledger(pool).await; let result = create_tbl_dex_decode_replay_ledger(pool).await;
if let Err(error) = result { if let Err(error) = result {
return Err(error); return Err(error);
@@ -1423,6 +1443,104 @@ ON k_sol_chain_instructions (program_id)
.await; .await;
} }
/// Creates `k_sol_instruction_observations`.
async fn create_tbl_instruction_observations(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_tbl_instruction_observations",
r#"
CREATE TABLE IF NOT EXISTS k_sol_instruction_observations (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
observation_key TEXT NOT NULL,
transaction_id INTEGER NOT NULL,
signature TEXT NOT NULL,
slot INTEGER NULL,
block_time INTEGER NULL,
failed INTEGER NOT NULL,
instruction_id INTEGER NOT NULL,
parent_instruction_id INTEGER NULL,
instruction_index INTEGER NOT NULL,
inner_instruction_index INTEGER NULL,
program_id TEXT NOT NULL,
decoder_code TEXT NULL,
discriminator_hex TEXT NULL,
instruction_name TEXT NULL,
accounts_json TEXT NOT NULL,
data_json TEXT NULL,
pool_account TEXT NULL,
decoded_event_kind TEXT NULL,
decoded_event_id INTEGER NULL,
observed_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(transaction_id) REFERENCES k_sol_chain_transactions(id),
FOREIGN KEY(instruction_id) REFERENCES k_sol_chain_instructions(id),
FOREIGN KEY(decoded_event_id) REFERENCES k_sol_dex_decoded_events(id)
)
"#,
)
.await;
}
/// Creates unique index on instruction observation key.
async fn create_uix_instruction_observations_key(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_uix_instruction_observations_key",
r#"
CREATE UNIQUE INDEX IF NOT EXISTS uix_instruction_observations_key
ON k_sol_instruction_observations (observation_key)
"#,
)
.await;
}
/// Creates lookup index on decoder/discriminator.
async fn create_idx_instruction_observations_decoder_discriminator(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_instruction_observations_decoder_discriminator",
r#"
CREATE INDEX IF NOT EXISTS idx_instruction_observations_decoder_discriminator
ON k_sol_instruction_observations (decoder_code, discriminator_hex)
"#,
)
.await;
}
/// Creates lookup index on signature.
async fn create_idx_instruction_observations_signature(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_instruction_observations_signature",
r#"
CREATE INDEX IF NOT EXISTS idx_instruction_observations_signature
ON k_sol_instruction_observations (signature)
"#,
)
.await;
}
/// Creates lookup index on instruction name.
async fn create_idx_instruction_observations_instruction_name(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_instruction_observations_instruction_name",
r#"
CREATE INDEX IF NOT EXISTS idx_instruction_observations_instruction_name
ON k_sol_instruction_observations (instruction_name)
"#,
)
.await;
}
/// Creates `k_sol_dex_decoded_events`. /// Creates `k_sol_dex_decoded_events`.
async fn create_tbl_dex_decoded_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> { async fn create_tbl_dex_decoded_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement( return execute_sqlite_schema_statement(

View File

@@ -77,6 +77,10 @@ pub use raydium_clmm::RaydiumClmmSwapLegacyDecoded;
pub use raydium_clmm::RaydiumClmmSwapV2Decoded; pub use raydium_clmm::RaydiumClmmSwapV2Decoded;
pub use raydium_clmm::decode_raydium_clmm_instruction; pub use raydium_clmm::decode_raydium_clmm_instruction;
pub use raydium_cpmm::RaydiumCpmmDecodedEvent; pub use raydium_cpmm::RaydiumCpmmDecodedEvent;
pub use raydium_cpmm::RaydiumCpmmLpChangeEventDecoded;
pub use raydium_cpmm::RaydiumCpmmSwapDecoded; pub use raydium_cpmm::RaydiumCpmmSwapDecoded;
pub use raydium_cpmm::RaydiumCpmmSwapEventDecoded;
pub use raydium_cpmm::RaydiumCpmmSwapMode; pub use raydium_cpmm::RaydiumCpmmSwapMode;
pub use raydium_cpmm::classify_raydium_cpmm_instruction_data;
pub use raydium_cpmm::decode_raydium_cpmm_instruction; pub use raydium_cpmm::decode_raydium_cpmm_instruction;
pub use raydium_cpmm::decode_raydium_cpmm_program_data_event;

View File

@@ -3009,7 +3009,7 @@ fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSid
mod tests { mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto { fn make_create_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new( let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-damm-v1-create-1".to_string(), "sig-meteora_damm_v1-create-1".to_string(),
Some(890001), Some(890001),
Some(1779500001), Some(1779500001),
Some("helius_primary_http".to_string()), Some("helius_primary_http".to_string()),
@@ -3042,7 +3042,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()), Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
Some("meteora-damm-v1".to_string()), Some("meteora_damm_v1".to_string()),
Some(1), Some(1),
serde_json::json!([ serde_json::json!([
"DammV1Pool111", "DammV1Pool111",
@@ -3074,7 +3074,7 @@ mod tests {
fn make_swap_transaction() -> crate::ChainTransactionDto { fn make_swap_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new( let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-damm-v1-swap-1".to_string(), "sig-meteora_damm_v1-swap-1".to_string(),
Some(890002), Some(890002),
Some(1779500002), Some(1779500002),
Some("helius_primary_http".to_string()), Some("helius_primary_http".to_string()),
@@ -3107,7 +3107,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()), Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
Some("meteora-damm-v1".to_string()), Some("meteora_damm_v1".to_string()),
Some(1), Some(1),
serde_json::json!(["DammV1SwapPool111", "DammV1SwapTokenA111", crate::WSOL_MINT_ID]) serde_json::json!(["DammV1SwapPool111", "DammV1SwapTokenA111", crate::WSOL_MINT_ID])
.to_string(), .to_string(),
@@ -3141,7 +3141,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()), Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
Some("meteora-damm-v1".to_string()), Some("meteora_damm_v1".to_string()),
Some(1), Some(1),
accounts.to_string(), accounts.to_string(),
Some(format!("\"{}\"", bs58::encode(data).into_string())), Some(format!("\"{}\"", bs58::encode(data).into_string())),

View File

@@ -758,7 +758,7 @@ fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
mod tests { mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto { fn make_create_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new( let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-damm-v2-create-1".to_string(), "sig-meteora_damm_v2-create-1".to_string(),
Some(889001), Some(889001),
Some(1779400001), Some(1779400001),
Some("helius_primary_http".to_string()), Some("helius_primary_http".to_string()),
@@ -791,7 +791,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()), Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
Some("meteora-damm-v2".to_string()), Some("meteora_damm_v2".to_string()),
Some(1), Some(1),
serde_json::json!([ serde_json::json!([
"DammV2Pool111", "DammV2Pool111",
@@ -823,7 +823,7 @@ mod tests {
fn make_swap_transaction() -> crate::ChainTransactionDto { fn make_swap_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new( let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-damm-v2-swap-1".to_string(), "sig-meteora_damm_v2-swap-1".to_string(),
Some(889002), Some(889002),
Some(1779400002), Some(1779400002),
Some("helius_primary_http".to_string()), Some("helius_primary_http".to_string()),
@@ -856,7 +856,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()), Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
Some("meteora-damm-v2".to_string()), Some("meteora_damm_v2".to_string()),
Some(1), Some(1),
serde_json::json!(["DammV2SwapPool111", "DammV2SwapTokenA111", crate::WSOL_MINT_ID]) serde_json::json!(["DammV2SwapPool111", "DammV2SwapTokenA111", crate::WSOL_MINT_ID])
.to_string(), .to_string(),

View File

@@ -727,7 +727,7 @@ fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
mod tests { mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto { fn make_create_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new( let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dbc-create-1".to_string(), "sig-meteora_dbc-create-1".to_string(),
Some(888001), Some(888001),
Some(1779300001), Some(1779300001),
Some("helius_primary_http".to_string()), Some("helius_primary_http".to_string()),
@@ -760,7 +760,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()), Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
Some("meteora-dbc".to_string()), Some("meteora_dbc".to_string()),
Some(1), Some(1),
serde_json::json!([ serde_json::json!([
"DbcPool111", "DbcPool111",
@@ -791,7 +791,7 @@ mod tests {
fn make_swap_transaction() -> crate::ChainTransactionDto { fn make_swap_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new( let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dbc-swap-1".to_string(), "sig-meteora_dbc-swap-1".to_string(),
Some(888002), Some(888002),
Some(1779300002), Some(1779300002),
Some("helius_primary_http".to_string()), Some("helius_primary_http".to_string()),
@@ -824,7 +824,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()), Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
Some("meteora-dbc".to_string()), Some("meteora_dbc".to_string()),
Some(1), Some(1),
serde_json::json!(["DbcPoolSwap111", "DbcSwapTokenA111", crate::WSOL_MINT_ID]) serde_json::json!(["DbcPoolSwap111", "DbcSwapTokenA111", crate::WSOL_MINT_ID])
.to_string(), .to_string(),

View File

@@ -2646,7 +2646,7 @@ fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
mod tests { mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto { fn make_create_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new( let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dlmm-create-1".to_string(), "sig-meteora_dlmm-create-1".to_string(),
Some(888101), Some(888101),
Some(1779400001), Some(1779400001),
Some("helius_primary_http".to_string()), Some("helius_primary_http".to_string()),
@@ -2679,7 +2679,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()), Some("meteora_dlmm".to_string()),
Some(1), Some(1),
serde_json::json!([ serde_json::json!([
"DlmmPair111", "DlmmPair111",
@@ -2708,7 +2708,7 @@ mod tests {
fn make_swap_transaction() -> crate::ChainTransactionDto { fn make_swap_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new( let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dlmm-swap-1".to_string(), "sig-meteora_dlmm-swap-1".to_string(),
Some(888102), Some(888102),
Some(1779400002), Some(1779400002),
Some("helius_primary_http".to_string()), Some("helius_primary_http".to_string()),
@@ -2741,7 +2741,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()), Some("meteora_dlmm".to_string()),
Some(1), Some(1),
serde_json::json!(["DlmmPairSwap111", "DlmmSwapTokenX111", crate::WSOL_MINT_ID]) serde_json::json!(["DlmmPairSwap111", "DlmmSwapTokenX111", crate::WSOL_MINT_ID])
.to_string(), .to_string(),
@@ -2906,7 +2906,7 @@ mod tests {
fn meteora_dlmm_inner_swap2_instruction_is_not_skipped() { fn meteora_dlmm_inner_swap2_instruction_is_not_skipped() {
let decoder = crate::MeteoraDlmmDecoder::new(); let decoder = crate::MeteoraDlmmDecoder::new();
let mut transaction = crate::ChainTransactionDto::new( let mut transaction = crate::ChainTransactionDto::new(
"sig-meteora-dlmm-inner-swap2".to_string(), "sig-meteora_dlmm-inner-swap2".to_string(),
Some(888103), Some(888103),
Some(1779400003), Some(1779400003),
Some("helius_primary_http".to_string()), Some("helius_primary_http".to_string()),
@@ -2933,7 +2933,7 @@ mod tests {
3, 3,
Some(14), Some(14),
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()), Some("meteora_dlmm".to_string()),
Some(2), Some(2),
serde_json::json!([ serde_json::json!([
"LbPair111", "LbPair111",
@@ -3030,7 +3030,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()), Some("meteora_dlmm".to_string()),
Some(1), Some(1),
serde_json::json!([ serde_json::json!([
"Position111", "Position111",
@@ -3083,7 +3083,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()), Some("meteora_dlmm".to_string()),
Some(1), Some(1),
serde_json::json!([ serde_json::json!([
"DlmmPairFee111", "DlmmPairFee111",
@@ -3132,7 +3132,7 @@ mod tests {
0, 0,
None, None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()), Some("meteora_dlmm".to_string()),
Some(1), Some(1),
serde_json::json!([ serde_json::json!([
"Position111", "Position111",

View File

@@ -102,7 +102,7 @@ impl OpenBookV2Decoder {
Some(registry_match) => registry_match, Some(registry_match) => registry_match,
None => continue, None => continue,
}; };
if registry_match.decoder_code.as_str() != "openbook-v2" { if registry_match.decoder_code.as_str() != "openbook_v2" {
continue; continue;
} }
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str()); let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());

View File

@@ -532,7 +532,7 @@ mod tests {
0, 0,
None, None,
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()), Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
Some("orca-whirlpools".to_string()), Some("orca_whirlpools".to_string()),
Some(1), Some(1),
serde_json::json!([ serde_json::json!([
"OrcaPool111", "OrcaPool111",
@@ -599,7 +599,7 @@ mod tests {
0, 0,
None, None,
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()), Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
Some("orca-whirlpools".to_string()), Some("orca_whirlpools".to_string()),
Some(1), Some(1),
serde_json::json!(["OrcaSwapPool111", "OrcaSwapTokenA111", crate::WSOL_MINT_ID]) serde_json::json!(["OrcaSwapPool111", "OrcaSwapTokenA111", crate::WSOL_MINT_ID])
.to_string(), .to_string(),

View File

@@ -101,7 +101,7 @@ impl PhoenixV1Decoder {
Some(registry_match) => registry_match, Some(registry_match) => registry_match,
None => continue, None => continue,
}; };
if registry_match.decoder_code.as_str() != "phoenix-v1" { if registry_match.decoder_code.as_str() != "phoenix_v1" {
continue; continue;
} }
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str()); let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());

View File

@@ -1080,7 +1080,7 @@ mod tests {
0, 0,
None, None,
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()), Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
Some("raydium-amm-v4".to_string()), Some("raydium_amm_v4".to_string()),
Some(1), Some(1),
serde_json::json!([ serde_json::json!([
"Account0", "Account0",
@@ -1215,7 +1215,7 @@ mod tests {
4, 4,
Some(0), Some(0),
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()), Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
Some("raydium-amm-v4".to_string()), Some("raydium_amm_v4".to_string()),
Some(2), Some(2),
serde_json::json!([ serde_json::json!([
crate::SPL_TOKEN_PROGRAM_ID, crate::SPL_TOKEN_PROGRAM_ID,

File diff suppressed because it is too large Load Diff

View File

@@ -1012,11 +1012,11 @@ impl DexDecodeService {
"raydium_cpmm", "raydium_cpmm",
crate::RAYDIUM_CPMM_PROGRAM_ID.to_string(), crate::RAYDIUM_CPMM_PROGRAM_ID.to_string(),
event_kind.as_str(), event_kind.as_str(),
Some(decoded_event.pool_account().to_string()), decoded_event.pool_account().map(|value| return value.to_string()),
None,
Some(decoded_event.base_mint().to_string()),
Some(decoded_event.quote_mint().to_string()),
None, None,
decoded_event.base_mint().map(|value| return value.to_string()),
decoded_event.quote_mint().map(|value| return value.to_string()),
decoded_event.lp_mint().map(|value| return value.to_string()),
payload_value, payload_value,
) )
.await; .await;
@@ -1174,6 +1174,7 @@ impl DexDecodeService {
instructions: &[crate::ChainInstructionDto], instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> { ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let mut persisted = std::vec::Vec::new(); let mut persisted = std::vec::Vec::new();
let mut program_data_events = collect_raydium_cpmm_program_data_events(transaction);
for instruction in instructions { for instruction in instructions {
let program_id = match instruction.program_id.as_ref() { let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id, Some(program_id) => program_id,
@@ -1186,6 +1187,8 @@ impl DexDecodeService {
Some(data_json) => data_json, Some(data_json) => data_json,
None => continue, None => continue,
}; };
let instruction_kind =
crate::classify_raydium_cpmm_instruction_data(data_json.as_str());
let decoded_events = crate::decode_raydium_cpmm_instruction( let decoded_events = crate::decode_raydium_cpmm_instruction(
instruction.accounts_json.as_str(), instruction.accounts_json.as_str(),
data_json.as_str(), data_json.as_str(),
@@ -1199,6 +1202,18 @@ impl DexDecodeService {
}; };
persisted.push(persisted_event); persisted.push(persisted_event);
} }
let program_data_persist_result = persist_matching_raydium_cpmm_program_data_event(
self,
transaction,
instruction,
instruction_kind,
&mut program_data_events,
&mut persisted,
)
.await;
if let Err(error) = program_data_persist_result {
return Err(error);
}
} }
return Ok(persisted); return Ok(persisted);
} }
@@ -1808,6 +1823,11 @@ struct RaydiumMappedNonTradeInstructionSpec {
enum RaydiumMappedNonTradeAmountLayout { enum RaydiumMappedNonTradeAmountLayout {
None, None,
ClmmLiquidityV2, ClmmLiquidityV2,
CpmmAmmConfig,
CpmmDeposit,
CpmmFeePair,
CpmmInitialize,
CpmmPoolStatus,
CpmmWithdraw, CpmmWithdraw,
} }
@@ -1894,26 +1914,81 @@ fn raydium_mapped_non_trade_instruction_spec(
} }
} }
if protocol_name == "raydium_cpmm" { if protocol_name == "raydium_cpmm" {
if discriminator_hex == "1416567bc61cdb84" && account_count >= 14 { if discriminator_hex == "9c5420764587467b" && account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec { return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_creator_fee", instruction_name: "close_permission_pda",
event_kind: "raydium_cpmm.collect_creator_fee", event_kind: "raydium_cpmm.close_permission_pda",
pool_account_index: Some(3), pool_account_index: None,
token_a_mint_index: None, token_a_mint_index: None,
token_b_mint_index: None, token_b_mint_index: None,
lp_mint_index: None, lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None, amount_layout: RaydiumMappedNonTradeAmountLayout::None,
}); });
} }
if discriminator_hex == "b712469c946da122" && account_count >= 14 { if discriminator_hex == "1416567bc61cdb84" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec { return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw", instruction_name: "collect_creator_fee",
event_kind: "raydium_cpmm.withdraw", event_kind: "raydium_cpmm.collect_creator_fee",
pool_account_index: Some(3), pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "a78a4e95dfc2067e" && account_count >= 12 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_fund_fee",
event_kind: "raydium_cpmm.collect_fund_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
});
}
if discriminator_hex == "8888fcddc2427e59" && account_count >= 12 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_protocol_fee",
event_kind: "raydium_cpmm.collect_protocol_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
});
}
if discriminator_hex == "8934edd4d7756c68" && account_count >= 3 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_amm_config",
event_kind: "raydium_cpmm.create_amm_config",
pool_account_index: None,
token_a_mint_index: None, token_a_mint_index: None,
token_b_mint_index: None, token_b_mint_index: None,
lp_mint_index: None, lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw, amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
});
}
if discriminator_hex == "878802d889a9b5ca" && account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_permission_pda",
event_kind: "raydium_cpmm.create_permission_pda",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "f223c68952e1f2b6" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "deposit",
event_kind: "raydium_cpmm.deposit",
pool_account_index: Some(2),
token_a_mint_index: Some(10),
token_b_mint_index: Some(11),
lp_mint_index: Some(12),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmDeposit,
}); });
} }
if discriminator_hex == "afaf6d1f0d989bed" && account_count >= 20 { if discriminator_hex == "afaf6d1f0d989bed" && account_count >= 20 {
@@ -1923,8 +1998,52 @@ fn raydium_mapped_non_trade_instruction_spec(
pool_account_index: Some(3), pool_account_index: Some(3),
token_a_mint_index: Some(4), token_a_mint_index: Some(4),
token_b_mint_index: Some(5), token_b_mint_index: Some(5),
lp_mint_index: Some(13), lp_mint_index: Some(6),
amount_layout: RaydiumMappedNonTradeAmountLayout::None, amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
});
}
if discriminator_hex == "3f37fe4131b25979" && account_count >= 21 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "initialize_with_permission",
event_kind: "raydium_cpmm.initialize_with_permission",
pool_account_index: Some(4),
token_a_mint_index: Some(5),
token_b_mint_index: Some(6),
lp_mint_index: Some(7),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
});
}
if discriminator_hex == "313cae889a1c74c8" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_amm_config",
event_kind: "raydium_cpmm.update_amm_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
});
}
if discriminator_hex == "82576c062ee0757b" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_pool_status",
event_kind: "raydium_cpmm.update_pool_status",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus,
});
}
if discriminator_hex == "b712469c946da122" && account_count >= 14 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw",
event_kind: "raydium_cpmm.withdraw",
pool_account_index: Some(2),
token_a_mint_index: Some(10),
token_b_mint_index: Some(11),
lp_mint_index: Some(12),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw,
}); });
} }
} }
@@ -1979,6 +2098,15 @@ fn enrich_raydium_mapped_non_trade_payload(
"instructionName".to_string(), "instructionName".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()), serde_json::Value::String(mapped_spec.instruction_name.to_string()),
); );
object.insert(
"upstreamInstructionName".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert("localSpecializedDecoder".to_string(), serde_json::Value::Bool(true));
object.insert(
"adminAction".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(true)); object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(true));
object.insert( object.insert(
"auditReason".to_string(), "auditReason".to_string(),
@@ -2032,6 +2160,94 @@ fn insert_raydium_mapped_amounts(
); );
} }
}, },
RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig => {
if let Some(param) = read_u8_from_bytes(data, 8) {
object.insert(
"configParam".to_string(),
serde_json::Value::Number(serde_json::Number::from(param as u64)),
);
}
if let Some(value) = read_u64_le_from_bytes(data, 9) {
object.insert(
"configValue".to_string(),
serde_json::Value::String(value.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmDeposit => {
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
object.insert(
"lpAmountRaw".to_string(),
serde_json::Value::String(lp_amount.to_string()),
);
object.insert(
"liquidity".to_string(),
serde_json::Value::String(lp_amount.to_string()),
);
}
if let Some(amount_0) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 24) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmFeePair => {
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
object.insert(
"amount0RequestedRaw".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
object.insert(
"amount1RequestedRaw".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmInitialize => {
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
if let Some(open_time) = read_u64_le_from_bytes(data, 24) {
object.insert(
"openTime".to_string(),
serde_json::Value::String(open_time.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus => {
if let Some(status) = read_u8_from_bytes(data, 8) {
object.insert(
"poolStatus".to_string(),
serde_json::Value::Number(serde_json::Number::from(status as u64)),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmWithdraw => { RaydiumMappedNonTradeAmountLayout::CpmmWithdraw => {
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) { if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
object.insert( object.insert(
@@ -2073,6 +2289,13 @@ fn instruction_data_bytes_from_base58(
} }
} }
fn read_u8_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u8> {
if data.len() < offset + 1 {
return None;
}
return Some(data[offset]);
}
fn read_u64_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u64> { fn read_u64_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u64> {
if data.len() < offset + 8 { if data.len() < offset + 8 {
return None; return None;
@@ -2453,6 +2676,150 @@ fn append_persisted_events(
} }
} }
#[derive(Clone, Debug)]
struct RaydiumCpmmProgramDataEventCandidate {
decoded_event: crate::RaydiumCpmmDecodedEvent,
consumed: bool,
}
fn collect_raydium_cpmm_program_data_events(
transaction: &crate::ChainTransactionDto,
) -> std::vec::Vec<RaydiumCpmmProgramDataEventCandidate> {
let logs = extract_transaction_log_messages(transaction.transaction_json.as_str());
let mut events = std::vec::Vec::new();
let mut cpmm_stack_depth = 0_u32;
for log_message in logs {
if is_program_invoke_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
cpmm_stack_depth += 1;
continue;
}
if is_program_success_or_failed_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
cpmm_stack_depth = cpmm_stack_depth.saturating_sub(1);
continue;
}
if cpmm_stack_depth == 0 {
continue;
}
let data_base64 = match log_message.strip_prefix("Program data: ") {
Some(data_base64) => data_base64.trim(),
None => continue,
};
if data_base64.is_empty() {
continue;
}
let decoded_event = crate::decode_raydium_cpmm_program_data_event(data_base64);
if let Some(decoded_event) = decoded_event {
events.push(RaydiumCpmmProgramDataEventCandidate { decoded_event, consumed: false });
}
}
return events;
}
async fn persist_matching_raydium_cpmm_program_data_event(
service: &DexDecodeService,
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
instruction_kind: std::option::Option<&str>,
program_data_events: &mut [RaydiumCpmmProgramDataEventCandidate],
persisted: &mut std::vec::Vec<crate::DexDecodedEventDto>,
) -> Result<(), crate::Error> {
let expected_event_kind = match instruction_kind {
Some("swap_base_input") => Some("swap_event"),
Some("swap_base_output") => Some("swap_event"),
Some("deposit") => Some("lp_change_event"),
Some("withdraw") => Some("lp_change_event"),
_ => None,
};
let expected_event_kind = match expected_event_kind {
Some(expected_event_kind) => expected_event_kind,
None => return Ok(()),
};
let mut index = 0_usize;
while index < program_data_events.len() {
if program_data_events[index].consumed {
index += 1;
continue;
}
let event_matches = match (&program_data_events[index].decoded_event, expected_event_kind) {
(crate::RaydiumCpmmDecodedEvent::SwapEvent(_), "swap_event") => true,
(crate::RaydiumCpmmDecodedEvent::LpChangeEvent(_), "lp_change_event") => true,
_ => false,
};
if !event_matches {
index += 1;
continue;
}
program_data_events[index].consumed = true;
let persist_result = service
.persist_raydium_cpmm_event(
transaction,
instruction,
&program_data_events[index].decoded_event,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
return Ok(());
}
return Ok(());
}
fn extract_transaction_log_messages(transaction_json: &str) -> std::vec::Vec<std::string::String> {
let value_result = serde_json::from_str::<serde_json::Value>(transaction_json);
let value = match value_result {
Ok(value) => value,
Err(_) => return std::vec::Vec::new(),
};
let meta = match value.get("meta") {
Some(meta) => meta,
None => return std::vec::Vec::new(),
};
let logs = match meta.get("logMessages") {
Some(logs) => logs,
None => return std::vec::Vec::new(),
};
let logs = match logs.as_array() {
Some(logs) => logs,
None => return std::vec::Vec::new(),
};
let mut output = std::vec::Vec::new();
for log in logs {
if let Some(log) = log.as_str() {
output.push(log.to_string());
}
}
return output;
}
fn is_program_invoke_log(log_message: &str, program_id: &str) -> bool {
if !log_message.starts_with("Program ") {
return false;
}
if !log_message.contains(" invoke [") {
return false;
}
return log_message.contains(program_id);
}
fn is_program_success_or_failed_log(log_message: &str, program_id: &str) -> bool {
if !log_message.starts_with("Program ") {
return false;
}
if !log_message.contains(program_id) {
return false;
}
if log_message.ends_with(" success") {
return true;
}
if log_message.contains(" failed: ") {
return true;
}
return false;
}
fn decoded_instruction_ids_from_persisted_events( fn decoded_instruction_ids_from_persisted_events(
persisted: &[crate::DexDecodedEventDto], persisted: &[crate::DexDecodedEventDto],
) -> std::collections::HashSet<i64> { ) -> std::collections::HashSet<i64> {
@@ -2603,7 +2970,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID, "programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
"program": "raydium-amm-v4", "program": "raydium_amm_v4",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"Account0", "Account0",
@@ -2887,7 +3254,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::METEORA_DBC_PROGRAM_ID, "programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora-dbc", "program": "meteora_dbc",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"DbcPoolDecode111", "DbcPoolDecode111",
@@ -2962,7 +3329,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID, "programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
"program": "meteora-damm-v2", "program": "meteora_damm_v2",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"DammV2DecodePool111", "DammV2DecodePool111",
@@ -3039,7 +3406,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID, "programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
"program": "meteora-damm-v1", "program": "meteora_damm_v1",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"DammV1DecodePool111", "DammV1DecodePool111",
@@ -3116,7 +3483,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID, "programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
"program": "orca-whirlpools", "program": "orca_whirlpools",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"OrcaDecodePool111", "OrcaDecodePool111",
@@ -3488,36 +3855,32 @@ mod tests {
#[test] #[test]
fn maps_observed_raydium_cpmm_non_swap_discriminators() { fn maps_observed_raydium_cpmm_non_swap_discriminators() {
let collect_creator_fee = super::raydium_mapped_non_trade_instruction_spec( let expected = [
"raydium_cpmm", ("9c5420764587467b", 4_usize, "raydium_cpmm.close_permission_pda"),
Some("1416567bc61cdb84"), ("1416567bc61cdb84", 13_usize, "raydium_cpmm.collect_creator_fee"),
14, ("a78a4e95dfc2067e", 12_usize, "raydium_cpmm.collect_fund_fee"),
); ("8888fcddc2427e59", 12_usize, "raydium_cpmm.collect_protocol_fee"),
let collect_creator_fee = match collect_creator_fee { ("8934edd4d7756c68", 3_usize, "raydium_cpmm.create_amm_config"),
Some(collect_creator_fee) => collect_creator_fee, ("878802d889a9b5ca", 4_usize, "raydium_cpmm.create_permission_pda"),
None => panic!("collect_creator_fee discriminator must be mapped"), ("f223c68952e1f2b6", 13_usize, "raydium_cpmm.deposit"),
}; ("afaf6d1f0d989bed", 20_usize, "raydium_cpmm.initialize"),
assert_eq!(collect_creator_fee.event_kind, "raydium_cpmm.collect_creator_fee"); ("3f37fe4131b25979", 21_usize, "raydium_cpmm.initialize_with_permission"),
let withdraw = super::raydium_mapped_non_trade_instruction_spec( ("313cae889a1c74c8", 2_usize, "raydium_cpmm.update_amm_config"),
"raydium_cpmm", ("82576c062ee0757b", 2_usize, "raydium_cpmm.update_pool_status"),
Some("b712469c946da122"), ("b712469c946da122", 14_usize, "raydium_cpmm.withdraw"),
14, ];
); for (discriminator, account_count, event_kind) in expected {
let withdraw = match withdraw { let mapped = super::raydium_mapped_non_trade_instruction_spec(
Some(withdraw) => withdraw, "raydium_cpmm",
None => panic!("withdraw discriminator must be mapped"), Some(discriminator),
}; account_count,
assert_eq!(withdraw.event_kind, "raydium_cpmm.withdraw"); );
let initialize = super::raydium_mapped_non_trade_instruction_spec( let mapped = match mapped {
"raydium_cpmm", Some(mapped) => mapped,
Some("afaf6d1f0d989bed"), None => panic!("raydium cpmm discriminator must be mapped: {}", discriminator),
20, };
); assert_eq!(mapped.event_kind, event_kind);
let initialize = match initialize { }
Some(initialize) => initialize,
None => panic!("initialize discriminator must be mapped"),
};
assert_eq!(initialize.event_kind, "raydium_cpmm.initialize");
} }
#[test] #[test]
@@ -3573,7 +3936,7 @@ mod tests {
let registry_match = crate::UpstreamRegistryEntryDto { let registry_match = crate::UpstreamRegistryEntryDto {
source_repo: Some("sevenlabs-hq/carbon".to_string()), source_repo: Some("sevenlabs-hq/carbon".to_string()),
source_path: Some("decoders/example.rs".to_string()), source_path: Some("decoders/example.rs".to_string()),
decoder_code: "meteora-damm-v2".to_string(), decoder_code: "meteora_damm_v2".to_string(),
program_id: Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()), program_id: Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
program_family: "meteora".to_string(), program_family: "meteora".to_string(),
surface_kind: "amm".to_string(), surface_kind: "amm".to_string(),

View File

@@ -1036,7 +1036,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID, "programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
"program": "raydium-amm-v4", "program": "raydium_amm_v4",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"Account0", "Account0",
@@ -1462,7 +1462,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::METEORA_DBC_PROGRAM_ID, "programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora-dbc", "program": "meteora_dbc",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"DbcDetectPool111", "DbcDetectPool111",
@@ -1581,7 +1581,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID, "programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
"program": "meteora-damm-v2", "program": "meteora_damm_v2",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"DammV2DetectPool111", "DammV2DetectPool111",
@@ -1701,7 +1701,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID, "programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
"program": "meteora-damm-v1", "program": "meteora_damm_v1",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"DammV1DetectPool111", "DammV1DetectPool111",
@@ -1821,7 +1821,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID, "programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
"program": "orca-whirlpools", "program": "orca_whirlpools",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"OrcaDetectPool111", "OrcaDetectPool111",

View File

@@ -320,6 +320,9 @@ pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".deposit") { if event_kind.contains(".deposit") {
return true; return true;
} }
if event_kind.contains(".lp_change_event") {
return true;
}
if event_kind.contains(".withdraw") { if event_kind.contains(".withdraw") {
return true; return true;
} }
@@ -518,6 +521,9 @@ pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
/// Returns true for pool creation or initialization events. /// Returns true for pool creation or initialization events.
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool { pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
if event_kind.contains("amm_config") {
return false;
}
if event_kind.contains(".initialize_position") { if event_kind.contains(".initialize_position") {
return false; return false;
} }
@@ -552,6 +558,9 @@ pub fn is_dex_pair_creation_event_kind(event_kind: &str) -> bool {
/// Returns true for admin, configuration or permission changes. /// Returns true for admin, configuration or permission changes.
pub fn is_dex_admin_event_kind(event_kind: &str) -> bool { pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".initialize_with_permission") {
return false;
}
if event_kind.contains(".lock_liquidity") { if event_kind.contains(".lock_liquidity") {
return true; return true;
} }
@@ -1152,6 +1161,15 @@ mod tests {
); );
} }
#[test]
fn classifies_initialize_with_permission_as_lifecycle_only() {
let event_kind = "raydium_cpmm.initialize_with_permission";
assert!(super::is_dex_pool_lifecycle_event_kind(event_kind));
assert!(!super::is_dex_admin_event_kind(event_kind));
assert_eq!(super::classify_dex_event_category_code(event_kind), "pool_lifecycle");
assert_eq!(super::classify_dex_event_lifecycle_kind_code(event_kind), "pool_creation");
}
#[test] #[test]
fn classifies_audit_suffix_events_as_informational() { fn classifies_audit_suffix_events_as_informational() {
assert!(super::is_dex_informational_event_kind("openbook_v2.settle_funds_audit")); assert!(super::is_dex_informational_event_kind("openbook_v2.settle_funds_audit"));

View File

@@ -38,15 +38,10 @@ impl DexEventCoverageService {
}; };
} }
/// Synchronizes static upstream registry entries into SQLite coverage rows. async fn upsert_upstream_registry_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, &self,
decoder_code: std::option::Option<std::string::String>, decoder_code: std::option::Option<std::string::String>,
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> { ) -> Result<(usize, usize), crate::Error> {
let request = crate::UpstreamRegistrySearchRequestDto { let request = crate::UpstreamRegistrySearchRequestDto {
decoder_code: decoder_code.clone(), decoder_code: decoder_code.clone(),
program_id: None, program_id: None,
@@ -70,6 +65,30 @@ impl DexEventCoverageService {
Err(error) => return Err(error), Err(error) => return Err(error),
} }
} }
return Ok((search_result.entries.len(), upserted_entry_count));
}
async fn ensure_upstream_registry_rows_if_needed(
&self,
decoder_code: std::option::Option<std::string::String>,
) -> Result<(usize, usize), crate::Error> {
return self.upsert_upstream_registry_rows(decoder_code).await;
}
/// 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 sync_counts = self.upsert_upstream_registry_rows(decoder_code.clone()).await;
let (upstream_entry_count, upserted_entry_count) = match sync_counts {
Ok(sync_counts) => sync_counts,
Err(error) => return Err(error),
};
let refreshed_entry_count = match &decoder_code { let refreshed_entry_count = match &decoder_code {
Some(decoder_code) => { Some(decoder_code) => {
let refresh_result = let refresh_result =
@@ -103,7 +122,7 @@ impl DexEventCoverageService {
}; };
return Ok(crate::DexEventCoverageSyncResult { return Ok(crate::DexEventCoverageSyncResult {
decoder_code, decoder_code,
upstream_entry_count: search_result.entries.len(), upstream_entry_count,
upserted_entry_count, upserted_entry_count,
refreshed_entry_count, refreshed_entry_count,
summaries, summaries,
@@ -115,6 +134,11 @@ impl DexEventCoverageService {
&self, &self,
decoder_code: std::option::Option<std::string::String>, decoder_code: std::option::Option<std::string::String>,
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> { ) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
let sync_counts = self.ensure_upstream_registry_rows_if_needed(decoder_code.clone()).await;
let (upstream_entry_count, upserted_entry_count) = match sync_counts {
Ok(sync_counts) => sync_counts,
Err(error) => return Err(error),
};
let refreshed_entry_count = match &decoder_code { let refreshed_entry_count = match &decoder_code {
Some(decoder_code) => { Some(decoder_code) => {
let refresh_result = let refresh_result =
@@ -148,8 +172,8 @@ impl DexEventCoverageService {
}; };
return Ok(crate::DexEventCoverageSyncResult { return Ok(crate::DexEventCoverageSyncResult {
decoder_code, decoder_code,
upstream_entry_count: 0, upstream_entry_count,
upserted_entry_count: 0, upserted_entry_count,
refreshed_entry_count, refreshed_entry_count,
summaries, summaries,
}); });
@@ -160,8 +184,12 @@ fn build_coverage_entry_from_upstream(
entry: &crate::UpstreamRegistryEntryDto, entry: &crate::UpstreamRegistryEntryDto,
) -> crate::DexEventCoverageEntryDto { ) -> crate::DexEventCoverageEntryDto {
let event_family = infer_event_family(entry.entry_name.as_str(), entry.entry_kind.as_str()); let event_family = infer_event_family(entry.entry_name.as_str(), entry.entry_kind.as_str());
let expected_db_target = let expected_db_target = infer_expected_db_target_for_entry(
infer_expected_db_target(event_family.as_deref(), entry.entry_kind.as_str()); entry.decoder_code.as_str(),
entry.entry_name.as_str(),
event_family.as_deref(),
entry.entry_kind.as_str(),
);
let local_event_kind = let local_event_kind =
known_local_event_kind(entry.decoder_code.as_str(), entry.entry_name.as_str()); known_local_event_kind(entry.decoder_code.as_str(), entry.entry_name.as_str());
let mut coverage_entry = crate::DexEventCoverageEntryDto::from_upstream_registry_entry( let mut coverage_entry = crate::DexEventCoverageEntryDto::from_upstream_registry_entry(
@@ -177,6 +205,18 @@ fn build_coverage_entry_from_upstream(
return coverage_entry; return coverage_entry;
} }
fn infer_expected_db_target_for_entry(
decoder_code: &str,
entry_name: &str,
event_family: std::option::Option<&str>,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if decoder_code == "raydium_cpmm" && entry_name == "swap_event" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
return infer_expected_db_target(event_family, entry_kind);
}
fn infer_expected_db_target( fn infer_expected_db_target(
event_family: std::option::Option<&str>, event_family: std::option::Option<&str>,
entry_kind: &str, entry_kind: &str,
@@ -195,6 +235,7 @@ fn infer_expected_db_target(
let target = match family { let target = match family {
"swap" => crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS, "swap" => crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS,
"pool_create" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS, "pool_create" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
"liquidity" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
"liquidity_add" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS, "liquidity_add" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
"liquidity_remove" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS, "liquidity_remove" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
"position_open" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS, "position_open" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
@@ -235,6 +276,9 @@ fn infer_event_family(
return None; return None;
} }
let normalized = entry_name.to_ascii_lowercase(); let normalized = entry_name.to_ascii_lowercase();
if normalized == "lp_change_event" {
return Some("liquidity".to_string());
}
if contains_any(normalized.as_str(), &["swap", "buy", "sell", "trade"]) { if contains_any(normalized.as_str(), &["swap", "buy", "sell", "trade"]) {
return Some("swap".to_string()); return Some("swap".to_string());
} }
@@ -360,29 +404,58 @@ fn known_local_event_kind(
entry_name: &str, entry_name: &str,
) -> std::option::Option<std::string::String> { ) -> std::option::Option<std::string::String> {
match (decoder_code, entry_name) { match (decoder_code, entry_name) {
("raydium-cpmm", "swap_base_input") => { ("raydium_cpmm", "swap_base_input") => {
return Some("raydium_cpmm.swap_base_input".to_string()); return Some("raydium_cpmm.swap_base_input".to_string());
}, },
("raydium-cpmm", "swap_base_output") => { ("raydium_cpmm", "swap_base_output") => {
return Some("raydium_cpmm.swap_base_output".to_string()); return Some("raydium_cpmm.swap_base_output".to_string());
}, },
("raydium-cpmm", "collect_creator_fee") => { ("raydium_cpmm", "close_permission_pda") => {
return Some("raydium_cpmm.close_permission_pda".to_string());
},
("raydium_cpmm", "collect_creator_fee") => {
return Some("raydium_cpmm.collect_creator_fee".to_string()); return Some("raydium_cpmm.collect_creator_fee".to_string());
}, },
("raydium-cpmm", "withdraw") => return Some("raydium_cpmm.withdraw".to_string()), ("raydium_cpmm", "collect_fund_fee") => {
("raydium-cpmm", "initialize") => return Some("raydium_cpmm.initialize".to_string()), return Some("raydium_cpmm.collect_fund_fee".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_cpmm", "collect_protocol_fee") => {
("raydium-clmm", "increase_liquidity_v2") => { return Some("raydium_cpmm.collect_protocol_fee".to_string());
},
("raydium_cpmm", "create_amm_config") => {
return Some("raydium_cpmm.create_amm_config".to_string());
},
("raydium_cpmm", "create_permission_pda") => {
return Some("raydium_cpmm.create_permission_pda".to_string());
},
("raydium_cpmm", "deposit") => return Some("raydium_cpmm.deposit".to_string()),
("raydium_cpmm", "initialize") => return Some("raydium_cpmm.initialize".to_string()),
("raydium_cpmm", "initialize_with_permission") => {
return Some("raydium_cpmm.initialize_with_permission".to_string());
},
("raydium_cpmm", "lp_change_event") => {
return Some("raydium_cpmm.lp_change_event".to_string());
},
("raydium_cpmm", "swap_event") => return Some("raydium_cpmm.swap_event".to_string()),
("raydium_cpmm", "update_amm_config") => {
return Some("raydium_cpmm.update_amm_config".to_string());
},
("raydium_cpmm", "update_pool_status") => {
return Some("raydium_cpmm.update_pool_status".to_string());
},
("raydium_cpmm", "withdraw") => return Some("raydium_cpmm.withdraw".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()); return Some("raydium_clmm.increase_liquidity_v2".to_string());
}, },
("raydium-clmm", "decrease_liquidity_v2") => { ("raydium_clmm", "decrease_liquidity_v2") => {
return Some("raydium_clmm.decrease_liquidity_v2".to_string()); return Some("raydium_clmm.decrease_liquidity_v2".to_string());
}, },
("raydium-clmm", "open_position_with_token22_nft") => { ("raydium_clmm", "open_position_with_token22_nft") => {
return Some("raydium_clmm.open_position_with_token22_nft".to_string()); return Some("raydium_clmm.open_position_with_token22_nft".to_string());
}, },
("raydium-clmm", "close_position") => { ("raydium_clmm", "close_position") => {
return Some("raydium_clmm.close_position".to_string()); return Some("raydium_clmm.close_position".to_string());
}, },
_ => return None, _ => return None,
@@ -442,7 +515,7 @@ mod tests {
async fn sync_upstream_registry_persists_raydium_cpmm_coverage_rows() { async fn sync_upstream_registry_persists_raydium_cpmm_coverage_rows() {
let database = make_database().await; let database = make_database().await;
let service = crate::DexEventCoverageService::new(database.clone()); let service = crate::DexEventCoverageService::new(database.clone());
let result = service.sync_upstream_registry(Some("raydium-cpmm".to_string())).await; let result = service.sync_upstream_registry(Some("raydium_cpmm".to_string())).await;
let result = match result { let result = match result {
Ok(result) => result, Ok(result) => result,
Err(error) => panic!("coverage sync must succeed: {}", error), Err(error) => panic!("coverage sync must succeed: {}", error),
@@ -451,7 +524,7 @@ mod tests {
assert_eq!(result.upstream_entry_count, result.upserted_entry_count); assert_eq!(result.upstream_entry_count, result.upserted_entry_count);
let rows_result = crate::query_dex_event_coverage_entries_list_by_decoder( let rows_result = crate::query_dex_event_coverage_entries_list_by_decoder(
database.as_ref(), database.as_ref(),
"raydium-cpmm", "raydium_cpmm",
) )
.await; .await;
let rows = match rows_result { let rows = match rows_result {
@@ -467,7 +540,46 @@ mod tests {
assert!(rows.iter().any(|row| return { assert!(rows.iter().any(|row| return {
row.entry_name == "deposit" row.entry_name == "deposit"
&& row.event_family == Some("liquidity_add".to_string()) && row.event_family == Some("liquidity_add".to_string())
&& row.local_event_kind.is_none() && row.local_event_kind == Some("raydium_cpmm.deposit".to_string())
}));
assert!(rows.iter().any(|row| return {
row.entry_name == "lp_change_event"
&& row.event_family == Some("liquidity".to_string())
&& row.expected_db_target
== Some(crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS.to_string())
&& row.local_event_kind == Some("raydium_cpmm.lp_change_event".to_string())
}));
assert!(rows.iter().any(|row| return {
row.entry_name == "swap_event"
&& row.event_family == Some("swap".to_string())
&& row.expected_db_target
== Some(
crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string(),
)
&& row.local_event_kind == Some("raydium_cpmm.swap_event".to_string())
})); }));
} }
#[tokio::test]
async fn refresh_local_counts_auto_syncs_empty_coverage_table() {
let database = make_database().await;
let service = crate::DexEventCoverageService::new(database.clone());
let result = service.refresh_local_counts(Some("raydium_cpmm".to_string())).await;
let result = match result {
Ok(result) => result,
Err(error) => panic!("coverage refresh 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!(!rows.is_empty());
}
} }

View File

@@ -628,7 +628,7 @@ const DEX_SUPPORT_MATRIX_ENTRIES: &[DexSupportMatrixEntry] = &[
version: "unknown", version: "unknown",
surface_type: "launch", surface_type: "launch",
surface_role: "launch_surface", surface_role: "launch_surface",
program_id: Some(crate::BOOP_PROGRAM_ID), program_id: Some(crate::BOOP_FUN_PROGRAM_ID),
router_program_id: None, router_program_id: None,
program_id_status: "to_verify", program_id_status: "to_verify",
observed: false, observed: false,
@@ -2934,7 +2934,7 @@ mod tests {
("zora", crate::ZORA_PROGRAM_ID), ("zora", crate::ZORA_PROGRAM_ID),
("raydium_liquidity_locking", crate::RAYDIUM_LIQUIDITY_LOCKING_PROGRAM_ID), ("raydium_liquidity_locking", crate::RAYDIUM_LIQUIDITY_LOCKING_PROGRAM_ID),
("okx_dex", crate::OKX_DEX_PROGRAM_ID), ("okx_dex", crate::OKX_DEX_PROGRAM_ID),
("boop_fun", crate::BOOP_PROGRAM_ID), ("boop_fun", crate::BOOP_FUN_PROGRAM_ID),
("heaven", crate::HEAVEN_PROGRAM_ID), ("heaven", crate::HEAVEN_PROGRAM_ID),
("bonkswap", crate::BONKSWAP_PROGRAM_ID), ("bonkswap", crate::BONKSWAP_PROGRAM_ID),
("metadao_launchpad_v0_7_0", crate::METADAO_LAUNCHPAD_V0_7_0_PROGRAM_ID), ("metadao_launchpad_v0_7_0", crate::METADAO_LAUNCHPAD_V0_7_0_PROGRAM_ID),

View File

@@ -0,0 +1,351 @@
// file: kb_lib/src/instruction_observation_index.rs
//! Local technical index of observed Solana instructions.
//!
//! This index is not a business materialization table. It is an audit/search
//! aid used to find local corpus evidence by program, decoder, instruction
//! discriminator and instruction name.
#[derive(Debug, Clone, sqlx::FromRow)]
struct InstructionObservationSourceRow {
transaction_id: i64,
signature: std::string::String,
slot: std::option::Option<i64>,
block_time: std::option::Option<i64>,
err_json: std::option::Option<std::string::String>,
instruction_id: i64,
parent_instruction_id: std::option::Option<i64>,
instruction_index: i64,
inner_instruction_index: std::option::Option<i64>,
program_id: std::option::Option<std::string::String>,
accounts_json: std::string::String,
data_json: std::option::Option<std::string::String>,
pool_account: std::option::Option<std::string::String>,
decoded_event_kind: std::option::Option<std::string::String>,
decoded_event_id: std::option::Option<i64>,
}
/// Result of refreshing the instruction-observation index.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstructionObservationIndexRefreshResult {
/// Number of source instruction rows scanned.
pub scanned_instruction_count: usize,
/// Number of observation rows upserted.
pub upserted_observation_count: usize,
}
/// Service that builds and refreshes `k_sol_instruction_observations`.
#[derive(Debug, Clone)]
pub struct InstructionObservationIndexService {
database: std::sync::Arc<crate::Database>,
}
impl InstructionObservationIndexService {
/// Creates a new instruction-observation index service.
pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
return Self { database };
}
/// Refreshes observations for one transaction signature.
pub async fn refresh_signature(
&self,
signature: &str,
) -> Result<crate::InstructionObservationIndexRefreshResult, crate::Error> {
let rows_result = self.list_source_rows_by_signature(signature).await;
let rows = match rows_result {
Ok(rows) => rows,
Err(error) => return Err(error),
};
return self.upsert_source_rows(rows).await;
}
/// Refreshes observations for recently persisted instructions.
pub async fn refresh_recent(
&self,
limit: u32,
) -> Result<crate::InstructionObservationIndexRefreshResult, crate::Error> {
let rows_result = self.list_recent_source_rows(limit).await;
let rows = match rows_result {
Ok(rows) => rows,
Err(error) => return Err(error),
};
return self.upsert_source_rows(rows).await;
}
async fn upsert_source_rows(
&self,
rows: std::vec::Vec<InstructionObservationSourceRow>,
) -> Result<crate::InstructionObservationIndexRefreshResult, crate::Error> {
let mut result = crate::InstructionObservationIndexRefreshResult::default();
for row in rows {
result.scanned_instruction_count += 1;
let dto_option = build_instruction_observation_dto(row);
let dto = match dto_option {
Some(dto) => dto,
None => continue,
};
let upsert_result =
crate::query_instruction_observations_upsert(self.database.as_ref(), &dto).await;
match upsert_result {
Ok(_) => result.upserted_observation_count += 1,
Err(error) => return Err(error),
}
}
return Ok(result);
}
async fn list_source_rows_by_signature(
&self,
signature: &str,
) -> Result<std::vec::Vec<InstructionObservationSourceRow>, crate::Error> {
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, InstructionObservationSourceRow>(
r#"
SELECT
tx.id AS transaction_id,
tx.signature AS signature,
tx.slot AS slot,
tx.block_time_unix AS block_time,
tx.err_json AS err_json,
ins.id AS instruction_id,
ins.parent_instruction_id AS parent_instruction_id,
ins.instruction_index AS instruction_index,
ins.inner_instruction_index AS inner_instruction_index,
ins.program_id AS program_id,
ins.accounts_json AS accounts_json,
ins.data_json AS data_json,
de.pool_account AS pool_account,
de.event_kind AS decoded_event_kind,
de.id AS decoded_event_id
FROM k_sol_chain_instructions ins
JOIN k_sol_chain_transactions tx
ON tx.id = ins.transaction_id
LEFT JOIN k_sol_dex_decoded_events de
ON de.transaction_id = tx.id
AND de.instruction_id = ins.id
WHERE tx.signature = ?
ORDER BY ins.instruction_index ASC, ins.inner_instruction_index ASC, ins.id ASC
"#,
)
.bind(signature.to_string())
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list instruction observation source rows for signature '{}': {}",
signature, error
)));
},
}
},
}
}
async fn list_recent_source_rows(
&self,
limit: u32,
) -> Result<std::vec::Vec<InstructionObservationSourceRow>, crate::Error> {
if limit == 0 {
return Ok(std::vec::Vec::new());
}
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, InstructionObservationSourceRow>(
r#"
SELECT
tx.id AS transaction_id,
tx.signature AS signature,
tx.slot AS slot,
tx.block_time_unix AS block_time,
tx.err_json AS err_json,
ins.id AS instruction_id,
ins.parent_instruction_id AS parent_instruction_id,
ins.instruction_index AS instruction_index,
ins.inner_instruction_index AS inner_instruction_index,
ins.program_id AS program_id,
ins.accounts_json AS accounts_json,
ins.data_json AS data_json,
de.pool_account AS pool_account,
de.event_kind AS decoded_event_kind,
de.id AS decoded_event_id
FROM k_sol_chain_instructions ins
JOIN k_sol_chain_transactions tx
ON tx.id = ins.transaction_id
LEFT JOIN k_sol_dex_decoded_events de
ON de.transaction_id = tx.id
AND de.instruction_id = ins.id
ORDER BY ins.id DESC
LIMIT ?
"#,
)
.bind(i64::from(limit))
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list recent instruction observation source rows: {}",
error
)));
},
}
},
}
}
}
fn build_instruction_observation_dto(
row: InstructionObservationSourceRow,
) -> std::option::Option<crate::InstructionObservationDto> {
let program_id = match row.program_id.clone() {
Some(program_id) => program_id,
None => return None,
};
let discriminator_hex = discriminator_hex_from_data_json(row.data_json.as_ref());
let decoder_code = resolve_decoder_code(program_id.as_str());
let instruction_name = resolve_instruction_name(
program_id.as_str(),
decoder_code.as_deref(),
discriminator_hex.as_deref(),
);
let observation_key = format!(
"{}|{}|{}|{}",
row.signature,
row.instruction_index,
option_i64_key(row.inner_instruction_index),
discriminator_hex.clone().unwrap_or_default()
);
return Some(crate::InstructionObservationDto::new(
observation_key,
row.transaction_id,
row.signature,
row.slot,
row.block_time,
row.err_json.is_some(),
row.instruction_id,
row.parent_instruction_id,
row.instruction_index,
row.inner_instruction_index,
program_id,
decoder_code,
discriminator_hex,
instruction_name,
row.accounts_json,
row.data_json,
row.pool_account,
row.decoded_event_kind,
row.decoded_event_id,
));
}
fn resolve_decoder_code(program_id: &str) -> std::option::Option<std::string::String> {
let entry = crate::dex_support_matrix_entry_by_program_id(program_id);
match entry {
Some(entry) => return Some(entry.code.to_string()),
None => return None,
}
}
fn resolve_instruction_name(
program_id: &str,
decoder_code: std::option::Option<&str>,
discriminator_hex: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
let discriminator_hex = match discriminator_hex {
Some(discriminator_hex) => discriminator_hex,
None => return None,
};
if program_id == crate::RAYDIUM_CPMM_PROGRAM_ID || decoder_code == Some("raydium_cpmm") {
let name = match discriminator_hex {
"9c5420764587467b" => "raydium_cpmm.close_permission_pda",
"1416567bc61cdb84" => "raydium_cpmm.collect_creator_fee",
"a78a4e95dfc2067e" => "raydium_cpmm.collect_fund_fee",
"8888fcddc2427e59" => "raydium_cpmm.collect_protocol_fee",
"8934edd4d7756c68" => "raydium_cpmm.create_amm_config",
"878802d889a9b5ca" => "raydium_cpmm.create_permission_pda",
"f223c68952e1f2b6" => "raydium_cpmm.deposit",
"afaf6d1f0d989bed" => "raydium_cpmm.initialize",
"3f37fe4131b25979" => "raydium_cpmm.initialize_with_permission",
"8fbe5adac41e33de" => "raydium_cpmm.swap_base_input",
"37d96256a34ab4ad" => "raydium_cpmm.swap_base_output",
"313cae889a1c74c8" => "raydium_cpmm.update_amm_config",
"82576c062ee0757b" => "raydium_cpmm.update_pool_status",
"b712469c946da122" => "raydium_cpmm.withdraw",
_ => return None,
};
return Some(name.to_string());
}
return None;
}
fn discriminator_hex_from_data_json(
data_json: std::option::Option<&std::string::String>,
) -> std::option::Option<std::string::String> {
let decoded = match decode_data_json_as_bytes(data_json) {
Some(decoded) => decoded,
None => return None,
};
if decoded.len() < 8 {
return None;
}
return Some(bytes_to_hex(&decoded[0..8]));
}
fn decode_data_json_as_bytes(
data_json: std::option::Option<&std::string::String>,
) -> std::option::Option<std::vec::Vec<u8>> {
let data_json = match data_json {
Some(data_json) => data_json,
None => return None,
};
let parsed_result = serde_json::from_str::<serde_json::Value>(data_json.as_str());
let parsed = match parsed_result {
Ok(parsed) => parsed,
Err(_) => return None,
};
match parsed {
serde_json::Value::String(base58_text) => {
let decoded_result = bs58::decode(base58_text.as_str()).into_vec();
match decoded_result {
Ok(decoded) => return Some(decoded),
Err(_) => return None,
}
},
serde_json::Value::Array(values) => {
let first = match values.first() {
Some(first) => first,
None => return None,
};
let base58_text = match first.as_str() {
Some(base58_text) => base58_text,
None => return None,
};
let decoded_result = bs58::decode(base58_text).into_vec();
match decoded_result {
Ok(decoded) => return Some(decoded),
Err(_) => return None,
}
},
_ => return None,
}
}
fn bytes_to_hex(bytes: &[u8]) -> std::string::String {
let mut text = std::string::String::new();
for byte in bytes {
text.push_str(format!("{:02x}", byte).as_str());
}
return text;
}
fn option_i64_key(value: std::option::Option<i64>) -> std::string::String {
match value {
Some(value) => return value.to_string(),
None => return "-".to_string(),
}
}

View File

@@ -736,7 +736,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::METEORA_DBC_PROGRAM_ID, "programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora-dbc", "program": "meteora_dbc",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"DbcDetectPool111", "DbcDetectPool111",
@@ -829,7 +829,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID, "programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
"program": "meteora-damm-v2", "program": "meteora_damm_v2",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"MoonitDammV2Pool111", "MoonitDammV2Pool111",

View File

@@ -51,6 +51,8 @@ mod error;
mod http_client; mod http_client;
/// HTTP endpoint pool and routing. /// HTTP endpoint pool and routing.
mod http_pool; mod http_pool;
/// Technical index for observed chain instructions.
mod instruction_observation_index;
/// Generic JSON-RPC 2.0 WebSocket helpers. /// Generic JSON-RPC 2.0 WebSocket helpers.
mod json_rpc_ws; mod json_rpc_ws;
/// Launch surface attribution service. /// Launch surface attribution service.
@@ -173,7 +175,7 @@ pub use constants::BONK_MINT_ID;
/// Bonkswap program id extracted from upstream Git decoder source. /// Bonkswap program id extracted from upstream Git decoder source.
pub use constants::BONKSWAP_PROGRAM_ID; pub use constants::BONKSWAP_PROGRAM_ID;
/// Boop program id extracted from upstream Git decoder source. /// Boop program id extracted from upstream Git decoder source.
pub use constants::BOOP_PROGRAM_ID; pub use constants::BOOP_FUN_PROGRAM_ID;
/// BPF Loader program identifier. ("BPFLoader1111111111111111111111111111111111"). /// BPF Loader program identifier. ("BPFLoader1111111111111111111111111111111111").
/// @see solana_sdk::pubkey::Pubkey = solana_sdk_ids::bpf_loader_deprecated::ID /// @see solana_sdk::pubkey::Pubkey = solana_sdk_ids::bpf_loader_deprecated::ID
pub use constants::BPF_LOADER_DEPRECATED_PROGRAM_ID; pub use constants::BPF_LOADER_DEPRECATED_PROGRAM_ID;
@@ -491,6 +493,10 @@ pub use db::DexEventCoverageSummaryEntity;
pub use db::FeeEventDto; pub use db::FeeEventDto;
/// Persisted fee event row. /// Persisted fee event row.
pub use db::FeeEventEntity; pub use db::FeeEventEntity;
/// Application-facing on-chain observation DTO.
pub use db::InstructionObservationDto;
/// Persisted technical observation for one Solana instruction.
pub use db::InstructionObservationEntity;
/// Application-facing known HTTP endpoint DTO. /// Application-facing known HTTP endpoint DTO.
pub use db::KnownHttpEndpointDto; pub use db::KnownHttpEndpointDto;
/// Application-facing known WebSocket endpoint DTO. /// Application-facing known WebSocket endpoint DTO.
@@ -767,6 +773,9 @@ pub use db::query_fee_events_get_by_decoded_event_id;
pub use db::query_fee_events_list_recent; pub use db::query_fee_events_list_recent;
/// Inserts or updates one normalized fee event row. /// Inserts or updates one normalized fee event row.
pub use db::query_fee_events_upsert; pub use db::query_fee_events_upsert;
/// Inserts one on-chain observation row and returns its numeric id.
pub use db::query_instruction_observations_list_by_filter;
pub use db::query_instruction_observations_upsert;
/// Reads one known HTTP endpoint by name. /// Reads one known HTTP endpoint by name.
pub use db::query_known_http_endpoints_get; pub use db::query_known_http_endpoints_get;
/// Lists all known HTTP endpoints. /// Lists all known HTTP endpoints.
@@ -1141,14 +1150,22 @@ pub use dex::RaydiumClmmSwapLegacyDecoded;
pub use dex::RaydiumClmmSwapV2Decoded; pub use dex::RaydiumClmmSwapV2Decoded;
/// Raydium CPMM decoded event. /// Raydium CPMM decoded event.
pub use dex::RaydiumCpmmDecodedEvent; pub use dex::RaydiumCpmmDecodedEvent;
/// Raydium CPMM Anchor CPI liquidity-change event.
pub use dex::RaydiumCpmmLpChangeEventDecoded;
/// Raydium CPMM decoded swap. /// Raydium CPMM decoded swap.
pub use dex::RaydiumCpmmSwapDecoded; pub use dex::RaydiumCpmmSwapDecoded;
/// Raydium CPMM Anchor CPI swap event retained as audit evidence.
pub use dex::RaydiumCpmmSwapEventDecoded;
/// Raydium CPMM swap mode. /// Raydium CPMM swap mode.
pub use dex::RaydiumCpmmSwapMode; pub use dex::RaydiumCpmmSwapMode;
/// Decodes one Raydium CPMM instruction from projected instruction fields.
pub use dex::classify_raydium_cpmm_instruction_data;
/// Decodes a Raydium CLMM instruction. /// Decodes a Raydium CLMM instruction.
pub use dex::decode_raydium_clmm_instruction; pub use dex::decode_raydium_clmm_instruction;
/// Decodes one Raydium CPMM instruction from projected instruction fields. /// Decodes one Raydium CPMM instruction from projected instruction fields.
pub use dex::decode_raydium_cpmm_instruction; pub use dex::decode_raydium_cpmm_instruction;
/// Decodes Raydium CPMM Anchor events emitted in `Program data:` logs.
pub use dex::decode_raydium_cpmm_program_data_event;
/// DEX decode service. /// DEX decode service.
pub use dex_decode::DexDecodeService; pub use dex_decode::DexDecodeService;
/// Business-level DEX detection service. /// Business-level DEX detection service.
@@ -1263,6 +1280,10 @@ pub use http_client::parse_json_rpc_http_response_value;
pub use http_pool::HttpEndpointPool; pub use http_pool::HttpEndpointPool;
/// Snapshot of one pooled HTTP endpoint. /// Snapshot of one pooled HTTP endpoint.
pub use http_pool::HttpPoolClientSnapshot; pub use http_pool::HttpPoolClientSnapshot;
/// Instruction-observation index refresh result.
pub use instruction_observation_index::InstructionObservationIndexRefreshResult;
/// Technical service that indexes observed Solana instructions.
pub use instruction_observation_index::InstructionObservationIndexService;
/// JSON-RPC 2.0 error object. /// JSON-RPC 2.0 error object.
pub use json_rpc_ws::JsonRpcWsErrorObject; pub use json_rpc_ws::JsonRpcWsErrorObject;
/// JSON-RPC 2.0 error response. /// JSON-RPC 2.0 error response.

View File

@@ -825,17 +825,14 @@ async fn query_validation_i64(
async fn load_event_coverage_summaries( async fn load_event_coverage_summaries(
database: &crate::Database, database: &crate::Database,
) -> Result<std::vec::Vec<crate::DexEventCoverageSummaryDto>, crate::Error> { ) -> Result<std::vec::Vec<crate::DexEventCoverageSummaryDto>, crate::Error> {
let refresh_result = let coverage_service =
crate::query_dex_event_coverage_entries_refresh_local_counts(database).await; crate::DexEventCoverageService::new(std::sync::Arc::new(database.clone()));
if let Err(error) = refresh_result { let refresh_result = coverage_service.refresh_local_counts(None).await;
return Err(error); let refresh_result = match refresh_result {
} Ok(refresh_result) => refresh_result,
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), Err(error) => return Err(error),
} };
return Ok(refresh_result.summaries);
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -183,6 +183,8 @@ impl LocalPipelineReplayService {
let pair_analytic_signal = crate::PairAnalyticSignalService::new(self.database.clone()); let pair_analytic_signal = crate::PairAnalyticSignalService::new(self.database.clone());
let transaction_classification = let transaction_classification =
crate::TransactionClassificationService::new(self.database.clone()); crate::TransactionClassificationService::new(self.database.clone());
let instruction_observation_index =
crate::InstructionObservationIndexService::new(self.database.clone());
let mut result = LocalPipelineReplayResult { let mut result = LocalPipelineReplayResult {
selected_transaction_count: signatures.len(), selected_transaction_count: signatures.len(),
reset_market_materialization_deleted_count, reset_market_materialization_deleted_count,
@@ -424,6 +426,24 @@ impl LocalPipelineReplayService {
); );
}, },
} }
let instruction_index_result =
instruction_observation_index.refresh_signature(signature.as_str()).await;
match instruction_index_result {
Ok(index_result) => {
tracing::debug!(
signature = %signature,
upserted_observation_count = index_result.upserted_observation_count,
"instruction observation index refreshed during local replay"
);
},
Err(error) => {
tracing::warn!(
signature = %signature,
error = %error,
"instruction observation index refresh failed during local replay"
);
},
}
result.replayed_transaction_count += 1; result.replayed_transaction_count += 1;
} }
if config.refresh_missing_token_metadata { if config.refresh_missing_token_metadata {
@@ -451,9 +471,31 @@ impl LocalPipelineReplayService {
}, },
} }
} }
self.refresh_event_coverage_best_effort().await;
return Ok(result); return Ok(result);
} }
async fn refresh_event_coverage_best_effort(&self) {
let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
let refresh_result = coverage_service.refresh_local_counts(None).await;
match refresh_result {
Ok(refresh_result) => {
tracing::debug!(
upserted_entry_count = refresh_result.upserted_entry_count,
refreshed_entry_count = refresh_result.refreshed_entry_count,
summary_count = refresh_result.summaries.len(),
"dex event coverage refreshed after local pipeline replay"
);
},
Err(error) => {
tracing::warn!(
error = %error,
"dex event coverage refresh failed after local pipeline replay"
);
},
}
}
async fn get_certified_dex_decode_skip_ledger( async fn get_certified_dex_decode_skip_ledger(
&self, &self,
config: &crate::LocalPipelineReplayConfig, config: &crate::LocalPipelineReplayConfig,
@@ -777,7 +819,12 @@ mod tests {
let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice()) let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice())
.expect("ledger must build"); .expect("ledger must build");
assert_eq!(ledger.event_count, 2); assert_eq!(ledger.event_count, 2);
assert_eq!(ledger.status_reason.as_deref(), Some("decode completed and certified for skip: event_count=2, effective_event_count=0, instruction_audit_count=2, distinct_token_mint_count=2")); assert_eq!(
ledger.status_reason.as_deref(),
Some(
"decode completed and certified for skip: event_count=2, effective_event_count=0, instruction_audit_count=2, distinct_token_mint_count=2"
)
);
assert!(!ledger.force_replay_required); assert!(!ledger.force_replay_required);
assert!(ledger.can_skip_decode()); assert!(ledger.can_skip_decode());
} }

View File

@@ -1824,7 +1824,7 @@ mod tests {
summary.event_coverage_upstream_git_local_corpus_observed_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_upstream_git_local_corpus_materialized_entry_count = 1;
summary.event_coverage_summaries.push(crate::DexEventCoverageSummaryDto { summary.event_coverage_summaries.push(crate::DexEventCoverageSummaryDto {
decoder_code: "raydium-cpmm".to_string(), decoder_code: "raydium_cpmm".to_string(),
listed_entry_count: 4, listed_entry_count: 4,
decoded_entry_count: 3, decoded_entry_count: 3,
observed_entry_count: 2, observed_entry_count: 2,

View File

@@ -102,7 +102,17 @@ impl NonTradeEventMaterializationService {
continue; continue;
}, },
}; };
if crate::is_dex_liquidity_event_kind(decoded_event.event_kind.as_str()) { if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str()) {
let cleanup_result =
self.delete_stale_pool_admin_event_for_lifecycle(decoded_event).await;
match cleanup_result {
Ok(_) => {},
Err(error) => return Err(error),
}
}
if crate::is_dex_liquidity_event_kind(decoded_event.event_kind.as_str())
&& !decoded_event.event_kind.ends_with(".lp_change_event")
{
let materialized = self let materialized = self
.materialize_liquidity_event( .materialize_liquidity_event(
&transaction, &transaction,
@@ -159,7 +169,9 @@ impl NonTradeEventMaterializationService {
Err(error) => return Err(error), Err(error) => return Err(error),
} }
} }
if crate::is_dex_admin_event_kind(decoded_event.event_kind.as_str()) { if crate::is_dex_admin_event_kind(decoded_event.event_kind.as_str())
&& !crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str())
{
let materialized = self let materialized = self
.materialize_pool_admin_event( .materialize_pool_admin_event(
&transaction, &transaction,
@@ -178,6 +190,36 @@ impl NonTradeEventMaterializationService {
} }
} }
} }
for decoded_event in &decoded_events {
if !decoded_event.event_kind.ends_with(".lp_change_event") {
continue;
}
let payload_result =
serde_json::from_str::<serde_json::Value>(decoded_event.payload_json.as_str());
let payload = match payload_result {
Ok(payload) => payload,
Err(error) => {
tracing::warn!(
signature = %transaction.signature,
event_kind = %decoded_event.event_kind,
error = %error,
"skipping postponed lp_change_event materialization for invalid decoded payload"
);
continue;
},
};
let materialized = self
.materialize_liquidity_event(&transaction, transaction_id, decoded_event, &payload)
.await;
match materialized {
Ok(was_materialized) => {
if was_materialized {
result.liquidity_event_count += 1;
}
},
Err(error) => return Err(error),
}
}
return Ok(result); return Ok(result);
} }
@@ -274,6 +316,12 @@ impl NonTradeEventMaterializationService {
"fund_fee_amount", "fund_fee_amount",
"creatorFeeAmount", "creatorFeeAmount",
"creator_fee_amount", "creator_fee_amount",
"amount0RequestedRaw",
"amount_0_requested_raw",
"amount1RequestedRaw",
"amount_1_requested_raw",
"tokenAAmount",
"tokenBAmount",
"amount", "amount",
], ],
); );
@@ -370,6 +418,48 @@ impl NonTradeEventMaterializationService {
} }
} }
async fn delete_stale_pool_admin_event_for_lifecycle(
&self,
decoded_event: &crate::DexDecodedEventDto,
) -> Result<(), crate::Error> {
let decoded_event_id = match decoded_event.id {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(()),
};
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_pool_admin_events
WHERE decoded_event_id = ?
"#,
)
.bind(decoded_event_id)
.execute(pool)
.await;
let delete_result = match delete_result {
Ok(delete_result) => delete_result,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete stale k_sol_pool_admin_events for lifecycle decoded_event_id '{}' on sqlite: {}",
decoded_event_id, error
)));
},
};
let deleted_count = delete_result.rows_affected();
if deleted_count > 0 {
tracing::debug!(
decoded_event_id = decoded_event_id,
event_kind = %decoded_event.event_kind,
deleted_count = deleted_count,
"removed stale pool admin materialization for lifecycle event"
);
}
return Ok(());
},
}
}
async fn materialize_pool_admin_event( async fn materialize_pool_admin_event(
&self, &self,
transaction: &crate::ChainTransactionDto, transaction: &crate::ChainTransactionDto,
@@ -437,6 +527,16 @@ impl NonTradeEventMaterializationService {
Ok(context) => context, Ok(context) => context,
Err(error) => return Err(error), Err(error) => return Err(error),
}; };
let context = if context.pool_id.is_some() && context.pair.is_some() {
context
} else {
let ensured_context =
self.ensure_liquidity_context_from_decoded_event(decoded_event, context).await;
match ensured_context {
Ok(ensured_context) => ensured_context,
Err(error) => return Err(error),
}
};
let dex_id = match context.dex_id { let dex_id = match context.dex_id {
Some(dex_id) => dex_id, Some(dex_id) => dex_id,
None => return Ok(false), None => return Ok(false),
@@ -458,6 +558,12 @@ impl NonTradeEventMaterializationService {
crate::LiquidityEventKind::PositionOpen crate::LiquidityEventKind::PositionOpen
} else if crate::is_dex_position_close_event_kind(decoded_event.event_kind.as_str()) { } else if crate::is_dex_position_close_event_kind(decoded_event.event_kind.as_str()) {
crate::LiquidityEventKind::PositionClose crate::LiquidityEventKind::PositionClose
} else if decoded_event.event_kind.ends_with(".lp_change_event") {
let change_type = extract_first_u64(payload, &["changeType", "change_type"]);
match change_type {
Some(1) => crate::LiquidityEventKind::Remove,
_ => crate::LiquidityEventKind::Add,
}
} else if crate::is_dex_liquidity_remove_event_kind(decoded_event.event_kind.as_str()) { } else if crate::is_dex_liquidity_remove_event_kind(decoded_event.event_kind.as_str()) {
crate::LiquidityEventKind::Remove crate::LiquidityEventKind::Remove
} else { } else {
@@ -487,6 +593,10 @@ impl NonTradeEventMaterializationService {
"amount_base", "amount_base",
"tokenAAmount", "tokenAAmount",
"token_a_amount", "token_a_amount",
"token0AmountRaw",
"token_0_amount_raw",
"amount0RequestedRaw",
"amount_0_requested_raw",
"amountA", "amountA",
"amount_a", "amount_a",
], ],
@@ -502,6 +612,10 @@ impl NonTradeEventMaterializationService {
"amount_quote", "amount_quote",
"tokenBAmount", "tokenBAmount",
"token_b_amount", "token_b_amount",
"token1AmountRaw",
"token_1_amount_raw",
"amount1RequestedRaw",
"amount_1_requested_raw",
"amountB", "amountB",
"amount_b", "amount_b",
], ],
@@ -559,6 +673,54 @@ impl NonTradeEventMaterializationService {
} }
} }
async fn ensure_liquidity_context_from_decoded_event(
&self,
decoded_event: &crate::DexDecodedEventDto,
context: NonTradeDecodedEventContext,
) -> Result<NonTradeDecodedEventContext, crate::Error> {
let dex_id = match context.dex_id {
Some(dex_id) => dex_id,
None => return Ok(context),
};
if context.pool_id.is_some() && context.pair.is_some() {
return Ok(context);
}
if decoded_event.pool_account.is_none()
|| decoded_event.token_a_mint.is_none()
|| decoded_event.token_b_mint.is_none()
{
return Ok(context);
}
let materialization_input_result =
crate::dex_pool_materialization::DexPoolMaterializationInput::from_decoded_event(
decoded_event,
dex_id,
crate::PoolKind::Amm,
crate::PoolStatus::Active,
crate::dex_pool_materialization::DexPoolTokenOrder::AlreadyBaseQuote,
None,
None,
None,
);
let materialization_input = match materialization_input_result {
Ok(materialization_input) => materialization_input,
Err(_) => return Ok(context),
};
let materialization_result = crate::dex_pool_materialization::materialize_dex_pool(
self.database.as_ref(),
&materialization_input,
)
.await;
if let Err(error) = materialization_result {
return Err(error);
}
let refreshed_context = self.resolve_decoded_event_context(decoded_event).await;
match refreshed_context {
Ok(refreshed_context) => return Ok(refreshed_context),
Err(error) => return Err(error),
}
}
async fn resolve_decoded_event_context( async fn resolve_decoded_event_context(
&self, &self,
decoded_event: &crate::DexDecodedEventDto, decoded_event: &crate::DexDecodedEventDto,
@@ -627,6 +789,29 @@ impl NonTradeEventMaterializationService {
} }
} }
fn extract_first_u64(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<u64> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let candidate_value = object.get(*candidate_key);
if let Some(candidate_value) = candidate_value {
if let Some(number) = candidate_value.as_u64() {
return Some(number);
}
if let Some(text) = candidate_value.as_str() {
let parsed = text.parse::<u64>();
if let Ok(parsed) = parsed {
return Some(parsed);
}
}
}
}
}
return None;
}
fn extract_first_amount_string( fn extract_first_amount_string(
value: &serde_json::Value, value: &serde_json::Value,
candidate_keys: &[&str], candidate_keys: &[&str],

View File

@@ -37,6 +37,12 @@ pub struct OnchainDexPairDiscoveryRequestDto {
pub scan_order: std::option::Option<std::string::String>, pub scan_order: std::option::Option<std::string::String>,
/// Optional target event family used to score and filter candidate signatures. /// Optional target event family used to score and filter candidate signatures.
pub target_event: std::option::Option<std::string::String>, pub target_event: std::option::Option<std::string::String>,
/// Optional instruction-name filter, for example `raydium_cpmm.withdraw` or `withdraw`.
#[serde(default)]
pub target_instruction_name: std::option::Option<std::string::String>,
/// Optional first-eight-byte discriminator filter as lower hex; accepts comma/space separated values.
#[serde(default)]
pub target_discriminator_hex: std::option::Option<std::string::String>,
/// Whether transactions containing swap-like logs should be skipped. /// Whether transactions containing swap-like logs should be skipped.
pub exclude_swaps: bool, pub exclude_swaps: bool,
/// Whether failed transactions should be returned as candidates. /// Whether failed transactions should be returned as candidates.
@@ -209,6 +215,8 @@ pub struct OnchainDexPairCandidateDto {
pub instruction_name: std::option::Option<std::string::String>, pub instruction_name: std::option::Option<std::string::String>,
/// Prefix of the raw base58 instruction data, useful for audit grouping. /// Prefix of the raw base58 instruction data, useful for audit grouping.
pub instruction_data_prefix: std::option::Option<std::string::String>, pub instruction_data_prefix: std::option::Option<std::string::String>,
/// First eight instruction-data bytes as lower hex.
pub instruction_discriminator_hex: std::option::Option<std::string::String>,
/// Candidate pool address when it can be extracted safely or heuristically. /// Candidate pool address when it can be extracted safely or heuristically.
pub pool_address: std::option::Option<std::string::String>, pub pool_address: std::option::Option<std::string::String>,
/// Candidate token A/base mint when it can be extracted. /// Candidate token A/base mint when it can be extracted.
@@ -382,6 +390,8 @@ impl OnchainDexPairDiscoveryService {
let logs = extract_log_messages(&transaction_value); let logs = extract_log_messages(&transaction_value);
let target_keeps_mixed_swaps = target_event_keeps_mixed_swap_transactions( let target_keeps_mixed_swaps = target_event_keeps_mixed_swap_transactions(
normalized_request.target_event.as_deref(), normalized_request.target_event.as_deref(),
normalized_request.target_instruction_name.as_deref(),
normalized_request.target_discriminator_hex.as_deref(),
); );
if normalized_request.exclude_swaps if normalized_request.exclude_swaps
&& logs_contain_swap(logs.as_slice()) && logs_contain_swap(logs.as_slice())
@@ -396,6 +406,8 @@ impl OnchainDexPairDiscoveryService {
resolved.program_id.as_str(), resolved.program_id.as_str(),
resolved.dex_code.clone(), resolved.dex_code.clone(),
normalized_request.target_event.as_deref(), normalized_request.target_event.as_deref(),
normalized_request.target_instruction_name.as_deref(),
normalized_request.target_discriminator_hex.as_deref(),
); );
result.scanned_instruction_count += extraction.scanned_instruction_count; result.scanned_instruction_count += extraction.scanned_instruction_count;
result.target_program_instruction_count += extraction.target_program_instruction_count; result.target_program_instruction_count += extraction.target_program_instruction_count;
@@ -683,6 +695,8 @@ fn normalize_request(
max_pages: clamp_u32(default_if_zero(request.max_pages, 1), 1, 25), max_pages: clamp_u32(default_if_zero(request.max_pages, 1), 1, 25),
scan_order: Some(normalize_scan_order(request.scan_order.as_deref()).to_string()), scan_order: Some(normalize_scan_order(request.scan_order.as_deref()).to_string()),
target_event: normalize_target_event(request.target_event), target_event: normalize_target_event(request.target_event),
target_instruction_name: normalize_instruction_name_filter(request.target_instruction_name),
target_discriminator_hex: normalize_discriminator_filter(request.target_discriminator_hex),
exclude_swaps: request.exclude_swaps, exclude_swaps: request.exclude_swaps,
include_failed: request.include_failed, include_failed: request.include_failed,
http_role, http_role,
@@ -773,6 +787,56 @@ fn normalize_target_event(
return Some(targets.join(",")); return Some(targets.join(","));
} }
fn normalize_instruction_name_filter(
value: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
let value = match normalize_optional_string(value) {
Some(value) => value,
None => return None,
};
let mut normalized = std::vec::Vec::new();
for token in value.split(|character: char| {
return character == ',' || character == ';' || character.is_whitespace();
}) {
let token = token.trim().to_ascii_lowercase();
if token.is_empty() {
continue;
}
push_unique_string(&mut normalized, token.replace('-', "_"));
}
if normalized.is_empty() {
return None;
}
return Some(normalized.join(","));
}
fn normalize_discriminator_filter(
value: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
let value = match normalize_optional_string(value) {
Some(value) => value,
None => return None,
};
let mut normalized = std::vec::Vec::new();
for token in value.split(|character: char| {
return character == ',' || character == ';' || character.is_whitespace();
}) {
let mut token = token.trim().to_ascii_lowercase();
if token.starts_with("0x") {
token = token[2..].to_string();
}
if token.len() != 16 || !token.chars().all(|character| return character.is_ascii_hexdigit())
{
continue;
}
push_unique_string(&mut normalized, token);
}
if normalized.is_empty() {
return None;
}
return Some(normalized.join(","));
}
fn normalize_signature_source( fn normalize_signature_source(
value: std::option::Option<std::string::String>, value: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> { ) -> std::option::Option<std::string::String> {
@@ -974,6 +1038,8 @@ fn extract_candidates_from_transaction(
target_program_id: &str, target_program_id: &str,
dex_code: std::option::Option<std::string::String>, dex_code: std::option::Option<std::string::String>,
target_event: std::option::Option<&str>, target_event: std::option::Option<&str>,
target_instruction_name: std::option::Option<&str>,
target_discriminator_hex: std::option::Option<&str>,
) -> OnchainCandidateExtraction { ) -> OnchainCandidateExtraction {
let mut candidates = std::vec::Vec::new(); let mut candidates = std::vec::Vec::new();
let mut rejected_candidate_summary = std::vec::Vec::new(); let mut rejected_candidate_summary = std::vec::Vec::new();
@@ -1036,6 +1102,19 @@ fn extract_candidates_from_transaction(
); );
continue; continue;
} }
if !candidate_matches_instruction_filters(
&candidate,
target_instruction_name,
target_discriminator_hex,
) {
target_rejected_candidate_count += 1;
push_rejected_candidate_summary(
&mut rejected_candidate_summary,
&candidate,
"target_instruction_filter",
);
continue;
}
if candidates.iter().any(|existing| { if candidates.iter().any(|existing| {
return candidate_identity_key(existing) == candidate_identity_key(&candidate); return candidate_identity_key(existing) == candidate_identity_key(&candidate);
}) { }) {
@@ -1144,6 +1223,9 @@ fn decode_raydium_clmm_candidate(
inner_instruction_index: instruction.inner_instruction_index, inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some("raydium_clmm.swap".to_string()), instruction_name: Some("raydium_clmm.swap".to_string()),
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()), instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(
instruction.data.as_deref(),
),
pool_address: Some(event.pool_state.clone()), pool_address: Some(event.pool_state.clone()),
token_a_mint: Some(event.base_mint), token_a_mint: Some(event.base_mint),
token_b_mint: Some(event.quote_mint), token_b_mint: Some(event.quote_mint),
@@ -1176,6 +1258,9 @@ fn decode_raydium_clmm_candidate(
inner_instruction_index: instruction.inner_instruction_index, inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some("raydium_clmm.swap_v2".to_string()), instruction_name: Some("raydium_clmm.swap_v2".to_string()),
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()), instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(
instruction.data.as_deref(),
),
pool_address: Some(event.pool_state.clone()), pool_address: Some(event.pool_state.clone()),
token_a_mint: Some(event.base_mint), token_a_mint: Some(event.base_mint),
token_b_mint: Some(event.quote_mint), token_b_mint: Some(event.quote_mint),
@@ -1257,9 +1342,31 @@ fn decode_raydium_cpmm_candidate(
event.quote_mint, event.quote_mint,
)); ));
}, },
_ => return None,
} }
} }
return None; let raw_data = match decode_onchain_instruction_data(instruction.data.as_deref()) {
Some(raw_data) => raw_data,
None => return None,
};
let instruction_kind = classify_demo3_raydium_cpmm_instruction(raw_data.as_slice());
if instruction_kind == Demo3RaydiumCpmmInstructionKind::Unknown
|| instruction_kind == Demo3RaydiumCpmmInstructionKind::SwapBaseInput
|| instruction_kind == Demo3RaydiumCpmmInstructionKind::SwapBaseOutput
{
return None;
}
return Some(build_raydium_cpmm_non_swap_candidate(
signature,
slot,
block_time,
failed,
program_id,
dex_code,
instruction,
logs,
instruction_kind,
));
} }
fn build_raydium_cpmm_candidate( fn build_raydium_cpmm_candidate(
@@ -1289,6 +1396,7 @@ fn build_raydium_cpmm_candidate(
inner_instruction_index: instruction.inner_instruction_index, inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some(instruction_name.to_string()), instruction_name: Some(instruction_name.to_string()),
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()), instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
pool_address: Some(pool_address.clone()), pool_address: Some(pool_address.clone()),
token_a_mint: Some(token_a_mint), token_a_mint: Some(token_a_mint),
token_b_mint: Some(token_b_mint), token_b_mint: Some(token_b_mint),
@@ -1304,6 +1412,267 @@ fn build_raydium_cpmm_candidate(
}; };
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Demo3RaydiumCpmmInstructionKind {
Unknown,
ClosePermissionPda,
CollectCreatorFee,
CollectFundFee,
CollectProtocolFee,
CreateAmmConfig,
CreatePermissionPda,
Deposit,
Initialize,
InitializeWithPermission,
SwapBaseInput,
SwapBaseOutput,
UpdateAmmConfig,
UpdatePoolStatus,
Withdraw,
}
fn build_raydium_cpmm_non_swap_candidate(
signature: &str,
slot: std::option::Option<u64>,
block_time: std::option::Option<i64>,
failed: bool,
program_id: &str,
dex_code: std::option::Option<std::string::String>,
instruction: &OnchainInstructionCandidate,
logs: &[std::string::String],
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> crate::OnchainDexPairCandidateDto {
let pool_address =
demo3_raydium_cpmm_pool_account(instruction.accounts.as_slice(), instruction_kind);
let token_a_mint =
demo3_raydium_cpmm_token_a_mint(instruction.accounts.as_slice(), instruction_kind);
let token_b_mint =
demo3_raydium_cpmm_token_b_mint(instruction.accounts.as_slice(), instruction_kind);
let candidate_kind = demo3_raydium_cpmm_candidate_kind(instruction_kind).to_string();
let instruction_name = demo3_raydium_cpmm_instruction_name(instruction_kind).to_string();
let verified_pool_address = pool_address.clone();
let backfill_hint = demo3_raydium_cpmm_backfill_hint(
instruction_kind,
candidate_kind.as_str(),
pool_address.as_deref(),
signature,
);
return crate::OnchainDexPairCandidateDto {
signature: signature.to_string(),
slot,
block_time,
failed,
program_id: program_id.to_string(),
dex_code,
candidate_kind,
confidence: "high".to_string(),
instruction_index: instruction.instruction_index,
inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some(instruction_name),
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
pool_address,
token_a_mint,
token_b_mint,
verified_pool_address,
observed_token_mints: std::vec::Vec::new(),
token_balance_deltas: std::vec::Vec::new(),
candidate_pool_accounts: std::vec::Vec::new(),
candidate_token_vault_accounts: std::vec::Vec::new(),
candidate_program_accounts: std::vec::Vec::new(),
account_samples: sample_strings(instruction.accounts.as_slice(), 12),
log_samples: sample_logs(logs, 8),
backfill_hint,
};
}
fn classify_demo3_raydium_cpmm_instruction(data: &[u8]) -> Demo3RaydiumCpmmInstructionKind {
if data.len() < 8 {
return Demo3RaydiumCpmmInstructionKind::Unknown;
}
let discriminator = &data[0..8];
if discriminator == [156, 84, 32, 118, 69, 135, 70, 123] {
return Demo3RaydiumCpmmInstructionKind::ClosePermissionPda;
}
if discriminator == [20, 22, 86, 123, 198, 28, 219, 132] {
return Demo3RaydiumCpmmInstructionKind::CollectCreatorFee;
}
if discriminator == [167, 138, 78, 149, 223, 194, 6, 126] {
return Demo3RaydiumCpmmInstructionKind::CollectFundFee;
}
if discriminator == [136, 136, 252, 221, 194, 66, 126, 89] {
return Demo3RaydiumCpmmInstructionKind::CollectProtocolFee;
}
if discriminator == [137, 52, 237, 212, 215, 117, 108, 104] {
return Demo3RaydiumCpmmInstructionKind::CreateAmmConfig;
}
if discriminator == [135, 136, 2, 216, 137, 169, 181, 202] {
return Demo3RaydiumCpmmInstructionKind::CreatePermissionPda;
}
if discriminator == [242, 35, 198, 137, 82, 225, 242, 182] {
return Demo3RaydiumCpmmInstructionKind::Deposit;
}
if discriminator == [175, 175, 109, 31, 13, 152, 155, 237] {
return Demo3RaydiumCpmmInstructionKind::Initialize;
}
if discriminator == [63, 55, 254, 65, 49, 178, 89, 121] {
return Demo3RaydiumCpmmInstructionKind::InitializeWithPermission;
}
if discriminator == [143, 190, 90, 218, 196, 30, 51, 222] {
return Demo3RaydiumCpmmInstructionKind::SwapBaseInput;
}
if discriminator == [55, 217, 98, 86, 163, 74, 180, 173] {
return Demo3RaydiumCpmmInstructionKind::SwapBaseOutput;
}
if discriminator == [49, 60, 174, 136, 154, 28, 116, 200] {
return Demo3RaydiumCpmmInstructionKind::UpdateAmmConfig;
}
if discriminator == [130, 87, 108, 6, 46, 224, 117, 123] {
return Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus;
}
if discriminator == [183, 18, 70, 156, 148, 109, 161, 34] {
return Demo3RaydiumCpmmInstructionKind::Withdraw;
}
return Demo3RaydiumCpmmInstructionKind::Unknown;
}
fn demo3_raydium_cpmm_candidate_kind(
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> &'static str {
match instruction_kind {
Demo3RaydiumCpmmInstructionKind::ClosePermissionPda => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => return "claim_fee",
Demo3RaydiumCpmmInstructionKind::CollectFundFee => return "claim_fee",
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => return "claim_fee",
Demo3RaydiumCpmmInstructionKind::CreateAmmConfig => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::CreatePermissionPda => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::Deposit => return "add_liquidity",
Demo3RaydiumCpmmInstructionKind::Initialize => return "create_pool",
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => return "create_pool",
Demo3RaydiumCpmmInstructionKind::UpdateAmmConfig => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::Withdraw => return "remove_liquidity",
_ => return "unclassified_instruction",
}
}
fn demo3_raydium_cpmm_instruction_name(
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> &'static str {
match instruction_kind {
Demo3RaydiumCpmmInstructionKind::ClosePermissionPda => {
return "raydium_cpmm.close_permission_pda";
},
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => {
return "raydium_cpmm.collect_creator_fee";
},
Demo3RaydiumCpmmInstructionKind::CollectFundFee => return "raydium_cpmm.collect_fund_fee",
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => {
return "raydium_cpmm.collect_protocol_fee";
},
Demo3RaydiumCpmmInstructionKind::CreateAmmConfig => return "raydium_cpmm.create_amm_config",
Demo3RaydiumCpmmInstructionKind::CreatePermissionPda => {
return "raydium_cpmm.create_permission_pda";
},
Demo3RaydiumCpmmInstructionKind::Deposit => return "raydium_cpmm.deposit",
Demo3RaydiumCpmmInstructionKind::Initialize => return "raydium_cpmm.initialize",
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => {
return "raydium_cpmm.initialize_with_permission";
},
Demo3RaydiumCpmmInstructionKind::UpdateAmmConfig => return "raydium_cpmm.update_amm_config",
Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus => {
return "raydium_cpmm.update_pool_status";
},
Demo3RaydiumCpmmInstructionKind::Withdraw => return "raydium_cpmm.withdraw",
Demo3RaydiumCpmmInstructionKind::SwapBaseInput => return "raydium_cpmm.swap_base_input",
Demo3RaydiumCpmmInstructionKind::SwapBaseOutput => return "raydium_cpmm.swap_base_output",
Demo3RaydiumCpmmInstructionKind::Unknown => return "raydium_cpmm.unknown",
}
}
fn demo3_raydium_cpmm_pool_account(
accounts: &[std::string::String],
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> std::option::Option<std::string::String> {
let index = match instruction_kind {
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => Some(2usize),
Demo3RaydiumCpmmInstructionKind::CollectFundFee => Some(2usize),
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => Some(2usize),
Demo3RaydiumCpmmInstructionKind::Deposit => Some(2usize),
Demo3RaydiumCpmmInstructionKind::Initialize => Some(3usize),
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => Some(4usize),
Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus => Some(1usize),
Demo3RaydiumCpmmInstructionKind::Withdraw => Some(2usize),
_ => None,
};
let index = match index {
Some(index) => index,
None => return None,
};
return demo3_account_at(accounts, index);
}
fn demo3_raydium_cpmm_token_a_mint(
accounts: &[std::string::String],
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> std::option::Option<std::string::String> {
let index = match instruction_kind {
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => Some(6usize),
Demo3RaydiumCpmmInstructionKind::CollectFundFee => Some(6usize),
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => Some(6usize),
Demo3RaydiumCpmmInstructionKind::Deposit => Some(10usize),
Demo3RaydiumCpmmInstructionKind::Initialize => Some(4usize),
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => Some(5usize),
Demo3RaydiumCpmmInstructionKind::Withdraw => Some(10usize),
_ => None,
};
let index = match index {
Some(index) => index,
None => return None,
};
return demo3_account_at(accounts, index);
}
fn demo3_raydium_cpmm_token_b_mint(
accounts: &[std::string::String],
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> std::option::Option<std::string::String> {
let index = match instruction_kind {
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => Some(7usize),
Demo3RaydiumCpmmInstructionKind::CollectFundFee => Some(7usize),
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => Some(7usize),
Demo3RaydiumCpmmInstructionKind::Deposit => Some(11usize),
Demo3RaydiumCpmmInstructionKind::Initialize => Some(5usize),
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => Some(6usize),
Demo3RaydiumCpmmInstructionKind::Withdraw => Some(11usize),
_ => None,
};
let index = match index {
Some(index) => index,
None => return None,
};
return demo3_account_at(accounts, index);
}
fn demo3_raydium_cpmm_backfill_hint(
instruction_kind: Demo3RaydiumCpmmInstructionKind,
candidate_kind: &str,
pool_address: std::option::Option<&str>,
signature: &str,
) -> std::string::String {
let instruction_name = demo3_raydium_cpmm_instruction_name(instruction_kind);
if let Some(pool_address) = pool_address {
return format!(
"Raydium CPMM {} candidate '{}'; backfill pool in Demo Pipeline 2: {} ; signature: {}",
candidate_kind, instruction_name, pool_address, signature
);
}
return format!(
"Raydium CPMM {} candidate '{}'; inspect/backfill transaction signature: {}",
candidate_kind, instruction_name, signature
);
}
fn decode_meteora_damm_v1_candidate( fn decode_meteora_damm_v1_candidate(
signature: &str, signature: &str,
slot: std::option::Option<u64>, slot: std::option::Option<u64>,
@@ -1345,6 +1714,7 @@ fn decode_meteora_damm_v1_candidate(
inner_instruction_index: instruction.inner_instruction_index, inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some(instruction_name), instruction_name: Some(instruction_name),
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()), instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
pool_address, pool_address,
token_a_mint, token_a_mint,
token_b_mint, token_b_mint,
@@ -1616,6 +1986,7 @@ fn build_heuristic_candidate(
inner_instruction_index: instruction.inner_instruction_index, inner_instruction_index: instruction.inner_instruction_index,
instruction_name, instruction_name,
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()), instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
pool_address: pool_address.clone(), pool_address: pool_address.clone(),
token_a_mint, token_a_mint,
token_b_mint, token_b_mint,
@@ -2789,8 +3160,21 @@ fn target_event_prefers_instruction_local_classification(
return false; return false;
} }
fn target_event_keeps_mixed_swap_transactions(target_event: std::option::Option<&str>) -> bool { fn target_event_keeps_mixed_swap_transactions(
return !split_target_event_filter(target_event).is_empty(); target_event: std::option::Option<&str>,
target_instruction_name: std::option::Option<&str>,
target_discriminator_hex: std::option::Option<&str>,
) -> bool {
if !split_target_event_filter(target_event).is_empty() {
return true;
}
if !split_comma_filter(target_instruction_name).is_empty() {
return true;
}
if !split_comma_filter(target_discriminator_hex).is_empty() {
return true;
}
return false;
} }
fn instruction_data_prefix( fn instruction_data_prefix(
@@ -2813,6 +3197,23 @@ fn instruction_data_prefix(
return Some(prefix); return Some(prefix);
} }
fn instruction_discriminator_hex(
data: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
let decoded = match decode_onchain_instruction_data(data) {
Some(decoded) => decoded,
None => return None,
};
if decoded.len() < 8 {
return None;
}
let mut text = std::string::String::new();
for byte in &decoded[0..8] {
text.push_str(format!("{:02x}", byte).as_str());
}
return Some(text);
}
fn text_matches_non_swap_target(lower: &str) -> bool { fn text_matches_non_swap_target(lower: &str) -> bool {
return text_matches_pool_admin(lower) return text_matches_pool_admin(lower)
|| text_matches_claim_reward(lower) || text_matches_claim_reward(lower)
@@ -3016,6 +3417,77 @@ fn first_matching_target_event(
return None; return None;
} }
fn candidate_matches_instruction_filters(
candidate: &crate::OnchainDexPairCandidateDto,
target_instruction_name: std::option::Option<&str>,
target_discriminator_hex: std::option::Option<&str>,
) -> bool {
if !candidate_matches_instruction_name_filter(candidate, target_instruction_name) {
return false;
}
if !candidate_matches_discriminator_filter(candidate, target_discriminator_hex) {
return false;
}
return true;
}
fn candidate_matches_instruction_name_filter(
candidate: &crate::OnchainDexPairCandidateDto,
target_instruction_name: std::option::Option<&str>,
) -> bool {
let targets = split_comma_filter(target_instruction_name);
if targets.is_empty() {
return true;
}
let instruction_name = match &candidate.instruction_name {
Some(instruction_name) => instruction_name.to_ascii_lowercase(),
None => return false,
};
for target in targets {
if instruction_name == target || instruction_name.ends_with(format!(".{}", target).as_str())
{
return true;
}
}
return false;
}
fn candidate_matches_discriminator_filter(
candidate: &crate::OnchainDexPairCandidateDto,
target_discriminator_hex: std::option::Option<&str>,
) -> bool {
let targets = split_comma_filter(target_discriminator_hex);
if targets.is_empty() {
return true;
}
let discriminator_hex = match &candidate.instruction_discriminator_hex {
Some(discriminator_hex) => discriminator_hex.to_ascii_lowercase(),
None => return false,
};
for target in targets {
if discriminator_hex == target {
return true;
}
}
return false;
}
fn split_comma_filter(value: std::option::Option<&str>) -> std::vec::Vec<std::string::String> {
let value = match value {
Some(value) => value,
None => return std::vec::Vec::new(),
};
let mut output = std::vec::Vec::new();
for token in value.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
push_unique_string(&mut output, token.to_string());
}
return output;
}
fn candidate_matches_target_event( fn candidate_matches_target_event(
candidate: &crate::OnchainDexPairCandidateDto, candidate: &crate::OnchainDexPairCandidateDto,
target_event: std::option::Option<&str>, target_event: std::option::Option<&str>,
@@ -3027,10 +3499,10 @@ fn candidate_matches_single_target_event(
candidate: &crate::OnchainDexPairCandidateDto, candidate: &crate::OnchainDexPairCandidateDto,
target_event: &str, target_event: &str,
) -> bool { ) -> bool {
if target_event == "unknown_non_swap" if target_event == "unknown_non_swap" {
|| target_event == "audit_non_swap_like" return candidate_is_unknown_non_swap_candidate(candidate);
|| target_event == "non_swap" }
{ if target_event == "audit_non_swap_like" || target_event == "non_swap" {
return candidate_is_non_swap_audit_candidate(candidate); return candidate_is_non_swap_audit_candidate(candidate);
} }
if target_event == "unclassified_instruction" { if target_event == "unclassified_instruction" {
@@ -3180,6 +3652,24 @@ fn candidate_kind_is_explicit_surface(candidate_kind: &str) -> bool {
|| candidate_kind == "close_market"; || candidate_kind == "close_market";
} }
fn candidate_is_unknown_non_swap_candidate(candidate: &crate::OnchainDexPairCandidateDto) -> bool {
if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) {
return false;
}
if candidate_kind_is_explicit_surface(candidate.candidate_kind.as_str()) {
return false;
}
if candidate.candidate_kind == "unclassified_instruction"
|| candidate.candidate_kind == "program_activity"
|| candidate.candidate_kind == "liquidity_or_position"
|| candidate.candidate_kind == "non_swap_activity"
|| candidate.candidate_kind == "upstream_git_instruction"
{
return true;
}
return candidate.instruction_data_prefix.is_some() && candidate.instruction_name.is_none();
}
fn candidate_is_non_swap_audit_candidate(candidate: &crate::OnchainDexPairCandidateDto) -> bool { fn candidate_is_non_swap_audit_candidate(candidate: &crate::OnchainDexPairCandidateDto) -> bool {
if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) { if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) {
return false; return false;
@@ -3659,6 +4149,8 @@ mod tests {
max_pages: 1, max_pages: 1,
scan_order: None, scan_order: None,
target_event: None, target_event: None,
target_instruction_name: None,
target_discriminator_hex: None,
exclude_swaps: false, exclude_swaps: false,
include_failed: true, include_failed: true,
http_role: "history_backfill".to_string(), http_role: "history_backfill".to_string(),
@@ -3689,6 +4181,8 @@ mod tests {
max_pages: 1, max_pages: 1,
scan_order: None, scan_order: None,
target_event: None, target_event: None,
target_instruction_name: None,
target_discriminator_hex: None,
exclude_swaps: false, exclude_swaps: false,
include_failed: true, include_failed: true,
http_role: "history_backfill".to_string(), http_role: "history_backfill".to_string(),
@@ -3713,6 +4207,8 @@ mod tests {
max_pages: 1, max_pages: 1,
scan_order: None, scan_order: None,
target_event: None, target_event: None,
target_instruction_name: None,
target_discriminator_hex: None,
exclude_swaps: false, exclude_swaps: false,
include_failed: true, include_failed: true,
http_role: "history_backfill".to_string(), http_role: "history_backfill".to_string(),
@@ -3762,11 +4258,13 @@ mod tests {
} }
}); });
let extraction = super::extract_candidates_from_transaction( let extraction = super::extract_candidates_from_transaction(
"sig-openbook-v2-raw", "sig-openbook_v2-raw",
&transaction, &transaction,
crate::OPENBOOK_V2_PROGRAM_ID, crate::OPENBOOK_V2_PROGRAM_ID,
Some("openbook_v2".to_string()), Some("openbook_v2".to_string()),
Some("order_place"), Some("order_place"),
None,
None,
); );
assert_eq!(extraction.extracted_candidate_count, 1); assert_eq!(extraction.extracted_candidate_count, 1);
assert_eq!(extraction.target_rejected_candidate_count, 0); assert_eq!(extraction.target_rejected_candidate_count, 0);
@@ -3833,6 +4331,7 @@ mod tests {
inner_instruction_index: Some(2), inner_instruction_index: Some(2),
instruction_name: None, instruction_name: None,
instruction_data_prefix: Some("EVM9wLnauu9H41Gf".to_string()), instruction_data_prefix: Some("EVM9wLnauu9H41Gf".to_string()),
instruction_discriminator_hex: None,
pool_address: None, pool_address: None,
token_a_mint: None, token_a_mint: None,
token_b_mint: None, token_b_mint: None,
@@ -3987,6 +4486,8 @@ mod tests {
max_pages: 2, max_pages: 2,
scan_order: Some("oldest_first".to_string()), scan_order: Some("oldest_first".to_string()),
target_event: Some("claim_fee,remove_liquidity".to_string()), target_event: Some("claim_fee,remove_liquidity".to_string()),
target_instruction_name: None,
target_discriminator_hex: None,
exclude_swaps: true, exclude_swaps: true,
include_failed: true, include_failed: true,
http_role: "history_backfill".to_string(), http_role: "history_backfill".to_string(),
@@ -4036,6 +4537,7 @@ mod tests {
inner_instruction_index: None, inner_instruction_index: None,
instruction_name: Some(instruction_name.to_string()), instruction_name: Some(instruction_name.to_string()),
instruction_data_prefix: Some("prefix".to_string()), instruction_data_prefix: Some("prefix".to_string()),
instruction_discriminator_hex: None,
pool_address: None, pool_address: None,
token_a_mint: None, token_a_mint: None,
token_b_mint: None, token_b_mint: None,
@@ -4076,7 +4578,41 @@ mod tests {
fn target_filter_accepts_open_orders_close_candidates() { fn target_filter_accepts_open_orders_close_candidates() {
let candidate = make_candidate("open_orders_close", "CloseOpenOrdersAccount"); let candidate = make_candidate("open_orders_close", "CloseOpenOrdersAccount");
assert!(super::candidate_matches_target_event(&candidate, Some("open_orders_close"))); assert!(super::candidate_matches_target_event(&candidate, Some("open_orders_close")));
assert!(super::candidate_matches_target_event(&candidate, Some("unknown_non_swap"))); assert!(!super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
assert!(super::candidate_matches_target_event(&candidate, Some("audit_non_swap_like")));
assert!(!super::candidate_matches_target_event(&candidate, Some("close_market"))); assert!(!super::candidate_matches_target_event(&candidate, Some("close_market")));
} }
#[test]
fn target_filter_rejects_known_surface_for_unknown_non_swap() {
let candidate = make_candidate("claim_fee", "CollectProtocolFee");
assert!(!super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
assert!(super::candidate_matches_target_event(&candidate, Some("audit_non_swap_like")));
}
#[test]
fn target_filter_accepts_unclassified_instruction_for_unknown_non_swap() {
let candidate = make_candidate("unclassified_instruction", "UnknownInstruction");
assert!(super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
}
#[test]
fn target_instruction_filters_match_name_and_discriminator() {
let mut candidate = make_candidate("remove_liquidity", "raydium_cpmm.withdraw");
candidate.instruction_discriminator_hex = Some("b712469c946da122".to_string());
assert!(super::candidate_matches_instruction_filters(
&candidate,
Some("withdraw"),
Some("b712469c946da122")
));
assert!(!super::candidate_matches_instruction_filters(
&candidate,
Some("deposit"),
Some("b712469c946da122")
));
assert!(!super::candidate_matches_instruction_filters(
&candidate,
Some("withdraw"),
Some("f223c68952e1f2b6")
));
}
} }

View File

@@ -299,7 +299,7 @@ mod tests {
"instructions": [ "instructions": [
{ {
"programId": crate::METEORA_DBC_PROGRAM_ID, "programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora-dbc", "program": "meteora_dbc",
"stackHeight": 1, "stackHeight": 1,
"accounts": [ "accounts": [
"DbcOriginPool111", "DbcOriginPool111",

View File

@@ -288,6 +288,7 @@ impl TokenBackfillService {
} }
} }
self.backfill_missing_token_metadata_best_effort(100).await; self.backfill_missing_token_metadata_best_effort(100).await;
self.refresh_event_coverage_best_effort().await;
let summary_payload = serde_json::json!({ let summary_payload = serde_json::json!({
"tokenMint": result.token_mint, "tokenMint": result.token_mint,
"mintSignatureCount": result.mint_signature_count, "mintSignatureCount": result.mint_signature_count,
@@ -572,6 +573,26 @@ impl TokenBackfillService {
if let Err(error) = transaction_classification_result { if let Err(error) = transaction_classification_result {
return Err(error); return Err(error);
} }
let instruction_observation_index =
crate::InstructionObservationIndexService::new(self.database.clone());
let instruction_observation_result =
instruction_observation_index.refresh_signature(signature.as_str()).await;
match instruction_observation_result {
Ok(index_result) => {
tracing::debug!(
signature = %signature,
upserted_observation_count = index_result.upserted_observation_count,
"instruction observation index refreshed after signature replay"
);
},
Err(error) => {
tracing::warn!(
signature = %signature,
error = %error,
"instruction observation index refresh failed after signature replay"
);
},
}
return Ok(TokenBackfillSignatureResult { return Ok(TokenBackfillSignatureResult {
resolved_transaction_count: 1, resolved_transaction_count: 1,
missing_transaction_count: 0, missing_transaction_count: 0,
@@ -715,6 +736,7 @@ impl TokenBackfillService {
} }
} }
self.backfill_missing_token_metadata_best_effort(100).await; self.backfill_missing_token_metadata_best_effort(100).await;
self.refresh_event_coverage_best_effort().await;
let summary_payload = serde_json::json!({ let summary_payload = serde_json::json!({
"poolAddress": result.pool_address, "poolAddress": result.pool_address,
"poolSignatureCount": result.pool_signature_count, "poolSignatureCount": result.pool_signature_count,
@@ -785,6 +807,7 @@ impl TokenBackfillService {
Err(error) => return Err(error), Err(error) => return Err(error),
}; };
self.backfill_missing_token_metadata_best_effort(100).await; self.backfill_missing_token_metadata_best_effort(100).await;
self.refresh_event_coverage_best_effort().await;
let result = crate::SignatureBackfillResult { let result = crate::SignatureBackfillResult {
signature: trimmed_signature.clone(), signature: trimmed_signature.clone(),
resolved_transaction_count: replay.resolved_transaction_count, resolved_transaction_count: replay.resolved_transaction_count,
@@ -918,6 +941,26 @@ impl TokenBackfillService {
}, },
} }
} }
async fn refresh_event_coverage_best_effort(&self) {
let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
let refresh_result = coverage_service.refresh_local_counts(None).await;
match refresh_result {
Ok(refresh_result) => {
tracing::debug!(
upserted_entry_count = refresh_result.upserted_entry_count,
summary_count = refresh_result.summaries.len(),
"dex event coverage refreshed after historical replay"
);
},
Err(error) => {
tracing::warn!(
error = %error,
"dex event coverage refresh failed after historical replay"
);
},
}
}
} }
fn token_backfill_should_retry_http_error(error: &crate::Error) -> bool { fn token_backfill_should_retry_http_error(error: &crate::Error) -> bool {

View File

@@ -43,7 +43,7 @@ mod tests {
fn service_search_preserves_normalized_request() { fn service_search_preserves_normalized_request() {
let service = crate::UpstreamRegistryService::new(); let service = crate::UpstreamRegistryService::new();
let request = crate::UpstreamRegistrySearchRequestDto { let request = crate::UpstreamRegistrySearchRequestDto {
decoder_code: Some("raydium-cpmm".to_string()), decoder_code: Some("raydium_cpmm".to_string()),
program_id: None, program_id: None,
program_family: None, program_family: None,
surface_kind: None, surface_kind: None,

File diff suppressed because it is too large Load Diff

View File

@@ -280,7 +280,7 @@ mod tests {
for (entry_name, discriminator_hex) in expected { for (entry_name, discriminator_hex) in expected {
let mut found = false; let mut found = false;
for entry in all_entries.as_slice() { for entry in all_entries.as_slice() {
if entry.decoder_code == "openbook-v2" if entry.decoder_code == "openbook_v2"
&& entry.program_id.as_deref() == Some(crate::OPENBOOK_V2_PROGRAM_ID) && entry.program_id.as_deref() == Some(crate::OPENBOOK_V2_PROGRAM_ID)
&& entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION && entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION
&& entry.entry_name == entry_name && entry.entry_name == entry_name
@@ -312,7 +312,7 @@ mod tests {
Some(matched) => matched, Some(matched) => matched,
None => panic!("OpenBook v2 place_take_order discriminator must match"), None => panic!("OpenBook v2 place_take_order discriminator must match"),
}; };
assert_eq!(matched.decoder_code, "openbook-v2".to_string()); assert_eq!(matched.decoder_code, "openbook_v2".to_string());
assert_eq!(matched.entry_name, "place_take_order".to_string()); assert_eq!(matched.entry_name, "place_take_order".to_string());
assert_eq!(matched.discriminator_hex, Some("032c47031ac7cb55".to_string())); assert_eq!(matched.discriminator_hex, Some("032c47031ac7cb55".to_string()));
} }
@@ -320,65 +320,65 @@ mod tests {
#[test] #[test]
fn registry_contains_priority_family_program_seeds() { fn registry_contains_priority_family_program_seeds() {
let expected_codes = [ let expected_codes = [
"meteora-damm-v2", "meteora_damm_v2",
"meteora-dbc", "meteora_dbc",
"meteora-dlmm", "meteora_dlmm",
"meteora-vault", "meteora_vault",
"raydium-amm-v4", "raydium_amm_v4",
"raydium-clmm", "raydium_clmm",
"raydium-cpmm", "raydium_cpmm",
"raydium-launchpad", "raydium_launchlab",
"raydium-liquidity-locking", "raydium_liquidity_locking",
"raydium-stable-swap", "raydium_stable_swap",
"orca-whirlpool", "orca_whirlpools",
"fluxbeam", "fluxbeam",
"lifinity-amm-v2", "lifinity_v2",
"phoenix-v1", "phoenix_v1",
"openbook-v2", "openbook_v2",
"stabble-stable-swap", "stabble_stable_swap",
"stabble-weighted-swap", "stabble_weighted_swap",
"bonkswap", "bonkswap",
"boop", "boop_fun",
"moonshot", "moonshot",
"heaven", "heaven",
"okx-dex", "okx_dex",
"pancake-swap", "pancake_swap",
"vertigo", "vertigo",
"virtuals", "virtuals",
"wavebreak", "wavebreak",
"onchain-labs-dex-v1", "onchain_labs_dex_v1",
"onchain-labs-dex-v2", "onchain_labs_dex_v2",
"jupiter-swap", "jupiter_swap",
"jupiter-dca", "jupiter_dca",
"jupiter-limit-order", "jupiter_limit_order",
"jupiter-limit-order-2", "jupiter_limit_order_2",
"jupiter-perpetuals", "jupiter_perpetuals",
"jupiter-lend", "jupiter_lend",
"kamino-lending", "kamino_lending",
"kamino-vault", "kamino_vault",
"kamino-farms", "kamino_farms",
"kamino-limit-order", "kamino_limit_order",
"drift-v2", "drift_v2",
"marginfi-v2", "marginfi_v2",
"dflow-aggregator-v4", "dflow_aggregator_v4",
"zeta", "zeta",
"system-program", "system_program",
"token-program", "token_program",
"token-2022", "token_2022",
"associated-token-account", "associated_token_account",
"address-lookup-table", "address_lookup_table",
"memo-program", "memo_program",
"stake-program", "stake_program",
"mpl-token-metadata", "mpl_token_metadata",
"mpl-core", "mpl_core",
"bubblegum", "bubblegum",
"name-service", "name_service",
"marinade-finance", "marinade_finance",
"solayer-restaking-program", "solayer_restaking_program",
"swig", "swig",
"sharky", "sharky",
"circle-message-transmitter-v2", "circle_message_transmitter_v2",
"circle-token-messenger-v2", "circle_token_messenger_v2",
]; ];
let all_entries = crate::upstream_registry_match::upstream_registry_all_entries(); let all_entries = crate::upstream_registry_match::upstream_registry_all_entries();
for expected_code in expected_codes { for expected_code in expected_codes {
@@ -527,7 +527,7 @@ mod tests {
for expected_entry in expected_entries { for expected_entry in expected_entries {
let mut found = false; let mut found = false;
for entry in all_entries.as_slice() { for entry in all_entries.as_slice() {
if entry.decoder_code == "meteora-damm-v2" if entry.decoder_code == "meteora_damm_v2"
&& entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION && entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION
&& entry.entry_name == expected_entry.0 && entry.entry_name == expected_entry.0
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1) && entry.discriminator_hex.as_deref() == Some(expected_entry.1)
@@ -555,7 +555,7 @@ mod tests {
for expected_entry in expected_entries { for expected_entry in expected_entries {
let mut found = false; let mut found = false;
for entry in all_entries.as_slice() { for entry in all_entries.as_slice() {
if entry.decoder_code == "meteora-damm-v2" if entry.decoder_code == "meteora_damm_v2"
&& entry.entry_kind == crate::ENTRY_KIND_EVENT && entry.entry_kind == crate::ENTRY_KIND_EVENT
&& entry.entry_name == expected_entry.0 && entry.entry_name == expected_entry.0
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1) && entry.discriminator_hex.as_deref() == Some(expected_entry.1)
@@ -581,9 +581,9 @@ mod tests {
); );
let matched = match matched { let matched = match matched {
Some(matched) => matched, Some(matched) => matched,
None => panic!("missing meteora-damm-v2 add_liquidity registry match"), None => panic!("missing meteora_damm_v2 add_liquidity registry match"),
}; };
assert_eq!(matched.decoder_code, "meteora-damm-v2"); assert_eq!(matched.decoder_code, "meteora_damm_v2");
assert_eq!(matched.entry_name, "add_liquidity"); assert_eq!(matched.entry_name, "add_liquidity");
assert_eq!(matched.discriminator_hex.as_deref(), Some("b59d59438fb63448")); assert_eq!(matched.discriminator_hex.as_deref(), Some("b59d59438fb63448"));
assert_eq!(matched.proof_status, crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED); assert_eq!(matched.proof_status, crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED);
@@ -599,9 +599,9 @@ mod tests {
); );
let matched = match matched { let matched = match matched {
Some(matched) => matched, Some(matched) => matched,
None => panic!("missing phoenix-v1 swap registry match"), None => panic!("missing phoenix_v1 swap registry match"),
}; };
assert_eq!(matched.decoder_code, "phoenix-v1"); assert_eq!(matched.decoder_code, "phoenix_v1");
assert_eq!(matched.entry_name, "swap"); assert_eq!(matched.entry_name, "swap");
assert_eq!(matched.discriminator_hex.as_deref(), Some("00")); assert_eq!(matched.discriminator_hex.as_deref(), Some("00"));
assert_eq!(matched.discriminator_len, Some(1)); assert_eq!(matched.discriminator_len, Some(1));
@@ -621,7 +621,7 @@ mod tests {
let result = crate::upstream_registry_match::upstream_registry_search(&request); let result = crate::upstream_registry_match::upstream_registry_search(&request);
assert!(result.entries.len() >= 2); assert!(result.entries.len() >= 2);
for entry in result.entries.as_slice() { for entry in result.entries.as_slice() {
assert_eq!(entry.decoder_code, "raydium-cpmm"); assert_eq!(entry.decoder_code, "raydium_cpmm");
} }
} }