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 - 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-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.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 - 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-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-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-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.47-doc-event-coverage - Ajout d'une matrice événementielle complémentaire `DEX_EVENT_COVERAGE_MATRIX.md` pour suivre, par DEX/version, les familles `swap`, `pool_create`, `liquidity`, `position`, `fee`, `reward`, `admin/config`, `mint`, `burn`, `transfer`, `account_create/close`, `wrap/unwrap`, `orderbook`, `vault`, `lock/unlock`, `launch` et `migration`; ajout de `DB_EVENT_MODEL_REVIEW.md` pour clarifier que `k_sol_dex_decoded_events` suffit à l'audit-only mais que des tables transversales sont nécessaires pour exploiter transfers, orderbook, vault, launch/migration et coverage upstream en requêtes métier.
|
0.7.48-pre-event-coverage-fix-docs - Correction du refresh SQL `k_sol_dex_event_coverage_entries` pour éviter les requêtes dynamiques non compatibles avec `sqlx::query` 0.9 ; mise à jour documentaire README/ROADMAP pour acter `0.7.48-pre` comme checkpoint DB/reporting et réaligner la suite sur l’ordre Raydium avant Meteora (`0.7.48 raydium_cpmm`, `0.7.49 raydium_clmm`, puis Pump/Meteora).
|
||||||
0.7.48-pre-event-coverage-sync - Raccordement de `k_sol_dex_event_coverage_entries` au registre upstream Git : ajout de `DexEventCoverageService`, sync des entrées registry vers SQLite, inférence conservatoire `event_family` / `expected_db_target`, mapping local limité aux events Raydium déjà connus, refresh des compteurs observés/matérialisés depuis `k_sol_dex_decoded_events` et tables non-trade existantes, sans modification des decoders ni de la matérialisation trade/candle.
|
0.7.48-pre-event-coverage-report - Clôture du checkpoint `0.7.48-pre` : raccordement des summaries `k_sol_dex_event_coverage_entries` aux diagnostics locaux, ajout des compteurs agrégés de couverture au `LocalPipelineDiagnosticSummaryDto` et au `LocalPipelineValidationReportDto`, ajout du profil `0.7.48-pre_event_coverage_db_checkpoint`, exposition du profil dans Demo Pipeline 2, et maintien des invariants : aucun decoder DEX modifié, aucun trade/candle créé, aucun `program_id` promu sans corpus.
|
||||||
0.7.48-pre-event-coverage-fix-docs - Correction du refresh SQL `k_sol_dex_event_coverage_entries` pour éviter les requêtes dynamiques non compatibles avec `sqlx::query` 0.9 ; mise à jour documentaire README/ROADMAP pour acter `0.7.48-pre` comme checkpoint DB/reporting et réaligner la suite sur l’ordre Raydium avant Meteora (`0.7.48 raydium_cpmm`, `0.7.49 raydium_clmm`, puis Pump/Meteora).
|
0.7.48-pre-event-coverage-validation-scope - Correction du profil `0.7.48-pre_event_coverage_db_checkpoint` : le contrôle bloquant des trade candidates non matérialisés est maintenant borné aux DEX attendus de la tranche Raydium (`raydium_cpmm`, `raydium_clmm`, `raydium_amm_v4`) afin qu’un DEX partiel hors scope, comme `fluxbeam`, reste diagnostiqué sans bloquer le checkpoint DB/event coverage.
|
||||||
0.7.48-pre-event-coverage-report - Clôture du checkpoint `0.7.48-pre` : raccordement des summaries `k_sol_dex_event_coverage_entries` aux diagnostics locaux, ajout des compteurs agrégés de couverture au `LocalPipelineDiagnosticSummaryDto` et au `LocalPipelineValidationReportDto`, ajout du profil `0.7.48-pre_event_coverage_db_checkpoint`, exposition du profil dans Demo Pipeline 2, et maintien des invariants : aucun decoder DEX modifié, aucun trade/candle créé, aucun `program_id` promu sans corpus.
|
0.7.48-raydium-cpmm-program-data - Complément Raydium CPMM : auto-sync conservatoire de `k_sol_dex_event_coverage_entries` lors des diagnostics/backfills si la table de coverage est vide, décodage des events CPMM `Program data` `swap_event` et `lp_change_event` sans sélecteur Anchor self-CPI, conservation explicite de `swap_event` en audit-only pour éviter tout doublon de trade/candle, matérialisation liquidity conservatoire de `lp_change_event` via `changeType`, et maintien des invariants failed transaction / non-trade / upstream Git.
|
||||||
0.7.48-pre-event-coverage-validation-scope - Correction du profil `0.7.48-pre_event_coverage_db_checkpoint` : le contrôle bloquant des trade candidates non matérialisés est maintenant borné aux DEX attendus de la tranche Raydium (`raydium_cpmm`, `raydium_clmm`, `raydium_amm_v4`) afin qu’un DEX partiel hors scope, comme `fluxbeam`, reste diagnostiqué sans bloquer le checkpoint DB/event coverage.
|
0.7.48 - Raydium CPMM event coverage clôturé : décodage spécialisé des instructions/events CPMM Carbon/Raydium/fnzero, auto-sync coverage, index technique `k_sol_instruction_observations`, recherche Demo3 par instruction/discriminant, matérialisation validée des swaps, lifecycle, fees, admin/config, deposit/withdraw et `lp_change_event`, conservation de `swap_event` et de 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]
|
[workspace.package]
|
||||||
version = "0.7.47"
|
version = "0.7.48"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"
|
||||||
@@ -89,3 +89,4 @@ manual_unwrap_or_default = "allow"
|
|||||||
manual_find = "allow"
|
manual_find = "allow"
|
||||||
explicit_counter_loop = "allow"
|
explicit_counter_loop = "allow"
|
||||||
get_first = "allow"
|
get_first = "allow"
|
||||||
|
implicit_saturating_sub = "allow"
|
||||||
@@ -289,3 +289,20 @@ Objectif :
|
|||||||
- materialized events,
|
- materialized events,
|
||||||
- missing DB target,
|
- missing DB target,
|
||||||
- trade_count invariant.
|
- trade_count invariant.
|
||||||
|
|
||||||
|
## Note `0.7.48` — Raydium CPMM sans nouvelle table DB
|
||||||
|
|
||||||
|
La tranche `0.7.48` confirme que `k_sol_dex_decoded_events` suffit pour continuer la couverture exhaustive CPMM en audit-only.
|
||||||
|
|
||||||
|
Aucune nouvelle table n'est ajoutée pour CPMM :
|
||||||
|
|
||||||
|
- les swaps exploitables restent dans `k_sol_trade_events` via les chemins existants ;
|
||||||
|
- `deposit` / `withdraw` utilisent les tables non-trade existantes seulement si le corpus et le rattachement pool/pair sont fiables ;
|
||||||
|
- les fees/admin/config/permission restent non-trade et ne peuvent pas produire candles ;
|
||||||
|
- les transfers SPL, account lifecycle, wrap/unwrap SOL, vault et launch/migration restent des familles transversales futures, à promouvoir seulement si plusieurs DEX en justifient le besoin.
|
||||||
|
|
||||||
|
## Note 0.7.48 final — Instruction observations et CPMM
|
||||||
|
|
||||||
|
La tranche `raydium_cpmm` ajoute `k_sol_instruction_observations` comme table technique 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. |
|
| 13 | `meteora_vault` | `to_verify` | Présent comme indice upstream / compte associé. | Corpus direct obligatoire ; decoder séparé si events vault réels ; aucune promotion via DAMM indirect. |
|
||||||
| 14 | `fluxbeam` | `partial/to_verify` | Decoder initial existant ; Demo3 peut produire des candidats. | Vérifier source/IDL ; compléter swap, pool, liquidity, fees/admin ; matérialisation uniquement après corpus. |
|
| 14 | `fluxbeam` | `partial/to_verify` | Decoder initial existant ; Demo3 peut produire des candidats. | Vérifier source/IDL ; compléter swap, pool, liquidity, fees/admin ; matérialisation uniquement après corpus. |
|
||||||
| 15 | `dexlab` | `partial/to_verify` | Decoder initial historique ; ancienne entrée beta supprimée. | Reconfirmer program id/source ; décoder events disponibles ; distinguer DexLab natif et liens OpenBook/market. |
|
| 15 | `dexlab` | `partial/to_verify` | Decoder initial historique ; ancienne entrée beta supprimée. | Reconfirmer program id/source ; décoder events disponibles ; distinguer DexLab natif et liens OpenBook/market. |
|
||||||
| 16 | `lifinity_amm_v2` | `to_verify` | Program id listé par sources externes/Vybe ; pas de corpus concluant. | Trouver IDL/source ; Demo3 par program/market ; audit-only 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. |
|
| 17 | `stabble_stable_swap` / `stabble_weighted_swap` | `to_verify` | Program ids/indices via sources externes ; candidats Demo3 observables. | Source/IDL + corpus + decoder audit-only ; déterminer surface AMM et montants exploitables. |
|
||||||
| 18 | `bonkswap` | `to_verify` | Program id/Carbon/Vybe selon registre ; swaps candidats possibles. | Vérifier program id, source et corpus ; décoder tous events ; pas de trade sans montants. |
|
| 18 | `bonkswap` | `to_verify` | Program id/Carbon/Vybe selon registre ; swaps candidats possibles. | Vérifier program id, source et corpus ; décoder tous events ; pas de trade sans montants. |
|
||||||
| 19 | `boop` / `boop_fun` | `to_verify / launch` | Entrée de découverte. | Séparer launch surface et swap effectif ; corpus + source obligatoire. |
|
| 19 | `boop` / `boop_fun` | `to_verify / launch` | Entrée de découverte. | Séparer launch surface et swap effectif ; corpus + source obligatoire. |
|
||||||
@@ -202,3 +202,24 @@ Un event peut devenir `materialized` uniquement si :
|
|||||||
| `aquifer` | `to_verify` | `unknown` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source |
|
| `aquifer` | `to_verify` | `unknown` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source |
|
||||||
| `humidifi` | `to_verify` | `unknown` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source |
|
| `humidifi` | `to_verify` | `unknown` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source |
|
||||||
| `solfi_v2` | `to_verify` | `AMM` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source |
|
| `solfi_v2` | `to_verify` | `AMM` | `to_verify` | non | non | non | `to_verify` | vybe_supported_dex_amm_requires_local_corpus_and_decoder_source |
|
||||||
|
|
||||||
|
## Note `0.7.48` — Raydium CPMM
|
||||||
|
|
||||||
|
`raydium_cpmm` reste `supported`, mais sa couverture est maintenant explicitée au niveau entry/event coverage.
|
||||||
|
|
||||||
|
Entrées CPMM couvertes localement depuis Carbon/fnzero/IDL :
|
||||||
|
|
||||||
|
- swaps : `swap_base_input`, `swap_base_output` ;
|
||||||
|
- events Anchor self-CPI audit-only : `lp_change_event`, `swap_event` ;
|
||||||
|
- pool/lifecycle : `initialize`, `initialize_with_permission` ;
|
||||||
|
- liquidity : `deposit`, `withdraw` ;
|
||||||
|
- fees : `collect_creator_fee`, `collect_fund_fee`, `collect_protocol_fee` ;
|
||||||
|
- admin/config/permission : `create_amm_config`, `update_amm_config`, `update_pool_status`, `create_permission_pda`, `close_permission_pda`.
|
||||||
|
|
||||||
|
`create_amm_config` est traité comme admin/config, pas comme pool creation. `swap_event` est conservé comme audit-only pour ne pas doubler les trades matérialisés depuis les instructions `swap_base_input` / `swap_base_output`.
|
||||||
|
|
||||||
|
## Note `0.7.48 final` — Raydium CPMM
|
||||||
|
|
||||||
|
`raydium_cpmm` est considéré `supported` et clôturable pour la tranche `0.7.48`. Les entrées matérialisées couvrent swaps (`swap_base_input`, `swap_base_output`), liquidity (`deposit`, `withdraw`, `lp_change_event`), lifecycle (`initialize`, `initialize_with_permission`), fees (`collect_creator_fee`, `collect_fund_fee`, `collect_protocol_fee`) et admin/config (`create_amm_config`, `create_permission_pda`, `update_amm_config`).
|
||||||
|
|
||||||
|
`swap_event` reste audit-only pour éviter tout doublon de trade/candle. `close_permission_pda` et `update_pool_status` restent connus upstream mais non observés localement.
|
||||||
|
|||||||
@@ -1,232 +1,71 @@
|
|||||||
# DEX Event Coverage Matrix — `khadhroony-bobobot` `0.7.47-1FE5`
|
# DEX Event Coverage Matrix — `khadhroony-bobobot` `0.7.48`
|
||||||
|
|
||||||
Cette matrice complète `DEX_DECODER_MATRIX.md`.
|
Cette matrice complète `DEX_DECODER_MATRIX.md` avec une lecture par familles d'événements. Elle ne remplace pas la preuve locale : une entrée Git/IDL reste un indice tant qu'elle n'est pas observée dans le corpus local puis validée par replay et SQL.
|
||||||
|
|
||||||
La matrice précédente répondait à la question : **quel DEX/version est couvert ?**
|
## Règles de statut
|
||||||
|
|
||||||
Cette matrice répond à la question : **quels events/instructions doivent être décodés, audit-only ou matérialisés ?**
|
| Statut | Sens |
|
||||||
|
|
||||||
## Principe
|
|
||||||
|
|
||||||
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 |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| `listed` | L’event/instruction existe dans une source Git/IDL/Carbon/Vybe/autre. |
|
| `decoded` | Un decoder local produit un event spécialisé ou un event audit-only classé. |
|
||||||
| `decoded_audit` | Le code local reconnaît 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 existante validée par corpus. |
|
||||||
| `materialized` | L’event alimente une table métier spécialisée : trade, liquidity, lifecycle, fee, reward, admin, mint, burn, orderbook, vault, launch/migration, etc. |
|
| `audit-only` | L'event reste dans `k_sol_dex_decoded_events` et ne produit jamais trade/candle. |
|
||||||
|
| `upstream_git_mapped_unverified` | L'entrée est connue depuis Carbon/fnzero/IDL, mais non observée localement. |
|
||||||
|
| `not_applicable` | La famille n'existe pas pour ce DEX/version ou appartient à un autre programme. |
|
||||||
|
|
||||||
Ne pas sauter directement de `listed` à `materialized`.
|
## `0.7.48` — `raydium_cpmm`
|
||||||
|
|
||||||
## Univers minimal 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 |
|
## Validation attendue
|
||||||
|---|---|---|---|
|
|
||||||
| `swap/trade` | swap, buy, sell, route swap, exact in/out | Prix, volume, candles | `k_sol_trade_events`, `k_sol_pair_metrics`, `k_sol_pair_candles` |
|
|
||||||
| `pool_create` | initialize, create_pool, initialize_market | Découverte pool/pair | `k_sol_pool_lifecycle_events`, `k_sol_pools`, `k_sol_pairs` |
|
|
||||||
| `liquidity_add` | add_liquidity, deposit liquidity, bootstrap | Profondeur, risque, market cap indirect | `k_sol_liquidity_events` |
|
|
||||||
| `liquidity_remove` | remove_liquidity, withdraw liquidity | Rug/liquidity drain | `k_sol_liquidity_events` |
|
|
||||||
| `position_open` | open_position, init_position | CLMM/DLMM state | `k_sol_pool_lifecycle_events` ou future `k_sol_position_events` |
|
|
||||||
| `position_close` | close_position, close_position_if_empty | Sortie de LP, risque | `k_sol_pool_lifecycle_events` ou future `k_sol_position_events` |
|
|
||||||
| `fee` | claim_fee, collect_protocol_fee, collect_creator_fee | Rentabilité, activité pool | `k_sol_fee_events` |
|
|
||||||
| `reward` | claim_reward, fund_reward, update_reward | Incitations, farming | `k_sol_reward_events` |
|
|
||||||
| `admin/config` | set_config, update_fee, pause, whitelist, authority change | Risque protocole/pool | `k_sol_pool_admin_events` |
|
|
||||||
| `mint` | token mint, LP mint, position NFT mint | Supply, launch, LP | `k_sol_token_mint_events` + future token activity |
|
|
||||||
| `burn` | token burn, LP burn, position NFT burn | Supply, LP burn, risque/réassurance | `k_sol_token_burn_events` + future token activity |
|
|
||||||
| `transfer` | SPL transfer, Token-2022 transfer, routed transfer | Flux wallet/vault, whale movement | future `k_sol_token_transfer_events` |
|
|
||||||
| `account_create` | ATA create, token account init | Préparation trade/wallet/vault | future `k_sol_token_account_events` |
|
|
||||||
| `account_close` | close token account, close open orders | Sortie position/wallet cleanup | future `k_sol_token_account_events` ou `k_sol_orderbook_events` |
|
|
||||||
| `wrap/unwrap` | wrap SOL, unwrap SOL, close WSOL ATA | Routing, PnL, trade prep | future token account/activity |
|
|
||||||
| `order_place` | place_order, post_only, IOC | Orderbook pressure | future `k_sol_orderbook_events` |
|
|
||||||
| `order_cancel` | cancel_order, cancel_all, reduce order | Changement intention | future `k_sol_orderbook_events` |
|
|
||||||
| `order_fill` | FillLog, fill event, trade event | Trade réel orderbook | future `k_sol_orderbook_events`; trade seulement après validation économique |
|
|
||||||
| `settle_funds` | settle_funds, withdraw funds | Finalisation CLOB | future `k_sol_orderbook_events` |
|
|
||||||
| `consume_events` | crank/event queue processing | Orderbook fill/out | future `k_sol_orderbook_events` |
|
|
||||||
| `vault_deposit` | deposit into vault | TVL/risque | future `k_sol_vault_events` |
|
|
||||||
| `vault_withdraw` | withdraw from vault | TVL drain | future `k_sol_vault_events` |
|
|
||||||
| `lock` | lock_liquidity, create_lock_escrow | LP lock/risk | `k_sol_pool_lifecycle_events` ou future lock table |
|
|
||||||
| `unlock` | unlock, release escrow | Risque de retrait LP | future lock/lifecycle |
|
|
||||||
| `launch` | create bonding curve, launch pool | Origine token | future `k_sol_launch_events` |
|
|
||||||
| `migration` | migrate to DEX, migrate liquidity | Passage launch → tradable | future `k_sol_launch_events`, pool origins |
|
|
||||||
| `stake/unstake` | stake LP/token, unstake | Incentives/withdraw risk | future staking/reward events |
|
|
||||||
| `oracle/price` | oracle update, price account update | Pricing/risk | future oracle/context events |
|
|
||||||
| `unknown` | unmapped discriminator | Dette de décodage | `k_sol_dex_decoded_events` audit-only |
|
|
||||||
|
|
||||||
## Matrice de couverture par DEX/version
|
- `k_sol_dex_event_coverage_entries.decoder_code = 'raydium_cpmm'` contient toutes les entrées Carbon/fnzero synchronisées.
|
||||||
|
- `upstream_git.instruction_match` ne doit plus apparaître pour une instruction CPMM remplacée par un decoder local spécialisé.
|
||||||
|
- Les familles non-trade CPMM doivent rester `trade_count = 0`.
|
||||||
|
- Les transactions failed CPMM doivent rester décodées/auditables mais non matérialisées en trade/candle.
|
||||||
|
|
||||||
Légende :
|
|
||||||
|
|
||||||
- `M` = matérialisé déjà ou historiquement validé ;
|
## Note `0.7.48-part2-fix2` — CPMM official instruction parity
|
||||||
- `A` = audit-only local ;
|
|
||||||
- `P` = partiel / doit être complété ;
|
|
||||||
- `L` = listé upstream, non validé localement ;
|
|
||||||
- `-` = non applicable connu ;
|
|
||||||
- `?` = à vérifier.
|
|
||||||
|
|
||||||
| DEX/version | swap | pool create | liq add/remove | position | fee/reward | admin/config | mint/burn | transfer/account | orderbook | vault | launch/migration | état immédiat |
|
La couverture `raydium_cpmm` est alignée avec les instructions exposées par le programme officiel Raydium CP-Swap et par Carbon : `create_amm_config`, `update_amm_config`, `update_pool_status`, `collect_protocol_fee`, `collect_fund_fee`, `collect_creator_fee`, `create_permission_pda`, `close_permission_pda`, `initialize`, `initialize_with_permission`, `deposit`, `withdraw`, `swap_base_input`, `swap_base_output`, `lp_change_event` et `swap_event`.
|
||||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|
|
|
||||||
| `raydium_cpmm` | M | P | P | - | P | P | ? | ? | - | - | ? | Reprendre en `0.7.48`, comparer tous events Carbon/IDL/fnzero. |
|
|
||||||
| `raydium_clmm` | M | P | M/P | M/P | P | P | P | ? | - | - | ? | Reprendre en `0.7.49`, compléter positions/rewards/fees/admin. |
|
|
||||||
| `pump_swap` | M | P | P | - | P | P | ? | P | - | - | P | Reprendre en `0.7.50`, couvrir buy/sell/cashback/fee/volume/admin. |
|
|
||||||
| `pump_fun` | P | P | - | - | ? | P | P | P | - | - | M/P | Reprendre en `0.7.51`, launch/bonding/migration/buy/sell/create/update. |
|
|
||||||
| `meteora_dbc` | P | P | P | - | P | P | P | ? | - | P | M/P | Reprendre en `0.7.52`, séparer bonding, swap, migration, config, fees. |
|
|
||||||
| `meteora_dlmm` | M | M | M | M | M/P | P | ? | ? | - | ? | ? | Reprendre en `0.7.53` pour exhaustive upstream + audits résiduels. |
|
|
||||||
| `meteora_damm_v1` | M/P | M | M/P | - | M/P | P | ? | ? | - | P | ? | Reprendre en `0.7.54`, vérifier toutes surfaces upstream non observées. |
|
|
||||||
| `meteora_damm_v2` | P | P | L/P | - | L/P | L/P | ? | ? | - | P | ? | Reprendre en `0.7.55`, decoder tous events Carbon/source. |
|
|
||||||
| `phoenix_v1` | A | A/P | - | - | A/P | A/P | - | A/P | A | - | - | Continuer audit des events Git, pas de trade/candle. |
|
|
||||||
| `openbook_v2` | A | A/P | - | - | A/P | A/P | - | A/P | A | - | - | Audit-only avancé, matérialisation orderbook future. |
|
|
||||||
| `orca_whirlpools` | P | P | L/P | L/P | L/P | L/P | P | ? | - | - | ? | Reprendre en `0.7.58`, IDL complet + corpus dédié. |
|
|
||||||
| `raydium_launchlab` | - | P | ? | - | ? | P | P | P | - | - | P | Launch/migration après DEX effectifs. |
|
|
||||||
| `bonkswap` | L | L | L | - | L | L | ? | ? | - | - | L | Vérifier source/corpus. |
|
|
||||||
| `moonshot` | L/P | P | ? | - | ? | P | P | P | - | - | P | Séparer launch, buy/sell, migration. |
|
|
||||||
| `heaven` | L/P | L/P | L/P | - | L/P | L/P | P | P | - | ? | P | Vérifier AMM vs launch. |
|
|
||||||
| `goosefx_v1` | L/P | L/P | L/P | ? | ? | ? | ? | ? | ? | ? | ? | Vybe/Demo3 candidates ; source nécessaire. |
|
|
||||||
| `obric_v2` | L/P | L/P | ? | ? | ? | ? | ? | ? | ? | ? | ? | Bon candidat après sources. |
|
|
||||||
| `solfi_v2` | L/P | L/P | ? | ? | ? | ? | ? | ? | ? | ? | ? | Bon candidat après sources. |
|
|
||||||
|
|
||||||
## Points critiques manquants dans 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 ;
|
Validation finale locale : `deposit` = `11/11` liquidity, `withdraw` = `14/14` liquidity, `lp_change_event` = `25/25` liquidity, fees = `26/26` fee, admin/config observés = `23/23` admin, lifecycle = `9/9`, `swap_event` = audit-only avec `0` trade, et trades matérialisés uniquement depuis `swap_base_input` / `swap_base_output`.
|
||||||
- burn de supply token ;
|
|
||||||
- fermeture ou destruction indirecte de position ;
|
|
||||||
- réduction du risque de dump si le burn est réel et vérifié ;
|
|
||||||
- au contraire, faux signal si le burn ne concerne pas la bonne mint ou si le compte propriétaire est ambigu.
|
|
||||||
|
|
||||||
Action : ajouter `burn` à toutes les checklists DEX et aux diagnostics de couverture.
|
`close_permission_pda` et `update_pool_status` restent `upstream_git_mapped_unverified` faute de corpus local. Les filtres Solscan `instruction=9c5420764587467b` et `instruction=82576c062ee0757b` n'ont pas fourni de signatures exploitables dans l'horizon testé.
|
||||||
|
|
||||||
### `transfer`
|
|
||||||
|
|
||||||
Les transfers ne sont pas des trades par défaut, mais ils sont nécessaires pour :
|
|
||||||
|
|
||||||
- repérer vault movements ;
|
|
||||||
- détecter migration / liquidity routing ;
|
|
||||||
- comprendre des orderbook settle/fill ;
|
|
||||||
- analyser whale movement ou sortie de pool.
|
|
||||||
|
|
||||||
Action : prévoir une table dédiée plutôt que tout stocker uniquement dans `payload_json`.
|
|
||||||
|
|
||||||
### `account close/create`
|
|
||||||
|
|
||||||
Les close/create ATA sont utiles pour détecter :
|
|
||||||
|
|
||||||
- fin de route WSOL ;
|
|
||||||
- sortie de position ;
|
|
||||||
- cleanup après swap ;
|
|
||||||
- close open-orders account ;
|
|
||||||
- activité de bots.
|
|
||||||
|
|
||||||
Action : famille dédiée `token_account` / `account_lifecycle`.
|
|
||||||
|
|
||||||
## Checklist exhaustive par DEX
|
|
||||||
|
|
||||||
Pour chaque DEX/version, la tranche doit remplir un tableau événementiel :
|
|
||||||
|
|
||||||
| Colonne | Description |
|
|
||||||
|---|---|
|
|
||||||
| `source_repo` | Git/IDL/source utilisée. |
|
|
||||||
| `source_path` | Chemin exact du fichier source. |
|
|
||||||
| `decoder_code` | Code interne/upstream. |
|
|
||||||
| `program_id` | Program id ou `to_verify`. |
|
|
||||||
| `entry_kind` | `instruction`, `event`, `account`, `log`, `program_data`. |
|
|
||||||
| `entry_name` | Nom source exact. |
|
|
||||||
| `discriminator_hex` | Discriminator ou tag. |
|
|
||||||
| `discriminator_len` | Longueur en octets. |
|
|
||||||
| `event_family` | Famille commune : swap, burn, admin, order_fill, etc. |
|
|
||||||
| `local_event_kind` | Event local produit. |
|
|
||||||
| `local_status` | `not_implemented`, `audit_only`, `decoded`, `materialized`. |
|
|
||||||
| `db_target` | Table cible ou `k_sol_dex_decoded_events_only`. |
|
|
||||||
| `proof_status` | Statut upstream/local. |
|
|
||||||
| `observed_count` | Count local après replay. |
|
|
||||||
| `materialized_count` | Count table métier. |
|
|
||||||
| `trade_count` | Count trades générés, doit être 0 sauf swap validé. |
|
|
||||||
| `notes` | Ambiguïtés, layout, corpus, reste à faire. |
|
|
||||||
|
|
||||||
## Requêtes SQL génériques de couverture
|
|
||||||
|
|
||||||
### Couverture decoded events par programme
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
protocol_name,
|
|
||||||
event_kind,
|
|
||||||
program_id,
|
|
||||||
json_extract(payload_json, '$.upstreamEntryName') AS upstream_entry_name,
|
|
||||||
json_extract(payload_json, '$.upstreamDiscriminatorHex') AS upstream_discriminator_hex,
|
|
||||||
COUNT(*) AS n
|
|
||||||
FROM k_sol_dex_decoded_events
|
|
||||||
GROUP BY
|
|
||||||
protocol_name,
|
|
||||||
event_kind,
|
|
||||||
program_id,
|
|
||||||
upstream_entry_name,
|
|
||||||
upstream_discriminator_hex
|
|
||||||
ORDER BY protocol_name, n DESC;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sécurité trades pour audit-only
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
de.protocol_name,
|
|
||||||
de.event_kind,
|
|
||||||
COUNT(te.id) AS trade_count
|
|
||||||
FROM k_sol_dex_decoded_events de
|
|
||||||
LEFT JOIN k_sol_trade_events te
|
|
||||||
ON te.decoded_event_id = de.id
|
|
||||||
WHERE de.event_kind LIKE '%_audit'
|
|
||||||
OR de.protocol_name IN ('upstream_git', 'phoenix_v1', 'openbook_v2')
|
|
||||||
GROUP BY de.protocol_name, de.event_kind
|
|
||||||
ORDER BY trade_count DESC;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Vérifier burn/mint présents dans decoded payloads
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
protocol_name,
|
|
||||||
event_kind,
|
|
||||||
json_extract(payload_json, '$.eventLifecycleKind') AS lifecycle,
|
|
||||||
json_extract(payload_json, '$.eventActionability') AS actionability,
|
|
||||||
COUNT(*) AS n
|
|
||||||
FROM k_sol_dex_decoded_events
|
|
||||||
WHERE event_kind LIKE '%burn%'
|
|
||||||
OR event_kind LIKE '%mint%'
|
|
||||||
OR lifecycle IN ('burn', 'mint')
|
|
||||||
GROUP BY protocol_name, event_kind, lifecycle, actionability
|
|
||||||
ORDER BY n DESC;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Décision
|
|
||||||
|
|
||||||
À partir de maintenant, une tranche DEX 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.
|
|
||||||
|
|||||||
@@ -118,75 +118,75 @@ Les entrées de registre doivent être exposées à `kb_demo_app` via une comman
|
|||||||
DEX / AMM / CLMM / orderbook :
|
DEX / AMM / CLMM / orderbook :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
meteora-damm-v2
|
meteora_damm_v2
|
||||||
meteora-dbc
|
meteora_dbc
|
||||||
meteora-dlmm
|
meteora_dlmm
|
||||||
meteora-vault
|
meteora_vault
|
||||||
raydium-amm-v4
|
raydium_amm_v4
|
||||||
raydium-clmm
|
raydium_clmm
|
||||||
raydium-cpmm
|
raydium_cpmm
|
||||||
raydium-launchpad
|
raydium_launchpad
|
||||||
raydium-liquidity-locking
|
raydium_liquidity_locking
|
||||||
raydium-stable-swap
|
raydium_stable_swap
|
||||||
orca-whirlpool
|
orca_whirlpools
|
||||||
fluxbeam
|
fluxbeam
|
||||||
lifinity-amm-v2
|
lifinity_v2
|
||||||
phoenix-v1
|
phoenix_v1
|
||||||
openbook-v2
|
openbook_v2
|
||||||
stabble-stable-swap
|
stabble_stable_swap
|
||||||
stabble-weighted-swap
|
stabble_weighted_swap
|
||||||
bonkswap
|
bonkswap
|
||||||
boop
|
boop
|
||||||
moonshot
|
moonshot
|
||||||
heaven
|
heaven
|
||||||
okx-dex
|
okx_dex
|
||||||
pancake-swap
|
pancake_swap
|
||||||
vertigo
|
vertigo
|
||||||
virtuals
|
virtuals
|
||||||
wavebreak
|
wavebreak
|
||||||
onchain-labs-dex-v1
|
onchain_labs_dex_v1
|
||||||
onchain-labs-dex-v2
|
onchain_labs_dex_v2
|
||||||
```
|
```
|
||||||
|
|
||||||
Agrégateurs / ordres / perps / lending :
|
Agrégateurs / ordres / perps / lending :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
jupiter-swap
|
jupiter_swap
|
||||||
jupiter-dca
|
jupiter_dca
|
||||||
jupiter-limit-order
|
jupiter_limit_order
|
||||||
jupiter-limit-order-2
|
jupiter_limit_order_2
|
||||||
jupiter-perpetuals
|
jupiter_perpetuals
|
||||||
jupiter-lend
|
jupiter_lend
|
||||||
kamino-lending
|
kamino_lending
|
||||||
kamino-vault
|
kamino_vault
|
||||||
kamino-farms
|
kamino_farms
|
||||||
kamino-limit-order
|
kamino_limit_order
|
||||||
drift-v2
|
drift_v2
|
||||||
marginfi-v2
|
marginfi_v2
|
||||||
dflow-aggregator-v4
|
dflow_aggregator_v4
|
||||||
zeta
|
zeta
|
||||||
```
|
```
|
||||||
|
|
||||||
Contexte transactionnel non DEX :
|
Contexte transactionnel non DEX :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
system-program
|
system_program
|
||||||
token-program
|
token_program
|
||||||
token-2022
|
token_2022
|
||||||
associated-token-account
|
associated_token_account
|
||||||
address-lookup-table
|
address_lookup_table
|
||||||
memo-program
|
memo_program
|
||||||
stake-program
|
stake_program
|
||||||
mpl-token-metadata
|
mpl_token_metadata
|
||||||
mpl-core
|
mpl_core
|
||||||
bubblegum
|
bubblegum
|
||||||
name-service
|
name_service
|
||||||
marinade-finance
|
marinade_finance
|
||||||
solayer-restaking-program
|
solayer_restaking_program
|
||||||
swig
|
swig
|
||||||
sharky
|
sharky
|
||||||
circle-message-transmitter-v2
|
circle_message_transmitter_v2
|
||||||
circle-token-messenger-v2
|
circle_token_messenger_v2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Règles de validation
|
## Règles de validation
|
||||||
|
|||||||
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` ;
|
- `pump_fun` ;
|
||||||
- `raydium_launchlab` ;
|
- `raydium_launchlab` ;
|
||||||
- `raydium_launchpad` ;
|
|
||||||
- `letsbonk` / `bonk_fun` ;
|
- `letsbonk` / `bonk_fun` ;
|
||||||
- `bags` ;
|
- `bags` ;
|
||||||
- `moonshot` ;
|
- `moonshot` ;
|
||||||
@@ -351,9 +350,12 @@ La priorité immédiate après le point de reprise `0.7.43-E5C` est :
|
|||||||
3. `0.7.45` : reprise séparée de `meteora_dlmm` — clôturée côté corpus DLMM actuel ;
|
3. `0.7.45` : reprise séparée de `meteora_dlmm` — clôturée côté corpus DLMM actuel ;
|
||||||
4. `0.7.46` : reprise séparée de `meteora_damm_v1` — clôturée côté corpus DAMM v1 local ;
|
4. `0.7.46` : reprise séparée de `meteora_damm_v1` — clôturée côté corpus DAMM v1 local ;
|
||||||
5. `0.7.47` : Upstream Git Registry / DEX discovery preparation — registre générique des `program_id`, discriminants, instructions et events issus de dépôts Git externes, tous non vérifiés par défaut ;
|
5. `0.7.47` : Upstream Git Registry / DEX discovery preparation — registre générique des `program_id`, discriminants, instructions et events issus de dépôts Git externes, tous non vérifiés par défaut ;
|
||||||
6. `0.7.48` : reprise séparée de `meteora_damm_v2` ;
|
6. `0.7.48-pre` : event coverage + DB model checkpoint — table coverage, sync upstream, refresh counts, diagnostics et profil validation ;
|
||||||
7. `0.7.49` : reprise séparée de `meteora_dbc` ;
|
7. `0.7.48` : reprise séparée de `raydium_cpmm` ;
|
||||||
8. `0.7.50+` : validation progressive des autres DEX/surfaces issus du registre upstream Git : Orca, FluxBeam, DexLab, Lifinity, Phoenix, OpenBook, Stabble, BonkSwap, Boop, Moonshot, Heaven, Wavebreak, Vertigo, Virtuals, Pancake Swap, OKX DEX, Raydium Launchpad/Stable/Locking, puis launch surfaces.
|
8. `0.7.49` : reprise séparée de `raydium_clmm` ;
|
||||||
|
9. `0.7.50` : reprise séparée de `pump_swap` ;
|
||||||
|
10. `0.7.51` : reprise séparée de `pump_fun` ;
|
||||||
|
11. `0.7.52+` : Meteora puis validation progressive des autres DEX/surfaces issus du registre upstream Git : Orca, FluxBeam, DexLab, Lifinity, Phoenix, OpenBook, Stabble, BonkSwap, Boop, Moonshot, Heaven, Wavebreak, Vertigo, Virtuals, Pancake Swap, OKX DEX, Raydium LaunchLab/Stable/Locking, puis launch surfaces.
|
||||||
|
|
||||||
Garde-fous constants :
|
Garde-fous constants :
|
||||||
|
|
||||||
@@ -463,3 +465,37 @@ La suite fonctionnelle reprend par Raydium avant Meteora :
|
|||||||
3. `0.7.50` — `pump_swap` ;
|
3. `0.7.50` — `pump_swap` ;
|
||||||
4. `0.7.51` — `pump_fun` ;
|
4. `0.7.51` — `pump_fun` ;
|
||||||
5. `0.7.52+` — Meteora puis les autres DEX/surfaces.
|
5. `0.7.52+` — Meteora puis les autres DEX/surfaces.
|
||||||
|
|
||||||
|
## Note 0.7.48 — Raydium CPMM event coverage
|
||||||
|
|
||||||
|
La tranche `0.7.48` reprend `raydium_cpmm` avant Meteora, en s'appuyant sur la table `k_sol_dex_event_coverage_entries` ajoutée en `0.7.48-pre`.
|
||||||
|
|
||||||
|
Le scope CPMM couvre désormais les entrées Carbon/fnzero/IDL suivantes : `close_permission_pda`, `collect_creator_fee`, `collect_fund_fee`, `collect_protocol_fee`, `create_amm_config`, `create_permission_pda`, `deposit`, `initialize`, `initialize_with_permission`, `lp_change_event`, `swap_base_input`, `swap_base_output`, `swap_event`, `update_amm_config`, `update_pool_status` et `withdraw`.
|
||||||
|
|
||||||
|
Le decoder local spécialisé remplace le fallback `upstream_git.instruction_match` pour les instructions CPMM couvertes localement. Les events Anchor self-CPI `lp_change_event` et `swap_event` sont décodés comme preuve audit/coverage, mais ne produisent pas directement `trade_events`, metrics ou candles. Les swaps matérialisables restent les chemins instruction-scoped `swap_base_input` et `swap_base_output` lorsque les montants et le sens économique sont exploitables.
|
||||||
|
|
||||||
|
Aucune nouvelle table DB n'est ajoutée en `0.7.48`. Les transfers, token account lifecycle, vaults, orderbook, launch/migration et locking restent documentés comme familles transversales futures, à promouvoir seulement après preuve multi-DEX.
|
||||||
|
|
||||||
|
Voir aussi :
|
||||||
|
|
||||||
|
- `DEX_EVENT_COVERAGE_MATRIX.md` pour la couverture par familles ;
|
||||||
|
- `RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md` pour le rapport de tranche ;
|
||||||
|
- `SQL_VALIDATION_RAYDIUM_CPMM_0_7_48.sql` pour les requêtes de validation.
|
||||||
|
|
||||||
|
Complément `0.7.48-raydium-cpmm-program-data` : la coverage DB est maintenant synchronisée automatiquement quand un refresh de coverage est demandé sur une base neuve et qu'aucune ligne n'existe encore. Les backfills token/pool/signature déclenchent aussi un refresh best-effort de la coverage afin d'éviter un état incohérent où `k_sol_dex_decoded_events` est rempli mais `k_sol_dex_event_coverage_entries` reste vide.
|
||||||
|
|
||||||
|
Le decoder CPMM lit désormais les events `Program data:` émis par le programme Raydium CPMM. Les layouts `lp_change_event` et `swap_event` sont décodés depuis le payload event direct, en plus du chemin Anchor self-CPI `e445a52e51cb9a1d + event_discriminator`. `swap_event` reste `tradeCandidate=false` / `candleCandidate=false` : les seuls trades CPMM matérialisables restent `swap_base_input` et `swap_base_output`. `lp_change_event` peut alimenter la matérialisation liquidity si le corpus fournit un pool/pair fiable et si `changeType` distingue dépôt/retrait.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Note 0.7.48-part2-fix2 — Raydium CPMM coverage finalization
|
||||||
|
|
||||||
|
La tranche CPMM reconnaît désormais tous les discriminants instruction-level listés par Carbon / Raydium CP-Swap côté classificateur local. `lp_change_event` est traité comme famille bidirectionnelle `liquidity`, avec sens add/remove résolu par `changeType`, et le refresh coverage est confirmé après replay local sans validation séparée.
|
||||||
|
|
||||||
|
## Note 0.7.48 final — Raydium CPMM clôturable
|
||||||
|
|
||||||
|
La tranche `0.7.48` clôture la couverture Raydium CPMM sur corpus local. Les entrées `deposit`, `withdraw` et `lp_change_event` matérialisent `k_sol_liquidity_events` sans créer de trade/candle ; `initialize` et `initialize_with_permission` matérialisent seulement `k_sol_pool_lifecycle_events` ; les fees matérialisent `k_sol_fee_events` ; les entrées admin/config observées matérialisent `k_sol_pool_admin_events`.
|
||||||
|
|
||||||
|
La table technique `k_sol_instruction_observations` est ajoutée pour indexer localement les instructions observées par `decoder_code`, `instruction_name` et `discriminator_hex`. Demo3 peut rechercher par instruction/discriminant, ce qui reproduit localement 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_cpmm` | AMM | supporté | conserver trades/candles |
|
||||||
| `raydium_clmm` | CLMM | supporté | conserver trades/candles |
|
| `raydium_clmm` | CLMM | supporté | conserver trades/candles |
|
||||||
| `raydium_launchlab` | launch surface | planifié, program id local connu | ajouter decoder/materialization dédiée |
|
| `raydium_launchlab` | launch surface | planifié, program id local connu | ajouter decoder/materialization dédiée |
|
||||||
| `raydium_launchpad` | launch surface | à vérifier | ne pas inventer de program id |
|
|
||||||
| `raydium_amm_v4` | AMM legacy | partiel | corpus dédié après autres Raydium |
|
| `raydium_amm_v4` | AMM legacy | partiel | corpus dédié après autres Raydium |
|
||||||
| `raydium_router` | router | partiel | ne pas matérialiser en trade direct avant preuve |
|
| `raydium_router` | router | partiel | ne pas matérialiser en trade direct avant preuve |
|
||||||
| `raydium_stable_swap` | AMM legacy | planifié | traiter seulement si corpus pertinent |
|
| `raydium_stable_swap` | AMM legacy | planifié | traiter seulement si corpus pertinent |
|
||||||
@@ -1231,9 +1230,9 @@ Objectif : accélérer la découverte multi-DEX en indexant les `program_id`, di
|
|||||||
|
|
||||||
Familles prioritaires à indexer en premier :
|
Familles prioritaires à indexer en premier :
|
||||||
|
|
||||||
- DEX / AMM / CLMM / orderbook : `meteora-damm-v2`, `meteora-dbc`, `meteora-dlmm`, `meteora-vault`, `raydium-amm-v4`, `raydium-clmm`, `raydium-cpmm`, `raydium-launchpad`, `raydium-liquidity-locking`, `raydium-stable-swap`, `orca-whirlpool`, `fluxbeam`, `lifinity-amm-v2`, `phoenix-v1`, `openbook-v2`, `stabble-stable-swap`, `stabble-weighted-swap`, `bonkswap`, `boop`, `moonshot`, `heaven`, `okx-dex`, `pancake-swap`, `vertigo`, `virtuals`, `wavebreak`, `onchain-labs-dex-v1`, `onchain-labs-dex-v2` ;
|
- DEX / AMM / CLMM / orderbook : `meteora_damm_v2`, `meteora_dbc`, `meteora_dlmm`, `meteora_vault`, `raydium_amm_v4`, `raydium_clmm`, `raydium_cpmm`, `raydium_launchlab`, `raydium_liquidity_locking`, `raydium_stable_swap`, `orca_whirlpools`, `fluxbeam`, `lifinity_v2`, `phoenix_v1`, `openbook_v2`, `stabble_stable_swap`, `stabble_weighted_swap`, `bonkswap`, `boop`, `moonshot`, `heaven`, `okx_dex`, `pancake_swap`, `vertigo`, `virtuals`, `wavebreak`, `onchain_labs_dex_v1`, `onchain_labs_dex_v2` ;
|
||||||
- agrégateurs / ordres / perps / lending utiles au routage ou à 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` ;
|
- 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`.
|
- 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.
|
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.
|
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.
|
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">Leave all unchecked for generic discovery. Check several surfaces to scan once and keep candidates matching any selected target.</div>
|
||||||
<div class="form-text">Use this to find corpus signatures for non-swap decoders without promoting unverified events. Leave all unchecked to request target='any'.</div>
|
<div class="form-text">Use this to find corpus signatures for non-swap decoders without promoting unverified events. Leave all unchecked to request target='any'.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label for="demo3TargetInstructionNameInput" class="form-label">Target instruction name</label>
|
||||||
|
<input id="demo3TargetInstructionNameInput" type="text" class="form-control font-monospace" placeholder="withdraw, raydium_cpmm.deposit" />
|
||||||
|
<div class="form-text">Optional exact instruction-name filter. Accepts comma/space separated names.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label for="demo3TargetDiscriminatorHexInput" class="form-label">Target discriminator hex</label>
|
||||||
|
<input id="demo3TargetDiscriminatorHexInput" type="text" class="form-control font-monospace" placeholder="b712469c946da122, f223c68952e1f2b6" />
|
||||||
|
<div class="form-text">Optional first 8 bytes of instruction data, matching the Solscan instruction filter.</div>
|
||||||
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label for="demo3HttpRoleInput" class="form-label">HTTP role</label>
|
<label for="demo3HttpRoleInput" class="form-label">HTTP role</label>
|
||||||
<input id="demo3HttpRoleInput" type="text" class="form-control" value="history_backfill" />
|
<input id="demo3HttpRoleInput" type="text" class="form-control" value="history_backfill" />
|
||||||
@@ -335,6 +345,7 @@
|
|||||||
<th>Kind</th>
|
<th>Kind</th>
|
||||||
<th>Confidence</th>
|
<th>Confidence</th>
|
||||||
<th>Data prefix</th>
|
<th>Data prefix</th>
|
||||||
|
<th>Discriminator</th>
|
||||||
<th>Verified pool</th>
|
<th>Verified pool</th>
|
||||||
<th>Token A</th>
|
<th>Token A</th>
|
||||||
<th>Token B</th>
|
<th>Token B</th>
|
||||||
@@ -346,7 +357,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="demo3OnchainCandidateTableBody">
|
<tbody id="demo3OnchainCandidateTableBody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="12" class="text-body-secondary">No on-chain candidate.</td>
|
<td colspan="13" class="text-body-secondary">No on-chain candidate.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -416,7 +416,7 @@
|
|||||||
Aucun jeu de candles chargé.
|
Aucun jeu de candles chargé.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="demoPipeline2Chart" class="w-100 border rounded bg-body" style="height: 560px;"></div>
|
<div id="demoPipeline2Chart" class="w-100 border rounded bg-body" style="height: 680px; min-height: 680px;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ scanOrder: string | null,
|
|||||||
* Optional target event family used to find non-swap signatures.
|
* Optional target event family used to find non-swap signatures.
|
||||||
*/
|
*/
|
||||||
targetEvent: string | null,
|
targetEvent: string | null,
|
||||||
|
/**
|
||||||
|
* Optional instruction name filter, e.g. `withdraw` or `raydium_cpmm.withdraw`.
|
||||||
|
*/
|
||||||
|
targetInstructionName: string | null,
|
||||||
|
/**
|
||||||
|
* Optional instruction discriminator filter as 16-char lower hex, comma-separated when needed.
|
||||||
|
*/
|
||||||
|
targetDiscriminatorHex: string | null,
|
||||||
/**
|
/**
|
||||||
* Whether transactions containing swap-like logs should be skipped.
|
* Whether transactions containing swap-like logs should be skipped.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ instructionName: string | null,
|
|||||||
* Prefix of the raw base58 instruction data, useful for audit grouping.
|
* Prefix of the raw base58 instruction data, useful for audit grouping.
|
||||||
*/
|
*/
|
||||||
instructionDataPrefix: string | null,
|
instructionDataPrefix: string | null,
|
||||||
|
/**
|
||||||
|
* First eight instruction-data bytes as lower hex.
|
||||||
|
*/
|
||||||
|
instructionDiscriminatorHex: string | null,
|
||||||
/**
|
/**
|
||||||
* Candidate pool address.
|
* Candidate pool address.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -187,6 +187,20 @@ function validateOnchainRequest(request: Demo3OnchainDexDiscoveryRequest): void
|
|||||||
}
|
}
|
||||||
throw new Error("Program id filter must be a valid Solana program id, or empty when using a preset that resolves it.");
|
throw new Error("Program id filter must be a valid Solana program id, or empty when using a preset that resolves it.");
|
||||||
}
|
}
|
||||||
|
validateDiscriminatorFilter(request.targetDiscriminatorHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDiscriminatorFilter(value: string | null): void {
|
||||||
|
if (value === null || value.trim() === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tokens = value.split(/[\s,]+/).map((token) => token.trim()).filter((token) => token !== "");
|
||||||
|
for (const token of tokens) {
|
||||||
|
const normalized = token.startsWith("0x") || token.startsWith("0X") ? token.slice(2) : token;
|
||||||
|
if (!/^[0-9a-fA-F]{16}$/.test(normalized)) {
|
||||||
|
throw new Error(`Target discriminator '${token}' must be exactly 8 bytes / 16 hex characters.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function numberValueOrNull(value: string): number | null {
|
function numberValueOrNull(value: string): number | null {
|
||||||
@@ -320,6 +334,8 @@ function readOnchainRequest(): Demo3OnchainDexDiscoveryRequest {
|
|||||||
maxPages: intValue("demo3MaxPagesInput", 1),
|
maxPages: intValue("demo3MaxPagesInput", 1),
|
||||||
scanOrder: valueOrNull(byId<HTMLSelectElement>("demo3ScanOrderSelect").value),
|
scanOrder: valueOrNull(byId<HTMLSelectElement>("demo3ScanOrderSelect").value),
|
||||||
targetEvent: readTargetEventFilter(),
|
targetEvent: readTargetEventFilter(),
|
||||||
|
targetInstructionName: valueOrNull(byId<HTMLInputElement>("demo3TargetInstructionNameInput").value),
|
||||||
|
targetDiscriminatorHex: valueOrNull(byId<HTMLInputElement>("demo3TargetDiscriminatorHexInput").value),
|
||||||
excludeSwaps: byId<HTMLInputElement>("demo3ExcludeSwapsInput").checked,
|
excludeSwaps: byId<HTMLInputElement>("demo3ExcludeSwapsInput").checked,
|
||||||
includeFailed: byId<HTMLInputElement>("demo3IncludeFailedInput").checked,
|
includeFailed: byId<HTMLInputElement>("demo3IncludeFailedInput").checked,
|
||||||
httpRole: byId<HTMLInputElement>("demo3HttpRoleInput").value.trim() || "history_backfill",
|
httpRole: byId<HTMLInputElement>("demo3HttpRoleInput").value.trim() || "history_backfill",
|
||||||
@@ -375,8 +391,10 @@ function renderOnchainResult(result: Demo3OnchainDexDiscoveryResult): void {
|
|||||||
byId<HTMLElement>("demo3SummaryRejectedCandidateCount").textContent = String(result.targetRejectedCandidateCount);
|
byId<HTMLElement>("demo3SummaryRejectedCandidateCount").textContent = String(result.targetRejectedCandidateCount);
|
||||||
byId<HTMLElement>("demo3SummaryCandidateCount").textContent = String(result.candidateCount);
|
byId<HTMLElement>("demo3SummaryCandidateCount").textContent = String(result.candidateCount);
|
||||||
const targetEvent = targetEventLabel(result.request.targetEvent);
|
const targetEvent = targetEventLabel(result.request.targetEvent);
|
||||||
|
const targetInstruction = result.request.targetInstructionName ?? "any";
|
||||||
|
const targetDiscriminator = result.request.targetDiscriminatorHex ?? "any";
|
||||||
const sourceText = result.resolvedSignatureAddresses.length === 0 ? result.resolvedSignatureAddress : result.resolvedSignatureAddresses.join(",");
|
const sourceText = result.resolvedSignatureAddresses.length === 0 ? result.resolvedSignatureAddress : result.resolvedSignatureAddresses.join(",");
|
||||||
byId<HTMLElement>("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / program=${result.resolvedProgramId} / source=${result.resolvedSignatureSource}:${sourceText} / target=${targetEvent} / order=${result.request.scanOrder ?? "newest_first"}`;
|
byId<HTMLElement>("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / program=${result.resolvedProgramId} / source=${result.resolvedSignatureSource}:${sourceText} / target=${targetEvent} / instr=${targetInstruction} / disc=${targetDiscriminator} / order=${result.request.scanOrder ?? "newest_first"}`;
|
||||||
byId<HTMLElement>("demo3UniqueSignatureText").textContent = result.uniqueBackfillSignatures.length === 0 ? "-" : result.uniqueBackfillSignatures.join(", ");
|
byId<HTMLElement>("demo3UniqueSignatureText").textContent = result.uniqueBackfillSignatures.length === 0 ? "-" : result.uniqueBackfillSignatures.join(", ");
|
||||||
byId<HTMLElement>("demo3NextBeforeText").textContent = result.nextBeforeByAddress.length === 0 ? "-" : result.nextBeforeByAddress.map((cursor) => `${cursor.address}:${cursor.nextBeforeSignature ?? "-"}`).join(" | ");
|
byId<HTMLElement>("demo3NextBeforeText").textContent = result.nextBeforeByAddress.length === 0 ? "-" : result.nextBeforeByAddress.map((cursor) => `${cursor.address}:${cursor.nextBeforeSignature ?? "-"}`).join(" | ");
|
||||||
renderRejectedSummary(result);
|
renderRejectedSummary(result);
|
||||||
@@ -402,7 +420,7 @@ function renderRejectedSummary(result: Demo3OnchainDexDiscoveryResult): void {
|
|||||||
function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): void {
|
function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): void {
|
||||||
const body = byId<HTMLTableSectionElement>("demo3OnchainCandidateTableBody");
|
const body = byId<HTMLTableSectionElement>("demo3OnchainCandidateTableBody");
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
body.innerHTML = '<tr><td colspan="12" class="text-body-secondary">No on-chain candidate.</td></tr>';
|
body.innerHTML = '<tr><td colspan="13" class="text-body-secondary">No on-chain candidate.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
body.innerHTML = candidates.map((candidate) => {
|
body.innerHTML = candidates.map((candidate) => {
|
||||||
@@ -428,6 +446,7 @@ function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): vo
|
|||||||
<td><span class="badge text-bg-info">${escapeHtml(candidate.candidateKind)}</span></td>
|
<td><span class="badge text-bg-info">${escapeHtml(candidate.candidateKind)}</span></td>
|
||||||
<td><span class="badge text-bg-${candidate.confidence === "high" ? "success" : candidate.confidence === "medium" ? "warning" : "secondary"}">${escapeHtml(candidate.confidence)}</span></td>
|
<td><span class="badge text-bg-${candidate.confidence === "high" ? "success" : candidate.confidence === "medium" ? "warning" : "secondary"}">${escapeHtml(candidate.confidence)}</span></td>
|
||||||
<td class="font-monospace" title="${escapeHtml(candidate.instructionDataPrefix ?? "")}">${escapeHtml(shortText(candidate.instructionDataPrefix, 14))}</td>
|
<td class="font-monospace" title="${escapeHtml(candidate.instructionDataPrefix ?? "")}">${escapeHtml(shortText(candidate.instructionDataPrefix, 14))}</td>
|
||||||
|
<td class="font-monospace" title="${escapeHtml(candidate.instructionDiscriminatorHex ?? "")}">${escapeHtml(shortText(candidate.instructionDiscriminatorHex, 16))}</td>
|
||||||
<td class="font-monospace" title="${escapeHtml(verifiedPool ?? "")}">${escapeHtml(shortText(verifiedPool, 14))}</td>
|
<td class="font-monospace" title="${escapeHtml(verifiedPool ?? "")}">${escapeHtml(shortText(verifiedPool, 14))}</td>
|
||||||
<td class="font-monospace" title="${escapeHtml(candidate.tokenAMint ?? "")}">${escapeHtml(shortText(candidate.tokenAMint, 14))}</td>
|
<td class="font-monospace" title="${escapeHtml(candidate.tokenAMint ?? "")}">${escapeHtml(shortText(candidate.tokenAMint, 14))}</td>
|
||||||
<td class="font-monospace" title="${escapeHtml(candidate.tokenBMint ?? "")}">${escapeHtml(shortText(candidate.tokenBMint, 14))}</td>
|
<td class="font-monospace" title="${escapeHtml(candidate.tokenBMint ?? "")}">${escapeHtml(shortText(candidate.tokenBMint, 14))}</td>
|
||||||
@@ -469,7 +488,7 @@ async function discoverOnchain(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStatus("running", "text-bg-warning");
|
setStatus("running", "text-bg-warning");
|
||||||
appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' source='${request.signatureSource ?? "program_id"}:${request.sourceAddresses.join(",")}' target='${targetEventLabel(request.targetEvent)}' pages='${request.maxPages}' order='${request.scanOrder ?? "newest_first"}' before='${request.beforeSignature ?? ""}' until='${request.untilSignature ?? ""}' excludeSwaps='${request.excludeSwaps}' role='${request.httpRole}'`);
|
appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' source='${request.signatureSource ?? "program_id"}:${request.sourceAddresses.join(",")}' target='${targetEventLabel(request.targetEvent)}' instruction='${request.targetInstructionName ?? ""}' discriminator='${request.targetDiscriminatorHex ?? ""}' pages='${request.maxPages}' order='${request.scanOrder ?? "newest_first"}' before='${request.beforeSignature ?? ""}' until='${request.untilSignature ?? ""}' excludeSwaps='${request.excludeSwaps}' role='${request.httpRole}'`);
|
||||||
try {
|
try {
|
||||||
const payload = await invoke<Demo3OnchainDexDiscoveryPayload>("demo3_discover_onchain_dex_pairs", { request });
|
const payload = await invoke<Demo3OnchainDexDiscoveryPayload>("demo3_discover_onchain_dex_pairs", { request });
|
||||||
lastResultJson = payload.resultJson;
|
lastResultJson = payload.resultJson;
|
||||||
|
|||||||
@@ -186,6 +186,32 @@ function renderCatalogTextareas(
|
|||||||
pairsTextarea.value = JSON.stringify(catalog.pairs, null, 2);
|
pairsTextarea.value = JSON.stringify(catalog.pairs, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function toChartNumber(value: number | string | null | undefined): number {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return Number.isFinite(value) ? value : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number.parseFloat(value);
|
||||||
|
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateVisibleWindowStart(totalCandles: number): number {
|
||||||
|
if (totalCandles <= 90) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(0, ((totalCandles - 90) / totalCandles) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
function parseCandlesJson(raw: string): PairCandle[] {
|
function parseCandlesJson(raw: string): PairCandle[] {
|
||||||
if (raw.trim() === "") {
|
if (raw.trim() === "") {
|
||||||
return [];
|
return [];
|
||||||
@@ -239,16 +265,18 @@ function renderCandlesChart(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ohlcData = sorted.map((candle) => [
|
const ohlcData = sorted.map((candle) => [
|
||||||
candle.open_price_quote_per_base,
|
toChartNumber(candle.open_price_quote_per_base),
|
||||||
candle.close_price_quote_per_base,
|
toChartNumber(candle.close_price_quote_per_base),
|
||||||
candle.low_price_quote_per_base,
|
toChartNumber(candle.low_price_quote_per_base),
|
||||||
candle.high_price_quote_per_base,
|
toChartNumber(candle.high_price_quote_per_base),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const volumeData = sorted.map((candle) =>
|
const volumeData = sorted.map((candle) =>
|
||||||
parseVolume(candle.quote_volume_raw, candle.trade_count),
|
parseVolume(candle.quote_volume_raw, candle.trade_count),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const zoomStart = calculateVisibleWindowStart(sorted.length);
|
||||||
|
|
||||||
chartMeta.textContent =
|
chartMeta.textContent =
|
||||||
`Pair #${pairId.toString()} • timeframe ${timeframeSeconds.toString()}s • ${sorted.length} candles`;
|
`Pair #${pairId.toString()} • timeframe ${timeframeSeconds.toString()}s • ${sorted.length} candles`;
|
||||||
|
|
||||||
@@ -256,7 +284,8 @@ function renderCandlesChart(
|
|||||||
animation: false,
|
animation: false,
|
||||||
legend: {
|
legend: {
|
||||||
data: ["OHLC", "Volume"],
|
data: ["OHLC", "Volume"],
|
||||||
top: 0,
|
top: 4,
|
||||||
|
left: 16,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "axis",
|
trigger: "axis",
|
||||||
@@ -268,8 +297,8 @@ function renderCandlesChart(
|
|||||||
link: [{ xAxisIndex: "all" }],
|
link: [{ xAxisIndex: "all" }],
|
||||||
},
|
},
|
||||||
grid: [
|
grid: [
|
||||||
{ left: 60, right: 24, top: 40, height: "58%" },
|
{ left: 76, right: 32, top: 52, height: "58%" },
|
||||||
{ left: 60, right: 24, top: "74%", height: "16%" },
|
{ left: 76, right: 32, top: "77%", height: "12%" },
|
||||||
],
|
],
|
||||||
xAxis: [
|
xAxis: [
|
||||||
{
|
{
|
||||||
@@ -277,7 +306,9 @@ function renderCandlesChart(
|
|||||||
data: categoryData,
|
data: categoryData,
|
||||||
boundaryGap: true,
|
boundaryGap: true,
|
||||||
axisLine: { onZero: false },
|
axisLine: { onZero: false },
|
||||||
splitLine: { show: false },
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitLine: { show: true },
|
||||||
|
axisLabel: { hideOverlap: true, margin: 14 },
|
||||||
min: "dataMin",
|
min: "dataMin",
|
||||||
max: "dataMax",
|
max: "dataMax",
|
||||||
},
|
},
|
||||||
@@ -287,9 +318,9 @@ function renderCandlesChart(
|
|||||||
data: categoryData,
|
data: categoryData,
|
||||||
boundaryGap: true,
|
boundaryGap: true,
|
||||||
axisLine: { onZero: false },
|
axisLine: { onZero: false },
|
||||||
axisTick: { show: false },
|
axisTick: { alignWithLabel: true },
|
||||||
splitLine: { show: false },
|
splitLine: { show: false },
|
||||||
axisLabel: { show: false },
|
axisLabel: { hideOverlap: true, margin: 10 },
|
||||||
min: "dataMin",
|
min: "dataMin",
|
||||||
max: "dataMax",
|
max: "dataMax",
|
||||||
},
|
},
|
||||||
@@ -297,27 +328,33 @@ function renderCandlesChart(
|
|||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
scale: true,
|
scale: true,
|
||||||
|
splitNumber: 5,
|
||||||
splitArea: { show: false },
|
splitArea: { show: false },
|
||||||
|
axisLabel: { margin: 12 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
gridIndex: 1,
|
gridIndex: 1,
|
||||||
scale: true,
|
scale: true,
|
||||||
splitNumber: 2,
|
splitNumber: 2,
|
||||||
|
axisLabel: { margin: 12 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
dataZoom: [
|
dataZoom: [
|
||||||
{
|
{
|
||||||
type: "inside",
|
type: "inside",
|
||||||
xAxisIndex: [0, 1],
|
xAxisIndex: [0, 1],
|
||||||
start: 0,
|
filterMode: "none",
|
||||||
|
start: zoomStart,
|
||||||
end: 100,
|
end: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
show: true,
|
show: true,
|
||||||
type: "slider",
|
type: "slider",
|
||||||
xAxisIndex: [0, 1],
|
xAxisIndex: [0, 1],
|
||||||
bottom: 6,
|
filterMode: "none",
|
||||||
start: 0,
|
bottom: 8,
|
||||||
|
height: 22,
|
||||||
|
start: zoomStart,
|
||||||
end: 100,
|
end: 100,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -326,11 +363,12 @@ function renderCandlesChart(
|
|||||||
name: "OHLC",
|
name: "OHLC",
|
||||||
type: "candlestick",
|
type: "candlestick",
|
||||||
data: ohlcData,
|
data: ohlcData,
|
||||||
|
xAxisIndex: 0,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
barMinWidth: 4,
|
||||||
|
barMaxWidth: 16,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: "#16a34a",
|
borderWidth: 1.4,
|
||||||
color0: "#dc2626",
|
|
||||||
borderColor: "#15803d",
|
|
||||||
borderColor0: "#b91c1c",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -339,9 +377,13 @@ function renderCandlesChart(
|
|||||||
xAxisIndex: 1,
|
xAxisIndex: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
data: volumeData,
|
data: volumeData,
|
||||||
|
barMinWidth: 2,
|
||||||
|
barMaxWidth: 10,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
window.setTimeout(() => chart.resize(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
@@ -468,6 +510,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const chart = echarts.init(safeChartElement);
|
const chart = echarts.init(safeChartElement);
|
||||||
setEmptyChart(chart, safeChartMeta, "Aucune candle disponible.");
|
setEmptyChart(chart, safeChartMeta, "Aucune candle disponible.");
|
||||||
window.addEventListener("resize", () => chart.resize());
|
window.addEventListener("resize", () => chart.resize());
|
||||||
|
const chartCollapse = document.querySelector<HTMLDivElement>("#demoPipeline2ChartCollapse");
|
||||||
|
chartCollapse?.addEventListener("shown.bs.collapse", () => {
|
||||||
|
window.setTimeout(() => chart.resize(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
clearLogButton.addEventListener("click", () => {
|
clearLogButton.addEventListener("click", () => {
|
||||||
logTextarea.value = "";
|
logTextarea.value = "";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kb-demo-app",
|
"name": "kb-demo-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.47",
|
"version": "0.7.48",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -287,7 +287,10 @@ pub(crate) async fn demo3_search_local_dex_corpus(
|
|||||||
|
|
||||||
/// Search request for the static upstream registry exposed through Demo3.
|
/// Search request for the static upstream registry exposed through Demo3.
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS)]
|
||||||
#[ts(export, export_to = "../frontend/ts/bindings/Demo3UpstreamRegistrySearchRequest.ts")]
|
#[ts(
|
||||||
|
export,
|
||||||
|
export_to = "../frontend/ts/bindings/Demo3UpstreamRegistrySearchRequest.ts"
|
||||||
|
)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct Demo3UpstreamRegistrySearchRequest {
|
pub(crate) struct Demo3UpstreamRegistrySearchRequest {
|
||||||
/// Optional decoder-code filter.
|
/// Optional decoder-code filter.
|
||||||
@@ -423,10 +426,7 @@ pub(crate) fn demo3_search_upstream_registry(
|
|||||||
return Err(format!("cannot serialize upstream registry result: {}", error));
|
return Err(format!("cannot serialize upstream registry result: {}", error));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return Ok(Demo3UpstreamRegistryPayload {
|
return Ok(Demo3UpstreamRegistryPayload { result_json, result: ui_result });
|
||||||
result_json,
|
|
||||||
result: ui_result,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_lib_upstream_registry_request(
|
fn to_lib_upstream_registry_request(
|
||||||
@@ -491,8 +491,7 @@ fn from_lib_upstream_registry_summary(
|
|||||||
account_entry_count: summary.account_entry_count,
|
account_entry_count: summary.account_entry_count,
|
||||||
upstream_git_unverified_count: summary.upstream_git_unverified_count,
|
upstream_git_unverified_count: summary.upstream_git_unverified_count,
|
||||||
upstream_git_mapped_unverified_count: summary.upstream_git_mapped_unverified_count,
|
upstream_git_mapped_unverified_count: summary.upstream_git_mapped_unverified_count,
|
||||||
upstream_git_local_corpus_observed_count: summary
|
upstream_git_local_corpus_observed_count: summary.upstream_git_local_corpus_observed_count,
|
||||||
.upstream_git_local_corpus_observed_count,
|
|
||||||
upstream_git_local_corpus_materialized_count: summary
|
upstream_git_local_corpus_materialized_count: summary
|
||||||
.upstream_git_local_corpus_materialized_count,
|
.upstream_git_local_corpus_materialized_count,
|
||||||
upstream_git_layout_unverified_count: summary.upstream_git_layout_unverified_count,
|
upstream_git_layout_unverified_count: summary.upstream_git_layout_unverified_count,
|
||||||
@@ -685,6 +684,12 @@ pub(crate) struct Demo3OnchainDexDiscoveryRequest {
|
|||||||
pub scan_order: std::option::Option<std::string::String>,
|
pub scan_order: std::option::Option<std::string::String>,
|
||||||
/// Optional target event family used to find non-swap signatures.
|
/// Optional target event family used to find non-swap signatures.
|
||||||
pub target_event: std::option::Option<std::string::String>,
|
pub target_event: std::option::Option<std::string::String>,
|
||||||
|
/// Optional instruction name filter, e.g. `withdraw` or `raydium_cpmm.withdraw`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_instruction_name: std::option::Option<std::string::String>,
|
||||||
|
/// Optional instruction discriminator filter as 16-char lower hex, comma-separated when needed.
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_discriminator_hex: std::option::Option<std::string::String>,
|
||||||
/// Whether transactions containing swap-like logs should be skipped.
|
/// Whether transactions containing swap-like logs should be skipped.
|
||||||
pub exclude_swaps: bool,
|
pub exclude_swaps: bool,
|
||||||
/// Whether failed transactions should be returned as candidates.
|
/// Whether failed transactions should be returned as candidates.
|
||||||
@@ -846,6 +851,8 @@ pub(crate) struct Demo3OnchainDexPairCandidate {
|
|||||||
pub instruction_name: std::option::Option<std::string::String>,
|
pub instruction_name: std::option::Option<std::string::String>,
|
||||||
/// Prefix of the raw base58 instruction data, useful for audit grouping.
|
/// Prefix of the raw base58 instruction data, useful for audit grouping.
|
||||||
pub instruction_data_prefix: std::option::Option<std::string::String>,
|
pub instruction_data_prefix: std::option::Option<std::string::String>,
|
||||||
|
/// First eight instruction-data bytes as lower hex.
|
||||||
|
pub instruction_discriminator_hex: std::option::Option<std::string::String>,
|
||||||
/// Candidate pool address.
|
/// Candidate pool address.
|
||||||
pub pool_address: std::option::Option<std::string::String>,
|
pub pool_address: std::option::Option<std::string::String>,
|
||||||
/// Candidate token A mint.
|
/// Candidate token A mint.
|
||||||
@@ -966,6 +973,8 @@ fn to_lib_onchain_request(
|
|||||||
max_pages: request.max_pages,
|
max_pages: request.max_pages,
|
||||||
scan_order: normalize_optional_text(request.scan_order.clone()),
|
scan_order: normalize_optional_text(request.scan_order.clone()),
|
||||||
target_event: normalize_optional_text(request.target_event.clone()),
|
target_event: normalize_optional_text(request.target_event.clone()),
|
||||||
|
target_instruction_name: normalize_optional_text(request.target_instruction_name.clone()),
|
||||||
|
target_discriminator_hex: normalize_optional_text(request.target_discriminator_hex.clone()),
|
||||||
exclude_swaps: request.exclude_swaps,
|
exclude_swaps: request.exclude_swaps,
|
||||||
include_failed: request.include_failed,
|
include_failed: request.include_failed,
|
||||||
http_role: request.http_role.trim().to_string(),
|
http_role: request.http_role.trim().to_string(),
|
||||||
@@ -994,6 +1003,8 @@ fn from_lib_onchain_result(
|
|||||||
max_pages: result.request.max_pages,
|
max_pages: result.request.max_pages,
|
||||||
scan_order: result.request.scan_order,
|
scan_order: result.request.scan_order,
|
||||||
target_event: result.request.target_event,
|
target_event: result.request.target_event,
|
||||||
|
target_instruction_name: result.request.target_instruction_name,
|
||||||
|
target_discriminator_hex: result.request.target_discriminator_hex,
|
||||||
exclude_swaps: result.request.exclude_swaps,
|
exclude_swaps: result.request.exclude_swaps,
|
||||||
include_failed: result.request.include_failed,
|
include_failed: result.request.include_failed,
|
||||||
http_role: result.request.http_role,
|
http_role: result.request.http_role,
|
||||||
@@ -1074,6 +1085,7 @@ fn from_lib_onchain_candidate(
|
|||||||
inner_instruction_index: candidate.inner_instruction_index,
|
inner_instruction_index: candidate.inner_instruction_index,
|
||||||
instruction_name: candidate.instruction_name,
|
instruction_name: candidate.instruction_name,
|
||||||
instruction_data_prefix: candidate.instruction_data_prefix,
|
instruction_data_prefix: candidate.instruction_data_prefix,
|
||||||
|
instruction_discriminator_hex: candidate.instruction_discriminator_hex,
|
||||||
pool_address: candidate.pool_address,
|
pool_address: candidate.pool_address,
|
||||||
token_a_mint: candidate.token_a_mint,
|
token_a_mint: candidate.token_a_mint,
|
||||||
token_b_mint: candidate.token_b_mint,
|
token_b_mint: candidate.token_b_mint,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "kb-demo-app",
|
"productName": "kb-demo-app",
|
||||||
"version": "0.7.47",
|
"version": "0.7.48",
|
||||||
"identifier": "com.sasedev.kb-demo-app",
|
"identifier": "com.sasedev.kb-demo-app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ pub const RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID: &str = "5quBtoiQqxF9Jv6KYKctB59NT3
|
|||||||
pub const BONKSWAP_PROGRAM_ID: &str = "BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p";
|
pub const BONKSWAP_PROGRAM_ID: &str = "BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p";
|
||||||
|
|
||||||
/// Boop program id extracted from upstream Git decoder source.
|
/// Boop program id extracted from upstream Git decoder source.
|
||||||
pub const BOOP_PROGRAM_ID: &str = "boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4";
|
pub const BOOP_FUN_PROGRAM_ID: &str = "boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4";
|
||||||
|
|
||||||
/// DFlow Aggregator v4 program id extracted from upstream Git decoder source.
|
/// DFlow Aggregator v4 program id extracted from upstream Git decoder source.
|
||||||
pub const DFLOW_AGGREGATOR_V4_PROGRAM_ID: &str = "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH";
|
pub const DFLOW_AGGREGATOR_V4_PROGRAM_ID: &str = "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH";
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub use dtos::DexDto;
|
|||||||
pub use dtos::DexEventCoverageEntryDto;
|
pub use dtos::DexEventCoverageEntryDto;
|
||||||
pub use dtos::DexEventCoverageSummaryDto;
|
pub use dtos::DexEventCoverageSummaryDto;
|
||||||
pub use dtos::FeeEventDto;
|
pub use dtos::FeeEventDto;
|
||||||
|
pub use dtos::InstructionObservationDto;
|
||||||
pub use dtos::KnownHttpEndpointDto;
|
pub use dtos::KnownHttpEndpointDto;
|
||||||
pub use dtos::KnownWsEndpointDto;
|
pub use dtos::KnownWsEndpointDto;
|
||||||
pub use dtos::LaunchAttributionDto;
|
pub use dtos::LaunchAttributionDto;
|
||||||
@@ -96,6 +97,7 @@ pub use entities::DexEntity;
|
|||||||
pub use entities::DexEventCoverageEntryEntity;
|
pub use entities::DexEventCoverageEntryEntity;
|
||||||
pub use entities::DexEventCoverageSummaryEntity;
|
pub use entities::DexEventCoverageSummaryEntity;
|
||||||
pub use entities::FeeEventEntity;
|
pub use entities::FeeEventEntity;
|
||||||
|
pub use entities::InstructionObservationEntity;
|
||||||
pub use entities::KnownHttpEndpointEntity;
|
pub use entities::KnownHttpEndpointEntity;
|
||||||
pub use entities::KnownWsEndpointEntity;
|
pub use entities::KnownWsEndpointEntity;
|
||||||
pub use entities::LaunchAttributionEntity;
|
pub use entities::LaunchAttributionEntity;
|
||||||
@@ -168,6 +170,8 @@ pub use queries::query_dexs_upsert;
|
|||||||
pub use queries::query_fee_events_get_by_decoded_event_id;
|
pub use queries::query_fee_events_get_by_decoded_event_id;
|
||||||
pub use queries::query_fee_events_list_recent;
|
pub use queries::query_fee_events_list_recent;
|
||||||
pub use queries::query_fee_events_upsert;
|
pub use queries::query_fee_events_upsert;
|
||||||
|
pub use queries::query_instruction_observations_list_by_filter;
|
||||||
|
pub use queries::query_instruction_observations_upsert;
|
||||||
pub use queries::query_known_http_endpoints_get;
|
pub use queries::query_known_http_endpoints_get;
|
||||||
pub use queries::query_known_http_endpoints_list;
|
pub use queries::query_known_http_endpoints_list;
|
||||||
pub use queries::query_known_http_endpoints_upsert;
|
pub use queries::query_known_http_endpoints_upsert;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ mod dex_event_coverage_entry;
|
|||||||
mod fee_event;
|
mod fee_event;
|
||||||
mod known_http_endpoint;
|
mod known_http_endpoint;
|
||||||
mod known_ws_endpoint;
|
mod known_ws_endpoint;
|
||||||
|
mod instruction_observation;
|
||||||
mod launch_attribution;
|
mod launch_attribution;
|
||||||
mod launch_surface;
|
mod launch_surface;
|
||||||
mod launch_surface_key;
|
mod launch_surface_key;
|
||||||
@@ -84,6 +85,7 @@ pub use dex_event_coverage_entry::DexEventCoverageSummaryDto;
|
|||||||
pub use fee_event::FeeEventDto;
|
pub use fee_event::FeeEventDto;
|
||||||
pub use known_http_endpoint::KnownHttpEndpointDto;
|
pub use known_http_endpoint::KnownHttpEndpointDto;
|
||||||
pub use known_ws_endpoint::KnownWsEndpointDto;
|
pub use known_ws_endpoint::KnownWsEndpointDto;
|
||||||
|
pub use instruction_observation::InstructionObservationDto;
|
||||||
pub use launch_attribution::LaunchAttributionDto;
|
pub use launch_attribution::LaunchAttributionDto;
|
||||||
pub use launch_surface::LaunchSurfaceDto;
|
pub use launch_surface::LaunchSurfaceDto;
|
||||||
pub use launch_surface_key::LaunchSurfaceKeyDto;
|
pub use launch_surface_key::LaunchSurfaceKeyDto;
|
||||||
|
|||||||
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 fee_event;
|
||||||
mod known_http_endpoint;
|
mod known_http_endpoint;
|
||||||
mod known_ws_endpoint;
|
mod known_ws_endpoint;
|
||||||
|
mod instruction_observation;
|
||||||
mod launch_attribution;
|
mod launch_attribution;
|
||||||
mod launch_surface;
|
mod launch_surface;
|
||||||
mod launch_surface_key;
|
mod launch_surface_key;
|
||||||
@@ -62,6 +63,7 @@ pub use dex_event_coverage_entry::DexEventCoverageSummaryEntity;
|
|||||||
pub use fee_event::FeeEventEntity;
|
pub use fee_event::FeeEventEntity;
|
||||||
pub use known_http_endpoint::KnownHttpEndpointEntity;
|
pub use known_http_endpoint::KnownHttpEndpointEntity;
|
||||||
pub use known_ws_endpoint::KnownWsEndpointEntity;
|
pub use known_ws_endpoint::KnownWsEndpointEntity;
|
||||||
|
pub use instruction_observation::InstructionObservationEntity;
|
||||||
pub use launch_attribution::LaunchAttributionEntity;
|
pub use launch_attribution::LaunchAttributionEntity;
|
||||||
pub use launch_surface::LaunchSurfaceEntity;
|
pub use launch_surface::LaunchSurfaceEntity;
|
||||||
pub use launch_surface_key::LaunchSurfaceKeyEntity;
|
pub use launch_surface_key::LaunchSurfaceKeyEntity;
|
||||||
|
|||||||
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 fee_event;
|
||||||
mod known_http_endpoint;
|
mod known_http_endpoint;
|
||||||
mod known_ws_endpoint;
|
mod known_ws_endpoint;
|
||||||
|
mod instruction_observation;
|
||||||
mod launch_attribution;
|
mod launch_attribution;
|
||||||
mod launch_surface;
|
mod launch_surface;
|
||||||
mod launch_surface_key;
|
mod launch_surface_key;
|
||||||
@@ -92,6 +93,8 @@ pub use known_http_endpoint::query_known_http_endpoints_upsert;
|
|||||||
pub use known_ws_endpoint::query_known_ws_endpoints_get;
|
pub use known_ws_endpoint::query_known_ws_endpoints_get;
|
||||||
pub use known_ws_endpoint::query_known_ws_endpoints_list;
|
pub use known_ws_endpoint::query_known_ws_endpoints_list;
|
||||||
pub use known_ws_endpoint::query_known_ws_endpoints_upsert;
|
pub use known_ws_endpoint::query_known_ws_endpoints_upsert;
|
||||||
|
pub use instruction_observation::query_instruction_observations_list_by_filter;
|
||||||
|
pub use instruction_observation::query_instruction_observations_upsert;
|
||||||
pub use launch_attribution::query_launch_attributions_get_by_decoded_event_id;
|
pub use launch_attribution::query_launch_attributions_get_by_decoded_event_id;
|
||||||
pub use launch_attribution::query_launch_attributions_list_by_pool_id;
|
pub use launch_attribution::query_launch_attributions_list_by_pool_id;
|
||||||
pub use launch_attribution::query_launch_attributions_upsert;
|
pub use launch_attribution::query_launch_attributions_upsert;
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
||||||
Some("raydium-amm-v4".to_string()),
|
Some("raydium_amm_v4".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
r#"["Account0","Pool111","Lp111","TokenA111","TokenB111"]"#.to_string(),
|
r#"["Account0","Pool111","Lp111","TokenA111","TokenB111"]"#.to_string(),
|
||||||
None,
|
None,
|
||||||
@@ -529,7 +529,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dlmm".to_string()),
|
Some("meteora_dlmm".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
r#"["ParentAccount","Pool111"]"#.to_string(),
|
r#"["ParentAccount","Pool111"]"#.to_string(),
|
||||||
None,
|
None,
|
||||||
@@ -548,7 +548,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
Some(0),
|
Some(0),
|
||||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dlmm".to_string()),
|
Some("meteora_dlmm".to_string()),
|
||||||
Some(2),
|
Some(2),
|
||||||
r#"["ChildAccount","Pool111"]"#.to_string(),
|
r#"["ChildAccount","Pool111"]"#.to_string(),
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -735,7 +735,7 @@ mod tests {
|
|||||||
let database = make_database().await;
|
let database = make_database().await;
|
||||||
let upstream_service = crate::UpstreamRegistryService::new();
|
let upstream_service = crate::UpstreamRegistryService::new();
|
||||||
let request = crate::UpstreamRegistrySearchRequestDto {
|
let request = crate::UpstreamRegistrySearchRequestDto {
|
||||||
decoder_code: Some("raydium-cpmm".to_string()),
|
decoder_code: Some("raydium_cpmm".to_string()),
|
||||||
program_id: None,
|
program_id: None,
|
||||||
program_family: None,
|
program_family: None,
|
||||||
surface_kind: None,
|
surface_kind: None,
|
||||||
@@ -759,7 +759,7 @@ mod tests {
|
|||||||
.expect("coverage upsert must succeed");
|
.expect("coverage upsert must succeed");
|
||||||
assert!(id > 0);
|
assert!(id > 0);
|
||||||
let rows =
|
let rows =
|
||||||
crate::query_dex_event_coverage_entries_list_by_decoder(&database, "raydium-cpmm")
|
crate::query_dex_event_coverage_entries_list_by_decoder(&database, "raydium_cpmm")
|
||||||
.await
|
.await
|
||||||
.expect("coverage list must succeed");
|
.expect("coverage list must succeed");
|
||||||
assert_eq!(rows.len(), 1);
|
assert_eq!(rows.len(), 1);
|
||||||
@@ -768,7 +768,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("coverage summary must succeed");
|
.expect("coverage summary must succeed");
|
||||||
assert_eq!(summaries.len(), 1);
|
assert_eq!(summaries.len(), 1);
|
||||||
assert_eq!(summaries[0].decoder_code, "raydium-cpmm");
|
assert_eq!(summaries[0].decoder_code, "raydium_cpmm");
|
||||||
assert_eq!(summaries[0].listed_entry_count, 1);
|
assert_eq!(summaries[0].listed_entry_count, 1);
|
||||||
assert_eq!(summaries[0].decoded_entry_count, 1);
|
assert_eq!(summaries[0].decoded_entry_count, 1);
|
||||||
assert_eq!(summaries[0].observed_entry_count, 1);
|
assert_eq!(summaries[0].observed_entry_count, 1);
|
||||||
|
|||||||
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 {
|
if let Err(error) = result {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
let result = create_tbl_instruction_observations(pool).await;
|
||||||
|
if let Err(error) = result {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let result = create_uix_instruction_observations_key(pool).await;
|
||||||
|
if let Err(error) = result {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let result = create_idx_instruction_observations_decoder_discriminator(pool).await;
|
||||||
|
if let Err(error) = result {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let result = create_idx_instruction_observations_signature(pool).await;
|
||||||
|
if let Err(error) = result {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let result = create_idx_instruction_observations_instruction_name(pool).await;
|
||||||
|
if let Err(error) = result {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
let result = create_tbl_dex_decode_replay_ledger(pool).await;
|
let result = create_tbl_dex_decode_replay_ledger(pool).await;
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
@@ -1423,6 +1443,104 @@ ON k_sol_chain_instructions (program_id)
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates `k_sol_instruction_observations`.
|
||||||
|
async fn create_tbl_instruction_observations(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
|
||||||
|
return execute_sqlite_schema_statement(
|
||||||
|
pool,
|
||||||
|
"create_tbl_instruction_observations",
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS k_sol_instruction_observations (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
observation_key TEXT NOT NULL,
|
||||||
|
transaction_id INTEGER NOT NULL,
|
||||||
|
signature TEXT NOT NULL,
|
||||||
|
slot INTEGER NULL,
|
||||||
|
block_time INTEGER NULL,
|
||||||
|
failed INTEGER NOT NULL,
|
||||||
|
instruction_id INTEGER NOT NULL,
|
||||||
|
parent_instruction_id INTEGER NULL,
|
||||||
|
instruction_index INTEGER NOT NULL,
|
||||||
|
inner_instruction_index INTEGER NULL,
|
||||||
|
program_id TEXT NOT NULL,
|
||||||
|
decoder_code TEXT NULL,
|
||||||
|
discriminator_hex TEXT NULL,
|
||||||
|
instruction_name TEXT NULL,
|
||||||
|
accounts_json TEXT NOT NULL,
|
||||||
|
data_json TEXT NULL,
|
||||||
|
pool_account TEXT NULL,
|
||||||
|
decoded_event_kind TEXT NULL,
|
||||||
|
decoded_event_id INTEGER NULL,
|
||||||
|
observed_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(transaction_id) REFERENCES k_sol_chain_transactions(id),
|
||||||
|
FOREIGN KEY(instruction_id) REFERENCES k_sol_chain_instructions(id),
|
||||||
|
FOREIGN KEY(decoded_event_id) REFERENCES k_sol_dex_decoded_events(id)
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates unique index on instruction observation key.
|
||||||
|
async fn create_uix_instruction_observations_key(
|
||||||
|
pool: &sqlx::SqlitePool,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
return execute_sqlite_schema_statement(
|
||||||
|
pool,
|
||||||
|
"create_uix_instruction_observations_key",
|
||||||
|
r#"
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uix_instruction_observations_key
|
||||||
|
ON k_sol_instruction_observations (observation_key)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates lookup index on decoder/discriminator.
|
||||||
|
async fn create_idx_instruction_observations_decoder_discriminator(
|
||||||
|
pool: &sqlx::SqlitePool,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
return execute_sqlite_schema_statement(
|
||||||
|
pool,
|
||||||
|
"create_idx_instruction_observations_decoder_discriminator",
|
||||||
|
r#"
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_instruction_observations_decoder_discriminator
|
||||||
|
ON k_sol_instruction_observations (decoder_code, discriminator_hex)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates lookup index on signature.
|
||||||
|
async fn create_idx_instruction_observations_signature(
|
||||||
|
pool: &sqlx::SqlitePool,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
return execute_sqlite_schema_statement(
|
||||||
|
pool,
|
||||||
|
"create_idx_instruction_observations_signature",
|
||||||
|
r#"
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_instruction_observations_signature
|
||||||
|
ON k_sol_instruction_observations (signature)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates lookup index on instruction name.
|
||||||
|
async fn create_idx_instruction_observations_instruction_name(
|
||||||
|
pool: &sqlx::SqlitePool,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
return execute_sqlite_schema_statement(
|
||||||
|
pool,
|
||||||
|
"create_idx_instruction_observations_instruction_name",
|
||||||
|
r#"
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_instruction_observations_instruction_name
|
||||||
|
ON k_sol_instruction_observations (instruction_name)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates `k_sol_dex_decoded_events`.
|
/// Creates `k_sol_dex_decoded_events`.
|
||||||
async fn create_tbl_dex_decoded_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
|
async fn create_tbl_dex_decoded_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
|
||||||
return execute_sqlite_schema_statement(
|
return execute_sqlite_schema_statement(
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ pub use raydium_clmm::RaydiumClmmSwapLegacyDecoded;
|
|||||||
pub use raydium_clmm::RaydiumClmmSwapV2Decoded;
|
pub use raydium_clmm::RaydiumClmmSwapV2Decoded;
|
||||||
pub use raydium_clmm::decode_raydium_clmm_instruction;
|
pub use raydium_clmm::decode_raydium_clmm_instruction;
|
||||||
pub use raydium_cpmm::RaydiumCpmmDecodedEvent;
|
pub use raydium_cpmm::RaydiumCpmmDecodedEvent;
|
||||||
|
pub use raydium_cpmm::RaydiumCpmmLpChangeEventDecoded;
|
||||||
pub use raydium_cpmm::RaydiumCpmmSwapDecoded;
|
pub use raydium_cpmm::RaydiumCpmmSwapDecoded;
|
||||||
|
pub use raydium_cpmm::RaydiumCpmmSwapEventDecoded;
|
||||||
pub use raydium_cpmm::RaydiumCpmmSwapMode;
|
pub use raydium_cpmm::RaydiumCpmmSwapMode;
|
||||||
|
pub use raydium_cpmm::classify_raydium_cpmm_instruction_data;
|
||||||
pub use raydium_cpmm::decode_raydium_cpmm_instruction;
|
pub use raydium_cpmm::decode_raydium_cpmm_instruction;
|
||||||
|
pub use raydium_cpmm::decode_raydium_cpmm_program_data_event;
|
||||||
|
|||||||
@@ -3009,7 +3009,7 @@ fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSid
|
|||||||
mod tests {
|
mod tests {
|
||||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||||
let mut dto = crate::ChainTransactionDto::new(
|
let mut dto = crate::ChainTransactionDto::new(
|
||||||
"sig-meteora-damm-v1-create-1".to_string(),
|
"sig-meteora_damm_v1-create-1".to_string(),
|
||||||
Some(890001),
|
Some(890001),
|
||||||
Some(1779500001),
|
Some(1779500001),
|
||||||
Some("helius_primary_http".to_string()),
|
Some("helius_primary_http".to_string()),
|
||||||
@@ -3042,7 +3042,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-damm-v1".to_string()),
|
Some("meteora_damm_v1".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"DammV1Pool111",
|
"DammV1Pool111",
|
||||||
@@ -3074,7 +3074,7 @@ mod tests {
|
|||||||
|
|
||||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||||
let mut dto = crate::ChainTransactionDto::new(
|
let mut dto = crate::ChainTransactionDto::new(
|
||||||
"sig-meteora-damm-v1-swap-1".to_string(),
|
"sig-meteora_damm_v1-swap-1".to_string(),
|
||||||
Some(890002),
|
Some(890002),
|
||||||
Some(1779500002),
|
Some(1779500002),
|
||||||
Some("helius_primary_http".to_string()),
|
Some("helius_primary_http".to_string()),
|
||||||
@@ -3107,7 +3107,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-damm-v1".to_string()),
|
Some("meteora_damm_v1".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!(["DammV1SwapPool111", "DammV1SwapTokenA111", crate::WSOL_MINT_ID])
|
serde_json::json!(["DammV1SwapPool111", "DammV1SwapTokenA111", crate::WSOL_MINT_ID])
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -3141,7 +3141,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-damm-v1".to_string()),
|
Some("meteora_damm_v1".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
accounts.to_string(),
|
accounts.to_string(),
|
||||||
Some(format!("\"{}\"", bs58::encode(data).into_string())),
|
Some(format!("\"{}\"", bs58::encode(data).into_string())),
|
||||||
|
|||||||
@@ -758,7 +758,7 @@ fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||||
let mut dto = crate::ChainTransactionDto::new(
|
let mut dto = crate::ChainTransactionDto::new(
|
||||||
"sig-meteora-damm-v2-create-1".to_string(),
|
"sig-meteora_damm_v2-create-1".to_string(),
|
||||||
Some(889001),
|
Some(889001),
|
||||||
Some(1779400001),
|
Some(1779400001),
|
||||||
Some("helius_primary_http".to_string()),
|
Some("helius_primary_http".to_string()),
|
||||||
@@ -791,7 +791,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-damm-v2".to_string()),
|
Some("meteora_damm_v2".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"DammV2Pool111",
|
"DammV2Pool111",
|
||||||
@@ -823,7 +823,7 @@ mod tests {
|
|||||||
|
|
||||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||||
let mut dto = crate::ChainTransactionDto::new(
|
let mut dto = crate::ChainTransactionDto::new(
|
||||||
"sig-meteora-damm-v2-swap-1".to_string(),
|
"sig-meteora_damm_v2-swap-1".to_string(),
|
||||||
Some(889002),
|
Some(889002),
|
||||||
Some(1779400002),
|
Some(1779400002),
|
||||||
Some("helius_primary_http".to_string()),
|
Some("helius_primary_http".to_string()),
|
||||||
@@ -856,7 +856,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-damm-v2".to_string()),
|
Some("meteora_damm_v2".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!(["DammV2SwapPool111", "DammV2SwapTokenA111", crate::WSOL_MINT_ID])
|
serde_json::json!(["DammV2SwapPool111", "DammV2SwapTokenA111", crate::WSOL_MINT_ID])
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|||||||
@@ -727,7 +727,7 @@ fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||||
let mut dto = crate::ChainTransactionDto::new(
|
let mut dto = crate::ChainTransactionDto::new(
|
||||||
"sig-meteora-dbc-create-1".to_string(),
|
"sig-meteora_dbc-create-1".to_string(),
|
||||||
Some(888001),
|
Some(888001),
|
||||||
Some(1779300001),
|
Some(1779300001),
|
||||||
Some("helius_primary_http".to_string()),
|
Some("helius_primary_http".to_string()),
|
||||||
@@ -760,7 +760,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dbc".to_string()),
|
Some("meteora_dbc".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"DbcPool111",
|
"DbcPool111",
|
||||||
@@ -791,7 +791,7 @@ mod tests {
|
|||||||
|
|
||||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||||
let mut dto = crate::ChainTransactionDto::new(
|
let mut dto = crate::ChainTransactionDto::new(
|
||||||
"sig-meteora-dbc-swap-1".to_string(),
|
"sig-meteora_dbc-swap-1".to_string(),
|
||||||
Some(888002),
|
Some(888002),
|
||||||
Some(1779300002),
|
Some(1779300002),
|
||||||
Some("helius_primary_http".to_string()),
|
Some("helius_primary_http".to_string()),
|
||||||
@@ -824,7 +824,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dbc".to_string()),
|
Some("meteora_dbc".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!(["DbcPoolSwap111", "DbcSwapTokenA111", crate::WSOL_MINT_ID])
|
serde_json::json!(["DbcPoolSwap111", "DbcSwapTokenA111", crate::WSOL_MINT_ID])
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|||||||
@@ -2646,7 +2646,7 @@ fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||||
let mut dto = crate::ChainTransactionDto::new(
|
let mut dto = crate::ChainTransactionDto::new(
|
||||||
"sig-meteora-dlmm-create-1".to_string(),
|
"sig-meteora_dlmm-create-1".to_string(),
|
||||||
Some(888101),
|
Some(888101),
|
||||||
Some(1779400001),
|
Some(1779400001),
|
||||||
Some("helius_primary_http".to_string()),
|
Some("helius_primary_http".to_string()),
|
||||||
@@ -2679,7 +2679,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dlmm".to_string()),
|
Some("meteora_dlmm".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"DlmmPair111",
|
"DlmmPair111",
|
||||||
@@ -2708,7 +2708,7 @@ mod tests {
|
|||||||
|
|
||||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||||
let mut dto = crate::ChainTransactionDto::new(
|
let mut dto = crate::ChainTransactionDto::new(
|
||||||
"sig-meteora-dlmm-swap-1".to_string(),
|
"sig-meteora_dlmm-swap-1".to_string(),
|
||||||
Some(888102),
|
Some(888102),
|
||||||
Some(1779400002),
|
Some(1779400002),
|
||||||
Some("helius_primary_http".to_string()),
|
Some("helius_primary_http".to_string()),
|
||||||
@@ -2741,7 +2741,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dlmm".to_string()),
|
Some("meteora_dlmm".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!(["DlmmPairSwap111", "DlmmSwapTokenX111", crate::WSOL_MINT_ID])
|
serde_json::json!(["DlmmPairSwap111", "DlmmSwapTokenX111", crate::WSOL_MINT_ID])
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -2906,7 +2906,7 @@ mod tests {
|
|||||||
fn meteora_dlmm_inner_swap2_instruction_is_not_skipped() {
|
fn meteora_dlmm_inner_swap2_instruction_is_not_skipped() {
|
||||||
let decoder = crate::MeteoraDlmmDecoder::new();
|
let decoder = crate::MeteoraDlmmDecoder::new();
|
||||||
let mut transaction = crate::ChainTransactionDto::new(
|
let mut transaction = crate::ChainTransactionDto::new(
|
||||||
"sig-meteora-dlmm-inner-swap2".to_string(),
|
"sig-meteora_dlmm-inner-swap2".to_string(),
|
||||||
Some(888103),
|
Some(888103),
|
||||||
Some(1779400003),
|
Some(1779400003),
|
||||||
Some("helius_primary_http".to_string()),
|
Some("helius_primary_http".to_string()),
|
||||||
@@ -2933,7 +2933,7 @@ mod tests {
|
|||||||
3,
|
3,
|
||||||
Some(14),
|
Some(14),
|
||||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dlmm".to_string()),
|
Some("meteora_dlmm".to_string()),
|
||||||
Some(2),
|
Some(2),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"LbPair111",
|
"LbPair111",
|
||||||
@@ -3030,7 +3030,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dlmm".to_string()),
|
Some("meteora_dlmm".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"Position111",
|
"Position111",
|
||||||
@@ -3083,7 +3083,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dlmm".to_string()),
|
Some("meteora_dlmm".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"DlmmPairFee111",
|
"DlmmPairFee111",
|
||||||
@@ -3132,7 +3132,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
|
||||||
Some("meteora-dlmm".to_string()),
|
Some("meteora_dlmm".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"Position111",
|
"Position111",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ impl OpenBookV2Decoder {
|
|||||||
Some(registry_match) => registry_match,
|
Some(registry_match) => registry_match,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
if registry_match.decoder_code.as_str() != "openbook-v2" {
|
if registry_match.decoder_code.as_str() != "openbook_v2" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
|
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
|
||||||
|
|||||||
@@ -532,7 +532,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
||||||
Some("orca-whirlpools".to_string()),
|
Some("orca_whirlpools".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"OrcaPool111",
|
"OrcaPool111",
|
||||||
@@ -599,7 +599,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
||||||
Some("orca-whirlpools".to_string()),
|
Some("orca_whirlpools".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!(["OrcaSwapPool111", "OrcaSwapTokenA111", crate::WSOL_MINT_ID])
|
serde_json::json!(["OrcaSwapPool111", "OrcaSwapTokenA111", crate::WSOL_MINT_ID])
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ impl PhoenixV1Decoder {
|
|||||||
Some(registry_match) => registry_match,
|
Some(registry_match) => registry_match,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
if registry_match.decoder_code.as_str() != "phoenix-v1" {
|
if registry_match.decoder_code.as_str() != "phoenix_v1" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
|
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
|
||||||
|
|||||||
@@ -1080,7 +1080,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
||||||
Some("raydium-amm-v4".to_string()),
|
Some("raydium_amm_v4".to_string()),
|
||||||
Some(1),
|
Some(1),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
"Account0",
|
"Account0",
|
||||||
@@ -1215,7 +1215,7 @@ mod tests {
|
|||||||
4,
|
4,
|
||||||
Some(0),
|
Some(0),
|
||||||
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
||||||
Some("raydium-amm-v4".to_string()),
|
Some("raydium_amm_v4".to_string()),
|
||||||
Some(2),
|
Some(2),
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
crate::SPL_TOKEN_PROGRAM_ID,
|
crate::SPL_TOKEN_PROGRAM_ID,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1012,11 +1012,11 @@ impl DexDecodeService {
|
|||||||
"raydium_cpmm",
|
"raydium_cpmm",
|
||||||
crate::RAYDIUM_CPMM_PROGRAM_ID.to_string(),
|
crate::RAYDIUM_CPMM_PROGRAM_ID.to_string(),
|
||||||
event_kind.as_str(),
|
event_kind.as_str(),
|
||||||
Some(decoded_event.pool_account().to_string()),
|
decoded_event.pool_account().map(|value| return value.to_string()),
|
||||||
None,
|
|
||||||
Some(decoded_event.base_mint().to_string()),
|
|
||||||
Some(decoded_event.quote_mint().to_string()),
|
|
||||||
None,
|
None,
|
||||||
|
decoded_event.base_mint().map(|value| return value.to_string()),
|
||||||
|
decoded_event.quote_mint().map(|value| return value.to_string()),
|
||||||
|
decoded_event.lp_mint().map(|value| return value.to_string()),
|
||||||
payload_value,
|
payload_value,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -1174,6 +1174,7 @@ impl DexDecodeService {
|
|||||||
instructions: &[crate::ChainInstructionDto],
|
instructions: &[crate::ChainInstructionDto],
|
||||||
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
|
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
|
||||||
let mut persisted = std::vec::Vec::new();
|
let mut persisted = std::vec::Vec::new();
|
||||||
|
let mut program_data_events = collect_raydium_cpmm_program_data_events(transaction);
|
||||||
for instruction in instructions {
|
for instruction in instructions {
|
||||||
let program_id = match instruction.program_id.as_ref() {
|
let program_id = match instruction.program_id.as_ref() {
|
||||||
Some(program_id) => program_id,
|
Some(program_id) => program_id,
|
||||||
@@ -1186,6 +1187,8 @@ impl DexDecodeService {
|
|||||||
Some(data_json) => data_json,
|
Some(data_json) => data_json,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
let instruction_kind =
|
||||||
|
crate::classify_raydium_cpmm_instruction_data(data_json.as_str());
|
||||||
let decoded_events = crate::decode_raydium_cpmm_instruction(
|
let decoded_events = crate::decode_raydium_cpmm_instruction(
|
||||||
instruction.accounts_json.as_str(),
|
instruction.accounts_json.as_str(),
|
||||||
data_json.as_str(),
|
data_json.as_str(),
|
||||||
@@ -1199,6 +1202,18 @@ impl DexDecodeService {
|
|||||||
};
|
};
|
||||||
persisted.push(persisted_event);
|
persisted.push(persisted_event);
|
||||||
}
|
}
|
||||||
|
let program_data_persist_result = persist_matching_raydium_cpmm_program_data_event(
|
||||||
|
self,
|
||||||
|
transaction,
|
||||||
|
instruction,
|
||||||
|
instruction_kind,
|
||||||
|
&mut program_data_events,
|
||||||
|
&mut persisted,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Err(error) = program_data_persist_result {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Ok(persisted);
|
return Ok(persisted);
|
||||||
}
|
}
|
||||||
@@ -1808,6 +1823,11 @@ struct RaydiumMappedNonTradeInstructionSpec {
|
|||||||
enum RaydiumMappedNonTradeAmountLayout {
|
enum RaydiumMappedNonTradeAmountLayout {
|
||||||
None,
|
None,
|
||||||
ClmmLiquidityV2,
|
ClmmLiquidityV2,
|
||||||
|
CpmmAmmConfig,
|
||||||
|
CpmmDeposit,
|
||||||
|
CpmmFeePair,
|
||||||
|
CpmmInitialize,
|
||||||
|
CpmmPoolStatus,
|
||||||
CpmmWithdraw,
|
CpmmWithdraw,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1894,26 +1914,81 @@ fn raydium_mapped_non_trade_instruction_spec(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if protocol_name == "raydium_cpmm" {
|
if protocol_name == "raydium_cpmm" {
|
||||||
if discriminator_hex == "1416567bc61cdb84" && account_count >= 14 {
|
if discriminator_hex == "9c5420764587467b" && account_count >= 4 {
|
||||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
instruction_name: "collect_creator_fee",
|
instruction_name: "close_permission_pda",
|
||||||
event_kind: "raydium_cpmm.collect_creator_fee",
|
event_kind: "raydium_cpmm.close_permission_pda",
|
||||||
pool_account_index: Some(3),
|
pool_account_index: None,
|
||||||
token_a_mint_index: None,
|
token_a_mint_index: None,
|
||||||
token_b_mint_index: None,
|
token_b_mint_index: None,
|
||||||
lp_mint_index: None,
|
lp_mint_index: None,
|
||||||
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if discriminator_hex == "b712469c946da122" && account_count >= 14 {
|
if discriminator_hex == "1416567bc61cdb84" && account_count >= 13 {
|
||||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
instruction_name: "withdraw",
|
instruction_name: "collect_creator_fee",
|
||||||
event_kind: "raydium_cpmm.withdraw",
|
event_kind: "raydium_cpmm.collect_creator_fee",
|
||||||
pool_account_index: Some(3),
|
pool_account_index: Some(2),
|
||||||
|
token_a_mint_index: Some(6),
|
||||||
|
token_b_mint_index: Some(7),
|
||||||
|
lp_mint_index: None,
|
||||||
|
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if discriminator_hex == "a78a4e95dfc2067e" && account_count >= 12 {
|
||||||
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
|
instruction_name: "collect_fund_fee",
|
||||||
|
event_kind: "raydium_cpmm.collect_fund_fee",
|
||||||
|
pool_account_index: Some(2),
|
||||||
|
token_a_mint_index: Some(6),
|
||||||
|
token_b_mint_index: Some(7),
|
||||||
|
lp_mint_index: None,
|
||||||
|
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if discriminator_hex == "8888fcddc2427e59" && account_count >= 12 {
|
||||||
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
|
instruction_name: "collect_protocol_fee",
|
||||||
|
event_kind: "raydium_cpmm.collect_protocol_fee",
|
||||||
|
pool_account_index: Some(2),
|
||||||
|
token_a_mint_index: Some(6),
|
||||||
|
token_b_mint_index: Some(7),
|
||||||
|
lp_mint_index: None,
|
||||||
|
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if discriminator_hex == "8934edd4d7756c68" && account_count >= 3 {
|
||||||
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
|
instruction_name: "create_amm_config",
|
||||||
|
event_kind: "raydium_cpmm.create_amm_config",
|
||||||
|
pool_account_index: None,
|
||||||
token_a_mint_index: None,
|
token_a_mint_index: None,
|
||||||
token_b_mint_index: None,
|
token_b_mint_index: None,
|
||||||
lp_mint_index: None,
|
lp_mint_index: None,
|
||||||
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw,
|
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if discriminator_hex == "878802d889a9b5ca" && account_count >= 4 {
|
||||||
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
|
instruction_name: "create_permission_pda",
|
||||||
|
event_kind: "raydium_cpmm.create_permission_pda",
|
||||||
|
pool_account_index: None,
|
||||||
|
token_a_mint_index: None,
|
||||||
|
token_b_mint_index: None,
|
||||||
|
lp_mint_index: None,
|
||||||
|
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if discriminator_hex == "f223c68952e1f2b6" && account_count >= 13 {
|
||||||
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
|
instruction_name: "deposit",
|
||||||
|
event_kind: "raydium_cpmm.deposit",
|
||||||
|
pool_account_index: Some(2),
|
||||||
|
token_a_mint_index: Some(10),
|
||||||
|
token_b_mint_index: Some(11),
|
||||||
|
lp_mint_index: Some(12),
|
||||||
|
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmDeposit,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if discriminator_hex == "afaf6d1f0d989bed" && account_count >= 20 {
|
if discriminator_hex == "afaf6d1f0d989bed" && account_count >= 20 {
|
||||||
@@ -1923,8 +1998,52 @@ fn raydium_mapped_non_trade_instruction_spec(
|
|||||||
pool_account_index: Some(3),
|
pool_account_index: Some(3),
|
||||||
token_a_mint_index: Some(4),
|
token_a_mint_index: Some(4),
|
||||||
token_b_mint_index: Some(5),
|
token_b_mint_index: Some(5),
|
||||||
lp_mint_index: Some(13),
|
lp_mint_index: Some(6),
|
||||||
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if discriminator_hex == "3f37fe4131b25979" && account_count >= 21 {
|
||||||
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
|
instruction_name: "initialize_with_permission",
|
||||||
|
event_kind: "raydium_cpmm.initialize_with_permission",
|
||||||
|
pool_account_index: Some(4),
|
||||||
|
token_a_mint_index: Some(5),
|
||||||
|
token_b_mint_index: Some(6),
|
||||||
|
lp_mint_index: Some(7),
|
||||||
|
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if discriminator_hex == "313cae889a1c74c8" && account_count >= 2 {
|
||||||
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
|
instruction_name: "update_amm_config",
|
||||||
|
event_kind: "raydium_cpmm.update_amm_config",
|
||||||
|
pool_account_index: None,
|
||||||
|
token_a_mint_index: None,
|
||||||
|
token_b_mint_index: None,
|
||||||
|
lp_mint_index: None,
|
||||||
|
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if discriminator_hex == "82576c062ee0757b" && account_count >= 2 {
|
||||||
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
|
instruction_name: "update_pool_status",
|
||||||
|
event_kind: "raydium_cpmm.update_pool_status",
|
||||||
|
pool_account_index: Some(1),
|
||||||
|
token_a_mint_index: None,
|
||||||
|
token_b_mint_index: None,
|
||||||
|
lp_mint_index: None,
|
||||||
|
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if discriminator_hex == "b712469c946da122" && account_count >= 14 {
|
||||||
|
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||||
|
instruction_name: "withdraw",
|
||||||
|
event_kind: "raydium_cpmm.withdraw",
|
||||||
|
pool_account_index: Some(2),
|
||||||
|
token_a_mint_index: Some(10),
|
||||||
|
token_b_mint_index: Some(11),
|
||||||
|
lp_mint_index: Some(12),
|
||||||
|
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1979,6 +2098,15 @@ fn enrich_raydium_mapped_non_trade_payload(
|
|||||||
"instructionName".to_string(),
|
"instructionName".to_string(),
|
||||||
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
|
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
|
||||||
);
|
);
|
||||||
|
object.insert(
|
||||||
|
"upstreamInstructionName".to_string(),
|
||||||
|
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
|
||||||
|
);
|
||||||
|
object.insert("localSpecializedDecoder".to_string(), serde_json::Value::Bool(true));
|
||||||
|
object.insert(
|
||||||
|
"adminAction".to_string(),
|
||||||
|
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
|
||||||
|
);
|
||||||
object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(true));
|
object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(true));
|
||||||
object.insert(
|
object.insert(
|
||||||
"auditReason".to_string(),
|
"auditReason".to_string(),
|
||||||
@@ -2032,6 +2160,94 @@ fn insert_raydium_mapped_amounts(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig => {
|
||||||
|
if let Some(param) = read_u8_from_bytes(data, 8) {
|
||||||
|
object.insert(
|
||||||
|
"configParam".to_string(),
|
||||||
|
serde_json::Value::Number(serde_json::Number::from(param as u64)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(value) = read_u64_le_from_bytes(data, 9) {
|
||||||
|
object.insert(
|
||||||
|
"configValue".to_string(),
|
||||||
|
serde_json::Value::String(value.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RaydiumMappedNonTradeAmountLayout::CpmmDeposit => {
|
||||||
|
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
|
||||||
|
object.insert(
|
||||||
|
"lpAmountRaw".to_string(),
|
||||||
|
serde_json::Value::String(lp_amount.to_string()),
|
||||||
|
);
|
||||||
|
object.insert(
|
||||||
|
"liquidity".to_string(),
|
||||||
|
serde_json::Value::String(lp_amount.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(amount_0) = read_u64_le_from_bytes(data, 16) {
|
||||||
|
object.insert(
|
||||||
|
"tokenAAmount".to_string(),
|
||||||
|
serde_json::Value::String(amount_0.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(amount_1) = read_u64_le_from_bytes(data, 24) {
|
||||||
|
object.insert(
|
||||||
|
"tokenBAmount".to_string(),
|
||||||
|
serde_json::Value::String(amount_1.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RaydiumMappedNonTradeAmountLayout::CpmmFeePair => {
|
||||||
|
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
|
||||||
|
object.insert(
|
||||||
|
"tokenAAmount".to_string(),
|
||||||
|
serde_json::Value::String(amount_0.to_string()),
|
||||||
|
);
|
||||||
|
object.insert(
|
||||||
|
"amount0RequestedRaw".to_string(),
|
||||||
|
serde_json::Value::String(amount_0.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
|
||||||
|
object.insert(
|
||||||
|
"tokenBAmount".to_string(),
|
||||||
|
serde_json::Value::String(amount_1.to_string()),
|
||||||
|
);
|
||||||
|
object.insert(
|
||||||
|
"amount1RequestedRaw".to_string(),
|
||||||
|
serde_json::Value::String(amount_1.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RaydiumMappedNonTradeAmountLayout::CpmmInitialize => {
|
||||||
|
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
|
||||||
|
object.insert(
|
||||||
|
"tokenAAmount".to_string(),
|
||||||
|
serde_json::Value::String(amount_0.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
|
||||||
|
object.insert(
|
||||||
|
"tokenBAmount".to_string(),
|
||||||
|
serde_json::Value::String(amount_1.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(open_time) = read_u64_le_from_bytes(data, 24) {
|
||||||
|
object.insert(
|
||||||
|
"openTime".to_string(),
|
||||||
|
serde_json::Value::String(open_time.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus => {
|
||||||
|
if let Some(status) = read_u8_from_bytes(data, 8) {
|
||||||
|
object.insert(
|
||||||
|
"poolStatus".to_string(),
|
||||||
|
serde_json::Value::Number(serde_json::Number::from(status as u64)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
RaydiumMappedNonTradeAmountLayout::CpmmWithdraw => {
|
RaydiumMappedNonTradeAmountLayout::CpmmWithdraw => {
|
||||||
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
|
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
|
||||||
object.insert(
|
object.insert(
|
||||||
@@ -2073,6 +2289,13 @@ fn instruction_data_bytes_from_base58(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_u8_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u8> {
|
||||||
|
if data.len() < offset + 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(data[offset]);
|
||||||
|
}
|
||||||
|
|
||||||
fn read_u64_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u64> {
|
fn read_u64_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u64> {
|
||||||
if data.len() < offset + 8 {
|
if data.len() < offset + 8 {
|
||||||
return None;
|
return None;
|
||||||
@@ -2453,6 +2676,150 @@ fn append_persisted_events(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RaydiumCpmmProgramDataEventCandidate {
|
||||||
|
decoded_event: crate::RaydiumCpmmDecodedEvent,
|
||||||
|
consumed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_raydium_cpmm_program_data_events(
|
||||||
|
transaction: &crate::ChainTransactionDto,
|
||||||
|
) -> std::vec::Vec<RaydiumCpmmProgramDataEventCandidate> {
|
||||||
|
let logs = extract_transaction_log_messages(transaction.transaction_json.as_str());
|
||||||
|
let mut events = std::vec::Vec::new();
|
||||||
|
let mut cpmm_stack_depth = 0_u32;
|
||||||
|
for log_message in logs {
|
||||||
|
if is_program_invoke_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
|
||||||
|
cpmm_stack_depth += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if is_program_success_or_failed_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
|
||||||
|
cpmm_stack_depth = cpmm_stack_depth.saturating_sub(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if cpmm_stack_depth == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let data_base64 = match log_message.strip_prefix("Program data: ") {
|
||||||
|
Some(data_base64) => data_base64.trim(),
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
if data_base64.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let decoded_event = crate::decode_raydium_cpmm_program_data_event(data_base64);
|
||||||
|
if let Some(decoded_event) = decoded_event {
|
||||||
|
events.push(RaydiumCpmmProgramDataEventCandidate { decoded_event, consumed: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn persist_matching_raydium_cpmm_program_data_event(
|
||||||
|
service: &DexDecodeService,
|
||||||
|
transaction: &crate::ChainTransactionDto,
|
||||||
|
instruction: &crate::ChainInstructionDto,
|
||||||
|
instruction_kind: std::option::Option<&str>,
|
||||||
|
program_data_events: &mut [RaydiumCpmmProgramDataEventCandidate],
|
||||||
|
persisted: &mut std::vec::Vec<crate::DexDecodedEventDto>,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
let expected_event_kind = match instruction_kind {
|
||||||
|
Some("swap_base_input") => Some("swap_event"),
|
||||||
|
Some("swap_base_output") => Some("swap_event"),
|
||||||
|
Some("deposit") => Some("lp_change_event"),
|
||||||
|
Some("withdraw") => Some("lp_change_event"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let expected_event_kind = match expected_event_kind {
|
||||||
|
Some(expected_event_kind) => expected_event_kind,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
let mut index = 0_usize;
|
||||||
|
while index < program_data_events.len() {
|
||||||
|
if program_data_events[index].consumed {
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let event_matches = match (&program_data_events[index].decoded_event, expected_event_kind) {
|
||||||
|
(crate::RaydiumCpmmDecodedEvent::SwapEvent(_), "swap_event") => true,
|
||||||
|
(crate::RaydiumCpmmDecodedEvent::LpChangeEvent(_), "lp_change_event") => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if !event_matches {
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
program_data_events[index].consumed = true;
|
||||||
|
let persist_result = service
|
||||||
|
.persist_raydium_cpmm_event(
|
||||||
|
transaction,
|
||||||
|
instruction,
|
||||||
|
&program_data_events[index].decoded_event,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let persisted_event = match persist_result {
|
||||||
|
Ok(persisted_event) => persisted_event,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
persisted.push(persisted_event);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_transaction_log_messages(transaction_json: &str) -> std::vec::Vec<std::string::String> {
|
||||||
|
let value_result = serde_json::from_str::<serde_json::Value>(transaction_json);
|
||||||
|
let value = match value_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return std::vec::Vec::new(),
|
||||||
|
};
|
||||||
|
let meta = match value.get("meta") {
|
||||||
|
Some(meta) => meta,
|
||||||
|
None => return std::vec::Vec::new(),
|
||||||
|
};
|
||||||
|
let logs = match meta.get("logMessages") {
|
||||||
|
Some(logs) => logs,
|
||||||
|
None => return std::vec::Vec::new(),
|
||||||
|
};
|
||||||
|
let logs = match logs.as_array() {
|
||||||
|
Some(logs) => logs,
|
||||||
|
None => return std::vec::Vec::new(),
|
||||||
|
};
|
||||||
|
let mut output = std::vec::Vec::new();
|
||||||
|
for log in logs {
|
||||||
|
if let Some(log) = log.as_str() {
|
||||||
|
output.push(log.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_program_invoke_log(log_message: &str, program_id: &str) -> bool {
|
||||||
|
if !log_message.starts_with("Program ") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !log_message.contains(" invoke [") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return log_message.contains(program_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_program_success_or_failed_log(log_message: &str, program_id: &str) -> bool {
|
||||||
|
if !log_message.starts_with("Program ") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !log_message.contains(program_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if log_message.ends_with(" success") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if log_message.contains(" failed: ") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
fn decoded_instruction_ids_from_persisted_events(
|
fn decoded_instruction_ids_from_persisted_events(
|
||||||
persisted: &[crate::DexDecodedEventDto],
|
persisted: &[crate::DexDecodedEventDto],
|
||||||
) -> std::collections::HashSet<i64> {
|
) -> std::collections::HashSet<i64> {
|
||||||
@@ -2603,7 +2970,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
|
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
|
||||||
"program": "raydium-amm-v4",
|
"program": "raydium_amm_v4",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"Account0",
|
"Account0",
|
||||||
@@ -2887,7 +3254,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
||||||
"program": "meteora-dbc",
|
"program": "meteora_dbc",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"DbcPoolDecode111",
|
"DbcPoolDecode111",
|
||||||
@@ -2962,7 +3329,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
|
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
|
||||||
"program": "meteora-damm-v2",
|
"program": "meteora_damm_v2",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"DammV2DecodePool111",
|
"DammV2DecodePool111",
|
||||||
@@ -3039,7 +3406,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
|
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
|
||||||
"program": "meteora-damm-v1",
|
"program": "meteora_damm_v1",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"DammV1DecodePool111",
|
"DammV1DecodePool111",
|
||||||
@@ -3116,7 +3483,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
|
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
|
||||||
"program": "orca-whirlpools",
|
"program": "orca_whirlpools",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"OrcaDecodePool111",
|
"OrcaDecodePool111",
|
||||||
@@ -3488,36 +3855,32 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn maps_observed_raydium_cpmm_non_swap_discriminators() {
|
fn maps_observed_raydium_cpmm_non_swap_discriminators() {
|
||||||
let collect_creator_fee = super::raydium_mapped_non_trade_instruction_spec(
|
let expected = [
|
||||||
"raydium_cpmm",
|
("9c5420764587467b", 4_usize, "raydium_cpmm.close_permission_pda"),
|
||||||
Some("1416567bc61cdb84"),
|
("1416567bc61cdb84", 13_usize, "raydium_cpmm.collect_creator_fee"),
|
||||||
14,
|
("a78a4e95dfc2067e", 12_usize, "raydium_cpmm.collect_fund_fee"),
|
||||||
);
|
("8888fcddc2427e59", 12_usize, "raydium_cpmm.collect_protocol_fee"),
|
||||||
let collect_creator_fee = match collect_creator_fee {
|
("8934edd4d7756c68", 3_usize, "raydium_cpmm.create_amm_config"),
|
||||||
Some(collect_creator_fee) => collect_creator_fee,
|
("878802d889a9b5ca", 4_usize, "raydium_cpmm.create_permission_pda"),
|
||||||
None => panic!("collect_creator_fee discriminator must be mapped"),
|
("f223c68952e1f2b6", 13_usize, "raydium_cpmm.deposit"),
|
||||||
};
|
("afaf6d1f0d989bed", 20_usize, "raydium_cpmm.initialize"),
|
||||||
assert_eq!(collect_creator_fee.event_kind, "raydium_cpmm.collect_creator_fee");
|
("3f37fe4131b25979", 21_usize, "raydium_cpmm.initialize_with_permission"),
|
||||||
let withdraw = super::raydium_mapped_non_trade_instruction_spec(
|
("313cae889a1c74c8", 2_usize, "raydium_cpmm.update_amm_config"),
|
||||||
"raydium_cpmm",
|
("82576c062ee0757b", 2_usize, "raydium_cpmm.update_pool_status"),
|
||||||
Some("b712469c946da122"),
|
("b712469c946da122", 14_usize, "raydium_cpmm.withdraw"),
|
||||||
14,
|
];
|
||||||
);
|
for (discriminator, account_count, event_kind) in expected {
|
||||||
let withdraw = match withdraw {
|
let mapped = super::raydium_mapped_non_trade_instruction_spec(
|
||||||
Some(withdraw) => withdraw,
|
"raydium_cpmm",
|
||||||
None => panic!("withdraw discriminator must be mapped"),
|
Some(discriminator),
|
||||||
};
|
account_count,
|
||||||
assert_eq!(withdraw.event_kind, "raydium_cpmm.withdraw");
|
);
|
||||||
let initialize = super::raydium_mapped_non_trade_instruction_spec(
|
let mapped = match mapped {
|
||||||
"raydium_cpmm",
|
Some(mapped) => mapped,
|
||||||
Some("afaf6d1f0d989bed"),
|
None => panic!("raydium cpmm discriminator must be mapped: {}", discriminator),
|
||||||
20,
|
};
|
||||||
);
|
assert_eq!(mapped.event_kind, event_kind);
|
||||||
let initialize = match initialize {
|
}
|
||||||
Some(initialize) => initialize,
|
|
||||||
None => panic!("initialize discriminator must be mapped"),
|
|
||||||
};
|
|
||||||
assert_eq!(initialize.event_kind, "raydium_cpmm.initialize");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3573,7 +3936,7 @@ mod tests {
|
|||||||
let registry_match = crate::UpstreamRegistryEntryDto {
|
let registry_match = crate::UpstreamRegistryEntryDto {
|
||||||
source_repo: Some("sevenlabs-hq/carbon".to_string()),
|
source_repo: Some("sevenlabs-hq/carbon".to_string()),
|
||||||
source_path: Some("decoders/example.rs".to_string()),
|
source_path: Some("decoders/example.rs".to_string()),
|
||||||
decoder_code: "meteora-damm-v2".to_string(),
|
decoder_code: "meteora_damm_v2".to_string(),
|
||||||
program_id: Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
program_id: Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
||||||
program_family: "meteora".to_string(),
|
program_family: "meteora".to_string(),
|
||||||
surface_kind: "amm".to_string(),
|
surface_kind: "amm".to_string(),
|
||||||
|
|||||||
@@ -1036,7 +1036,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
|
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
|
||||||
"program": "raydium-amm-v4",
|
"program": "raydium_amm_v4",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"Account0",
|
"Account0",
|
||||||
@@ -1462,7 +1462,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
||||||
"program": "meteora-dbc",
|
"program": "meteora_dbc",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"DbcDetectPool111",
|
"DbcDetectPool111",
|
||||||
@@ -1581,7 +1581,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
|
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
|
||||||
"program": "meteora-damm-v2",
|
"program": "meteora_damm_v2",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"DammV2DetectPool111",
|
"DammV2DetectPool111",
|
||||||
@@ -1701,7 +1701,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
|
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
|
||||||
"program": "meteora-damm-v1",
|
"program": "meteora_damm_v1",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"DammV1DetectPool111",
|
"DammV1DetectPool111",
|
||||||
@@ -1821,7 +1821,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
|
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
|
||||||
"program": "orca-whirlpools",
|
"program": "orca_whirlpools",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"OrcaDetectPool111",
|
"OrcaDetectPool111",
|
||||||
|
|||||||
@@ -320,6 +320,9 @@ pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
|
|||||||
if event_kind.contains(".deposit") {
|
if event_kind.contains(".deposit") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if event_kind.contains(".lp_change_event") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if event_kind.contains(".withdraw") {
|
if event_kind.contains(".withdraw") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -518,6 +521,9 @@ pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
|
|||||||
|
|
||||||
/// Returns true for pool creation or initialization events.
|
/// Returns true for pool creation or initialization events.
|
||||||
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
|
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
|
||||||
|
if event_kind.contains("amm_config") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if event_kind.contains(".initialize_position") {
|
if event_kind.contains(".initialize_position") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -552,6 +558,9 @@ pub fn is_dex_pair_creation_event_kind(event_kind: &str) -> bool {
|
|||||||
|
|
||||||
/// Returns true for admin, configuration or permission changes.
|
/// Returns true for admin, configuration or permission changes.
|
||||||
pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
|
pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
|
||||||
|
if event_kind.contains(".initialize_with_permission") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if event_kind.contains(".lock_liquidity") {
|
if event_kind.contains(".lock_liquidity") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1152,6 +1161,15 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_initialize_with_permission_as_lifecycle_only() {
|
||||||
|
let event_kind = "raydium_cpmm.initialize_with_permission";
|
||||||
|
assert!(super::is_dex_pool_lifecycle_event_kind(event_kind));
|
||||||
|
assert!(!super::is_dex_admin_event_kind(event_kind));
|
||||||
|
assert_eq!(super::classify_dex_event_category_code(event_kind), "pool_lifecycle");
|
||||||
|
assert_eq!(super::classify_dex_event_lifecycle_kind_code(event_kind), "pool_creation");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn classifies_audit_suffix_events_as_informational() {
|
fn classifies_audit_suffix_events_as_informational() {
|
||||||
assert!(super::is_dex_informational_event_kind("openbook_v2.settle_funds_audit"));
|
assert!(super::is_dex_informational_event_kind("openbook_v2.settle_funds_audit"));
|
||||||
|
|||||||
@@ -38,15 +38,10 @@ impl DexEventCoverageService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronizes static upstream registry entries into SQLite coverage rows.
|
async fn upsert_upstream_registry_rows(
|
||||||
///
|
|
||||||
/// The resulting rows are still discovery/audit metadata. A row can become
|
|
||||||
/// observed or materialized only through local corpus replay and explicit
|
|
||||||
/// count refreshes.
|
|
||||||
pub async fn sync_upstream_registry(
|
|
||||||
&self,
|
&self,
|
||||||
decoder_code: std::option::Option<std::string::String>,
|
decoder_code: std::option::Option<std::string::String>,
|
||||||
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
|
) -> Result<(usize, usize), crate::Error> {
|
||||||
let request = crate::UpstreamRegistrySearchRequestDto {
|
let request = crate::UpstreamRegistrySearchRequestDto {
|
||||||
decoder_code: decoder_code.clone(),
|
decoder_code: decoder_code.clone(),
|
||||||
program_id: None,
|
program_id: None,
|
||||||
@@ -70,6 +65,30 @@ impl DexEventCoverageService {
|
|||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Ok((search_result.entries.len(), upserted_entry_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_upstream_registry_rows_if_needed(
|
||||||
|
&self,
|
||||||
|
decoder_code: std::option::Option<std::string::String>,
|
||||||
|
) -> Result<(usize, usize), crate::Error> {
|
||||||
|
return self.upsert_upstream_registry_rows(decoder_code).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronizes static upstream registry entries into SQLite coverage rows.
|
||||||
|
///
|
||||||
|
/// The resulting rows are still discovery/audit metadata. A row can become
|
||||||
|
/// observed or materialized only through local corpus replay and explicit
|
||||||
|
/// count refreshes.
|
||||||
|
pub async fn sync_upstream_registry(
|
||||||
|
&self,
|
||||||
|
decoder_code: std::option::Option<std::string::String>,
|
||||||
|
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
|
||||||
|
let sync_counts = self.upsert_upstream_registry_rows(decoder_code.clone()).await;
|
||||||
|
let (upstream_entry_count, upserted_entry_count) = match sync_counts {
|
||||||
|
Ok(sync_counts) => sync_counts,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
let refreshed_entry_count = match &decoder_code {
|
let refreshed_entry_count = match &decoder_code {
|
||||||
Some(decoder_code) => {
|
Some(decoder_code) => {
|
||||||
let refresh_result =
|
let refresh_result =
|
||||||
@@ -103,7 +122,7 @@ impl DexEventCoverageService {
|
|||||||
};
|
};
|
||||||
return Ok(crate::DexEventCoverageSyncResult {
|
return Ok(crate::DexEventCoverageSyncResult {
|
||||||
decoder_code,
|
decoder_code,
|
||||||
upstream_entry_count: search_result.entries.len(),
|
upstream_entry_count,
|
||||||
upserted_entry_count,
|
upserted_entry_count,
|
||||||
refreshed_entry_count,
|
refreshed_entry_count,
|
||||||
summaries,
|
summaries,
|
||||||
@@ -115,6 +134,11 @@ impl DexEventCoverageService {
|
|||||||
&self,
|
&self,
|
||||||
decoder_code: std::option::Option<std::string::String>,
|
decoder_code: std::option::Option<std::string::String>,
|
||||||
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
|
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
|
||||||
|
let sync_counts = self.ensure_upstream_registry_rows_if_needed(decoder_code.clone()).await;
|
||||||
|
let (upstream_entry_count, upserted_entry_count) = match sync_counts {
|
||||||
|
Ok(sync_counts) => sync_counts,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
let refreshed_entry_count = match &decoder_code {
|
let refreshed_entry_count = match &decoder_code {
|
||||||
Some(decoder_code) => {
|
Some(decoder_code) => {
|
||||||
let refresh_result =
|
let refresh_result =
|
||||||
@@ -148,8 +172,8 @@ impl DexEventCoverageService {
|
|||||||
};
|
};
|
||||||
return Ok(crate::DexEventCoverageSyncResult {
|
return Ok(crate::DexEventCoverageSyncResult {
|
||||||
decoder_code,
|
decoder_code,
|
||||||
upstream_entry_count: 0,
|
upstream_entry_count,
|
||||||
upserted_entry_count: 0,
|
upserted_entry_count,
|
||||||
refreshed_entry_count,
|
refreshed_entry_count,
|
||||||
summaries,
|
summaries,
|
||||||
});
|
});
|
||||||
@@ -160,8 +184,12 @@ fn build_coverage_entry_from_upstream(
|
|||||||
entry: &crate::UpstreamRegistryEntryDto,
|
entry: &crate::UpstreamRegistryEntryDto,
|
||||||
) -> crate::DexEventCoverageEntryDto {
|
) -> crate::DexEventCoverageEntryDto {
|
||||||
let event_family = infer_event_family(entry.entry_name.as_str(), entry.entry_kind.as_str());
|
let event_family = infer_event_family(entry.entry_name.as_str(), entry.entry_kind.as_str());
|
||||||
let expected_db_target =
|
let expected_db_target = infer_expected_db_target_for_entry(
|
||||||
infer_expected_db_target(event_family.as_deref(), entry.entry_kind.as_str());
|
entry.decoder_code.as_str(),
|
||||||
|
entry.entry_name.as_str(),
|
||||||
|
event_family.as_deref(),
|
||||||
|
entry.entry_kind.as_str(),
|
||||||
|
);
|
||||||
let local_event_kind =
|
let local_event_kind =
|
||||||
known_local_event_kind(entry.decoder_code.as_str(), entry.entry_name.as_str());
|
known_local_event_kind(entry.decoder_code.as_str(), entry.entry_name.as_str());
|
||||||
let mut coverage_entry = crate::DexEventCoverageEntryDto::from_upstream_registry_entry(
|
let mut coverage_entry = crate::DexEventCoverageEntryDto::from_upstream_registry_entry(
|
||||||
@@ -177,6 +205,18 @@ fn build_coverage_entry_from_upstream(
|
|||||||
return coverage_entry;
|
return coverage_entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn infer_expected_db_target_for_entry(
|
||||||
|
decoder_code: &str,
|
||||||
|
entry_name: &str,
|
||||||
|
event_family: std::option::Option<&str>,
|
||||||
|
entry_kind: &str,
|
||||||
|
) -> std::option::Option<std::string::String> {
|
||||||
|
if decoder_code == "raydium_cpmm" && entry_name == "swap_event" {
|
||||||
|
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
|
||||||
|
}
|
||||||
|
return infer_expected_db_target(event_family, entry_kind);
|
||||||
|
}
|
||||||
|
|
||||||
fn infer_expected_db_target(
|
fn infer_expected_db_target(
|
||||||
event_family: std::option::Option<&str>,
|
event_family: std::option::Option<&str>,
|
||||||
entry_kind: &str,
|
entry_kind: &str,
|
||||||
@@ -195,6 +235,7 @@ fn infer_expected_db_target(
|
|||||||
let target = match family {
|
let target = match family {
|
||||||
"swap" => crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS,
|
"swap" => crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS,
|
||||||
"pool_create" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
|
"pool_create" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
|
||||||
|
"liquidity" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
|
||||||
"liquidity_add" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
|
"liquidity_add" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
|
||||||
"liquidity_remove" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
|
"liquidity_remove" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
|
||||||
"position_open" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
|
"position_open" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
|
||||||
@@ -235,6 +276,9 @@ fn infer_event_family(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let normalized = entry_name.to_ascii_lowercase();
|
let normalized = entry_name.to_ascii_lowercase();
|
||||||
|
if normalized == "lp_change_event" {
|
||||||
|
return Some("liquidity".to_string());
|
||||||
|
}
|
||||||
if contains_any(normalized.as_str(), &["swap", "buy", "sell", "trade"]) {
|
if contains_any(normalized.as_str(), &["swap", "buy", "sell", "trade"]) {
|
||||||
return Some("swap".to_string());
|
return Some("swap".to_string());
|
||||||
}
|
}
|
||||||
@@ -360,29 +404,58 @@ fn known_local_event_kind(
|
|||||||
entry_name: &str,
|
entry_name: &str,
|
||||||
) -> std::option::Option<std::string::String> {
|
) -> std::option::Option<std::string::String> {
|
||||||
match (decoder_code, entry_name) {
|
match (decoder_code, entry_name) {
|
||||||
("raydium-cpmm", "swap_base_input") => {
|
("raydium_cpmm", "swap_base_input") => {
|
||||||
return Some("raydium_cpmm.swap_base_input".to_string());
|
return Some("raydium_cpmm.swap_base_input".to_string());
|
||||||
},
|
},
|
||||||
("raydium-cpmm", "swap_base_output") => {
|
("raydium_cpmm", "swap_base_output") => {
|
||||||
return Some("raydium_cpmm.swap_base_output".to_string());
|
return Some("raydium_cpmm.swap_base_output".to_string());
|
||||||
},
|
},
|
||||||
("raydium-cpmm", "collect_creator_fee") => {
|
("raydium_cpmm", "close_permission_pda") => {
|
||||||
|
return Some("raydium_cpmm.close_permission_pda".to_string());
|
||||||
|
},
|
||||||
|
("raydium_cpmm", "collect_creator_fee") => {
|
||||||
return Some("raydium_cpmm.collect_creator_fee".to_string());
|
return Some("raydium_cpmm.collect_creator_fee".to_string());
|
||||||
},
|
},
|
||||||
("raydium-cpmm", "withdraw") => return Some("raydium_cpmm.withdraw".to_string()),
|
("raydium_cpmm", "collect_fund_fee") => {
|
||||||
("raydium-cpmm", "initialize") => return Some("raydium_cpmm.initialize".to_string()),
|
return Some("raydium_cpmm.collect_fund_fee".to_string());
|
||||||
("raydium-clmm", "swap") => return Some("raydium_clmm.swap".to_string()),
|
},
|
||||||
("raydium-clmm", "swap_v2") => return Some("raydium_clmm.swap_v2".to_string()),
|
("raydium_cpmm", "collect_protocol_fee") => {
|
||||||
("raydium-clmm", "increase_liquidity_v2") => {
|
return Some("raydium_cpmm.collect_protocol_fee".to_string());
|
||||||
|
},
|
||||||
|
("raydium_cpmm", "create_amm_config") => {
|
||||||
|
return Some("raydium_cpmm.create_amm_config".to_string());
|
||||||
|
},
|
||||||
|
("raydium_cpmm", "create_permission_pda") => {
|
||||||
|
return Some("raydium_cpmm.create_permission_pda".to_string());
|
||||||
|
},
|
||||||
|
("raydium_cpmm", "deposit") => return Some("raydium_cpmm.deposit".to_string()),
|
||||||
|
("raydium_cpmm", "initialize") => return Some("raydium_cpmm.initialize".to_string()),
|
||||||
|
("raydium_cpmm", "initialize_with_permission") => {
|
||||||
|
return Some("raydium_cpmm.initialize_with_permission".to_string());
|
||||||
|
},
|
||||||
|
("raydium_cpmm", "lp_change_event") => {
|
||||||
|
return Some("raydium_cpmm.lp_change_event".to_string());
|
||||||
|
},
|
||||||
|
("raydium_cpmm", "swap_event") => return Some("raydium_cpmm.swap_event".to_string()),
|
||||||
|
("raydium_cpmm", "update_amm_config") => {
|
||||||
|
return Some("raydium_cpmm.update_amm_config".to_string());
|
||||||
|
},
|
||||||
|
("raydium_cpmm", "update_pool_status") => {
|
||||||
|
return Some("raydium_cpmm.update_pool_status".to_string());
|
||||||
|
},
|
||||||
|
("raydium_cpmm", "withdraw") => return Some("raydium_cpmm.withdraw".to_string()),
|
||||||
|
("raydium_clmm", "swap") => return Some("raydium_clmm.swap".to_string()),
|
||||||
|
("raydium_clmm", "swap_v2") => return Some("raydium_clmm.swap_v2".to_string()),
|
||||||
|
("raydium_clmm", "increase_liquidity_v2") => {
|
||||||
return Some("raydium_clmm.increase_liquidity_v2".to_string());
|
return Some("raydium_clmm.increase_liquidity_v2".to_string());
|
||||||
},
|
},
|
||||||
("raydium-clmm", "decrease_liquidity_v2") => {
|
("raydium_clmm", "decrease_liquidity_v2") => {
|
||||||
return Some("raydium_clmm.decrease_liquidity_v2".to_string());
|
return Some("raydium_clmm.decrease_liquidity_v2".to_string());
|
||||||
},
|
},
|
||||||
("raydium-clmm", "open_position_with_token22_nft") => {
|
("raydium_clmm", "open_position_with_token22_nft") => {
|
||||||
return Some("raydium_clmm.open_position_with_token22_nft".to_string());
|
return Some("raydium_clmm.open_position_with_token22_nft".to_string());
|
||||||
},
|
},
|
||||||
("raydium-clmm", "close_position") => {
|
("raydium_clmm", "close_position") => {
|
||||||
return Some("raydium_clmm.close_position".to_string());
|
return Some("raydium_clmm.close_position".to_string());
|
||||||
},
|
},
|
||||||
_ => return None,
|
_ => return None,
|
||||||
@@ -442,7 +515,7 @@ mod tests {
|
|||||||
async fn sync_upstream_registry_persists_raydium_cpmm_coverage_rows() {
|
async fn sync_upstream_registry_persists_raydium_cpmm_coverage_rows() {
|
||||||
let database = make_database().await;
|
let database = make_database().await;
|
||||||
let service = crate::DexEventCoverageService::new(database.clone());
|
let service = crate::DexEventCoverageService::new(database.clone());
|
||||||
let result = service.sync_upstream_registry(Some("raydium-cpmm".to_string())).await;
|
let result = service.sync_upstream_registry(Some("raydium_cpmm".to_string())).await;
|
||||||
let result = match result {
|
let result = match result {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(error) => panic!("coverage sync must succeed: {}", error),
|
Err(error) => panic!("coverage sync must succeed: {}", error),
|
||||||
@@ -451,7 +524,7 @@ mod tests {
|
|||||||
assert_eq!(result.upstream_entry_count, result.upserted_entry_count);
|
assert_eq!(result.upstream_entry_count, result.upserted_entry_count);
|
||||||
let rows_result = crate::query_dex_event_coverage_entries_list_by_decoder(
|
let rows_result = crate::query_dex_event_coverage_entries_list_by_decoder(
|
||||||
database.as_ref(),
|
database.as_ref(),
|
||||||
"raydium-cpmm",
|
"raydium_cpmm",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let rows = match rows_result {
|
let rows = match rows_result {
|
||||||
@@ -467,7 +540,46 @@ mod tests {
|
|||||||
assert!(rows.iter().any(|row| return {
|
assert!(rows.iter().any(|row| return {
|
||||||
row.entry_name == "deposit"
|
row.entry_name == "deposit"
|
||||||
&& row.event_family == Some("liquidity_add".to_string())
|
&& row.event_family == Some("liquidity_add".to_string())
|
||||||
&& row.local_event_kind.is_none()
|
&& row.local_event_kind == Some("raydium_cpmm.deposit".to_string())
|
||||||
|
}));
|
||||||
|
assert!(rows.iter().any(|row| return {
|
||||||
|
row.entry_name == "lp_change_event"
|
||||||
|
&& row.event_family == Some("liquidity".to_string())
|
||||||
|
&& row.expected_db_target
|
||||||
|
== Some(crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS.to_string())
|
||||||
|
&& row.local_event_kind == Some("raydium_cpmm.lp_change_event".to_string())
|
||||||
|
}));
|
||||||
|
assert!(rows.iter().any(|row| return {
|
||||||
|
row.entry_name == "swap_event"
|
||||||
|
&& row.event_family == Some("swap".to_string())
|
||||||
|
&& row.expected_db_target
|
||||||
|
== Some(
|
||||||
|
crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string(),
|
||||||
|
)
|
||||||
|
&& row.local_event_kind == Some("raydium_cpmm.swap_event".to_string())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn refresh_local_counts_auto_syncs_empty_coverage_table() {
|
||||||
|
let database = make_database().await;
|
||||||
|
let service = crate::DexEventCoverageService::new(database.clone());
|
||||||
|
let result = service.refresh_local_counts(Some("raydium_cpmm".to_string())).await;
|
||||||
|
let result = match result {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(error) => panic!("coverage refresh must succeed: {}", error),
|
||||||
|
};
|
||||||
|
assert!(result.upstream_entry_count > 0);
|
||||||
|
assert_eq!(result.upstream_entry_count, result.upserted_entry_count);
|
||||||
|
let rows_result = crate::query_dex_event_coverage_entries_list_by_decoder(
|
||||||
|
database.as_ref(),
|
||||||
|
"raydium_cpmm",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let rows = match rows_result {
|
||||||
|
Ok(rows) => rows,
|
||||||
|
Err(error) => panic!("coverage rows must load: {}", error),
|
||||||
|
};
|
||||||
|
assert!(!rows.is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ const DEX_SUPPORT_MATRIX_ENTRIES: &[DexSupportMatrixEntry] = &[
|
|||||||
version: "unknown",
|
version: "unknown",
|
||||||
surface_type: "launch",
|
surface_type: "launch",
|
||||||
surface_role: "launch_surface",
|
surface_role: "launch_surface",
|
||||||
program_id: Some(crate::BOOP_PROGRAM_ID),
|
program_id: Some(crate::BOOP_FUN_PROGRAM_ID),
|
||||||
router_program_id: None,
|
router_program_id: None,
|
||||||
program_id_status: "to_verify",
|
program_id_status: "to_verify",
|
||||||
observed: false,
|
observed: false,
|
||||||
@@ -2934,7 +2934,7 @@ mod tests {
|
|||||||
("zora", crate::ZORA_PROGRAM_ID),
|
("zora", crate::ZORA_PROGRAM_ID),
|
||||||
("raydium_liquidity_locking", crate::RAYDIUM_LIQUIDITY_LOCKING_PROGRAM_ID),
|
("raydium_liquidity_locking", crate::RAYDIUM_LIQUIDITY_LOCKING_PROGRAM_ID),
|
||||||
("okx_dex", crate::OKX_DEX_PROGRAM_ID),
|
("okx_dex", crate::OKX_DEX_PROGRAM_ID),
|
||||||
("boop_fun", crate::BOOP_PROGRAM_ID),
|
("boop_fun", crate::BOOP_FUN_PROGRAM_ID),
|
||||||
("heaven", crate::HEAVEN_PROGRAM_ID),
|
("heaven", crate::HEAVEN_PROGRAM_ID),
|
||||||
("bonkswap", crate::BONKSWAP_PROGRAM_ID),
|
("bonkswap", crate::BONKSWAP_PROGRAM_ID),
|
||||||
("metadao_launchpad_v0_7_0", crate::METADAO_LAUNCHPAD_V0_7_0_PROGRAM_ID),
|
("metadao_launchpad_v0_7_0", crate::METADAO_LAUNCHPAD_V0_7_0_PROGRAM_ID),
|
||||||
|
|||||||
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": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
||||||
"program": "meteora-dbc",
|
"program": "meteora_dbc",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"DbcDetectPool111",
|
"DbcDetectPool111",
|
||||||
@@ -829,7 +829,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
|
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
|
||||||
"program": "meteora-damm-v2",
|
"program": "meteora_damm_v2",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"MoonitDammV2Pool111",
|
"MoonitDammV2Pool111",
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ mod error;
|
|||||||
mod http_client;
|
mod http_client;
|
||||||
/// HTTP endpoint pool and routing.
|
/// HTTP endpoint pool and routing.
|
||||||
mod http_pool;
|
mod http_pool;
|
||||||
|
/// Technical index for observed chain instructions.
|
||||||
|
mod instruction_observation_index;
|
||||||
/// Generic JSON-RPC 2.0 WebSocket helpers.
|
/// Generic JSON-RPC 2.0 WebSocket helpers.
|
||||||
mod json_rpc_ws;
|
mod json_rpc_ws;
|
||||||
/// Launch surface attribution service.
|
/// Launch surface attribution service.
|
||||||
@@ -173,7 +175,7 @@ pub use constants::BONK_MINT_ID;
|
|||||||
/// Bonkswap program id extracted from upstream Git decoder source.
|
/// Bonkswap program id extracted from upstream Git decoder source.
|
||||||
pub use constants::BONKSWAP_PROGRAM_ID;
|
pub use constants::BONKSWAP_PROGRAM_ID;
|
||||||
/// Boop program id extracted from upstream Git decoder source.
|
/// Boop program id extracted from upstream Git decoder source.
|
||||||
pub use constants::BOOP_PROGRAM_ID;
|
pub use constants::BOOP_FUN_PROGRAM_ID;
|
||||||
/// BPF Loader program identifier. ("BPFLoader1111111111111111111111111111111111").
|
/// BPF Loader program identifier. ("BPFLoader1111111111111111111111111111111111").
|
||||||
/// @see solana_sdk::pubkey::Pubkey = solana_sdk_ids::bpf_loader_deprecated::ID
|
/// @see solana_sdk::pubkey::Pubkey = solana_sdk_ids::bpf_loader_deprecated::ID
|
||||||
pub use constants::BPF_LOADER_DEPRECATED_PROGRAM_ID;
|
pub use constants::BPF_LOADER_DEPRECATED_PROGRAM_ID;
|
||||||
@@ -491,6 +493,10 @@ pub use db::DexEventCoverageSummaryEntity;
|
|||||||
pub use db::FeeEventDto;
|
pub use db::FeeEventDto;
|
||||||
/// Persisted fee event row.
|
/// Persisted fee event row.
|
||||||
pub use db::FeeEventEntity;
|
pub use db::FeeEventEntity;
|
||||||
|
/// Application-facing on-chain observation DTO.
|
||||||
|
pub use db::InstructionObservationDto;
|
||||||
|
/// Persisted technical observation for one Solana instruction.
|
||||||
|
pub use db::InstructionObservationEntity;
|
||||||
/// Application-facing known HTTP endpoint DTO.
|
/// Application-facing known HTTP endpoint DTO.
|
||||||
pub use db::KnownHttpEndpointDto;
|
pub use db::KnownHttpEndpointDto;
|
||||||
/// Application-facing known WebSocket endpoint DTO.
|
/// Application-facing known WebSocket endpoint DTO.
|
||||||
@@ -767,6 +773,9 @@ pub use db::query_fee_events_get_by_decoded_event_id;
|
|||||||
pub use db::query_fee_events_list_recent;
|
pub use db::query_fee_events_list_recent;
|
||||||
/// Inserts or updates one normalized fee event row.
|
/// Inserts or updates one normalized fee event row.
|
||||||
pub use db::query_fee_events_upsert;
|
pub use db::query_fee_events_upsert;
|
||||||
|
/// Inserts one on-chain observation row and returns its numeric id.
|
||||||
|
pub use db::query_instruction_observations_list_by_filter;
|
||||||
|
pub use db::query_instruction_observations_upsert;
|
||||||
/// Reads one known HTTP endpoint by name.
|
/// Reads one known HTTP endpoint by name.
|
||||||
pub use db::query_known_http_endpoints_get;
|
pub use db::query_known_http_endpoints_get;
|
||||||
/// Lists all known HTTP endpoints.
|
/// Lists all known HTTP endpoints.
|
||||||
@@ -1141,14 +1150,22 @@ pub use dex::RaydiumClmmSwapLegacyDecoded;
|
|||||||
pub use dex::RaydiumClmmSwapV2Decoded;
|
pub use dex::RaydiumClmmSwapV2Decoded;
|
||||||
/// Raydium CPMM decoded event.
|
/// Raydium CPMM decoded event.
|
||||||
pub use dex::RaydiumCpmmDecodedEvent;
|
pub use dex::RaydiumCpmmDecodedEvent;
|
||||||
|
/// Raydium CPMM Anchor CPI liquidity-change event.
|
||||||
|
pub use dex::RaydiumCpmmLpChangeEventDecoded;
|
||||||
/// Raydium CPMM decoded swap.
|
/// Raydium CPMM decoded swap.
|
||||||
pub use dex::RaydiumCpmmSwapDecoded;
|
pub use dex::RaydiumCpmmSwapDecoded;
|
||||||
|
/// Raydium CPMM Anchor CPI swap event retained as audit evidence.
|
||||||
|
pub use dex::RaydiumCpmmSwapEventDecoded;
|
||||||
/// Raydium CPMM swap mode.
|
/// Raydium CPMM swap mode.
|
||||||
pub use dex::RaydiumCpmmSwapMode;
|
pub use dex::RaydiumCpmmSwapMode;
|
||||||
|
/// Decodes one Raydium CPMM instruction from projected instruction fields.
|
||||||
|
pub use dex::classify_raydium_cpmm_instruction_data;
|
||||||
/// Decodes a Raydium CLMM instruction.
|
/// Decodes a Raydium CLMM instruction.
|
||||||
pub use dex::decode_raydium_clmm_instruction;
|
pub use dex::decode_raydium_clmm_instruction;
|
||||||
/// Decodes one Raydium CPMM instruction from projected instruction fields.
|
/// Decodes one Raydium CPMM instruction from projected instruction fields.
|
||||||
pub use dex::decode_raydium_cpmm_instruction;
|
pub use dex::decode_raydium_cpmm_instruction;
|
||||||
|
/// Decodes Raydium CPMM Anchor events emitted in `Program data:` logs.
|
||||||
|
pub use dex::decode_raydium_cpmm_program_data_event;
|
||||||
/// DEX decode service.
|
/// DEX decode service.
|
||||||
pub use dex_decode::DexDecodeService;
|
pub use dex_decode::DexDecodeService;
|
||||||
/// Business-level DEX detection service.
|
/// Business-level DEX detection service.
|
||||||
@@ -1263,6 +1280,10 @@ pub use http_client::parse_json_rpc_http_response_value;
|
|||||||
pub use http_pool::HttpEndpointPool;
|
pub use http_pool::HttpEndpointPool;
|
||||||
/// Snapshot of one pooled HTTP endpoint.
|
/// Snapshot of one pooled HTTP endpoint.
|
||||||
pub use http_pool::HttpPoolClientSnapshot;
|
pub use http_pool::HttpPoolClientSnapshot;
|
||||||
|
/// Instruction-observation index refresh result.
|
||||||
|
pub use instruction_observation_index::InstructionObservationIndexRefreshResult;
|
||||||
|
/// Technical service that indexes observed Solana instructions.
|
||||||
|
pub use instruction_observation_index::InstructionObservationIndexService;
|
||||||
/// JSON-RPC 2.0 error object.
|
/// JSON-RPC 2.0 error object.
|
||||||
pub use json_rpc_ws::JsonRpcWsErrorObject;
|
pub use json_rpc_ws::JsonRpcWsErrorObject;
|
||||||
/// JSON-RPC 2.0 error response.
|
/// JSON-RPC 2.0 error response.
|
||||||
|
|||||||
@@ -825,17 +825,14 @@ async fn query_validation_i64(
|
|||||||
async fn load_event_coverage_summaries(
|
async fn load_event_coverage_summaries(
|
||||||
database: &crate::Database,
|
database: &crate::Database,
|
||||||
) -> Result<std::vec::Vec<crate::DexEventCoverageSummaryDto>, crate::Error> {
|
) -> Result<std::vec::Vec<crate::DexEventCoverageSummaryDto>, crate::Error> {
|
||||||
let refresh_result =
|
let coverage_service =
|
||||||
crate::query_dex_event_coverage_entries_refresh_local_counts(database).await;
|
crate::DexEventCoverageService::new(std::sync::Arc::new(database.clone()));
|
||||||
if let Err(error) = refresh_result {
|
let refresh_result = coverage_service.refresh_local_counts(None).await;
|
||||||
return Err(error);
|
let refresh_result = match refresh_result {
|
||||||
}
|
Ok(refresh_result) => refresh_result,
|
||||||
let summaries_result =
|
|
||||||
crate::query_dex_event_coverage_entries_list_summary_by_decoder(database).await;
|
|
||||||
match summaries_result {
|
|
||||||
Ok(summaries) => return Ok(summaries),
|
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
}
|
};
|
||||||
|
return Ok(refresh_result.summaries);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ impl LocalPipelineReplayService {
|
|||||||
let pair_analytic_signal = crate::PairAnalyticSignalService::new(self.database.clone());
|
let pair_analytic_signal = crate::PairAnalyticSignalService::new(self.database.clone());
|
||||||
let transaction_classification =
|
let transaction_classification =
|
||||||
crate::TransactionClassificationService::new(self.database.clone());
|
crate::TransactionClassificationService::new(self.database.clone());
|
||||||
|
let instruction_observation_index =
|
||||||
|
crate::InstructionObservationIndexService::new(self.database.clone());
|
||||||
let mut result = LocalPipelineReplayResult {
|
let mut result = LocalPipelineReplayResult {
|
||||||
selected_transaction_count: signatures.len(),
|
selected_transaction_count: signatures.len(),
|
||||||
reset_market_materialization_deleted_count,
|
reset_market_materialization_deleted_count,
|
||||||
@@ -424,6 +426,24 @@ impl LocalPipelineReplayService {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
let instruction_index_result =
|
||||||
|
instruction_observation_index.refresh_signature(signature.as_str()).await;
|
||||||
|
match instruction_index_result {
|
||||||
|
Ok(index_result) => {
|
||||||
|
tracing::debug!(
|
||||||
|
signature = %signature,
|
||||||
|
upserted_observation_count = index_result.upserted_observation_count,
|
||||||
|
"instruction observation index refreshed during local replay"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
signature = %signature,
|
||||||
|
error = %error,
|
||||||
|
"instruction observation index refresh failed during local replay"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
result.replayed_transaction_count += 1;
|
result.replayed_transaction_count += 1;
|
||||||
}
|
}
|
||||||
if config.refresh_missing_token_metadata {
|
if config.refresh_missing_token_metadata {
|
||||||
@@ -451,9 +471,31 @@ impl LocalPipelineReplayService {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.refresh_event_coverage_best_effort().await;
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn refresh_event_coverage_best_effort(&self) {
|
||||||
|
let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
|
||||||
|
let refresh_result = coverage_service.refresh_local_counts(None).await;
|
||||||
|
match refresh_result {
|
||||||
|
Ok(refresh_result) => {
|
||||||
|
tracing::debug!(
|
||||||
|
upserted_entry_count = refresh_result.upserted_entry_count,
|
||||||
|
refreshed_entry_count = refresh_result.refreshed_entry_count,
|
||||||
|
summary_count = refresh_result.summaries.len(),
|
||||||
|
"dex event coverage refreshed after local pipeline replay"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
error = %error,
|
||||||
|
"dex event coverage refresh failed after local pipeline replay"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_certified_dex_decode_skip_ledger(
|
async fn get_certified_dex_decode_skip_ledger(
|
||||||
&self,
|
&self,
|
||||||
config: &crate::LocalPipelineReplayConfig,
|
config: &crate::LocalPipelineReplayConfig,
|
||||||
@@ -777,7 +819,12 @@ mod tests {
|
|||||||
let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice())
|
let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice())
|
||||||
.expect("ledger must build");
|
.expect("ledger must build");
|
||||||
assert_eq!(ledger.event_count, 2);
|
assert_eq!(ledger.event_count, 2);
|
||||||
assert_eq!(ledger.status_reason.as_deref(), Some("decode completed and certified for skip: event_count=2, effective_event_count=0, instruction_audit_count=2, distinct_token_mint_count=2"));
|
assert_eq!(
|
||||||
|
ledger.status_reason.as_deref(),
|
||||||
|
Some(
|
||||||
|
"decode completed and certified for skip: event_count=2, effective_event_count=0, instruction_audit_count=2, distinct_token_mint_count=2"
|
||||||
|
)
|
||||||
|
);
|
||||||
assert!(!ledger.force_replay_required);
|
assert!(!ledger.force_replay_required);
|
||||||
assert!(ledger.can_skip_decode());
|
assert!(ledger.can_skip_decode());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1824,7 +1824,7 @@ mod tests {
|
|||||||
summary.event_coverage_upstream_git_local_corpus_observed_entry_count = 1;
|
summary.event_coverage_upstream_git_local_corpus_observed_entry_count = 1;
|
||||||
summary.event_coverage_upstream_git_local_corpus_materialized_entry_count = 1;
|
summary.event_coverage_upstream_git_local_corpus_materialized_entry_count = 1;
|
||||||
summary.event_coverage_summaries.push(crate::DexEventCoverageSummaryDto {
|
summary.event_coverage_summaries.push(crate::DexEventCoverageSummaryDto {
|
||||||
decoder_code: "raydium-cpmm".to_string(),
|
decoder_code: "raydium_cpmm".to_string(),
|
||||||
listed_entry_count: 4,
|
listed_entry_count: 4,
|
||||||
decoded_entry_count: 3,
|
decoded_entry_count: 3,
|
||||||
observed_entry_count: 2,
|
observed_entry_count: 2,
|
||||||
|
|||||||
@@ -102,7 +102,17 @@ impl NonTradeEventMaterializationService {
|
|||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if crate::is_dex_liquidity_event_kind(decoded_event.event_kind.as_str()) {
|
if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str()) {
|
||||||
|
let cleanup_result =
|
||||||
|
self.delete_stale_pool_admin_event_for_lifecycle(decoded_event).await;
|
||||||
|
match cleanup_result {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if crate::is_dex_liquidity_event_kind(decoded_event.event_kind.as_str())
|
||||||
|
&& !decoded_event.event_kind.ends_with(".lp_change_event")
|
||||||
|
{
|
||||||
let materialized = self
|
let materialized = self
|
||||||
.materialize_liquidity_event(
|
.materialize_liquidity_event(
|
||||||
&transaction,
|
&transaction,
|
||||||
@@ -159,7 +169,9 @@ impl NonTradeEventMaterializationService {
|
|||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if crate::is_dex_admin_event_kind(decoded_event.event_kind.as_str()) {
|
if crate::is_dex_admin_event_kind(decoded_event.event_kind.as_str())
|
||||||
|
&& !crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str())
|
||||||
|
{
|
||||||
let materialized = self
|
let materialized = self
|
||||||
.materialize_pool_admin_event(
|
.materialize_pool_admin_event(
|
||||||
&transaction,
|
&transaction,
|
||||||
@@ -178,6 +190,36 @@ impl NonTradeEventMaterializationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for decoded_event in &decoded_events {
|
||||||
|
if !decoded_event.event_kind.ends_with(".lp_change_event") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let payload_result =
|
||||||
|
serde_json::from_str::<serde_json::Value>(decoded_event.payload_json.as_str());
|
||||||
|
let payload = match payload_result {
|
||||||
|
Ok(payload) => payload,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
signature = %transaction.signature,
|
||||||
|
event_kind = %decoded_event.event_kind,
|
||||||
|
error = %error,
|
||||||
|
"skipping postponed lp_change_event materialization for invalid decoded payload"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let materialized = self
|
||||||
|
.materialize_liquidity_event(&transaction, transaction_id, decoded_event, &payload)
|
||||||
|
.await;
|
||||||
|
match materialized {
|
||||||
|
Ok(was_materialized) => {
|
||||||
|
if was_materialized {
|
||||||
|
result.liquidity_event_count += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +316,12 @@ impl NonTradeEventMaterializationService {
|
|||||||
"fund_fee_amount",
|
"fund_fee_amount",
|
||||||
"creatorFeeAmount",
|
"creatorFeeAmount",
|
||||||
"creator_fee_amount",
|
"creator_fee_amount",
|
||||||
|
"amount0RequestedRaw",
|
||||||
|
"amount_0_requested_raw",
|
||||||
|
"amount1RequestedRaw",
|
||||||
|
"amount_1_requested_raw",
|
||||||
|
"tokenAAmount",
|
||||||
|
"tokenBAmount",
|
||||||
"amount",
|
"amount",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -370,6 +418,48 @@ impl NonTradeEventMaterializationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_stale_pool_admin_event_for_lifecycle(
|
||||||
|
&self,
|
||||||
|
decoded_event: &crate::DexDecodedEventDto,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
let decoded_event_id = match decoded_event.id {
|
||||||
|
Some(decoded_event_id) => decoded_event_id,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
match self.database.connection() {
|
||||||
|
crate::DatabaseConnection::Sqlite(pool) => {
|
||||||
|
let delete_result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM k_sol_pool_admin_events
|
||||||
|
WHERE decoded_event_id = ?
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(decoded_event_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await;
|
||||||
|
let delete_result = match delete_result {
|
||||||
|
Ok(delete_result) => delete_result,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(crate::Error::Db(format!(
|
||||||
|
"cannot delete stale k_sol_pool_admin_events for lifecycle decoded_event_id '{}' on sqlite: {}",
|
||||||
|
decoded_event_id, error
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let deleted_count = delete_result.rows_affected();
|
||||||
|
if deleted_count > 0 {
|
||||||
|
tracing::debug!(
|
||||||
|
decoded_event_id = decoded_event_id,
|
||||||
|
event_kind = %decoded_event.event_kind,
|
||||||
|
deleted_count = deleted_count,
|
||||||
|
"removed stale pool admin materialization for lifecycle event"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn materialize_pool_admin_event(
|
async fn materialize_pool_admin_event(
|
||||||
&self,
|
&self,
|
||||||
transaction: &crate::ChainTransactionDto,
|
transaction: &crate::ChainTransactionDto,
|
||||||
@@ -437,6 +527,16 @@ impl NonTradeEventMaterializationService {
|
|||||||
Ok(context) => context,
|
Ok(context) => context,
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
|
let context = if context.pool_id.is_some() && context.pair.is_some() {
|
||||||
|
context
|
||||||
|
} else {
|
||||||
|
let ensured_context =
|
||||||
|
self.ensure_liquidity_context_from_decoded_event(decoded_event, context).await;
|
||||||
|
match ensured_context {
|
||||||
|
Ok(ensured_context) => ensured_context,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
};
|
||||||
let dex_id = match context.dex_id {
|
let dex_id = match context.dex_id {
|
||||||
Some(dex_id) => dex_id,
|
Some(dex_id) => dex_id,
|
||||||
None => return Ok(false),
|
None => return Ok(false),
|
||||||
@@ -458,6 +558,12 @@ impl NonTradeEventMaterializationService {
|
|||||||
crate::LiquidityEventKind::PositionOpen
|
crate::LiquidityEventKind::PositionOpen
|
||||||
} else if crate::is_dex_position_close_event_kind(decoded_event.event_kind.as_str()) {
|
} else if crate::is_dex_position_close_event_kind(decoded_event.event_kind.as_str()) {
|
||||||
crate::LiquidityEventKind::PositionClose
|
crate::LiquidityEventKind::PositionClose
|
||||||
|
} else if decoded_event.event_kind.ends_with(".lp_change_event") {
|
||||||
|
let change_type = extract_first_u64(payload, &["changeType", "change_type"]);
|
||||||
|
match change_type {
|
||||||
|
Some(1) => crate::LiquidityEventKind::Remove,
|
||||||
|
_ => crate::LiquidityEventKind::Add,
|
||||||
|
}
|
||||||
} else if crate::is_dex_liquidity_remove_event_kind(decoded_event.event_kind.as_str()) {
|
} else if crate::is_dex_liquidity_remove_event_kind(decoded_event.event_kind.as_str()) {
|
||||||
crate::LiquidityEventKind::Remove
|
crate::LiquidityEventKind::Remove
|
||||||
} else {
|
} else {
|
||||||
@@ -487,6 +593,10 @@ impl NonTradeEventMaterializationService {
|
|||||||
"amount_base",
|
"amount_base",
|
||||||
"tokenAAmount",
|
"tokenAAmount",
|
||||||
"token_a_amount",
|
"token_a_amount",
|
||||||
|
"token0AmountRaw",
|
||||||
|
"token_0_amount_raw",
|
||||||
|
"amount0RequestedRaw",
|
||||||
|
"amount_0_requested_raw",
|
||||||
"amountA",
|
"amountA",
|
||||||
"amount_a",
|
"amount_a",
|
||||||
],
|
],
|
||||||
@@ -502,6 +612,10 @@ impl NonTradeEventMaterializationService {
|
|||||||
"amount_quote",
|
"amount_quote",
|
||||||
"tokenBAmount",
|
"tokenBAmount",
|
||||||
"token_b_amount",
|
"token_b_amount",
|
||||||
|
"token1AmountRaw",
|
||||||
|
"token_1_amount_raw",
|
||||||
|
"amount1RequestedRaw",
|
||||||
|
"amount_1_requested_raw",
|
||||||
"amountB",
|
"amountB",
|
||||||
"amount_b",
|
"amount_b",
|
||||||
],
|
],
|
||||||
@@ -559,6 +673,54 @@ impl NonTradeEventMaterializationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ensure_liquidity_context_from_decoded_event(
|
||||||
|
&self,
|
||||||
|
decoded_event: &crate::DexDecodedEventDto,
|
||||||
|
context: NonTradeDecodedEventContext,
|
||||||
|
) -> Result<NonTradeDecodedEventContext, crate::Error> {
|
||||||
|
let dex_id = match context.dex_id {
|
||||||
|
Some(dex_id) => dex_id,
|
||||||
|
None => return Ok(context),
|
||||||
|
};
|
||||||
|
if context.pool_id.is_some() && context.pair.is_some() {
|
||||||
|
return Ok(context);
|
||||||
|
}
|
||||||
|
if decoded_event.pool_account.is_none()
|
||||||
|
|| decoded_event.token_a_mint.is_none()
|
||||||
|
|| decoded_event.token_b_mint.is_none()
|
||||||
|
{
|
||||||
|
return Ok(context);
|
||||||
|
}
|
||||||
|
let materialization_input_result =
|
||||||
|
crate::dex_pool_materialization::DexPoolMaterializationInput::from_decoded_event(
|
||||||
|
decoded_event,
|
||||||
|
dex_id,
|
||||||
|
crate::PoolKind::Amm,
|
||||||
|
crate::PoolStatus::Active,
|
||||||
|
crate::dex_pool_materialization::DexPoolTokenOrder::AlreadyBaseQuote,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let materialization_input = match materialization_input_result {
|
||||||
|
Ok(materialization_input) => materialization_input,
|
||||||
|
Err(_) => return Ok(context),
|
||||||
|
};
|
||||||
|
let materialization_result = crate::dex_pool_materialization::materialize_dex_pool(
|
||||||
|
self.database.as_ref(),
|
||||||
|
&materialization_input,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Err(error) = materialization_result {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let refreshed_context = self.resolve_decoded_event_context(decoded_event).await;
|
||||||
|
match refreshed_context {
|
||||||
|
Ok(refreshed_context) => return Ok(refreshed_context),
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn resolve_decoded_event_context(
|
async fn resolve_decoded_event_context(
|
||||||
&self,
|
&self,
|
||||||
decoded_event: &crate::DexDecodedEventDto,
|
decoded_event: &crate::DexDecodedEventDto,
|
||||||
@@ -627,6 +789,29 @@ impl NonTradeEventMaterializationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_first_u64(
|
||||||
|
value: &serde_json::Value,
|
||||||
|
candidate_keys: &[&str],
|
||||||
|
) -> std::option::Option<u64> {
|
||||||
|
if let Some(object) = value.as_object() {
|
||||||
|
for candidate_key in candidate_keys {
|
||||||
|
let candidate_value = object.get(*candidate_key);
|
||||||
|
if let Some(candidate_value) = candidate_value {
|
||||||
|
if let Some(number) = candidate_value.as_u64() {
|
||||||
|
return Some(number);
|
||||||
|
}
|
||||||
|
if let Some(text) = candidate_value.as_str() {
|
||||||
|
let parsed = text.parse::<u64>();
|
||||||
|
if let Ok(parsed) = parsed {
|
||||||
|
return Some(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
fn extract_first_amount_string(
|
fn extract_first_amount_string(
|
||||||
value: &serde_json::Value,
|
value: &serde_json::Value,
|
||||||
candidate_keys: &[&str],
|
candidate_keys: &[&str],
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ pub struct OnchainDexPairDiscoveryRequestDto {
|
|||||||
pub scan_order: std::option::Option<std::string::String>,
|
pub scan_order: std::option::Option<std::string::String>,
|
||||||
/// Optional target event family used to score and filter candidate signatures.
|
/// Optional target event family used to score and filter candidate signatures.
|
||||||
pub target_event: std::option::Option<std::string::String>,
|
pub target_event: std::option::Option<std::string::String>,
|
||||||
|
/// Optional instruction-name filter, for example `raydium_cpmm.withdraw` or `withdraw`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_instruction_name: std::option::Option<std::string::String>,
|
||||||
|
/// Optional first-eight-byte discriminator filter as lower hex; accepts comma/space separated values.
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_discriminator_hex: std::option::Option<std::string::String>,
|
||||||
/// Whether transactions containing swap-like logs should be skipped.
|
/// Whether transactions containing swap-like logs should be skipped.
|
||||||
pub exclude_swaps: bool,
|
pub exclude_swaps: bool,
|
||||||
/// Whether failed transactions should be returned as candidates.
|
/// Whether failed transactions should be returned as candidates.
|
||||||
@@ -209,6 +215,8 @@ pub struct OnchainDexPairCandidateDto {
|
|||||||
pub instruction_name: std::option::Option<std::string::String>,
|
pub instruction_name: std::option::Option<std::string::String>,
|
||||||
/// Prefix of the raw base58 instruction data, useful for audit grouping.
|
/// Prefix of the raw base58 instruction data, useful for audit grouping.
|
||||||
pub instruction_data_prefix: std::option::Option<std::string::String>,
|
pub instruction_data_prefix: std::option::Option<std::string::String>,
|
||||||
|
/// First eight instruction-data bytes as lower hex.
|
||||||
|
pub instruction_discriminator_hex: std::option::Option<std::string::String>,
|
||||||
/// Candidate pool address when it can be extracted safely or heuristically.
|
/// Candidate pool address when it can be extracted safely or heuristically.
|
||||||
pub pool_address: std::option::Option<std::string::String>,
|
pub pool_address: std::option::Option<std::string::String>,
|
||||||
/// Candidate token A/base mint when it can be extracted.
|
/// Candidate token A/base mint when it can be extracted.
|
||||||
@@ -382,6 +390,8 @@ impl OnchainDexPairDiscoveryService {
|
|||||||
let logs = extract_log_messages(&transaction_value);
|
let logs = extract_log_messages(&transaction_value);
|
||||||
let target_keeps_mixed_swaps = target_event_keeps_mixed_swap_transactions(
|
let target_keeps_mixed_swaps = target_event_keeps_mixed_swap_transactions(
|
||||||
normalized_request.target_event.as_deref(),
|
normalized_request.target_event.as_deref(),
|
||||||
|
normalized_request.target_instruction_name.as_deref(),
|
||||||
|
normalized_request.target_discriminator_hex.as_deref(),
|
||||||
);
|
);
|
||||||
if normalized_request.exclude_swaps
|
if normalized_request.exclude_swaps
|
||||||
&& logs_contain_swap(logs.as_slice())
|
&& logs_contain_swap(logs.as_slice())
|
||||||
@@ -396,6 +406,8 @@ impl OnchainDexPairDiscoveryService {
|
|||||||
resolved.program_id.as_str(),
|
resolved.program_id.as_str(),
|
||||||
resolved.dex_code.clone(),
|
resolved.dex_code.clone(),
|
||||||
normalized_request.target_event.as_deref(),
|
normalized_request.target_event.as_deref(),
|
||||||
|
normalized_request.target_instruction_name.as_deref(),
|
||||||
|
normalized_request.target_discriminator_hex.as_deref(),
|
||||||
);
|
);
|
||||||
result.scanned_instruction_count += extraction.scanned_instruction_count;
|
result.scanned_instruction_count += extraction.scanned_instruction_count;
|
||||||
result.target_program_instruction_count += extraction.target_program_instruction_count;
|
result.target_program_instruction_count += extraction.target_program_instruction_count;
|
||||||
@@ -683,6 +695,8 @@ fn normalize_request(
|
|||||||
max_pages: clamp_u32(default_if_zero(request.max_pages, 1), 1, 25),
|
max_pages: clamp_u32(default_if_zero(request.max_pages, 1), 1, 25),
|
||||||
scan_order: Some(normalize_scan_order(request.scan_order.as_deref()).to_string()),
|
scan_order: Some(normalize_scan_order(request.scan_order.as_deref()).to_string()),
|
||||||
target_event: normalize_target_event(request.target_event),
|
target_event: normalize_target_event(request.target_event),
|
||||||
|
target_instruction_name: normalize_instruction_name_filter(request.target_instruction_name),
|
||||||
|
target_discriminator_hex: normalize_discriminator_filter(request.target_discriminator_hex),
|
||||||
exclude_swaps: request.exclude_swaps,
|
exclude_swaps: request.exclude_swaps,
|
||||||
include_failed: request.include_failed,
|
include_failed: request.include_failed,
|
||||||
http_role,
|
http_role,
|
||||||
@@ -773,6 +787,56 @@ fn normalize_target_event(
|
|||||||
return Some(targets.join(","));
|
return Some(targets.join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_instruction_name_filter(
|
||||||
|
value: std::option::Option<std::string::String>,
|
||||||
|
) -> std::option::Option<std::string::String> {
|
||||||
|
let value = match normalize_optional_string(value) {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
let mut normalized = std::vec::Vec::new();
|
||||||
|
for token in value.split(|character: char| {
|
||||||
|
return character == ',' || character == ';' || character.is_whitespace();
|
||||||
|
}) {
|
||||||
|
let token = token.trim().to_ascii_lowercase();
|
||||||
|
if token.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push_unique_string(&mut normalized, token.replace('-', "_"));
|
||||||
|
}
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(normalized.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_discriminator_filter(
|
||||||
|
value: std::option::Option<std::string::String>,
|
||||||
|
) -> std::option::Option<std::string::String> {
|
||||||
|
let value = match normalize_optional_string(value) {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
let mut normalized = std::vec::Vec::new();
|
||||||
|
for token in value.split(|character: char| {
|
||||||
|
return character == ',' || character == ';' || character.is_whitespace();
|
||||||
|
}) {
|
||||||
|
let mut token = token.trim().to_ascii_lowercase();
|
||||||
|
if token.starts_with("0x") {
|
||||||
|
token = token[2..].to_string();
|
||||||
|
}
|
||||||
|
if token.len() != 16 || !token.chars().all(|character| return character.is_ascii_hexdigit())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push_unique_string(&mut normalized, token);
|
||||||
|
}
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(normalized.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_signature_source(
|
fn normalize_signature_source(
|
||||||
value: std::option::Option<std::string::String>,
|
value: std::option::Option<std::string::String>,
|
||||||
) -> std::option::Option<std::string::String> {
|
) -> std::option::Option<std::string::String> {
|
||||||
@@ -974,6 +1038,8 @@ fn extract_candidates_from_transaction(
|
|||||||
target_program_id: &str,
|
target_program_id: &str,
|
||||||
dex_code: std::option::Option<std::string::String>,
|
dex_code: std::option::Option<std::string::String>,
|
||||||
target_event: std::option::Option<&str>,
|
target_event: std::option::Option<&str>,
|
||||||
|
target_instruction_name: std::option::Option<&str>,
|
||||||
|
target_discriminator_hex: std::option::Option<&str>,
|
||||||
) -> OnchainCandidateExtraction {
|
) -> OnchainCandidateExtraction {
|
||||||
let mut candidates = std::vec::Vec::new();
|
let mut candidates = std::vec::Vec::new();
|
||||||
let mut rejected_candidate_summary = std::vec::Vec::new();
|
let mut rejected_candidate_summary = std::vec::Vec::new();
|
||||||
@@ -1036,6 +1102,19 @@ fn extract_candidates_from_transaction(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !candidate_matches_instruction_filters(
|
||||||
|
&candidate,
|
||||||
|
target_instruction_name,
|
||||||
|
target_discriminator_hex,
|
||||||
|
) {
|
||||||
|
target_rejected_candidate_count += 1;
|
||||||
|
push_rejected_candidate_summary(
|
||||||
|
&mut rejected_candidate_summary,
|
||||||
|
&candidate,
|
||||||
|
"target_instruction_filter",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if candidates.iter().any(|existing| {
|
if candidates.iter().any(|existing| {
|
||||||
return candidate_identity_key(existing) == candidate_identity_key(&candidate);
|
return candidate_identity_key(existing) == candidate_identity_key(&candidate);
|
||||||
}) {
|
}) {
|
||||||
@@ -1144,6 +1223,9 @@ fn decode_raydium_clmm_candidate(
|
|||||||
inner_instruction_index: instruction.inner_instruction_index,
|
inner_instruction_index: instruction.inner_instruction_index,
|
||||||
instruction_name: Some("raydium_clmm.swap".to_string()),
|
instruction_name: Some("raydium_clmm.swap".to_string()),
|
||||||
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
||||||
|
instruction_discriminator_hex: instruction_discriminator_hex(
|
||||||
|
instruction.data.as_deref(),
|
||||||
|
),
|
||||||
pool_address: Some(event.pool_state.clone()),
|
pool_address: Some(event.pool_state.clone()),
|
||||||
token_a_mint: Some(event.base_mint),
|
token_a_mint: Some(event.base_mint),
|
||||||
token_b_mint: Some(event.quote_mint),
|
token_b_mint: Some(event.quote_mint),
|
||||||
@@ -1176,6 +1258,9 @@ fn decode_raydium_clmm_candidate(
|
|||||||
inner_instruction_index: instruction.inner_instruction_index,
|
inner_instruction_index: instruction.inner_instruction_index,
|
||||||
instruction_name: Some("raydium_clmm.swap_v2".to_string()),
|
instruction_name: Some("raydium_clmm.swap_v2".to_string()),
|
||||||
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
||||||
|
instruction_discriminator_hex: instruction_discriminator_hex(
|
||||||
|
instruction.data.as_deref(),
|
||||||
|
),
|
||||||
pool_address: Some(event.pool_state.clone()),
|
pool_address: Some(event.pool_state.clone()),
|
||||||
token_a_mint: Some(event.base_mint),
|
token_a_mint: Some(event.base_mint),
|
||||||
token_b_mint: Some(event.quote_mint),
|
token_b_mint: Some(event.quote_mint),
|
||||||
@@ -1257,9 +1342,31 @@ fn decode_raydium_cpmm_candidate(
|
|||||||
event.quote_mint,
|
event.quote_mint,
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
_ => return None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return None;
|
let raw_data = match decode_onchain_instruction_data(instruction.data.as_deref()) {
|
||||||
|
Some(raw_data) => raw_data,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
let instruction_kind = classify_demo3_raydium_cpmm_instruction(raw_data.as_slice());
|
||||||
|
if instruction_kind == Demo3RaydiumCpmmInstructionKind::Unknown
|
||||||
|
|| instruction_kind == Demo3RaydiumCpmmInstructionKind::SwapBaseInput
|
||||||
|
|| instruction_kind == Demo3RaydiumCpmmInstructionKind::SwapBaseOutput
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(build_raydium_cpmm_non_swap_candidate(
|
||||||
|
signature,
|
||||||
|
slot,
|
||||||
|
block_time,
|
||||||
|
failed,
|
||||||
|
program_id,
|
||||||
|
dex_code,
|
||||||
|
instruction,
|
||||||
|
logs,
|
||||||
|
instruction_kind,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_raydium_cpmm_candidate(
|
fn build_raydium_cpmm_candidate(
|
||||||
@@ -1289,6 +1396,7 @@ fn build_raydium_cpmm_candidate(
|
|||||||
inner_instruction_index: instruction.inner_instruction_index,
|
inner_instruction_index: instruction.inner_instruction_index,
|
||||||
instruction_name: Some(instruction_name.to_string()),
|
instruction_name: Some(instruction_name.to_string()),
|
||||||
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
||||||
|
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
|
||||||
pool_address: Some(pool_address.clone()),
|
pool_address: Some(pool_address.clone()),
|
||||||
token_a_mint: Some(token_a_mint),
|
token_a_mint: Some(token_a_mint),
|
||||||
token_b_mint: Some(token_b_mint),
|
token_b_mint: Some(token_b_mint),
|
||||||
@@ -1304,6 +1412,267 @@ fn build_raydium_cpmm_candidate(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum Demo3RaydiumCpmmInstructionKind {
|
||||||
|
Unknown,
|
||||||
|
ClosePermissionPda,
|
||||||
|
CollectCreatorFee,
|
||||||
|
CollectFundFee,
|
||||||
|
CollectProtocolFee,
|
||||||
|
CreateAmmConfig,
|
||||||
|
CreatePermissionPda,
|
||||||
|
Deposit,
|
||||||
|
Initialize,
|
||||||
|
InitializeWithPermission,
|
||||||
|
SwapBaseInput,
|
||||||
|
SwapBaseOutput,
|
||||||
|
UpdateAmmConfig,
|
||||||
|
UpdatePoolStatus,
|
||||||
|
Withdraw,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_raydium_cpmm_non_swap_candidate(
|
||||||
|
signature: &str,
|
||||||
|
slot: std::option::Option<u64>,
|
||||||
|
block_time: std::option::Option<i64>,
|
||||||
|
failed: bool,
|
||||||
|
program_id: &str,
|
||||||
|
dex_code: std::option::Option<std::string::String>,
|
||||||
|
instruction: &OnchainInstructionCandidate,
|
||||||
|
logs: &[std::string::String],
|
||||||
|
instruction_kind: Demo3RaydiumCpmmInstructionKind,
|
||||||
|
) -> crate::OnchainDexPairCandidateDto {
|
||||||
|
let pool_address =
|
||||||
|
demo3_raydium_cpmm_pool_account(instruction.accounts.as_slice(), instruction_kind);
|
||||||
|
let token_a_mint =
|
||||||
|
demo3_raydium_cpmm_token_a_mint(instruction.accounts.as_slice(), instruction_kind);
|
||||||
|
let token_b_mint =
|
||||||
|
demo3_raydium_cpmm_token_b_mint(instruction.accounts.as_slice(), instruction_kind);
|
||||||
|
let candidate_kind = demo3_raydium_cpmm_candidate_kind(instruction_kind).to_string();
|
||||||
|
let instruction_name = demo3_raydium_cpmm_instruction_name(instruction_kind).to_string();
|
||||||
|
let verified_pool_address = pool_address.clone();
|
||||||
|
let backfill_hint = demo3_raydium_cpmm_backfill_hint(
|
||||||
|
instruction_kind,
|
||||||
|
candidate_kind.as_str(),
|
||||||
|
pool_address.as_deref(),
|
||||||
|
signature,
|
||||||
|
);
|
||||||
|
return crate::OnchainDexPairCandidateDto {
|
||||||
|
signature: signature.to_string(),
|
||||||
|
slot,
|
||||||
|
block_time,
|
||||||
|
failed,
|
||||||
|
program_id: program_id.to_string(),
|
||||||
|
dex_code,
|
||||||
|
candidate_kind,
|
||||||
|
confidence: "high".to_string(),
|
||||||
|
instruction_index: instruction.instruction_index,
|
||||||
|
inner_instruction_index: instruction.inner_instruction_index,
|
||||||
|
instruction_name: Some(instruction_name),
|
||||||
|
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
||||||
|
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
|
||||||
|
pool_address,
|
||||||
|
token_a_mint,
|
||||||
|
token_b_mint,
|
||||||
|
verified_pool_address,
|
||||||
|
observed_token_mints: std::vec::Vec::new(),
|
||||||
|
token_balance_deltas: std::vec::Vec::new(),
|
||||||
|
candidate_pool_accounts: std::vec::Vec::new(),
|
||||||
|
candidate_token_vault_accounts: std::vec::Vec::new(),
|
||||||
|
candidate_program_accounts: std::vec::Vec::new(),
|
||||||
|
account_samples: sample_strings(instruction.accounts.as_slice(), 12),
|
||||||
|
log_samples: sample_logs(logs, 8),
|
||||||
|
backfill_hint,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_demo3_raydium_cpmm_instruction(data: &[u8]) -> Demo3RaydiumCpmmInstructionKind {
|
||||||
|
if data.len() < 8 {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::Unknown;
|
||||||
|
}
|
||||||
|
let discriminator = &data[0..8];
|
||||||
|
if discriminator == [156, 84, 32, 118, 69, 135, 70, 123] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::ClosePermissionPda;
|
||||||
|
}
|
||||||
|
if discriminator == [20, 22, 86, 123, 198, 28, 219, 132] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::CollectCreatorFee;
|
||||||
|
}
|
||||||
|
if discriminator == [167, 138, 78, 149, 223, 194, 6, 126] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::CollectFundFee;
|
||||||
|
}
|
||||||
|
if discriminator == [136, 136, 252, 221, 194, 66, 126, 89] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::CollectProtocolFee;
|
||||||
|
}
|
||||||
|
if discriminator == [137, 52, 237, 212, 215, 117, 108, 104] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::CreateAmmConfig;
|
||||||
|
}
|
||||||
|
if discriminator == [135, 136, 2, 216, 137, 169, 181, 202] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::CreatePermissionPda;
|
||||||
|
}
|
||||||
|
if discriminator == [242, 35, 198, 137, 82, 225, 242, 182] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::Deposit;
|
||||||
|
}
|
||||||
|
if discriminator == [175, 175, 109, 31, 13, 152, 155, 237] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::Initialize;
|
||||||
|
}
|
||||||
|
if discriminator == [63, 55, 254, 65, 49, 178, 89, 121] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::InitializeWithPermission;
|
||||||
|
}
|
||||||
|
if discriminator == [143, 190, 90, 218, 196, 30, 51, 222] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::SwapBaseInput;
|
||||||
|
}
|
||||||
|
if discriminator == [55, 217, 98, 86, 163, 74, 180, 173] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::SwapBaseOutput;
|
||||||
|
}
|
||||||
|
if discriminator == [49, 60, 174, 136, 154, 28, 116, 200] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::UpdateAmmConfig;
|
||||||
|
}
|
||||||
|
if discriminator == [130, 87, 108, 6, 46, 224, 117, 123] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus;
|
||||||
|
}
|
||||||
|
if discriminator == [183, 18, 70, 156, 148, 109, 161, 34] {
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::Withdraw;
|
||||||
|
}
|
||||||
|
return Demo3RaydiumCpmmInstructionKind::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo3_raydium_cpmm_candidate_kind(
|
||||||
|
instruction_kind: Demo3RaydiumCpmmInstructionKind,
|
||||||
|
) -> &'static str {
|
||||||
|
match instruction_kind {
|
||||||
|
Demo3RaydiumCpmmInstructionKind::ClosePermissionPda => return "pool_admin",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => return "claim_fee",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectFundFee => return "claim_fee",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => return "claim_fee",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CreateAmmConfig => return "pool_admin",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CreatePermissionPda => return "pool_admin",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Deposit => return "add_liquidity",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Initialize => return "create_pool",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => return "create_pool",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::UpdateAmmConfig => return "pool_admin",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus => return "pool_admin",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Withdraw => return "remove_liquidity",
|
||||||
|
_ => return "unclassified_instruction",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo3_raydium_cpmm_instruction_name(
|
||||||
|
instruction_kind: Demo3RaydiumCpmmInstructionKind,
|
||||||
|
) -> &'static str {
|
||||||
|
match instruction_kind {
|
||||||
|
Demo3RaydiumCpmmInstructionKind::ClosePermissionPda => {
|
||||||
|
return "raydium_cpmm.close_permission_pda";
|
||||||
|
},
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => {
|
||||||
|
return "raydium_cpmm.collect_creator_fee";
|
||||||
|
},
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectFundFee => return "raydium_cpmm.collect_fund_fee",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => {
|
||||||
|
return "raydium_cpmm.collect_protocol_fee";
|
||||||
|
},
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CreateAmmConfig => return "raydium_cpmm.create_amm_config",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CreatePermissionPda => {
|
||||||
|
return "raydium_cpmm.create_permission_pda";
|
||||||
|
},
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Deposit => return "raydium_cpmm.deposit",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Initialize => return "raydium_cpmm.initialize",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => {
|
||||||
|
return "raydium_cpmm.initialize_with_permission";
|
||||||
|
},
|
||||||
|
Demo3RaydiumCpmmInstructionKind::UpdateAmmConfig => return "raydium_cpmm.update_amm_config",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus => {
|
||||||
|
return "raydium_cpmm.update_pool_status";
|
||||||
|
},
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Withdraw => return "raydium_cpmm.withdraw",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::SwapBaseInput => return "raydium_cpmm.swap_base_input",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::SwapBaseOutput => return "raydium_cpmm.swap_base_output",
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Unknown => return "raydium_cpmm.unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo3_raydium_cpmm_pool_account(
|
||||||
|
accounts: &[std::string::String],
|
||||||
|
instruction_kind: Demo3RaydiumCpmmInstructionKind,
|
||||||
|
) -> std::option::Option<std::string::String> {
|
||||||
|
let index = match instruction_kind {
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => Some(2usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectFundFee => Some(2usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => Some(2usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Deposit => Some(2usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Initialize => Some(3usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => Some(4usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus => Some(1usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Withdraw => Some(2usize),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let index = match index {
|
||||||
|
Some(index) => index,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
return demo3_account_at(accounts, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo3_raydium_cpmm_token_a_mint(
|
||||||
|
accounts: &[std::string::String],
|
||||||
|
instruction_kind: Demo3RaydiumCpmmInstructionKind,
|
||||||
|
) -> std::option::Option<std::string::String> {
|
||||||
|
let index = match instruction_kind {
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => Some(6usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectFundFee => Some(6usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => Some(6usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Deposit => Some(10usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Initialize => Some(4usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => Some(5usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Withdraw => Some(10usize),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let index = match index {
|
||||||
|
Some(index) => index,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
return demo3_account_at(accounts, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo3_raydium_cpmm_token_b_mint(
|
||||||
|
accounts: &[std::string::String],
|
||||||
|
instruction_kind: Demo3RaydiumCpmmInstructionKind,
|
||||||
|
) -> std::option::Option<std::string::String> {
|
||||||
|
let index = match instruction_kind {
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => Some(7usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectFundFee => Some(7usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => Some(7usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Deposit => Some(11usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Initialize => Some(5usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => Some(6usize),
|
||||||
|
Demo3RaydiumCpmmInstructionKind::Withdraw => Some(11usize),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let index = match index {
|
||||||
|
Some(index) => index,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
return demo3_account_at(accounts, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo3_raydium_cpmm_backfill_hint(
|
||||||
|
instruction_kind: Demo3RaydiumCpmmInstructionKind,
|
||||||
|
candidate_kind: &str,
|
||||||
|
pool_address: std::option::Option<&str>,
|
||||||
|
signature: &str,
|
||||||
|
) -> std::string::String {
|
||||||
|
let instruction_name = demo3_raydium_cpmm_instruction_name(instruction_kind);
|
||||||
|
if let Some(pool_address) = pool_address {
|
||||||
|
return format!(
|
||||||
|
"Raydium CPMM {} candidate '{}'; backfill pool in Demo Pipeline 2: {} ; signature: {}",
|
||||||
|
candidate_kind, instruction_name, pool_address, signature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return format!(
|
||||||
|
"Raydium CPMM {} candidate '{}'; inspect/backfill transaction signature: {}",
|
||||||
|
candidate_kind, instruction_name, signature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn decode_meteora_damm_v1_candidate(
|
fn decode_meteora_damm_v1_candidate(
|
||||||
signature: &str,
|
signature: &str,
|
||||||
slot: std::option::Option<u64>,
|
slot: std::option::Option<u64>,
|
||||||
@@ -1345,6 +1714,7 @@ fn decode_meteora_damm_v1_candidate(
|
|||||||
inner_instruction_index: instruction.inner_instruction_index,
|
inner_instruction_index: instruction.inner_instruction_index,
|
||||||
instruction_name: Some(instruction_name),
|
instruction_name: Some(instruction_name),
|
||||||
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
||||||
|
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
|
||||||
pool_address,
|
pool_address,
|
||||||
token_a_mint,
|
token_a_mint,
|
||||||
token_b_mint,
|
token_b_mint,
|
||||||
@@ -1616,6 +1986,7 @@ fn build_heuristic_candidate(
|
|||||||
inner_instruction_index: instruction.inner_instruction_index,
|
inner_instruction_index: instruction.inner_instruction_index,
|
||||||
instruction_name,
|
instruction_name,
|
||||||
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
|
||||||
|
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
|
||||||
pool_address: pool_address.clone(),
|
pool_address: pool_address.clone(),
|
||||||
token_a_mint,
|
token_a_mint,
|
||||||
token_b_mint,
|
token_b_mint,
|
||||||
@@ -2789,8 +3160,21 @@ fn target_event_prefers_instruction_local_classification(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn target_event_keeps_mixed_swap_transactions(target_event: std::option::Option<&str>) -> bool {
|
fn target_event_keeps_mixed_swap_transactions(
|
||||||
return !split_target_event_filter(target_event).is_empty();
|
target_event: std::option::Option<&str>,
|
||||||
|
target_instruction_name: std::option::Option<&str>,
|
||||||
|
target_discriminator_hex: std::option::Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
if !split_target_event_filter(target_event).is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if !split_comma_filter(target_instruction_name).is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if !split_comma_filter(target_discriminator_hex).is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn instruction_data_prefix(
|
fn instruction_data_prefix(
|
||||||
@@ -2813,6 +3197,23 @@ fn instruction_data_prefix(
|
|||||||
return Some(prefix);
|
return Some(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn instruction_discriminator_hex(
|
||||||
|
data: std::option::Option<&str>,
|
||||||
|
) -> std::option::Option<std::string::String> {
|
||||||
|
let decoded = match decode_onchain_instruction_data(data) {
|
||||||
|
Some(decoded) => decoded,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
if decoded.len() < 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut text = std::string::String::new();
|
||||||
|
for byte in &decoded[0..8] {
|
||||||
|
text.push_str(format!("{:02x}", byte).as_str());
|
||||||
|
}
|
||||||
|
return Some(text);
|
||||||
|
}
|
||||||
|
|
||||||
fn text_matches_non_swap_target(lower: &str) -> bool {
|
fn text_matches_non_swap_target(lower: &str) -> bool {
|
||||||
return text_matches_pool_admin(lower)
|
return text_matches_pool_admin(lower)
|
||||||
|| text_matches_claim_reward(lower)
|
|| text_matches_claim_reward(lower)
|
||||||
@@ -3016,6 +3417,77 @@ fn first_matching_target_event(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn candidate_matches_instruction_filters(
|
||||||
|
candidate: &crate::OnchainDexPairCandidateDto,
|
||||||
|
target_instruction_name: std::option::Option<&str>,
|
||||||
|
target_discriminator_hex: std::option::Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
if !candidate_matches_instruction_name_filter(candidate, target_instruction_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !candidate_matches_discriminator_filter(candidate, target_discriminator_hex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate_matches_instruction_name_filter(
|
||||||
|
candidate: &crate::OnchainDexPairCandidateDto,
|
||||||
|
target_instruction_name: std::option::Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
let targets = split_comma_filter(target_instruction_name);
|
||||||
|
if targets.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let instruction_name = match &candidate.instruction_name {
|
||||||
|
Some(instruction_name) => instruction_name.to_ascii_lowercase(),
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
for target in targets {
|
||||||
|
if instruction_name == target || instruction_name.ends_with(format!(".{}", target).as_str())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate_matches_discriminator_filter(
|
||||||
|
candidate: &crate::OnchainDexPairCandidateDto,
|
||||||
|
target_discriminator_hex: std::option::Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
let targets = split_comma_filter(target_discriminator_hex);
|
||||||
|
if targets.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let discriminator_hex = match &candidate.instruction_discriminator_hex {
|
||||||
|
Some(discriminator_hex) => discriminator_hex.to_ascii_lowercase(),
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
for target in targets {
|
||||||
|
if discriminator_hex == target {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_comma_filter(value: std::option::Option<&str>) -> std::vec::Vec<std::string::String> {
|
||||||
|
let value = match value {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return std::vec::Vec::new(),
|
||||||
|
};
|
||||||
|
let mut output = std::vec::Vec::new();
|
||||||
|
for token in value.split(',') {
|
||||||
|
let token = token.trim();
|
||||||
|
if token.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push_unique_string(&mut output, token.to_string());
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
fn candidate_matches_target_event(
|
fn candidate_matches_target_event(
|
||||||
candidate: &crate::OnchainDexPairCandidateDto,
|
candidate: &crate::OnchainDexPairCandidateDto,
|
||||||
target_event: std::option::Option<&str>,
|
target_event: std::option::Option<&str>,
|
||||||
@@ -3027,10 +3499,10 @@ fn candidate_matches_single_target_event(
|
|||||||
candidate: &crate::OnchainDexPairCandidateDto,
|
candidate: &crate::OnchainDexPairCandidateDto,
|
||||||
target_event: &str,
|
target_event: &str,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if target_event == "unknown_non_swap"
|
if target_event == "unknown_non_swap" {
|
||||||
|| target_event == "audit_non_swap_like"
|
return candidate_is_unknown_non_swap_candidate(candidate);
|
||||||
|| target_event == "non_swap"
|
}
|
||||||
{
|
if target_event == "audit_non_swap_like" || target_event == "non_swap" {
|
||||||
return candidate_is_non_swap_audit_candidate(candidate);
|
return candidate_is_non_swap_audit_candidate(candidate);
|
||||||
}
|
}
|
||||||
if target_event == "unclassified_instruction" {
|
if target_event == "unclassified_instruction" {
|
||||||
@@ -3180,6 +3652,24 @@ fn candidate_kind_is_explicit_surface(candidate_kind: &str) -> bool {
|
|||||||
|| candidate_kind == "close_market";
|
|| candidate_kind == "close_market";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn candidate_is_unknown_non_swap_candidate(candidate: &crate::OnchainDexPairCandidateDto) -> bool {
|
||||||
|
if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if candidate_kind_is_explicit_surface(candidate.candidate_kind.as_str()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if candidate.candidate_kind == "unclassified_instruction"
|
||||||
|
|| candidate.candidate_kind == "program_activity"
|
||||||
|
|| candidate.candidate_kind == "liquidity_or_position"
|
||||||
|
|| candidate.candidate_kind == "non_swap_activity"
|
||||||
|
|| candidate.candidate_kind == "upstream_git_instruction"
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return candidate.instruction_data_prefix.is_some() && candidate.instruction_name.is_none();
|
||||||
|
}
|
||||||
|
|
||||||
fn candidate_is_non_swap_audit_candidate(candidate: &crate::OnchainDexPairCandidateDto) -> bool {
|
fn candidate_is_non_swap_audit_candidate(candidate: &crate::OnchainDexPairCandidateDto) -> bool {
|
||||||
if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) {
|
if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) {
|
||||||
return false;
|
return false;
|
||||||
@@ -3659,6 +4149,8 @@ mod tests {
|
|||||||
max_pages: 1,
|
max_pages: 1,
|
||||||
scan_order: None,
|
scan_order: None,
|
||||||
target_event: None,
|
target_event: None,
|
||||||
|
target_instruction_name: None,
|
||||||
|
target_discriminator_hex: None,
|
||||||
exclude_swaps: false,
|
exclude_swaps: false,
|
||||||
include_failed: true,
|
include_failed: true,
|
||||||
http_role: "history_backfill".to_string(),
|
http_role: "history_backfill".to_string(),
|
||||||
@@ -3689,6 +4181,8 @@ mod tests {
|
|||||||
max_pages: 1,
|
max_pages: 1,
|
||||||
scan_order: None,
|
scan_order: None,
|
||||||
target_event: None,
|
target_event: None,
|
||||||
|
target_instruction_name: None,
|
||||||
|
target_discriminator_hex: None,
|
||||||
exclude_swaps: false,
|
exclude_swaps: false,
|
||||||
include_failed: true,
|
include_failed: true,
|
||||||
http_role: "history_backfill".to_string(),
|
http_role: "history_backfill".to_string(),
|
||||||
@@ -3713,6 +4207,8 @@ mod tests {
|
|||||||
max_pages: 1,
|
max_pages: 1,
|
||||||
scan_order: None,
|
scan_order: None,
|
||||||
target_event: None,
|
target_event: None,
|
||||||
|
target_instruction_name: None,
|
||||||
|
target_discriminator_hex: None,
|
||||||
exclude_swaps: false,
|
exclude_swaps: false,
|
||||||
include_failed: true,
|
include_failed: true,
|
||||||
http_role: "history_backfill".to_string(),
|
http_role: "history_backfill".to_string(),
|
||||||
@@ -3762,11 +4258,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let extraction = super::extract_candidates_from_transaction(
|
let extraction = super::extract_candidates_from_transaction(
|
||||||
"sig-openbook-v2-raw",
|
"sig-openbook_v2-raw",
|
||||||
&transaction,
|
&transaction,
|
||||||
crate::OPENBOOK_V2_PROGRAM_ID,
|
crate::OPENBOOK_V2_PROGRAM_ID,
|
||||||
Some("openbook_v2".to_string()),
|
Some("openbook_v2".to_string()),
|
||||||
Some("order_place"),
|
Some("order_place"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
assert_eq!(extraction.extracted_candidate_count, 1);
|
assert_eq!(extraction.extracted_candidate_count, 1);
|
||||||
assert_eq!(extraction.target_rejected_candidate_count, 0);
|
assert_eq!(extraction.target_rejected_candidate_count, 0);
|
||||||
@@ -3833,6 +4331,7 @@ mod tests {
|
|||||||
inner_instruction_index: Some(2),
|
inner_instruction_index: Some(2),
|
||||||
instruction_name: None,
|
instruction_name: None,
|
||||||
instruction_data_prefix: Some("EVM9wLnauu9H41Gf".to_string()),
|
instruction_data_prefix: Some("EVM9wLnauu9H41Gf".to_string()),
|
||||||
|
instruction_discriminator_hex: None,
|
||||||
pool_address: None,
|
pool_address: None,
|
||||||
token_a_mint: None,
|
token_a_mint: None,
|
||||||
token_b_mint: None,
|
token_b_mint: None,
|
||||||
@@ -3987,6 +4486,8 @@ mod tests {
|
|||||||
max_pages: 2,
|
max_pages: 2,
|
||||||
scan_order: Some("oldest_first".to_string()),
|
scan_order: Some("oldest_first".to_string()),
|
||||||
target_event: Some("claim_fee,remove_liquidity".to_string()),
|
target_event: Some("claim_fee,remove_liquidity".to_string()),
|
||||||
|
target_instruction_name: None,
|
||||||
|
target_discriminator_hex: None,
|
||||||
exclude_swaps: true,
|
exclude_swaps: true,
|
||||||
include_failed: true,
|
include_failed: true,
|
||||||
http_role: "history_backfill".to_string(),
|
http_role: "history_backfill".to_string(),
|
||||||
@@ -4036,6 +4537,7 @@ mod tests {
|
|||||||
inner_instruction_index: None,
|
inner_instruction_index: None,
|
||||||
instruction_name: Some(instruction_name.to_string()),
|
instruction_name: Some(instruction_name.to_string()),
|
||||||
instruction_data_prefix: Some("prefix".to_string()),
|
instruction_data_prefix: Some("prefix".to_string()),
|
||||||
|
instruction_discriminator_hex: None,
|
||||||
pool_address: None,
|
pool_address: None,
|
||||||
token_a_mint: None,
|
token_a_mint: None,
|
||||||
token_b_mint: None,
|
token_b_mint: None,
|
||||||
@@ -4076,7 +4578,41 @@ mod tests {
|
|||||||
fn target_filter_accepts_open_orders_close_candidates() {
|
fn target_filter_accepts_open_orders_close_candidates() {
|
||||||
let candidate = make_candidate("open_orders_close", "CloseOpenOrdersAccount");
|
let candidate = make_candidate("open_orders_close", "CloseOpenOrdersAccount");
|
||||||
assert!(super::candidate_matches_target_event(&candidate, Some("open_orders_close")));
|
assert!(super::candidate_matches_target_event(&candidate, Some("open_orders_close")));
|
||||||
assert!(super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
|
assert!(!super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
|
||||||
|
assert!(super::candidate_matches_target_event(&candidate, Some("audit_non_swap_like")));
|
||||||
assert!(!super::candidate_matches_target_event(&candidate, Some("close_market")));
|
assert!(!super::candidate_matches_target_event(&candidate, Some("close_market")));
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn target_filter_rejects_known_surface_for_unknown_non_swap() {
|
||||||
|
let candidate = make_candidate("claim_fee", "CollectProtocolFee");
|
||||||
|
assert!(!super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
|
||||||
|
assert!(super::candidate_matches_target_event(&candidate, Some("audit_non_swap_like")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn target_filter_accepts_unclassified_instruction_for_unknown_non_swap() {
|
||||||
|
let candidate = make_candidate("unclassified_instruction", "UnknownInstruction");
|
||||||
|
assert!(super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn target_instruction_filters_match_name_and_discriminator() {
|
||||||
|
let mut candidate = make_candidate("remove_liquidity", "raydium_cpmm.withdraw");
|
||||||
|
candidate.instruction_discriminator_hex = Some("b712469c946da122".to_string());
|
||||||
|
assert!(super::candidate_matches_instruction_filters(
|
||||||
|
&candidate,
|
||||||
|
Some("withdraw"),
|
||||||
|
Some("b712469c946da122")
|
||||||
|
));
|
||||||
|
assert!(!super::candidate_matches_instruction_filters(
|
||||||
|
&candidate,
|
||||||
|
Some("deposit"),
|
||||||
|
Some("b712469c946da122")
|
||||||
|
));
|
||||||
|
assert!(!super::candidate_matches_instruction_filters(
|
||||||
|
&candidate,
|
||||||
|
Some("withdraw"),
|
||||||
|
Some("f223c68952e1f2b6")
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ mod tests {
|
|||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
||||||
"program": "meteora-dbc",
|
"program": "meteora_dbc",
|
||||||
"stackHeight": 1,
|
"stackHeight": 1,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
"DbcOriginPool111",
|
"DbcOriginPool111",
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ impl TokenBackfillService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.backfill_missing_token_metadata_best_effort(100).await;
|
self.backfill_missing_token_metadata_best_effort(100).await;
|
||||||
|
self.refresh_event_coverage_best_effort().await;
|
||||||
let summary_payload = serde_json::json!({
|
let summary_payload = serde_json::json!({
|
||||||
"tokenMint": result.token_mint,
|
"tokenMint": result.token_mint,
|
||||||
"mintSignatureCount": result.mint_signature_count,
|
"mintSignatureCount": result.mint_signature_count,
|
||||||
@@ -572,6 +573,26 @@ impl TokenBackfillService {
|
|||||||
if let Err(error) = transaction_classification_result {
|
if let Err(error) = transaction_classification_result {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
let instruction_observation_index =
|
||||||
|
crate::InstructionObservationIndexService::new(self.database.clone());
|
||||||
|
let instruction_observation_result =
|
||||||
|
instruction_observation_index.refresh_signature(signature.as_str()).await;
|
||||||
|
match instruction_observation_result {
|
||||||
|
Ok(index_result) => {
|
||||||
|
tracing::debug!(
|
||||||
|
signature = %signature,
|
||||||
|
upserted_observation_count = index_result.upserted_observation_count,
|
||||||
|
"instruction observation index refreshed after signature replay"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
signature = %signature,
|
||||||
|
error = %error,
|
||||||
|
"instruction observation index refresh failed after signature replay"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
return Ok(TokenBackfillSignatureResult {
|
return Ok(TokenBackfillSignatureResult {
|
||||||
resolved_transaction_count: 1,
|
resolved_transaction_count: 1,
|
||||||
missing_transaction_count: 0,
|
missing_transaction_count: 0,
|
||||||
@@ -715,6 +736,7 @@ impl TokenBackfillService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.backfill_missing_token_metadata_best_effort(100).await;
|
self.backfill_missing_token_metadata_best_effort(100).await;
|
||||||
|
self.refresh_event_coverage_best_effort().await;
|
||||||
let summary_payload = serde_json::json!({
|
let summary_payload = serde_json::json!({
|
||||||
"poolAddress": result.pool_address,
|
"poolAddress": result.pool_address,
|
||||||
"poolSignatureCount": result.pool_signature_count,
|
"poolSignatureCount": result.pool_signature_count,
|
||||||
@@ -785,6 +807,7 @@ impl TokenBackfillService {
|
|||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
self.backfill_missing_token_metadata_best_effort(100).await;
|
self.backfill_missing_token_metadata_best_effort(100).await;
|
||||||
|
self.refresh_event_coverage_best_effort().await;
|
||||||
let result = crate::SignatureBackfillResult {
|
let result = crate::SignatureBackfillResult {
|
||||||
signature: trimmed_signature.clone(),
|
signature: trimmed_signature.clone(),
|
||||||
resolved_transaction_count: replay.resolved_transaction_count,
|
resolved_transaction_count: replay.resolved_transaction_count,
|
||||||
@@ -918,6 +941,26 @@ impl TokenBackfillService {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn refresh_event_coverage_best_effort(&self) {
|
||||||
|
let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
|
||||||
|
let refresh_result = coverage_service.refresh_local_counts(None).await;
|
||||||
|
match refresh_result {
|
||||||
|
Ok(refresh_result) => {
|
||||||
|
tracing::debug!(
|
||||||
|
upserted_entry_count = refresh_result.upserted_entry_count,
|
||||||
|
summary_count = refresh_result.summaries.len(),
|
||||||
|
"dex event coverage refreshed after historical replay"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
error = %error,
|
||||||
|
"dex event coverage refresh failed after historical replay"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn token_backfill_should_retry_http_error(error: &crate::Error) -> bool {
|
fn token_backfill_should_retry_http_error(error: &crate::Error) -> bool {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ mod tests {
|
|||||||
fn service_search_preserves_normalized_request() {
|
fn service_search_preserves_normalized_request() {
|
||||||
let service = crate::UpstreamRegistryService::new();
|
let service = crate::UpstreamRegistryService::new();
|
||||||
let request = crate::UpstreamRegistrySearchRequestDto {
|
let request = crate::UpstreamRegistrySearchRequestDto {
|
||||||
decoder_code: Some("raydium-cpmm".to_string()),
|
decoder_code: Some("raydium_cpmm".to_string()),
|
||||||
program_id: None,
|
program_id: None,
|
||||||
program_family: None,
|
program_family: None,
|
||||||
surface_kind: None,
|
surface_kind: None,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -280,7 +280,7 @@ mod tests {
|
|||||||
for (entry_name, discriminator_hex) in expected {
|
for (entry_name, discriminator_hex) in expected {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
for entry in all_entries.as_slice() {
|
for entry in all_entries.as_slice() {
|
||||||
if entry.decoder_code == "openbook-v2"
|
if entry.decoder_code == "openbook_v2"
|
||||||
&& entry.program_id.as_deref() == Some(crate::OPENBOOK_V2_PROGRAM_ID)
|
&& entry.program_id.as_deref() == Some(crate::OPENBOOK_V2_PROGRAM_ID)
|
||||||
&& entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION
|
&& entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION
|
||||||
&& entry.entry_name == entry_name
|
&& entry.entry_name == entry_name
|
||||||
@@ -312,7 +312,7 @@ mod tests {
|
|||||||
Some(matched) => matched,
|
Some(matched) => matched,
|
||||||
None => panic!("OpenBook v2 place_take_order discriminator must match"),
|
None => panic!("OpenBook v2 place_take_order discriminator must match"),
|
||||||
};
|
};
|
||||||
assert_eq!(matched.decoder_code, "openbook-v2".to_string());
|
assert_eq!(matched.decoder_code, "openbook_v2".to_string());
|
||||||
assert_eq!(matched.entry_name, "place_take_order".to_string());
|
assert_eq!(matched.entry_name, "place_take_order".to_string());
|
||||||
assert_eq!(matched.discriminator_hex, Some("032c47031ac7cb55".to_string()));
|
assert_eq!(matched.discriminator_hex, Some("032c47031ac7cb55".to_string()));
|
||||||
}
|
}
|
||||||
@@ -320,65 +320,65 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn registry_contains_priority_family_program_seeds() {
|
fn registry_contains_priority_family_program_seeds() {
|
||||||
let expected_codes = [
|
let expected_codes = [
|
||||||
"meteora-damm-v2",
|
"meteora_damm_v2",
|
||||||
"meteora-dbc",
|
"meteora_dbc",
|
||||||
"meteora-dlmm",
|
"meteora_dlmm",
|
||||||
"meteora-vault",
|
"meteora_vault",
|
||||||
"raydium-amm-v4",
|
"raydium_amm_v4",
|
||||||
"raydium-clmm",
|
"raydium_clmm",
|
||||||
"raydium-cpmm",
|
"raydium_cpmm",
|
||||||
"raydium-launchpad",
|
"raydium_launchlab",
|
||||||
"raydium-liquidity-locking",
|
"raydium_liquidity_locking",
|
||||||
"raydium-stable-swap",
|
"raydium_stable_swap",
|
||||||
"orca-whirlpool",
|
"orca_whirlpools",
|
||||||
"fluxbeam",
|
"fluxbeam",
|
||||||
"lifinity-amm-v2",
|
"lifinity_v2",
|
||||||
"phoenix-v1",
|
"phoenix_v1",
|
||||||
"openbook-v2",
|
"openbook_v2",
|
||||||
"stabble-stable-swap",
|
"stabble_stable_swap",
|
||||||
"stabble-weighted-swap",
|
"stabble_weighted_swap",
|
||||||
"bonkswap",
|
"bonkswap",
|
||||||
"boop",
|
"boop_fun",
|
||||||
"moonshot",
|
"moonshot",
|
||||||
"heaven",
|
"heaven",
|
||||||
"okx-dex",
|
"okx_dex",
|
||||||
"pancake-swap",
|
"pancake_swap",
|
||||||
"vertigo",
|
"vertigo",
|
||||||
"virtuals",
|
"virtuals",
|
||||||
"wavebreak",
|
"wavebreak",
|
||||||
"onchain-labs-dex-v1",
|
"onchain_labs_dex_v1",
|
||||||
"onchain-labs-dex-v2",
|
"onchain_labs_dex_v2",
|
||||||
"jupiter-swap",
|
"jupiter_swap",
|
||||||
"jupiter-dca",
|
"jupiter_dca",
|
||||||
"jupiter-limit-order",
|
"jupiter_limit_order",
|
||||||
"jupiter-limit-order-2",
|
"jupiter_limit_order_2",
|
||||||
"jupiter-perpetuals",
|
"jupiter_perpetuals",
|
||||||
"jupiter-lend",
|
"jupiter_lend",
|
||||||
"kamino-lending",
|
"kamino_lending",
|
||||||
"kamino-vault",
|
"kamino_vault",
|
||||||
"kamino-farms",
|
"kamino_farms",
|
||||||
"kamino-limit-order",
|
"kamino_limit_order",
|
||||||
"drift-v2",
|
"drift_v2",
|
||||||
"marginfi-v2",
|
"marginfi_v2",
|
||||||
"dflow-aggregator-v4",
|
"dflow_aggregator_v4",
|
||||||
"zeta",
|
"zeta",
|
||||||
"system-program",
|
"system_program",
|
||||||
"token-program",
|
"token_program",
|
||||||
"token-2022",
|
"token_2022",
|
||||||
"associated-token-account",
|
"associated_token_account",
|
||||||
"address-lookup-table",
|
"address_lookup_table",
|
||||||
"memo-program",
|
"memo_program",
|
||||||
"stake-program",
|
"stake_program",
|
||||||
"mpl-token-metadata",
|
"mpl_token_metadata",
|
||||||
"mpl-core",
|
"mpl_core",
|
||||||
"bubblegum",
|
"bubblegum",
|
||||||
"name-service",
|
"name_service",
|
||||||
"marinade-finance",
|
"marinade_finance",
|
||||||
"solayer-restaking-program",
|
"solayer_restaking_program",
|
||||||
"swig",
|
"swig",
|
||||||
"sharky",
|
"sharky",
|
||||||
"circle-message-transmitter-v2",
|
"circle_message_transmitter_v2",
|
||||||
"circle-token-messenger-v2",
|
"circle_token_messenger_v2",
|
||||||
];
|
];
|
||||||
let all_entries = crate::upstream_registry_match::upstream_registry_all_entries();
|
let all_entries = crate::upstream_registry_match::upstream_registry_all_entries();
|
||||||
for expected_code in expected_codes {
|
for expected_code in expected_codes {
|
||||||
@@ -527,7 +527,7 @@ mod tests {
|
|||||||
for expected_entry in expected_entries {
|
for expected_entry in expected_entries {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
for entry in all_entries.as_slice() {
|
for entry in all_entries.as_slice() {
|
||||||
if entry.decoder_code == "meteora-damm-v2"
|
if entry.decoder_code == "meteora_damm_v2"
|
||||||
&& entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION
|
&& entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION
|
||||||
&& entry.entry_name == expected_entry.0
|
&& entry.entry_name == expected_entry.0
|
||||||
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1)
|
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1)
|
||||||
@@ -555,7 +555,7 @@ mod tests {
|
|||||||
for expected_entry in expected_entries {
|
for expected_entry in expected_entries {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
for entry in all_entries.as_slice() {
|
for entry in all_entries.as_slice() {
|
||||||
if entry.decoder_code == "meteora-damm-v2"
|
if entry.decoder_code == "meteora_damm_v2"
|
||||||
&& entry.entry_kind == crate::ENTRY_KIND_EVENT
|
&& entry.entry_kind == crate::ENTRY_KIND_EVENT
|
||||||
&& entry.entry_name == expected_entry.0
|
&& entry.entry_name == expected_entry.0
|
||||||
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1)
|
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1)
|
||||||
@@ -581,9 +581,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let matched = match matched {
|
let matched = match matched {
|
||||||
Some(matched) => matched,
|
Some(matched) => matched,
|
||||||
None => panic!("missing meteora-damm-v2 add_liquidity registry match"),
|
None => panic!("missing meteora_damm_v2 add_liquidity registry match"),
|
||||||
};
|
};
|
||||||
assert_eq!(matched.decoder_code, "meteora-damm-v2");
|
assert_eq!(matched.decoder_code, "meteora_damm_v2");
|
||||||
assert_eq!(matched.entry_name, "add_liquidity");
|
assert_eq!(matched.entry_name, "add_liquidity");
|
||||||
assert_eq!(matched.discriminator_hex.as_deref(), Some("b59d59438fb63448"));
|
assert_eq!(matched.discriminator_hex.as_deref(), Some("b59d59438fb63448"));
|
||||||
assert_eq!(matched.proof_status, crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED);
|
assert_eq!(matched.proof_status, crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED);
|
||||||
@@ -599,9 +599,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let matched = match matched {
|
let matched = match matched {
|
||||||
Some(matched) => matched,
|
Some(matched) => matched,
|
||||||
None => panic!("missing phoenix-v1 swap registry match"),
|
None => panic!("missing phoenix_v1 swap registry match"),
|
||||||
};
|
};
|
||||||
assert_eq!(matched.decoder_code, "phoenix-v1");
|
assert_eq!(matched.decoder_code, "phoenix_v1");
|
||||||
assert_eq!(matched.entry_name, "swap");
|
assert_eq!(matched.entry_name, "swap");
|
||||||
assert_eq!(matched.discriminator_hex.as_deref(), Some("00"));
|
assert_eq!(matched.discriminator_hex.as_deref(), Some("00"));
|
||||||
assert_eq!(matched.discriminator_len, Some(1));
|
assert_eq!(matched.discriminator_len, Some(1));
|
||||||
@@ -621,7 +621,7 @@ mod tests {
|
|||||||
let result = crate::upstream_registry_match::upstream_registry_search(&request);
|
let result = crate::upstream_registry_match::upstream_registry_search(&request);
|
||||||
assert!(result.entries.len() >= 2);
|
assert!(result.entries.len() >= 2);
|
||||||
for entry in result.entries.as_slice() {
|
for entry in result.entries.as_slice() {
|
||||||
assert_eq!(entry.decoder_code, "raydium-cpmm");
|
assert_eq!(entry.decoder_code, "raydium_cpmm");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user