0.7.48
This commit is contained in:
22
CHANGELOG.md
22
CHANGELOG.md
@@ -80,14 +80,14 @@
|
||||
0.7.46-demo3 - Correction ciblée de Demo3 pour la découverte/backfill : ajout d’un 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 qu’un `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 d’un 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 d’une tranche DAMM v2 immédiate.
|
||||
|
||||
0.7.47 - Upstream Git Registry / DEX discovery preparation : ajout d’un registre générique `upstream_git` pour indexer `program_id`, discriminants d’instructions/events, noms d’entré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 l’invariant : aucune entrée upstream Git ne produit trade/candle sans decoder spécialisé et corpus local.
|
||||
0.7.47-openbook-v2-audit - Ajout d’un 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 d’un 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 d’une matrice DEX dédiée, ajout explicite des sources Git/IDL à consulter, et redécoupage du plan `0.7.48+` en un DEX/version par tranche afin d’éviter les lots “tous events/tous decoders” trop larges.
|
||||
|
||||
0.7.47-doc-event-coverage - Ajout d'une matrice événementielle complémentaire `DEX_EVENT_COVERAGE_MATRIX.md` pour suivre, par DEX/version, les familles `swap`, `pool_create`, `liquidity`, `position`, `fee`, `reward`, `admin/config`, `mint`, `burn`, `transfer`, `account_create/close`, `wrap/unwrap`, `orderbook`, `vault`, `lock/unlock`, `launch` et `migration`; ajout de `DB_EVENT_MODEL_REVIEW.md` pour clarifier que `k_sol_dex_decoded_events` suffit à l'audit-only mais que des tables transversales sont nécessaires pour exploiter transfers, orderbook, vault, launch/migration et coverage upstream en requêtes métier.
|
||||
0.7.48-pre-event-coverage-sync - Raccordement de `k_sol_dex_event_coverage_entries` au registre upstream Git : ajout de `DexEventCoverageService`, sync des entrées registry vers SQLite, inférence conservatoire `event_family` / `expected_db_target`, mapping local limité aux events Raydium déjà connus, refresh des compteurs observés/matérialisés depuis `k_sol_dex_decoded_events` et tables non-trade existantes, sans modification des decoders ni de la matérialisation trade/candle.
|
||||
0.7.48-pre-event-coverage-fix-docs - Correction du refresh SQL `k_sol_dex_event_coverage_entries` pour éviter les requêtes dynamiques non compatibles avec `sqlx::query` 0.9 ; mise à jour documentaire README/ROADMAP pour acter `0.7.48-pre` comme checkpoint DB/reporting et réaligner la suite sur l’ordre Raydium avant Meteora (`0.7.48 raydium_cpmm`, `0.7.49 raydium_clmm`, puis Pump/Meteora).
|
||||
0.7.48-pre-event-coverage-report - Clôture du checkpoint `0.7.48-pre` : raccordement des summaries `k_sol_dex_event_coverage_entries` aux diagnostics locaux, ajout des compteurs agrégés de couverture au `LocalPipelineDiagnosticSummaryDto` et au `LocalPipelineValidationReportDto`, ajout du profil `0.7.48-pre_event_coverage_db_checkpoint`, exposition du profil dans Demo Pipeline 2, et maintien des invariants : aucun decoder DEX modifié, aucun trade/candle créé, aucun `program_id` promu sans corpus.
|
||||
0.7.48-pre-event-coverage-validation-scope - Correction du profil `0.7.48-pre_event_coverage_db_checkpoint` : le contrôle bloquant des trade candidates non matérialisés est maintenant borné aux DEX attendus de la tranche Raydium (`raydium_cpmm`, `raydium_clmm`, `raydium_amm_v4`) afin qu’un DEX partiel hors scope, comme `fluxbeam`, reste diagnostiqué sans bloquer le checkpoint DB/event coverage.
|
||||
0.7.47 - Upstream Git Registry / DEX discovery preparation : ajout d’un registre générique `upstream_git` pour indexer `program_id`, discriminants d’instructions/events, noms d’entré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 l’invariant : aucune entrée upstream Git ne produit trade/candle sans decoder spécialisé et corpus local.
|
||||
0.7.47-openbook-v2-audit - Ajout d’un 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 d’un 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 d’une matrice DEX dédiée, ajout explicite des sources Git/IDL à consulter, et redécoupage du plan `0.7.48+` en un DEX/version par tranche afin d’éviter les lots “tous events/tous decoders” trop larges.
|
||||
0.7.47-doc-event-coverage - Ajout d'une matrice événementielle complémentaire `DEX_EVENT_COVERAGE_MATRIX.md` pour suivre, par DEX/version, les familles `swap`, `pool_create`, `liquidity`, `position`, `fee`, `reward`, `admin/config`, `mint`, `burn`, `transfer`, `account_create/close`, `wrap/unwrap`, `orderbook`, `vault`, `lock/unlock`, `launch` et `migration`; ajout de `DB_EVENT_MODEL_REVIEW.md` pour clarifier que `k_sol_dex_decoded_events` suffit à l'audit-only mais que des tables transversales sont nécessaires pour exploiter transfers, orderbook, vault, launch/migration et coverage upstream en requêtes métier.
|
||||
0.7.48-pre-event-coverage-sync - Raccordement de `k_sol_dex_event_coverage_entries` au registre upstream Git : ajout de `DexEventCoverageService`, sync des entrées registry vers SQLite, inférence conservatoire `event_family` / `expected_db_target`, mapping local limité aux events Raydium déjà connus, refresh des compteurs observés/matérialisés depuis `k_sol_dex_decoded_events` et tables non-trade existantes, sans modification des decoders ni de la matérialisation trade/candle.
|
||||
0.7.48-pre-event-coverage-fix-docs - Correction du refresh SQL `k_sol_dex_event_coverage_entries` pour éviter les requêtes dynamiques non compatibles avec `sqlx::query` 0.9 ; mise à jour documentaire README/ROADMAP pour acter `0.7.48-pre` comme checkpoint DB/reporting et réaligner la suite sur l’ordre Raydium avant Meteora (`0.7.48 raydium_cpmm`, `0.7.49 raydium_clmm`, puis Pump/Meteora).
|
||||
0.7.48-pre-event-coverage-report - Clôture du checkpoint `0.7.48-pre` : raccordement des summaries `k_sol_dex_event_coverage_entries` aux diagnostics locaux, ajout des compteurs agrégés de couverture au `LocalPipelineDiagnosticSummaryDto` et au `LocalPipelineValidationReportDto`, ajout du profil `0.7.48-pre_event_coverage_db_checkpoint`, exposition du profil dans Demo Pipeline 2, et maintien des invariants : aucun decoder DEX modifié, aucun trade/candle créé, aucun `program_id` promu sans corpus.
|
||||
0.7.48-pre-event-coverage-validation-scope - Correction du profil `0.7.48-pre_event_coverage_db_checkpoint` : le contrôle bloquant des trade candidates non matérialisés est maintenant borné aux DEX attendus de la tranche Raydium (`raydium_cpmm`, `raydium_clmm`, `raydium_amm_v4`) afin qu’un DEX partiel hors scope, comme `fluxbeam`, reste diagnostiqué sans bloquer le checkpoint DB/event coverage.
|
||||
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 - 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 l’instruction 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.
|
||||
|
||||
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.47"
|
||||
version = "0.7.48"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"
|
||||
@@ -89,3 +89,4 @@ manual_unwrap_or_default = "allow"
|
||||
manual_find = "allow"
|
||||
explicit_counter_loop = "allow"
|
||||
get_first = "allow"
|
||||
implicit_saturating_sub = "allow"
|
||||
@@ -289,3 +289,20 @@ Objectif :
|
||||
- materialized events,
|
||||
- missing DB target,
|
||||
- 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 d’index 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.
|
||||
|
||||
@@ -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. |
|
||||
| 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. |
|
||||
| 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 d’abord. |
|
||||
| 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 d’abord. |
|
||||
| 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. |
|
||||
| 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 |
|
||||
| `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 |
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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 ?**
|
||||
|
||||
## Principe
|
||||
|
||||
L’objectif n’est plus seulement de décoder les swaps. L’objectif 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 s’il ne produit jamais de `trade_event` ou de candle.
|
||||
|
||||
## Règle de couverture
|
||||
|
||||
Pour chaque DEX/version, on doit viser trois niveaux :
|
||||
|
||||
| Niveau | Description |
|
||||
| Statut | Sens |
|
||||
|---|---|
|
||||
| `listed` | L’event/instruction existe dans une source Git/IDL/Carbon/Vybe/autre. |
|
||||
| `decoded_audit` | Le code local reconnaît l’event et le persiste dans `k_sol_dex_decoded_events` avec payload structuré ou audit-only. |
|
||||
| `materialized` | L’event alimente une table métier spécialisée : trade, liquidity, lifecycle, fee, reward, admin, mint, burn, orderbook, vault, launch/migration, etc. |
|
||||
| `decoded` | Un decoder local produit un event spécialisé ou un event audit-only classé. |
|
||||
| `materialized` | L'event alimente une table métier existante validée par corpus. |
|
||||
| `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 d’events à 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 |
|
||||
|---|---|---|---|
|
||||
| `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 |
|
||||
## Validation attendue
|
||||
|
||||
## 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é ;
|
||||
- `A` = audit-only local ;
|
||||
- `P` = partiel / doit être complété ;
|
||||
- `L` = listé upstream, non validé localement ;
|
||||
- `-` = non applicable connu ;
|
||||
- `?` = à vérifier.
|
||||
## Note `0.7.48-part2-fix2` — CPMM official instruction parity
|
||||
|
||||
| DEX/version | swap | pool create | liq add/remove | position | fee/reward | admin/config | mint/burn | transfer/account | orderbook | vault | launch/migration | état immédiat |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|
|
||||
| `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. |
|
||||
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`.
|
||||
|
||||
## Points critiques manquants dans l’ancienne 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 ;
|
||||
- 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.
|
||||
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`.
|
||||
|
||||
Action : ajouter `burn` à toutes les checklists DEX et aux diagnostics de couverture.
|
||||
|
||||
### `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 n’est 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.
|
||||
`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é.
|
||||
|
||||
@@ -118,75 +118,75 @@ Les entrées de registre doivent être exposées à `kb_demo_app` via une comman
|
||||
DEX / AMM / CLMM / orderbook :
|
||||
|
||||
```text
|
||||
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
|
||||
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_whirlpools
|
||||
fluxbeam
|
||||
lifinity-amm-v2
|
||||
phoenix-v1
|
||||
openbook-v2
|
||||
stabble-stable-swap
|
||||
stabble-weighted-swap
|
||||
lifinity_v2
|
||||
phoenix_v1
|
||||
openbook_v2
|
||||
stabble_stable_swap
|
||||
stabble_weighted_swap
|
||||
bonkswap
|
||||
boop
|
||||
moonshot
|
||||
heaven
|
||||
okx-dex
|
||||
pancake-swap
|
||||
okx_dex
|
||||
pancake_swap
|
||||
vertigo
|
||||
virtuals
|
||||
wavebreak
|
||||
onchain-labs-dex-v1
|
||||
onchain-labs-dex-v2
|
||||
onchain_labs_dex_v1
|
||||
onchain_labs_dex_v2
|
||||
```
|
||||
|
||||
Agrégateurs / ordres / perps / lending :
|
||||
|
||||
```text
|
||||
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
|
||||
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 :
|
||||
|
||||
```text
|
||||
system-program
|
||||
token-program
|
||||
token-2022
|
||||
associated-token-account
|
||||
address-lookup-table
|
||||
memo-program
|
||||
stake-program
|
||||
mpl-token-metadata
|
||||
mpl-core
|
||||
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
|
||||
name_service
|
||||
marinade_finance
|
||||
solayer_restaking_program
|
||||
swig
|
||||
sharky
|
||||
circle-message-transmitter-v2
|
||||
circle-token-messenger-v2
|
||||
circle_message_transmitter_v2
|
||||
circle_token_messenger_v2
|
||||
```
|
||||
|
||||
## Règles de validation
|
||||
|
||||
327
NEXT_SESSION_PROMPT_0.7.49_RAYDIUM_CLMM.md
Normal file
327
NEXT_SESSION_PROMPT_0.7.49_RAYDIUM_CLMM.md
Normal 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
|
||||
```
|
||||
204
RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md
Normal file
204
RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md
Normal 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`.
|
||||
44
README.md
44
README.md
@@ -223,7 +223,6 @@ Chaque DEX ou variante de DEX doit avoir sa propre étape de validation. Les fam
|
||||
|
||||
- `pump_fun` ;
|
||||
- `raydium_launchlab` ;
|
||||
- `raydium_launchpad` ;
|
||||
- `letsbonk` / `bonk_fun` ;
|
||||
- `bags` ;
|
||||
- `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 ;
|
||||
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 ;
|
||||
6. `0.7.48` : reprise séparée de `meteora_damm_v2` ;
|
||||
7. `0.7.49` : reprise séparée de `meteora_dbc` ;
|
||||
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.
|
||||
6. `0.7.48-pre` : event coverage + DB model checkpoint — table coverage, sync upstream, refresh counts, diagnostics et profil validation ;
|
||||
7. `0.7.48` : reprise séparée de `raydium_cpmm` ;
|
||||
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 :
|
||||
|
||||
@@ -463,3 +465,37 @@ La suite fonctionnelle reprend par Raydium avant Meteora :
|
||||
3. `0.7.50` — `pump_swap` ;
|
||||
4. `0.7.51` — `pump_fun` ;
|
||||
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 l’usage 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`.
|
||||
|
||||
51
ROADMAP.md
51
ROADMAP.md
@@ -851,7 +851,6 @@ Matrice cible initiale :
|
||||
| `raydium_cpmm` | AMM | 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_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_router` | router | partiel | ne pas matérialiser en trade direct avant preuve |
|
||||
| `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 :
|
||||
|
||||
- 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` ;
|
||||
- agrégateurs / ordres / perps / lending utiles au routage ou à l’analyse : `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`.
|
||||
- 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 à l’analyse : `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`.
|
||||
|
||||
Aucun de ces programmes ne doit être marqué `verified_by_corpus` uniquement parce qu’il 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.
|
||||
|
||||
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 l’absence 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.
|
||||
|
||||
231
SQL_VALIDATION_RAYDIUM_CPMM_0_7_48.sql
Normal file
231
SQL_VALIDATION_RAYDIUM_CPMM_0_7_48.sql
Normal 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;
|
||||
@@ -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">Use this to find corpus signatures for non-swap decoders without promoting unverified events. Leave all unchecked to request target='any'.</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">
|
||||
<label for="demo3HttpRoleInput" class="form-label">HTTP role</label>
|
||||
<input id="demo3HttpRoleInput" type="text" class="form-control" value="history_backfill" />
|
||||
@@ -335,6 +345,7 @@
|
||||
<th>Kind</th>
|
||||
<th>Confidence</th>
|
||||
<th>Data prefix</th>
|
||||
<th>Discriminator</th>
|
||||
<th>Verified pool</th>
|
||||
<th>Token A</th>
|
||||
<th>Token B</th>
|
||||
@@ -346,7 +357,7 @@
|
||||
</thead>
|
||||
<tbody id="demo3OnchainCandidateTableBody">
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -416,7 +416,7 @@
|
||||
Aucun jeu de candles chargé.
|
||||
</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>
|
||||
|
||||
@@ -44,6 +44,14 @@ scanOrder: string | null,
|
||||
* Optional target event family used to find non-swap signatures.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -54,6 +54,10 @@ instructionName: string | null,
|
||||
* Prefix of the raw base58 instruction data, useful for audit grouping.
|
||||
*/
|
||||
instructionDataPrefix: string | null,
|
||||
/**
|
||||
* First eight instruction-data bytes as lower hex.
|
||||
*/
|
||||
instructionDiscriminatorHex: string | null,
|
||||
/**
|
||||
* Candidate pool address.
|
||||
*/
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
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 {
|
||||
@@ -320,6 +334,8 @@ function readOnchainRequest(): Demo3OnchainDexDiscoveryRequest {
|
||||
maxPages: intValue("demo3MaxPagesInput", 1),
|
||||
scanOrder: valueOrNull(byId<HTMLSelectElement>("demo3ScanOrderSelect").value),
|
||||
targetEvent: readTargetEventFilter(),
|
||||
targetInstructionName: valueOrNull(byId<HTMLInputElement>("demo3TargetInstructionNameInput").value),
|
||||
targetDiscriminatorHex: valueOrNull(byId<HTMLInputElement>("demo3TargetDiscriminatorHexInput").value),
|
||||
excludeSwaps: byId<HTMLInputElement>("demo3ExcludeSwapsInput").checked,
|
||||
includeFailed: byId<HTMLInputElement>("demo3IncludeFailedInput").checked,
|
||||
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>("demo3SummaryCandidateCount").textContent = String(result.candidateCount);
|
||||
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(",");
|
||||
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>("demo3NextBeforeText").textContent = result.nextBeforeByAddress.length === 0 ? "-" : result.nextBeforeByAddress.map((cursor) => `${cursor.address}:${cursor.nextBeforeSignature ?? "-"}`).join(" | ");
|
||||
renderRejectedSummary(result);
|
||||
@@ -402,7 +420,7 @@ function renderRejectedSummary(result: Demo3OnchainDexDiscoveryResult): void {
|
||||
function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): void {
|
||||
const body = byId<HTMLTableSectionElement>("demo3OnchainCandidateTableBody");
|
||||
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;
|
||||
}
|
||||
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-${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.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(candidate.tokenAMint ?? "")}">${escapeHtml(shortText(candidate.tokenAMint, 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;
|
||||
}
|
||||
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 {
|
||||
const payload = await invoke<Demo3OnchainDexDiscoveryPayload>("demo3_discover_onchain_dex_pairs", { request });
|
||||
lastResultJson = payload.resultJson;
|
||||
|
||||
@@ -186,6 +186,32 @@ function renderCatalogTextareas(
|
||||
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[] {
|
||||
if (raw.trim() === "") {
|
||||
return [];
|
||||
@@ -239,16 +265,18 @@ function renderCandlesChart(
|
||||
);
|
||||
|
||||
const ohlcData = sorted.map((candle) => [
|
||||
candle.open_price_quote_per_base,
|
||||
candle.close_price_quote_per_base,
|
||||
candle.low_price_quote_per_base,
|
||||
candle.high_price_quote_per_base,
|
||||
toChartNumber(candle.open_price_quote_per_base),
|
||||
toChartNumber(candle.close_price_quote_per_base),
|
||||
toChartNumber(candle.low_price_quote_per_base),
|
||||
toChartNumber(candle.high_price_quote_per_base),
|
||||
]);
|
||||
|
||||
const volumeData = sorted.map((candle) =>
|
||||
parseVolume(candle.quote_volume_raw, candle.trade_count),
|
||||
);
|
||||
|
||||
const zoomStart = calculateVisibleWindowStart(sorted.length);
|
||||
|
||||
chartMeta.textContent =
|
||||
`Pair #${pairId.toString()} • timeframe ${timeframeSeconds.toString()}s • ${sorted.length} candles`;
|
||||
|
||||
@@ -256,7 +284,8 @@ function renderCandlesChart(
|
||||
animation: false,
|
||||
legend: {
|
||||
data: ["OHLC", "Volume"],
|
||||
top: 0,
|
||||
top: 4,
|
||||
left: 16,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
@@ -268,8 +297,8 @@ function renderCandlesChart(
|
||||
link: [{ xAxisIndex: "all" }],
|
||||
},
|
||||
grid: [
|
||||
{ left: 60, right: 24, top: 40, height: "58%" },
|
||||
{ left: 60, right: 24, top: "74%", height: "16%" },
|
||||
{ left: 76, right: 32, top: 52, height: "58%" },
|
||||
{ left: 76, right: 32, top: "77%", height: "12%" },
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
@@ -277,7 +306,9 @@ function renderCandlesChart(
|
||||
data: categoryData,
|
||||
boundaryGap: true,
|
||||
axisLine: { onZero: false },
|
||||
splitLine: { show: false },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitLine: { show: true },
|
||||
axisLabel: { hideOverlap: true, margin: 14 },
|
||||
min: "dataMin",
|
||||
max: "dataMax",
|
||||
},
|
||||
@@ -287,9 +318,9 @@ function renderCandlesChart(
|
||||
data: categoryData,
|
||||
boundaryGap: true,
|
||||
axisLine: { onZero: false },
|
||||
axisTick: { show: false },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
axisLabel: { hideOverlap: true, margin: 10 },
|
||||
min: "dataMin",
|
||||
max: "dataMax",
|
||||
},
|
||||
@@ -297,27 +328,33 @@ function renderCandlesChart(
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
splitNumber: 5,
|
||||
splitArea: { show: false },
|
||||
axisLabel: { margin: 12 },
|
||||
},
|
||||
{
|
||||
gridIndex: 1,
|
||||
scale: true,
|
||||
splitNumber: 2,
|
||||
axisLabel: { margin: 12 },
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: "inside",
|
||||
xAxisIndex: [0, 1],
|
||||
start: 0,
|
||||
filterMode: "none",
|
||||
start: zoomStart,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
type: "slider",
|
||||
xAxisIndex: [0, 1],
|
||||
bottom: 6,
|
||||
start: 0,
|
||||
filterMode: "none",
|
||||
bottom: 8,
|
||||
height: 22,
|
||||
start: zoomStart,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
@@ -326,11 +363,12 @@ function renderCandlesChart(
|
||||
name: "OHLC",
|
||||
type: "candlestick",
|
||||
data: ohlcData,
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
barMinWidth: 4,
|
||||
barMaxWidth: 16,
|
||||
itemStyle: {
|
||||
color: "#16a34a",
|
||||
color0: "#dc2626",
|
||||
borderColor: "#15803d",
|
||||
borderColor0: "#b91c1c",
|
||||
borderWidth: 1.4,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -339,9 +377,13 @@ function renderCandlesChart(
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: volumeData,
|
||||
barMinWidth: 2,
|
||||
barMaxWidth: 10,
|
||||
},
|
||||
],
|
||||
}, true);
|
||||
|
||||
window.setTimeout(() => chart.resize(), 0);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
@@ -468,6 +510,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const chart = echarts.init(safeChartElement);
|
||||
setEmptyChart(chart, safeChartMeta, "Aucune candle disponible.");
|
||||
window.addEventListener("resize", () => chart.resize());
|
||||
const chartCollapse = document.querySelector<HTMLDivElement>("#demoPipeline2ChartCollapse");
|
||||
chartCollapse?.addEventListener("shown.bs.collapse", () => {
|
||||
window.setTimeout(() => chart.resize(), 0);
|
||||
});
|
||||
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kb-demo-app",
|
||||
"private": true,
|
||||
"version": "0.7.47",
|
||||
"version": "0.7.48",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -287,7 +287,10 @@ pub(crate) async fn demo3_search_local_dex_corpus(
|
||||
|
||||
/// Search request for the static upstream registry exposed through Demo3.
|
||||
#[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")]
|
||||
pub(crate) struct Demo3UpstreamRegistrySearchRequest {
|
||||
/// 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 Ok(Demo3UpstreamRegistryPayload {
|
||||
result_json,
|
||||
result: ui_result,
|
||||
});
|
||||
return Ok(Demo3UpstreamRegistryPayload { result_json, result: ui_result });
|
||||
}
|
||||
|
||||
fn to_lib_upstream_registry_request(
|
||||
@@ -491,8 +491,7 @@ fn from_lib_upstream_registry_summary(
|
||||
account_entry_count: summary.account_entry_count,
|
||||
upstream_git_unverified_count: summary.upstream_git_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,
|
||||
upstream_git_local_corpus_observed_count: summary.upstream_git_local_corpus_observed_count,
|
||||
upstream_git_local_corpus_materialized_count: summary
|
||||
.upstream_git_local_corpus_materialized_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>,
|
||||
/// Optional target event family used to find non-swap signatures.
|
||||
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.
|
||||
pub exclude_swaps: bool,
|
||||
/// 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>,
|
||||
/// Prefix of the raw base58 instruction data, useful for audit grouping.
|
||||
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.
|
||||
pub pool_address: std::option::Option<std::string::String>,
|
||||
/// Candidate token A mint.
|
||||
@@ -966,6 +973,8 @@ fn to_lib_onchain_request(
|
||||
max_pages: request.max_pages,
|
||||
scan_order: normalize_optional_text(request.scan_order.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,
|
||||
include_failed: request.include_failed,
|
||||
http_role: request.http_role.trim().to_string(),
|
||||
@@ -994,6 +1003,8 @@ fn from_lib_onchain_result(
|
||||
max_pages: result.request.max_pages,
|
||||
scan_order: result.request.scan_order,
|
||||
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,
|
||||
include_failed: result.request.include_failed,
|
||||
http_role: result.request.http_role,
|
||||
@@ -1074,6 +1085,7 @@ fn from_lib_onchain_candidate(
|
||||
inner_instruction_index: candidate.inner_instruction_index,
|
||||
instruction_name: candidate.instruction_name,
|
||||
instruction_data_prefix: candidate.instruction_data_prefix,
|
||||
instruction_discriminator_hex: candidate.instruction_discriminator_hex,
|
||||
pool_address: candidate.pool_address,
|
||||
token_a_mint: candidate.token_a_mint,
|
||||
token_b_mint: candidate.token_b_mint,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "kb-demo-app",
|
||||
"version": "0.7.47",
|
||||
"version": "0.7.48",
|
||||
"identifier": "com.sasedev.kb-demo-app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -288,7 +288,7 @@ pub const RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID: &str = "5quBtoiQqxF9Jv6KYKctB59NT3
|
||||
pub const BONKSWAP_PROGRAM_ID: &str = "BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p";
|
||||
|
||||
/// 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.
|
||||
pub const DFLOW_AGGREGATOR_V4_PROGRAM_ID: &str = "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH";
|
||||
|
||||
@@ -27,6 +27,7 @@ pub use dtos::DexDto;
|
||||
pub use dtos::DexEventCoverageEntryDto;
|
||||
pub use dtos::DexEventCoverageSummaryDto;
|
||||
pub use dtos::FeeEventDto;
|
||||
pub use dtos::InstructionObservationDto;
|
||||
pub use dtos::KnownHttpEndpointDto;
|
||||
pub use dtos::KnownWsEndpointDto;
|
||||
pub use dtos::LaunchAttributionDto;
|
||||
@@ -96,6 +97,7 @@ pub use entities::DexEntity;
|
||||
pub use entities::DexEventCoverageEntryEntity;
|
||||
pub use entities::DexEventCoverageSummaryEntity;
|
||||
pub use entities::FeeEventEntity;
|
||||
pub use entities::InstructionObservationEntity;
|
||||
pub use entities::KnownHttpEndpointEntity;
|
||||
pub use entities::KnownWsEndpointEntity;
|
||||
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_list_recent;
|
||||
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_list;
|
||||
pub use queries::query_known_http_endpoints_upsert;
|
||||
|
||||
@@ -15,6 +15,7 @@ mod dex_event_coverage_entry;
|
||||
mod fee_event;
|
||||
mod known_http_endpoint;
|
||||
mod known_ws_endpoint;
|
||||
mod instruction_observation;
|
||||
mod launch_attribution;
|
||||
mod launch_surface;
|
||||
mod launch_surface_key;
|
||||
@@ -84,6 +85,7 @@ pub use dex_event_coverage_entry::DexEventCoverageSummaryDto;
|
||||
pub use fee_event::FeeEventDto;
|
||||
pub use known_http_endpoint::KnownHttpEndpointDto;
|
||||
pub use known_ws_endpoint::KnownWsEndpointDto;
|
||||
pub use instruction_observation::InstructionObservationDto;
|
||||
pub use launch_attribution::LaunchAttributionDto;
|
||||
pub use launch_surface::LaunchSurfaceDto;
|
||||
pub use launch_surface_key::LaunchSurfaceKeyDto;
|
||||
|
||||
155
kb_lib/src/db/dtos/instruction_observation.rs
Normal file
155
kb_lib/src/db/dtos/instruction_observation.rs
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod dex_event_coverage_entry;
|
||||
mod fee_event;
|
||||
mod known_http_endpoint;
|
||||
mod known_ws_endpoint;
|
||||
mod instruction_observation;
|
||||
mod launch_attribution;
|
||||
mod launch_surface;
|
||||
mod launch_surface_key;
|
||||
@@ -62,6 +63,7 @@ pub use dex_event_coverage_entry::DexEventCoverageSummaryEntity;
|
||||
pub use fee_event::FeeEventEntity;
|
||||
pub use known_http_endpoint::KnownHttpEndpointEntity;
|
||||
pub use known_ws_endpoint::KnownWsEndpointEntity;
|
||||
pub use instruction_observation::InstructionObservationEntity;
|
||||
pub use launch_attribution::LaunchAttributionEntity;
|
||||
pub use launch_surface::LaunchSurfaceEntity;
|
||||
pub use launch_surface_key::LaunchSurfaceKeyEntity;
|
||||
|
||||
52
kb_lib/src/db/entities/instruction_observation.rs
Normal file
52
kb_lib/src/db/entities/instruction_observation.rs
Normal 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,
|
||||
}
|
||||
@@ -15,6 +15,7 @@ mod dex_event_coverage_entry;
|
||||
mod fee_event;
|
||||
mod known_http_endpoint;
|
||||
mod known_ws_endpoint;
|
||||
mod instruction_observation;
|
||||
mod launch_attribution;
|
||||
mod launch_surface;
|
||||
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_list;
|
||||
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_list_by_pool_id;
|
||||
pub use launch_attribution::query_launch_attributions_upsert;
|
||||
|
||||
@@ -442,7 +442,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
||||
Some("raydium-amm-v4".to_string()),
|
||||
Some("raydium_amm_v4".to_string()),
|
||||
Some(1),
|
||||
r#"["Account0","Pool111","Lp111","TokenA111","TokenB111"]"#.to_string(),
|
||||
None,
|
||||
@@ -529,7 +529,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dlmm".to_string()),
|
||||
Some("meteora_dlmm".to_string()),
|
||||
Some(1),
|
||||
r#"["ParentAccount","Pool111"]"#.to_string(),
|
||||
None,
|
||||
@@ -548,7 +548,7 @@ mod tests {
|
||||
0,
|
||||
Some(0),
|
||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dlmm".to_string()),
|
||||
Some("meteora_dlmm".to_string()),
|
||||
Some(2),
|
||||
r#"["ChildAccount","Pool111"]"#.to_string(),
|
||||
None,
|
||||
|
||||
@@ -735,7 +735,7 @@ mod tests {
|
||||
let database = make_database().await;
|
||||
let upstream_service = crate::UpstreamRegistryService::new();
|
||||
let request = crate::UpstreamRegistrySearchRequestDto {
|
||||
decoder_code: Some("raydium-cpmm".to_string()),
|
||||
decoder_code: Some("raydium_cpmm".to_string()),
|
||||
program_id: None,
|
||||
program_family: None,
|
||||
surface_kind: None,
|
||||
@@ -759,7 +759,7 @@ mod tests {
|
||||
.expect("coverage upsert must succeed");
|
||||
assert!(id > 0);
|
||||
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
|
||||
.expect("coverage list must succeed");
|
||||
assert_eq!(rows.len(), 1);
|
||||
@@ -768,7 +768,7 @@ mod tests {
|
||||
.await
|
||||
.expect("coverage summary must succeed");
|
||||
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].decoded_entry_count, 1);
|
||||
assert_eq!(summaries[0].observed_entry_count, 1);
|
||||
|
||||
173
kb_lib/src/db/queries/instruction_observation.rs
Normal file
173
kb_lib/src/db/queries/instruction_observation.rs
Normal 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);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -230,6 +230,26 @@ pub(crate) async fn ensure_schema(database: &crate::Database) -> Result<(), crat
|
||||
if let Err(error) = result {
|
||||
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;
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
@@ -1423,6 +1443,104 @@ ON k_sol_chain_instructions (program_id)
|
||||
.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`.
|
||||
async fn create_tbl_dex_decoded_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
|
||||
return execute_sqlite_schema_statement(
|
||||
|
||||
@@ -77,6 +77,10 @@ pub use raydium_clmm::RaydiumClmmSwapLegacyDecoded;
|
||||
pub use raydium_clmm::RaydiumClmmSwapV2Decoded;
|
||||
pub use raydium_clmm::decode_raydium_clmm_instruction;
|
||||
pub use raydium_cpmm::RaydiumCpmmDecodedEvent;
|
||||
pub use raydium_cpmm::RaydiumCpmmLpChangeEventDecoded;
|
||||
pub use raydium_cpmm::RaydiumCpmmSwapDecoded;
|
||||
pub use raydium_cpmm::RaydiumCpmmSwapEventDecoded;
|
||||
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_program_data_event;
|
||||
|
||||
@@ -3009,7 +3009,7 @@ fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSid
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
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(1779500001),
|
||||
Some("helius_primary_http".to_string()),
|
||||
@@ -3042,7 +3042,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||
Some("meteora-damm-v1".to_string()),
|
||||
Some("meteora_damm_v1".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DammV1Pool111",
|
||||
@@ -3074,7 +3074,7 @@ mod tests {
|
||||
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
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(1779500002),
|
||||
Some("helius_primary_http".to_string()),
|
||||
@@ -3107,7 +3107,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||
Some("meteora-damm-v1".to_string()),
|
||||
Some("meteora_damm_v1".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!(["DammV1SwapPool111", "DammV1SwapTokenA111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
@@ -3141,7 +3141,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||
Some("meteora-damm-v1".to_string()),
|
||||
Some("meteora_damm_v1".to_string()),
|
||||
Some(1),
|
||||
accounts.to_string(),
|
||||
Some(format!("\"{}\"", bs58::encode(data).into_string())),
|
||||
|
||||
@@ -758,7 +758,7 @@ fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
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(1779400001),
|
||||
Some("helius_primary_http".to_string()),
|
||||
@@ -791,7 +791,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
||||
Some("meteora-damm-v2".to_string()),
|
||||
Some("meteora_damm_v2".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DammV2Pool111",
|
||||
@@ -823,7 +823,7 @@ mod tests {
|
||||
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
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(1779400002),
|
||||
Some("helius_primary_http".to_string()),
|
||||
@@ -856,7 +856,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
||||
Some("meteora-damm-v2".to_string()),
|
||||
Some("meteora_damm_v2".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!(["DammV2SwapPool111", "DammV2SwapTokenA111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
|
||||
@@ -727,7 +727,7 @@ fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-dbc-create-1".to_string(),
|
||||
"sig-meteora_dbc-create-1".to_string(),
|
||||
Some(888001),
|
||||
Some(1779300001),
|
||||
Some("helius_primary_http".to_string()),
|
||||
@@ -760,7 +760,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dbc".to_string()),
|
||||
Some("meteora_dbc".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DbcPool111",
|
||||
@@ -791,7 +791,7 @@ mod tests {
|
||||
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-dbc-swap-1".to_string(),
|
||||
"sig-meteora_dbc-swap-1".to_string(),
|
||||
Some(888002),
|
||||
Some(1779300002),
|
||||
Some("helius_primary_http".to_string()),
|
||||
@@ -824,7 +824,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dbc".to_string()),
|
||||
Some("meteora_dbc".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!(["DbcPoolSwap111", "DbcSwapTokenA111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
|
||||
@@ -2646,7 +2646,7 @@ fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-dlmm-create-1".to_string(),
|
||||
"sig-meteora_dlmm-create-1".to_string(),
|
||||
Some(888101),
|
||||
Some(1779400001),
|
||||
Some("helius_primary_http".to_string()),
|
||||
@@ -2679,7 +2679,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dlmm".to_string()),
|
||||
Some("meteora_dlmm".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DlmmPair111",
|
||||
@@ -2708,7 +2708,7 @@ mod tests {
|
||||
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-dlmm-swap-1".to_string(),
|
||||
"sig-meteora_dlmm-swap-1".to_string(),
|
||||
Some(888102),
|
||||
Some(1779400002),
|
||||
Some("helius_primary_http".to_string()),
|
||||
@@ -2741,7 +2741,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dlmm".to_string()),
|
||||
Some("meteora_dlmm".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!(["DlmmPairSwap111", "DlmmSwapTokenX111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
@@ -2906,7 +2906,7 @@ mod tests {
|
||||
fn meteora_dlmm_inner_swap2_instruction_is_not_skipped() {
|
||||
let decoder = crate::MeteoraDlmmDecoder::new();
|
||||
let mut transaction = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-dlmm-inner-swap2".to_string(),
|
||||
"sig-meteora_dlmm-inner-swap2".to_string(),
|
||||
Some(888103),
|
||||
Some(1779400003),
|
||||
Some("helius_primary_http".to_string()),
|
||||
@@ -2933,7 +2933,7 @@ mod tests {
|
||||
3,
|
||||
Some(14),
|
||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dlmm".to_string()),
|
||||
Some("meteora_dlmm".to_string()),
|
||||
Some(2),
|
||||
serde_json::json!([
|
||||
"LbPair111",
|
||||
@@ -3030,7 +3030,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dlmm".to_string()),
|
||||
Some("meteora_dlmm".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"Position111",
|
||||
@@ -3083,7 +3083,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dlmm".to_string()),
|
||||
Some("meteora_dlmm".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DlmmPairFee111",
|
||||
@@ -3132,7 +3132,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dlmm".to_string()),
|
||||
Some("meteora_dlmm".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"Position111",
|
||||
|
||||
@@ -102,7 +102,7 @@ impl OpenBookV2Decoder {
|
||||
Some(registry_match) => registry_match,
|
||||
None => continue,
|
||||
};
|
||||
if registry_match.decoder_code.as_str() != "openbook-v2" {
|
||||
if registry_match.decoder_code.as_str() != "openbook_v2" {
|
||||
continue;
|
||||
}
|
||||
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
|
||||
|
||||
@@ -532,7 +532,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
||||
Some("orca-whirlpools".to_string()),
|
||||
Some("orca_whirlpools".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"OrcaPool111",
|
||||
@@ -599,7 +599,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
||||
Some("orca-whirlpools".to_string()),
|
||||
Some("orca_whirlpools".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!(["OrcaSwapPool111", "OrcaSwapTokenA111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
|
||||
@@ -101,7 +101,7 @@ impl PhoenixV1Decoder {
|
||||
Some(registry_match) => registry_match,
|
||||
None => continue,
|
||||
};
|
||||
if registry_match.decoder_code.as_str() != "phoenix-v1" {
|
||||
if registry_match.decoder_code.as_str() != "phoenix_v1" {
|
||||
continue;
|
||||
}
|
||||
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
|
||||
|
||||
@@ -1080,7 +1080,7 @@ mod tests {
|
||||
0,
|
||||
None,
|
||||
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
||||
Some("raydium-amm-v4".to_string()),
|
||||
Some("raydium_amm_v4".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"Account0",
|
||||
@@ -1215,7 +1215,7 @@ mod tests {
|
||||
4,
|
||||
Some(0),
|
||||
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
||||
Some("raydium-amm-v4".to_string()),
|
||||
Some("raydium_amm_v4".to_string()),
|
||||
Some(2),
|
||||
serde_json::json!([
|
||||
crate::SPL_TOKEN_PROGRAM_ID,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1012,11 +1012,11 @@ impl DexDecodeService {
|
||||
"raydium_cpmm",
|
||||
crate::RAYDIUM_CPMM_PROGRAM_ID.to_string(),
|
||||
event_kind.as_str(),
|
||||
Some(decoded_event.pool_account().to_string()),
|
||||
None,
|
||||
Some(decoded_event.base_mint().to_string()),
|
||||
Some(decoded_event.quote_mint().to_string()),
|
||||
decoded_event.pool_account().map(|value| return value.to_string()),
|
||||
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,
|
||||
)
|
||||
.await;
|
||||
@@ -1174,6 +1174,7 @@ impl DexDecodeService {
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
|
||||
let mut persisted = std::vec::Vec::new();
|
||||
let mut program_data_events = collect_raydium_cpmm_program_data_events(transaction);
|
||||
for instruction in instructions {
|
||||
let program_id = match instruction.program_id.as_ref() {
|
||||
Some(program_id) => program_id,
|
||||
@@ -1186,6 +1187,8 @@ impl DexDecodeService {
|
||||
Some(data_json) => data_json,
|
||||
None => continue,
|
||||
};
|
||||
let instruction_kind =
|
||||
crate::classify_raydium_cpmm_instruction_data(data_json.as_str());
|
||||
let decoded_events = crate::decode_raydium_cpmm_instruction(
|
||||
instruction.accounts_json.as_str(),
|
||||
data_json.as_str(),
|
||||
@@ -1199,6 +1202,18 @@ impl DexDecodeService {
|
||||
};
|
||||
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);
|
||||
}
|
||||
@@ -1808,6 +1823,11 @@ struct RaydiumMappedNonTradeInstructionSpec {
|
||||
enum RaydiumMappedNonTradeAmountLayout {
|
||||
None,
|
||||
ClmmLiquidityV2,
|
||||
CpmmAmmConfig,
|
||||
CpmmDeposit,
|
||||
CpmmFeePair,
|
||||
CpmmInitialize,
|
||||
CpmmPoolStatus,
|
||||
CpmmWithdraw,
|
||||
}
|
||||
|
||||
@@ -1894,26 +1914,81 @@ fn raydium_mapped_non_trade_instruction_spec(
|
||||
}
|
||||
}
|
||||
if protocol_name == "raydium_cpmm" {
|
||||
if discriminator_hex == "1416567bc61cdb84" && account_count >= 14 {
|
||||
if discriminator_hex == "9c5420764587467b" && account_count >= 4 {
|
||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: "collect_creator_fee",
|
||||
event_kind: "raydium_cpmm.collect_creator_fee",
|
||||
pool_account_index: Some(3),
|
||||
instruction_name: "close_permission_pda",
|
||||
event_kind: "raydium_cpmm.close_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 == "b712469c946da122" && account_count >= 14 {
|
||||
if discriminator_hex == "1416567bc61cdb84" && account_count >= 13 {
|
||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: "withdraw",
|
||||
event_kind: "raydium_cpmm.withdraw",
|
||||
pool_account_index: Some(3),
|
||||
instruction_name: "collect_creator_fee",
|
||||
event_kind: "raydium_cpmm.collect_creator_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::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_b_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 {
|
||||
@@ -1923,8 +1998,52 @@ fn raydium_mapped_non_trade_instruction_spec(
|
||||
pool_account_index: Some(3),
|
||||
token_a_mint_index: Some(4),
|
||||
token_b_mint_index: Some(5),
|
||||
lp_mint_index: Some(13),
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
||||
lp_mint_index: Some(6),
|
||||
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(),
|
||||
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(
|
||||
"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 => {
|
||||
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
|
||||
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> {
|
||||
if data.len() < offset + 8 {
|
||||
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(
|
||||
persisted: &[crate::DexDecodedEventDto],
|
||||
) -> std::collections::HashSet<i64> {
|
||||
@@ -2603,7 +2970,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
|
||||
"program": "raydium-amm-v4",
|
||||
"program": "raydium_amm_v4",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"Account0",
|
||||
@@ -2887,7 +3254,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
||||
"program": "meteora-dbc",
|
||||
"program": "meteora_dbc",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"DbcPoolDecode111",
|
||||
@@ -2962,7 +3329,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
|
||||
"program": "meteora-damm-v2",
|
||||
"program": "meteora_damm_v2",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"DammV2DecodePool111",
|
||||
@@ -3039,7 +3406,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
|
||||
"program": "meteora-damm-v1",
|
||||
"program": "meteora_damm_v1",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"DammV1DecodePool111",
|
||||
@@ -3116,7 +3483,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
|
||||
"program": "orca-whirlpools",
|
||||
"program": "orca_whirlpools",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"OrcaDecodePool111",
|
||||
@@ -3488,36 +3855,32 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn maps_observed_raydium_cpmm_non_swap_discriminators() {
|
||||
let collect_creator_fee = super::raydium_mapped_non_trade_instruction_spec(
|
||||
"raydium_cpmm",
|
||||
Some("1416567bc61cdb84"),
|
||||
14,
|
||||
);
|
||||
let collect_creator_fee = match collect_creator_fee {
|
||||
Some(collect_creator_fee) => collect_creator_fee,
|
||||
None => panic!("collect_creator_fee discriminator must be mapped"),
|
||||
};
|
||||
assert_eq!(collect_creator_fee.event_kind, "raydium_cpmm.collect_creator_fee");
|
||||
let withdraw = super::raydium_mapped_non_trade_instruction_spec(
|
||||
"raydium_cpmm",
|
||||
Some("b712469c946da122"),
|
||||
14,
|
||||
);
|
||||
let withdraw = match withdraw {
|
||||
Some(withdraw) => withdraw,
|
||||
None => panic!("withdraw discriminator must be mapped"),
|
||||
};
|
||||
assert_eq!(withdraw.event_kind, "raydium_cpmm.withdraw");
|
||||
let initialize = super::raydium_mapped_non_trade_instruction_spec(
|
||||
"raydium_cpmm",
|
||||
Some("afaf6d1f0d989bed"),
|
||||
20,
|
||||
);
|
||||
let initialize = match initialize {
|
||||
Some(initialize) => initialize,
|
||||
None => panic!("initialize discriminator must be mapped"),
|
||||
};
|
||||
assert_eq!(initialize.event_kind, "raydium_cpmm.initialize");
|
||||
let expected = [
|
||||
("9c5420764587467b", 4_usize, "raydium_cpmm.close_permission_pda"),
|
||||
("1416567bc61cdb84", 13_usize, "raydium_cpmm.collect_creator_fee"),
|
||||
("a78a4e95dfc2067e", 12_usize, "raydium_cpmm.collect_fund_fee"),
|
||||
("8888fcddc2427e59", 12_usize, "raydium_cpmm.collect_protocol_fee"),
|
||||
("8934edd4d7756c68", 3_usize, "raydium_cpmm.create_amm_config"),
|
||||
("878802d889a9b5ca", 4_usize, "raydium_cpmm.create_permission_pda"),
|
||||
("f223c68952e1f2b6", 13_usize, "raydium_cpmm.deposit"),
|
||||
("afaf6d1f0d989bed", 20_usize, "raydium_cpmm.initialize"),
|
||||
("3f37fe4131b25979", 21_usize, "raydium_cpmm.initialize_with_permission"),
|
||||
("313cae889a1c74c8", 2_usize, "raydium_cpmm.update_amm_config"),
|
||||
("82576c062ee0757b", 2_usize, "raydium_cpmm.update_pool_status"),
|
||||
("b712469c946da122", 14_usize, "raydium_cpmm.withdraw"),
|
||||
];
|
||||
for (discriminator, account_count, event_kind) in expected {
|
||||
let mapped = super::raydium_mapped_non_trade_instruction_spec(
|
||||
"raydium_cpmm",
|
||||
Some(discriminator),
|
||||
account_count,
|
||||
);
|
||||
let mapped = match mapped {
|
||||
Some(mapped) => mapped,
|
||||
None => panic!("raydium cpmm discriminator must be mapped: {}", discriminator),
|
||||
};
|
||||
assert_eq!(mapped.event_kind, event_kind);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3573,7 +3936,7 @@ mod tests {
|
||||
let registry_match = crate::UpstreamRegistryEntryDto {
|
||||
source_repo: Some("sevenlabs-hq/carbon".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_family: "meteora".to_string(),
|
||||
surface_kind: "amm".to_string(),
|
||||
|
||||
@@ -1036,7 +1036,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
|
||||
"program": "raydium-amm-v4",
|
||||
"program": "raydium_amm_v4",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"Account0",
|
||||
@@ -1462,7 +1462,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
||||
"program": "meteora-dbc",
|
||||
"program": "meteora_dbc",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"DbcDetectPool111",
|
||||
@@ -1581,7 +1581,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
|
||||
"program": "meteora-damm-v2",
|
||||
"program": "meteora_damm_v2",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"DammV2DetectPool111",
|
||||
@@ -1701,7 +1701,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
|
||||
"program": "meteora-damm-v1",
|
||||
"program": "meteora_damm_v1",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"DammV1DetectPool111",
|
||||
@@ -1821,7 +1821,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
|
||||
"program": "orca-whirlpools",
|
||||
"program": "orca_whirlpools",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"OrcaDetectPool111",
|
||||
|
||||
@@ -320,6 +320,9 @@ pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".deposit") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".lp_change_event") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".withdraw") {
|
||||
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.
|
||||
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") {
|
||||
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.
|
||||
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") {
|
||||
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]
|
||||
fn classifies_audit_suffix_events_as_informational() {
|
||||
assert!(super::is_dex_informational_event_kind("openbook_v2.settle_funds_audit"));
|
||||
|
||||
@@ -38,15 +38,10 @@ impl DexEventCoverageService {
|
||||
};
|
||||
}
|
||||
|
||||
/// 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(
|
||||
async fn upsert_upstream_registry_rows(
|
||||
&self,
|
||||
decoder_code: std::option::Option<std::string::String>,
|
||||
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
|
||||
) -> Result<(usize, usize), crate::Error> {
|
||||
let request = crate::UpstreamRegistrySearchRequestDto {
|
||||
decoder_code: decoder_code.clone(),
|
||||
program_id: None,
|
||||
@@ -70,6 +65,30 @@ impl DexEventCoverageService {
|
||||
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 {
|
||||
Some(decoder_code) => {
|
||||
let refresh_result =
|
||||
@@ -103,7 +122,7 @@ impl DexEventCoverageService {
|
||||
};
|
||||
return Ok(crate::DexEventCoverageSyncResult {
|
||||
decoder_code,
|
||||
upstream_entry_count: search_result.entries.len(),
|
||||
upstream_entry_count,
|
||||
upserted_entry_count,
|
||||
refreshed_entry_count,
|
||||
summaries,
|
||||
@@ -115,6 +134,11 @@ impl DexEventCoverageService {
|
||||
&self,
|
||||
decoder_code: std::option::Option<std::string::String>,
|
||||
) -> 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 {
|
||||
Some(decoder_code) => {
|
||||
let refresh_result =
|
||||
@@ -148,8 +172,8 @@ impl DexEventCoverageService {
|
||||
};
|
||||
return Ok(crate::DexEventCoverageSyncResult {
|
||||
decoder_code,
|
||||
upstream_entry_count: 0,
|
||||
upserted_entry_count: 0,
|
||||
upstream_entry_count,
|
||||
upserted_entry_count,
|
||||
refreshed_entry_count,
|
||||
summaries,
|
||||
});
|
||||
@@ -160,8 +184,12 @@ fn build_coverage_entry_from_upstream(
|
||||
entry: &crate::UpstreamRegistryEntryDto,
|
||||
) -> crate::DexEventCoverageEntryDto {
|
||||
let event_family = infer_event_family(entry.entry_name.as_str(), entry.entry_kind.as_str());
|
||||
let expected_db_target =
|
||||
infer_expected_db_target(event_family.as_deref(), entry.entry_kind.as_str());
|
||||
let expected_db_target = infer_expected_db_target_for_entry(
|
||||
entry.decoder_code.as_str(),
|
||||
entry.entry_name.as_str(),
|
||||
event_family.as_deref(),
|
||||
entry.entry_kind.as_str(),
|
||||
);
|
||||
let local_event_kind =
|
||||
known_local_event_kind(entry.decoder_code.as_str(), entry.entry_name.as_str());
|
||||
let mut coverage_entry = crate::DexEventCoverageEntryDto::from_upstream_registry_entry(
|
||||
@@ -177,6 +205,18 @@ fn build_coverage_entry_from_upstream(
|
||||
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(
|
||||
event_family: std::option::Option<&str>,
|
||||
entry_kind: &str,
|
||||
@@ -195,6 +235,7 @@ fn infer_expected_db_target(
|
||||
let target = match family {
|
||||
"swap" => crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_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_remove" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
|
||||
"position_open" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
|
||||
@@ -235,6 +276,9 @@ fn infer_event_family(
|
||||
return None;
|
||||
}
|
||||
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"]) {
|
||||
return Some("swap".to_string());
|
||||
}
|
||||
@@ -360,29 +404,58 @@ fn known_local_event_kind(
|
||||
entry_name: &str,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
match (decoder_code, entry_name) {
|
||||
("raydium-cpmm", "swap_base_input") => {
|
||||
("raydium_cpmm", "swap_base_input") => {
|
||||
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());
|
||||
},
|
||||
("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());
|
||||
},
|
||||
("raydium-cpmm", "withdraw") => return Some("raydium_cpmm.withdraw".to_string()),
|
||||
("raydium-cpmm", "initialize") => return Some("raydium_cpmm.initialize".to_string()),
|
||||
("raydium-clmm", "swap") => return Some("raydium_clmm.swap".to_string()),
|
||||
("raydium-clmm", "swap_v2") => return Some("raydium_clmm.swap_v2".to_string()),
|
||||
("raydium-clmm", "increase_liquidity_v2") => {
|
||||
("raydium_cpmm", "collect_fund_fee") => {
|
||||
return Some("raydium_cpmm.collect_fund_fee".to_string());
|
||||
},
|
||||
("raydium_cpmm", "collect_protocol_fee") => {
|
||||
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());
|
||||
},
|
||||
("raydium-clmm", "decrease_liquidity_v2") => {
|
||||
("raydium_clmm", "decrease_liquidity_v2") => {
|
||||
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());
|
||||
},
|
||||
("raydium-clmm", "close_position") => {
|
||||
("raydium_clmm", "close_position") => {
|
||||
return Some("raydium_clmm.close_position".to_string());
|
||||
},
|
||||
_ => return None,
|
||||
@@ -442,7 +515,7 @@ mod tests {
|
||||
async fn sync_upstream_registry_persists_raydium_cpmm_coverage_rows() {
|
||||
let database = make_database().await;
|
||||
let service = crate::DexEventCoverageService::new(database.clone());
|
||||
let result = service.sync_upstream_registry(Some("raydium-cpmm".to_string())).await;
|
||||
let result = service.sync_upstream_registry(Some("raydium_cpmm".to_string())).await;
|
||||
let result = match result {
|
||||
Ok(result) => result,
|
||||
Err(error) => panic!("coverage sync must succeed: {}", error),
|
||||
@@ -451,7 +524,7 @@ mod tests {
|
||||
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",
|
||||
"raydium_cpmm",
|
||||
)
|
||||
.await;
|
||||
let rows = match rows_result {
|
||||
@@ -467,7 +540,46 @@ mod tests {
|
||||
assert!(rows.iter().any(|row| return {
|
||||
row.entry_name == "deposit"
|
||||
&& 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,7 +628,7 @@ const DEX_SUPPORT_MATRIX_ENTRIES: &[DexSupportMatrixEntry] = &[
|
||||
version: "unknown",
|
||||
surface_type: "launch",
|
||||
surface_role: "launch_surface",
|
||||
program_id: Some(crate::BOOP_PROGRAM_ID),
|
||||
program_id: Some(crate::BOOP_FUN_PROGRAM_ID),
|
||||
router_program_id: None,
|
||||
program_id_status: "to_verify",
|
||||
observed: false,
|
||||
@@ -2934,7 +2934,7 @@ mod tests {
|
||||
("zora", crate::ZORA_PROGRAM_ID),
|
||||
("raydium_liquidity_locking", crate::RAYDIUM_LIQUIDITY_LOCKING_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),
|
||||
("bonkswap", crate::BONKSWAP_PROGRAM_ID),
|
||||
("metadao_launchpad_v0_7_0", crate::METADAO_LAUNCHPAD_V0_7_0_PROGRAM_ID),
|
||||
|
||||
351
kb_lib/src/instruction_observation_index.rs
Normal file
351
kb_lib/src/instruction_observation_index.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
@@ -736,7 +736,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
||||
"program": "meteora-dbc",
|
||||
"program": "meteora_dbc",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"DbcDetectPool111",
|
||||
@@ -829,7 +829,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
|
||||
"program": "meteora-damm-v2",
|
||||
"program": "meteora_damm_v2",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"MoonitDammV2Pool111",
|
||||
|
||||
@@ -51,6 +51,8 @@ mod error;
|
||||
mod http_client;
|
||||
/// HTTP endpoint pool and routing.
|
||||
mod http_pool;
|
||||
/// Technical index for observed chain instructions.
|
||||
mod instruction_observation_index;
|
||||
/// Generic JSON-RPC 2.0 WebSocket helpers.
|
||||
mod json_rpc_ws;
|
||||
/// Launch surface attribution service.
|
||||
@@ -173,7 +175,7 @@ pub use constants::BONK_MINT_ID;
|
||||
/// Bonkswap program id extracted from upstream Git decoder source.
|
||||
pub use constants::BONKSWAP_PROGRAM_ID;
|
||||
/// 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").
|
||||
/// @see solana_sdk::pubkey::Pubkey = solana_sdk_ids::bpf_loader_deprecated::ID
|
||||
pub use constants::BPF_LOADER_DEPRECATED_PROGRAM_ID;
|
||||
@@ -491,6 +493,10 @@ pub use db::DexEventCoverageSummaryEntity;
|
||||
pub use db::FeeEventDto;
|
||||
/// Persisted fee event row.
|
||||
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.
|
||||
pub use db::KnownHttpEndpointDto;
|
||||
/// 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;
|
||||
/// Inserts or updates one normalized fee event row.
|
||||
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.
|
||||
pub use db::query_known_http_endpoints_get;
|
||||
/// Lists all known HTTP endpoints.
|
||||
@@ -1141,14 +1150,22 @@ pub use dex::RaydiumClmmSwapLegacyDecoded;
|
||||
pub use dex::RaydiumClmmSwapV2Decoded;
|
||||
/// Raydium CPMM decoded event.
|
||||
pub use dex::RaydiumCpmmDecodedEvent;
|
||||
/// Raydium CPMM Anchor CPI liquidity-change event.
|
||||
pub use dex::RaydiumCpmmLpChangeEventDecoded;
|
||||
/// Raydium CPMM decoded swap.
|
||||
pub use dex::RaydiumCpmmSwapDecoded;
|
||||
/// Raydium CPMM Anchor CPI swap event retained as audit evidence.
|
||||
pub use dex::RaydiumCpmmSwapEventDecoded;
|
||||
/// Raydium CPMM swap mode.
|
||||
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.
|
||||
pub use dex::decode_raydium_clmm_instruction;
|
||||
/// Decodes one Raydium CPMM instruction from projected instruction fields.
|
||||
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.
|
||||
pub use dex_decode::DexDecodeService;
|
||||
/// Business-level DEX detection service.
|
||||
@@ -1263,6 +1280,10 @@ pub use http_client::parse_json_rpc_http_response_value;
|
||||
pub use http_pool::HttpEndpointPool;
|
||||
/// Snapshot of one pooled HTTP endpoint.
|
||||
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.
|
||||
pub use json_rpc_ws::JsonRpcWsErrorObject;
|
||||
/// JSON-RPC 2.0 error response.
|
||||
|
||||
@@ -825,17 +825,14 @@ async fn query_validation_i64(
|
||||
async fn load_event_coverage_summaries(
|
||||
database: &crate::Database,
|
||||
) -> Result<std::vec::Vec<crate::DexEventCoverageSummaryDto>, crate::Error> {
|
||||
let refresh_result =
|
||||
crate::query_dex_event_coverage_entries_refresh_local_counts(database).await;
|
||||
if let Err(error) = refresh_result {
|
||||
return Err(error);
|
||||
}
|
||||
let summaries_result =
|
||||
crate::query_dex_event_coverage_entries_list_summary_by_decoder(database).await;
|
||||
match summaries_result {
|
||||
Ok(summaries) => return Ok(summaries),
|
||||
let coverage_service =
|
||||
crate::DexEventCoverageService::new(std::sync::Arc::new(database.clone()));
|
||||
let refresh_result = coverage_service.refresh_local_counts(None).await;
|
||||
let refresh_result = match refresh_result {
|
||||
Ok(refresh_result) => refresh_result,
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
};
|
||||
return Ok(refresh_result.summaries);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -183,6 +183,8 @@ impl LocalPipelineReplayService {
|
||||
let pair_analytic_signal = crate::PairAnalyticSignalService::new(self.database.clone());
|
||||
let transaction_classification =
|
||||
crate::TransactionClassificationService::new(self.database.clone());
|
||||
let instruction_observation_index =
|
||||
crate::InstructionObservationIndexService::new(self.database.clone());
|
||||
let mut result = LocalPipelineReplayResult {
|
||||
selected_transaction_count: signatures.len(),
|
||||
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;
|
||||
}
|
||||
if config.refresh_missing_token_metadata {
|
||||
@@ -451,9 +471,31 @@ impl LocalPipelineReplayService {
|
||||
},
|
||||
}
|
||||
}
|
||||
self.refresh_event_coverage_best_effort().await;
|
||||
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(
|
||||
&self,
|
||||
config: &crate::LocalPipelineReplayConfig,
|
||||
@@ -777,7 +819,12 @@ mod tests {
|
||||
let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice())
|
||||
.expect("ledger must build");
|
||||
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.can_skip_decode());
|
||||
}
|
||||
|
||||
@@ -1824,7 +1824,7 @@ mod tests {
|
||||
summary.event_coverage_upstream_git_local_corpus_observed_entry_count = 1;
|
||||
summary.event_coverage_upstream_git_local_corpus_materialized_entry_count = 1;
|
||||
summary.event_coverage_summaries.push(crate::DexEventCoverageSummaryDto {
|
||||
decoder_code: "raydium-cpmm".to_string(),
|
||||
decoder_code: "raydium_cpmm".to_string(),
|
||||
listed_entry_count: 4,
|
||||
decoded_entry_count: 3,
|
||||
observed_entry_count: 2,
|
||||
|
||||
@@ -102,7 +102,17 @@ impl NonTradeEventMaterializationService {
|
||||
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
|
||||
.materialize_liquidity_event(
|
||||
&transaction,
|
||||
@@ -159,7 +169,9 @@ impl NonTradeEventMaterializationService {
|
||||
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
|
||||
.materialize_pool_admin_event(
|
||||
&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);
|
||||
}
|
||||
|
||||
@@ -274,6 +316,12 @@ impl NonTradeEventMaterializationService {
|
||||
"fund_fee_amount",
|
||||
"creatorFeeAmount",
|
||||
"creator_fee_amount",
|
||||
"amount0RequestedRaw",
|
||||
"amount_0_requested_raw",
|
||||
"amount1RequestedRaw",
|
||||
"amount_1_requested_raw",
|
||||
"tokenAAmount",
|
||||
"tokenBAmount",
|
||||
"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(
|
||||
&self,
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
@@ -437,6 +527,16 @@ impl NonTradeEventMaterializationService {
|
||||
Ok(context) => context,
|
||||
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 {
|
||||
Some(dex_id) => dex_id,
|
||||
None => return Ok(false),
|
||||
@@ -458,6 +558,12 @@ impl NonTradeEventMaterializationService {
|
||||
crate::LiquidityEventKind::PositionOpen
|
||||
} else if crate::is_dex_position_close_event_kind(decoded_event.event_kind.as_str()) {
|
||||
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()) {
|
||||
crate::LiquidityEventKind::Remove
|
||||
} else {
|
||||
@@ -487,6 +593,10 @@ impl NonTradeEventMaterializationService {
|
||||
"amount_base",
|
||||
"tokenAAmount",
|
||||
"token_a_amount",
|
||||
"token0AmountRaw",
|
||||
"token_0_amount_raw",
|
||||
"amount0RequestedRaw",
|
||||
"amount_0_requested_raw",
|
||||
"amountA",
|
||||
"amount_a",
|
||||
],
|
||||
@@ -502,6 +612,10 @@ impl NonTradeEventMaterializationService {
|
||||
"amount_quote",
|
||||
"tokenBAmount",
|
||||
"token_b_amount",
|
||||
"token1AmountRaw",
|
||||
"token_1_amount_raw",
|
||||
"amount1RequestedRaw",
|
||||
"amount_1_requested_raw",
|
||||
"amountB",
|
||||
"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(
|
||||
&self,
|
||||
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(
|
||||
value: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
|
||||
@@ -37,6 +37,12 @@ pub struct OnchainDexPairDiscoveryRequestDto {
|
||||
pub scan_order: std::option::Option<std::string::String>,
|
||||
/// Optional target event family used to score and filter candidate signatures.
|
||||
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.
|
||||
pub exclude_swaps: bool,
|
||||
/// Whether failed transactions should be returned as candidates.
|
||||
@@ -209,6 +215,8 @@ pub struct OnchainDexPairCandidateDto {
|
||||
pub instruction_name: std::option::Option<std::string::String>,
|
||||
/// Prefix of the raw base58 instruction data, useful for audit grouping.
|
||||
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.
|
||||
pub pool_address: std::option::Option<std::string::String>,
|
||||
/// Candidate token A/base mint when it can be extracted.
|
||||
@@ -382,6 +390,8 @@ impl OnchainDexPairDiscoveryService {
|
||||
let logs = extract_log_messages(&transaction_value);
|
||||
let target_keeps_mixed_swaps = target_event_keeps_mixed_swap_transactions(
|
||||
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
|
||||
&& logs_contain_swap(logs.as_slice())
|
||||
@@ -396,6 +406,8 @@ impl OnchainDexPairDiscoveryService {
|
||||
resolved.program_id.as_str(),
|
||||
resolved.dex_code.clone(),
|
||||
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.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),
|
||||
scan_order: Some(normalize_scan_order(request.scan_order.as_deref()).to_string()),
|
||||
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,
|
||||
include_failed: request.include_failed,
|
||||
http_role,
|
||||
@@ -773,6 +787,56 @@ fn normalize_target_event(
|
||||
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(
|
||||
value: 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,
|
||||
dex_code: std::option::Option<std::string::String>,
|
||||
target_event: std::option::Option<&str>,
|
||||
target_instruction_name: std::option::Option<&str>,
|
||||
target_discriminator_hex: std::option::Option<&str>,
|
||||
) -> OnchainCandidateExtraction {
|
||||
let mut candidates = std::vec::Vec::new();
|
||||
let mut rejected_candidate_summary = std::vec::Vec::new();
|
||||
@@ -1036,6 +1102,19 @@ fn extract_candidates_from_transaction(
|
||||
);
|
||||
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| {
|
||||
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,
|
||||
instruction_name: Some("raydium_clmm.swap".to_string()),
|
||||
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()),
|
||||
token_a_mint: Some(event.base_mint),
|
||||
token_b_mint: Some(event.quote_mint),
|
||||
@@ -1176,6 +1258,9 @@ fn decode_raydium_clmm_candidate(
|
||||
inner_instruction_index: instruction.inner_instruction_index,
|
||||
instruction_name: Some("raydium_clmm.swap_v2".to_string()),
|
||||
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()),
|
||||
token_a_mint: Some(event.base_mint),
|
||||
token_b_mint: Some(event.quote_mint),
|
||||
@@ -1257,9 +1342,31 @@ fn decode_raydium_cpmm_candidate(
|
||||
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(
|
||||
@@ -1289,6 +1396,7 @@ fn build_raydium_cpmm_candidate(
|
||||
inner_instruction_index: instruction.inner_instruction_index,
|
||||
instruction_name: Some(instruction_name.to_string()),
|
||||
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()),
|
||||
token_a_mint: Some(token_a_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(
|
||||
signature: &str,
|
||||
slot: std::option::Option<u64>,
|
||||
@@ -1345,6 +1714,7 @@ fn decode_meteora_damm_v1_candidate(
|
||||
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,
|
||||
@@ -1616,6 +1986,7 @@ fn build_heuristic_candidate(
|
||||
inner_instruction_index: instruction.inner_instruction_index,
|
||||
instruction_name,
|
||||
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(),
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
@@ -2789,8 +3160,21 @@ fn target_event_prefers_instruction_local_classification(
|
||||
return false;
|
||||
}
|
||||
|
||||
fn target_event_keeps_mixed_swap_transactions(target_event: std::option::Option<&str>) -> bool {
|
||||
return !split_target_event_filter(target_event).is_empty();
|
||||
fn target_event_keeps_mixed_swap_transactions(
|
||||
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(
|
||||
@@ -2813,6 +3197,23 @@ fn instruction_data_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 {
|
||||
return text_matches_pool_admin(lower)
|
||||
|| text_matches_claim_reward(lower)
|
||||
@@ -3016,6 +3417,77 @@ fn first_matching_target_event(
|
||||
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(
|
||||
candidate: &crate::OnchainDexPairCandidateDto,
|
||||
target_event: std::option::Option<&str>,
|
||||
@@ -3027,10 +3499,10 @@ fn candidate_matches_single_target_event(
|
||||
candidate: &crate::OnchainDexPairCandidateDto,
|
||||
target_event: &str,
|
||||
) -> bool {
|
||||
if target_event == "unknown_non_swap"
|
||||
|| target_event == "audit_non_swap_like"
|
||||
|| target_event == "non_swap"
|
||||
{
|
||||
if target_event == "unknown_non_swap" {
|
||||
return candidate_is_unknown_non_swap_candidate(candidate);
|
||||
}
|
||||
if target_event == "audit_non_swap_like" || target_event == "non_swap" {
|
||||
return candidate_is_non_swap_audit_candidate(candidate);
|
||||
}
|
||||
if target_event == "unclassified_instruction" {
|
||||
@@ -3180,6 +3652,24 @@ fn candidate_kind_is_explicit_surface(candidate_kind: &str) -> bool {
|
||||
|| 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 {
|
||||
if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) {
|
||||
return false;
|
||||
@@ -3659,6 +4149,8 @@ mod tests {
|
||||
max_pages: 1,
|
||||
scan_order: None,
|
||||
target_event: None,
|
||||
target_instruction_name: None,
|
||||
target_discriminator_hex: None,
|
||||
exclude_swaps: false,
|
||||
include_failed: true,
|
||||
http_role: "history_backfill".to_string(),
|
||||
@@ -3689,6 +4181,8 @@ mod tests {
|
||||
max_pages: 1,
|
||||
scan_order: None,
|
||||
target_event: None,
|
||||
target_instruction_name: None,
|
||||
target_discriminator_hex: None,
|
||||
exclude_swaps: false,
|
||||
include_failed: true,
|
||||
http_role: "history_backfill".to_string(),
|
||||
@@ -3713,6 +4207,8 @@ mod tests {
|
||||
max_pages: 1,
|
||||
scan_order: None,
|
||||
target_event: None,
|
||||
target_instruction_name: None,
|
||||
target_discriminator_hex: None,
|
||||
exclude_swaps: false,
|
||||
include_failed: true,
|
||||
http_role: "history_backfill".to_string(),
|
||||
@@ -3762,11 +4258,13 @@ mod tests {
|
||||
}
|
||||
});
|
||||
let extraction = super::extract_candidates_from_transaction(
|
||||
"sig-openbook-v2-raw",
|
||||
"sig-openbook_v2-raw",
|
||||
&transaction,
|
||||
crate::OPENBOOK_V2_PROGRAM_ID,
|
||||
Some("openbook_v2".to_string()),
|
||||
Some("order_place"),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(extraction.extracted_candidate_count, 1);
|
||||
assert_eq!(extraction.target_rejected_candidate_count, 0);
|
||||
@@ -3833,6 +4331,7 @@ mod tests {
|
||||
inner_instruction_index: Some(2),
|
||||
instruction_name: None,
|
||||
instruction_data_prefix: Some("EVM9wLnauu9H41Gf".to_string()),
|
||||
instruction_discriminator_hex: None,
|
||||
pool_address: None,
|
||||
token_a_mint: None,
|
||||
token_b_mint: None,
|
||||
@@ -3987,6 +4486,8 @@ mod tests {
|
||||
max_pages: 2,
|
||||
scan_order: Some("oldest_first".to_string()),
|
||||
target_event: Some("claim_fee,remove_liquidity".to_string()),
|
||||
target_instruction_name: None,
|
||||
target_discriminator_hex: None,
|
||||
exclude_swaps: true,
|
||||
include_failed: true,
|
||||
http_role: "history_backfill".to_string(),
|
||||
@@ -4036,6 +4537,7 @@ mod tests {
|
||||
inner_instruction_index: None,
|
||||
instruction_name: Some(instruction_name.to_string()),
|
||||
instruction_data_prefix: Some("prefix".to_string()),
|
||||
instruction_discriminator_hex: None,
|
||||
pool_address: None,
|
||||
token_a_mint: None,
|
||||
token_b_mint: None,
|
||||
@@ -4076,7 +4578,41 @@ mod tests {
|
||||
fn target_filter_accepts_open_orders_close_candidates() {
|
||||
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("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")));
|
||||
}
|
||||
#[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")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
||||
"program": "meteora-dbc",
|
||||
"program": "meteora_dbc",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"DbcOriginPool111",
|
||||
|
||||
@@ -288,6 +288,7 @@ impl TokenBackfillService {
|
||||
}
|
||||
}
|
||||
self.backfill_missing_token_metadata_best_effort(100).await;
|
||||
self.refresh_event_coverage_best_effort().await;
|
||||
let summary_payload = serde_json::json!({
|
||||
"tokenMint": result.token_mint,
|
||||
"mintSignatureCount": result.mint_signature_count,
|
||||
@@ -572,6 +573,26 @@ impl TokenBackfillService {
|
||||
if let Err(error) = transaction_classification_result {
|
||||
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 {
|
||||
resolved_transaction_count: 1,
|
||||
missing_transaction_count: 0,
|
||||
@@ -715,6 +736,7 @@ impl TokenBackfillService {
|
||||
}
|
||||
}
|
||||
self.backfill_missing_token_metadata_best_effort(100).await;
|
||||
self.refresh_event_coverage_best_effort().await;
|
||||
let summary_payload = serde_json::json!({
|
||||
"poolAddress": result.pool_address,
|
||||
"poolSignatureCount": result.pool_signature_count,
|
||||
@@ -785,6 +807,7 @@ impl TokenBackfillService {
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
self.backfill_missing_token_metadata_best_effort(100).await;
|
||||
self.refresh_event_coverage_best_effort().await;
|
||||
let result = crate::SignatureBackfillResult {
|
||||
signature: trimmed_signature.clone(),
|
||||
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 {
|
||||
|
||||
@@ -43,7 +43,7 @@ mod tests {
|
||||
fn service_search_preserves_normalized_request() {
|
||||
let service = crate::UpstreamRegistryService::new();
|
||||
let request = crate::UpstreamRegistrySearchRequestDto {
|
||||
decoder_code: Some("raydium-cpmm".to_string()),
|
||||
decoder_code: Some("raydium_cpmm".to_string()),
|
||||
program_id: None,
|
||||
program_family: None,
|
||||
surface_kind: None,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -280,7 +280,7 @@ mod tests {
|
||||
for (entry_name, discriminator_hex) in expected {
|
||||
let mut found = false;
|
||||
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.entry_kind == crate::ENTRY_KIND_INSTRUCTION
|
||||
&& entry.entry_name == entry_name
|
||||
@@ -312,7 +312,7 @@ mod tests {
|
||||
Some(matched) => matched,
|
||||
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.discriminator_hex, Some("032c47031ac7cb55".to_string()));
|
||||
}
|
||||
@@ -320,65 +320,65 @@ mod tests {
|
||||
#[test]
|
||||
fn registry_contains_priority_family_program_seeds() {
|
||||
let expected_codes = [
|
||||
"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",
|
||||
"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-amm-v2",
|
||||
"phoenix-v1",
|
||||
"openbook-v2",
|
||||
"stabble-stable-swap",
|
||||
"stabble-weighted-swap",
|
||||
"lifinity_v2",
|
||||
"phoenix_v1",
|
||||
"openbook_v2",
|
||||
"stabble_stable_swap",
|
||||
"stabble_weighted_swap",
|
||||
"bonkswap",
|
||||
"boop",
|
||||
"boop_fun",
|
||||
"moonshot",
|
||||
"heaven",
|
||||
"okx-dex",
|
||||
"pancake-swap",
|
||||
"okx_dex",
|
||||
"pancake_swap",
|
||||
"vertigo",
|
||||
"virtuals",
|
||||
"wavebreak",
|
||||
"onchain-labs-dex-v1",
|
||||
"onchain-labs-dex-v2",
|
||||
"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",
|
||||
"onchain_labs_dex_v1",
|
||||
"onchain_labs_dex_v2",
|
||||
"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",
|
||||
"system-program",
|
||||
"token-program",
|
||||
"token-2022",
|
||||
"associated-token-account",
|
||||
"address-lookup-table",
|
||||
"memo-program",
|
||||
"stake-program",
|
||||
"mpl-token-metadata",
|
||||
"mpl-core",
|
||||
"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",
|
||||
"name_service",
|
||||
"marinade_finance",
|
||||
"solayer_restaking_program",
|
||||
"swig",
|
||||
"sharky",
|
||||
"circle-message-transmitter-v2",
|
||||
"circle-token-messenger-v2",
|
||||
"circle_message_transmitter_v2",
|
||||
"circle_token_messenger_v2",
|
||||
];
|
||||
let all_entries = crate::upstream_registry_match::upstream_registry_all_entries();
|
||||
for expected_code in expected_codes {
|
||||
@@ -527,7 +527,7 @@ mod tests {
|
||||
for expected_entry in expected_entries {
|
||||
let mut found = false;
|
||||
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_name == expected_entry.0
|
||||
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1)
|
||||
@@ -555,7 +555,7 @@ mod tests {
|
||||
for expected_entry in expected_entries {
|
||||
let mut found = false;
|
||||
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_name == expected_entry.0
|
||||
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1)
|
||||
@@ -581,9 +581,9 @@ mod tests {
|
||||
);
|
||||
let matched = match 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.discriminator_hex.as_deref(), Some("b59d59438fb63448"));
|
||||
assert_eq!(matched.proof_status, crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED);
|
||||
@@ -599,9 +599,9 @@ mod tests {
|
||||
);
|
||||
let matched = match 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.discriminator_hex.as_deref(), Some("00"));
|
||||
assert_eq!(matched.discriminator_len, Some(1));
|
||||
@@ -621,7 +621,7 @@ mod tests {
|
||||
let result = crate::upstream_registry_match::upstream_registry_search(&request);
|
||||
assert!(result.entries.len() >= 2);
|
||||
for entry in result.entries.as_slice() {
|
||||
assert_eq!(entry.decoder_code, "raydium-cpmm");
|
||||
assert_eq!(entry.decoder_code, "raydium_cpmm");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user