From 58f9b369699580ad3817472bec682a08a923a32c Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Fri, 19 Jun 2026 10:29:17 +0200 Subject: [PATCH] 0.7.57 --- CHANGELOG.md | 1 + Cargo.toml | 19 +- README.md | 97 +- ROADMAP.md | 459 ++- docs/DEX_DECODER_MATRIX.md | 22 +- docs/DEX_EVENT_COVERAGE_MATRIX.md | 33 +- docs/VALIDATION_STATUS_0_7_57_FINAL.md | 73 + ..._0_7_58_DEMO4_PROGRAM_SURFACE_DISCOVERY.md | 245 ++ ..._0_7_58_demo4_program_surface_discovery.md | 245 ++ ..._59_SQLITE_DB_TRANSACTION_MERGER_BINARY.md | 249 ++ ..._59_sqlite_db_transaction_merger_binary.md | 249 ++ .../PROMPT_0_7_60_METEORA_DAMM_NEXT_DEX.md | 198 ++ .../PROMPT_0_7_60_meteora_damm_next_dex.md | 198 ++ .../METEORA_DLMM_EVENT_COVERAGE_REPORT.md | 182 ++ kb_demo_app/package.json | 2 +- kb_demo_app/tauri.conf.json | 2 +- kb_lib/src/dex/meteora_dlmm.rs | 2559 ++++++++++++++--- kb_lib/src/dex_decode.rs | 16 +- kb_lib/src/dex_detection_route.rs | 7 +- kb_lib/src/dex_event_classification.rs | 225 ++ kb_lib/src/dex_event_coverage.rs | 1233 +++++++- kb_lib/src/instruction_observation_index.rs | 121 + kb_lib/src/non_trade_event_materialization.rs | 647 ++++- .../SQL_VALIDATION_METEORA_DLMM_0_7_57.sql | 91 +- 24 files changed, 6432 insertions(+), 741 deletions(-) create mode 100644 docs/VALIDATION_STATUS_0_7_57_FINAL.md create mode 100644 docs/prompts/PROMPT_0_7_58_DEMO4_PROGRAM_SURFACE_DISCOVERY.md create mode 100644 docs/prompts/PROMPT_0_7_58_demo4_program_surface_discovery.md create mode 100644 docs/prompts/PROMPT_0_7_59_SQLITE_DB_TRANSACTION_MERGER_BINARY.md create mode 100644 docs/prompts/PROMPT_0_7_59_sqlite_db_transaction_merger_binary.md create mode 100644 docs/prompts/PROMPT_0_7_60_METEORA_DAMM_NEXT_DEX.md create mode 100644 docs/prompts/PROMPT_0_7_60_meteora_damm_next_dex.md create mode 100644 docs/reports/METEORA_DLMM_EVENT_COVERAGE_REPORT.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f54dc..2d472a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,3 +89,4 @@ 0.7.54 - Clôture Pump.fun : decoder maximal local depuis IDL Solscan/upstream, décodage des 40 instructions et 23 events Anchor connus, matérialisation validée des trades `buy/sell/buy_exact_sol_in` et `trade_event` v2/exact sans double-count, non-trades launch/fee/reward/admin selon contexte, validation SQL Pump.fun propre et ouverture de `0.7.55 pump_fees`. 0.7.55 - Clôture Pump Fees : decoder local maximal `pump_fees` depuis l'IDL locale, `29` instructions et `20` events Anchor couverts, tests synthétiques des Anchor IDL non observés, matérialisation prudente fee/reward/admin/lifecycle, `get_fees` decoded-only, transactions failed audit-only, aucun trade/candle direct et SQL de validation Pump Fees propre. 0.7.56 - Clôture Meteora DBC : decoder local maximal depuis l'IDL `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN`, couverture des 28 instructions et 23 events Anchor, matérialisation validée des swaps `swap/swap2`, lifecycle, admin/config, lockers/migrations et fees/surplus/leftover, ajout du modèle `k_sol_fee_event_amounts` pour les legs de montants fee, helper générique parent fee -> legs scalaires, recovery fee `allowlisted_inner_spl_transfer` strictement allowlistée pour anciens DEX, validation croisée Pump/Raydium, 446 tests passés, clippy OK et SQL de fermeture DBC propre. +0.7.57 - Clôture Meteora DLMM : decoder local maximal `meteora_dlmm` sur `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`, couverture des `76` instructions IDL, `30` events Anchor et `12` accounts, correction du discriminant non-IDL local `75c73e67068e1fcb` en `initialize_preset_parameter_v2`, matérialisation validée des swaps, exact-out, liquidity, bins, positions, lifecycle, fees, rewards, admin/config et limit orders, `swap_event/swap2_evt` routés en lifecycle `swap_log` sans double-count trade/candle, recovery fee/reward strictement allowlistée depuis inner SPL transfers, cleanup coverage duplicates, validation finale `460` tests passés, clippy OK et SQL de fermeture DLMM propre. diff --git a/Cargo.toml b/Cargo.toml index 82767d1..2e68586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.7.56" +version = "0.7.57" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" @@ -31,24 +31,23 @@ reqwest = { version = "^0.13", default-features = false, features = ["charset", rustls = { version = "^0.23", features = ["aws-lc-rs"] } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0", features = [] } -solana-account-decoder-client-types = { version = "^4.0", features = ["zstd"] } +solana-account-decoder-client-types = { version = ">=4.1.0-rc.1", features = ["zstd"] } solana-address-lookup-table-interface = { version = "^3.1", features = ["serde"] } solana-compute-budget-interface = { version = "^3.0", features = ["borsh", "serde"] } -solana-rpc-client-api = { version = ">=4.0.0-rc.0", features = [] } -#solana-rpc-client-types = { version = "^4.0", features = [] } +solana-rpc-client-api = { version = ">=4.1.0-rc.1", features = [] } solana-sdk = { version = "^4.0", features = ["full"] } solana-sdk-ids = { version = "^3.1", features = [] } solana-system-interface = { version = "^3.2", features = ["alloc", "serde", "std"] } -solana-transaction-status-client-types = { version = "^4.0", features = [] } +solana-transaction-status-client-types = { version = ">=4.1.0-rc.1", features = [] } spl-associated-token-account-interface = { version = "^2.0", features = ["borsh"] } spl-memo-interface = { version = "^2.1", features = [] } spl-token-interface = { version = "^3.0", features = [] } -spl-token-2022-interface = { version = "^3.0", features = [] } +spl-token-2022-interface = { version = "^3.1", features = [] } sqlx = { version = "^0.9", features = ["bigdecimal", "chrono", "default", "derive", "json", "runtime-tokio", "sqlite", "tls-rustls", "uuid"] } tauri = { version = "^2.11", features = ["default", "tray-icon"] } tauri-build = { version = "^2.6", features = [] } tauri-plugin-tracing = { version = "^0.3", default-features = false, features = [] } -tempfile = { version = "^3", features = [] } +tempfile = { version = "^3.27", features = [] } tokio = { version = "^1.52", features = ["full"] } tokio-stream = { version = "^0.1", features = ["full"] } tokio-tungstenite = { version = "^0.29", default-features = false, features = ["connect", "handshake", "rustls-tls-webpki-roots", "stream", "url"] } @@ -56,10 +55,10 @@ tracing = { version = "^0.1", features = [] } tracing-appender = { version = "^0.2", features = [] } tracing-subscriber = { version = "^0.3", features = ["ansi", "env-filter", "chrono", "serde", "json"] } ts-rs = { version = "^12.0", features = ["bigdecimal", "bson", "bytes", "chrono", "indexmap", "ordered-float", "serde_json", "tokio", "url", "uuid"] } -yellowstone-grpc-client = { version = "^13.0", features = [] } -yellowstone-grpc-proto = { version = "^12.2", features = [] } +yellowstone-grpc-client = { version = "^13.1", features = [] } +yellowstone-grpc-proto = { version = "^12.4", features = [] } uuid = { version = "^1.23", features = ["v4", "serde"] } -zeroize = { version = "^1.8", features = ["derive", "serde", "std"] } +zeroize = { version = "^1.9", features = ["derive", "serde", "std"] } [profile.dev] incremental = true # Compile your binary in smaller steps. diff --git a/README.md b/README.md index 9def016..9d2593f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,101 @@ # khadhroony-bobobot +## État final validé `0.7.57` — `meteora_dlmm` full decode / full materialization + +La tranche `0.7.57 meteora_dlmm` est clôturée côté decoder local maximal, coverage, tests synthétiques, matérialisation prudente et validation SQL sur base dédiée. Le programme traité est : + +```text +LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo +``` + +Source IDL locale prioritaire : + +```text +idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json +``` + +Surface couverte : `76` instructions IDL, `30` events Anchor et `12` accounts, avec correction locale du discriminant `75c73e67068e1fcb`, identifié comme `initialize_preset_parameter_v2` par les logs Anchor (`Instruction: InitializePresetParameterV2`) et par hash Anchor. Les entrées observées utiles ne restent pas en decoded-only : elles alimentent `trade`, `liquidity`, `lifecycle`, `fee`, `reward`, `admin` ou `orderbook` selon leur famille. + +Validation locale finale rapportée : + +```text +cargo test -p kb_lib -> 460 passed / 0 failed +cargo clippy -p kb_lib --all-targets -- -D warnings -> OK +769 replayed +0 decode skipped +769 ledger upserts +646 unsafe ledger rows +106 trades +664 liquidity +1107 lifecycle +0 tokenAccount +424 candle upserts +instructionObservations = 8062 +resetDeleted = 9898 +catalog = 169 tokens / 218 pools / 218 pairs +``` + +Règles métier verrouillées : + +- les trades/candles DLMM proviennent uniquement des instructions swap fiables : `swap`, `swap2`, `swap_exact_out`, `swap_exact_out2`, `swap_with_price_impact`, `swap_with_price_impact2` ; +- `swap_event` et `swap2_evt` sont matérialisés comme `swap_log` lifecycle afin d'éviter tout double comptage trade/candle ; +- les positions, bins, pool create, preset parameters, pair status, token badge et operator/config alimentent lifecycle/admin selon contexte ; +- les limit orders alimentent `k_sol_orderbook_events` et ne produisent jamais trade/candle ; +- les fees DLMM utilisent `k_sol_fee_events` + `k_sol_fee_event_amounts`, avec recovery strictement allowlistée depuis inner SPL transfers ; +- les rewards DLMM utilisent une recovery strictement allowlistée, sans inventer de montant pour les initialisations/configurations ; +- les transactions failed restent décodées/auditables mais ne matérialisent aucune table métier. + +Synthèse fee/reward finale observée : + +```text +claim_fee 64 parents / 64 legs +claim_fee2 63 parents / 88 legs +claim_fee_event 127 parents / 187 legs +claim_fee2_event 78 parents / 118 legs +composition_fee_event 51 parents / 63 legs +withdraw_protocol_fee 14 parents / 20 legs +zap_protocol_fee 13 parents / 13 legs +claim_reward 15 parents / 1 scalar amount +claim_reward2 19 parents / 10 scalar amounts +claim_reward_event 34 parents / 25 scalar amounts +claim_reward2_event 19 parents / 10 scalar amounts +fund_reward 10 parents / 10 scalar amounts +fund_reward_event 10 parents / 10 scalar amounts +initialize_reward 9 parents / 0 amount, attendu car init/config +initialize_reward_event 9 parents / 0 amount, attendu car init/config +``` + +Checks de fermeture `0.7.57` : + +- fallback `upstream_git` `meteora_dlmm` pour entrées couvertes localement : vide ; +- `instruction_audit` observé : `0` ; +- decoded `meteora_dlmm` sans coverage : vide ; +- successful non-materialized sans `skip*Reason` ou policy explicite : vide ; +- failed transaction avec business materialization : vide ; +- multi-target materialization : vide ; +- non-swap vers trade/candle : vide ; +- parent fee scalaire sans leg : vide ; +- fee amount legs orphelins : vide ; +- limit/orderbook vers trade/candle : vide ; +- duplicate logical coverage rows : vide. + +Écart coverage restant et expliqué : `close_bin_array` affiche `16 observed / 14 materialized`, car `14` transactions sont OK et matérialisées, tandis que `2` transactions failed (`Custom 6015`) restent audit-only. + +Documents de référence : + +```text +docs/reports/METEORA_DLMM_EVENT_COVERAGE_REPORT.md +docs/VALIDATION_STATUS_0_7_57_FINAL.md +validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql +docs/prompts/PROMPT_0_7_58_DEMO4_PROGRAM_SURFACE_DISCOVERY.md +docs/prompts/PROMPT_0_7_59_SQLITE_DB_TRANSACTION_MERGER_BINARY.md +docs/prompts/PROMPT_0_7_60_METEORA_DAMM_NEXT_DEX.md +``` + +Prochaine étape recommandée : `0.7.58 demo4 / program surface discovery`, une fonctionnalité d'affichage et de scoring des contenus non encore pris en compte (`program_id`, discriminators, Anchor logs/events, upstream fallback), sans matérialisation automatique et sans promotion automatique dans les decoders. + + ## État final validé `0.7.56` — `meteora_dbc` + socle `fee_event_amounts` @@ -88,7 +183,7 @@ validation_sql/SQL_VALIDATION_METEORA_DBC_0_7_56.sql docs/prompts/PROMPT_0_7_57_METEORA_DLMM_FULL_DECODE_MATERIALIZATION.md ``` -Prochaine tranche recommandée : `0.7.57 meteora_dlmm` en mode full decode / full materialization, sur le programme `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`, avec vérification complète de l'IDL locale `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json`. +La tranche suivante `0.7.57 meteora_dlmm` est désormais clôturée. La prochaine étape recommandée est `0.7.58 demo4 / program surface discovery`, puis `0.7.59` pour le binaire de consolidation de bases SQLite. ## État final validé `0.7.55` — `pump_fees` diff --git a/ROADMAP.md b/ROADMAP.md index de7c1c2..a14422e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,38 +2,45 @@ # Roadmap — khadhroony-bobobot -## État courant — clôture `0.7.56 meteora_dbc` et ouverture `0.7.57 meteora_dlmm full decode/materialization` +## État courant — clôture `0.7.57 meteora_dlmm` et plan intermédiaire `demo4` / consolidation DB -### `0.7.56 meteora_dbc` — clos +### `0.7.57 meteora_dlmm` — clos -- Program id : `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN`. -- Source prioritaire : `idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json`. -- Surface IDL : `28` instructions, `23` events Anchor, `9` accounts, `59` types. -- Build final : `cargo test -p kb_lib` -> `446 passed`; clippy `-D warnings` OK. -- Replay final DBC : `480 replayed`, `264 trades`, `1 liquidity`, `122 lifecycle`, `1056 candle upserts`, `instructionObservations=7167`, catalogue `86/60/60`. -- Fees DBC : `89` parents `k_sol_fee_events`, `96` legs `k_sol_fee_event_amounts`, aucun parent fee scalaire sans leg, aucun leg orphelin. -- Socle transversal ajouté : table `k_sol_fee_event_amounts`, helper générique parent fee + legs, backfill/normalisation safe, recovery `allowlisted_inner_spl_transfer` non globale. +- Program id : `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`. +- Source prioritaire : `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json`. +- Surface IDL : `76` instructions, `30` events Anchor, `12` accounts. +- Build final : `cargo test -p kb_lib` -> `460 passed`; clippy `-D warnings` OK. +- Replay final DLMM : `769 replayed`, `106 trades`, `664 liquidity`, `1107 lifecycle`, `424 candle upserts`, `instructionObservations=8062`, catalogue `169/218/218`. +- Dernier discriminant résiduel `75c73e67068e1fcb` : identifié comme `initialize_preset_parameter_v2`, matérialisé `4/4` en admin/config. +- `instruction_audit` observé : `0`; fallback `upstream_git` DLMM couvert localement : vide. +- `close_bin_array` : `16/14` coverage expliqué par `14` transactions OK matérialisées et `2` failed tx `Custom 6015` audit-only. +- Checks bloquants : decoded sans coverage, successful non-materialized, failed matérialisé, multi-target, non-swap trade/candle, parent fee sans leg, legs orphelins et coverage duplicates sont propres. -### Politique fee obligatoire à partir de `0.7.56` +### Politique de découverte automatique future -- Un parent `k_sol_fee_events` avec `fee_token_mint + fee_amount_raw` doit créer automatiquement un leg `k_sol_fee_event_amounts` d'index `0`. -- Un fee multi-mint/multi-leg ne doit pas agréger artificiellement le parent ; le parent reste sans montant scalaire, les legs portent les montants fiables. -- Les montants issus d'arguments `max`, de bornes ou de `u64::MAX` ne sont pas des fees exécutés. -- La recovery par CPI SPL inner transfer n'est pas globale : chaque futur décodeur doit déclarer explicitement sa policy et ses event kinds autorisés. -- Les `amount_source` connus à préserver : `parent_fee_event_amount`, `fee_event_amounts`, `inner_spl_transfer`, `lamport_balance_delta`, `allowlisted_inner_spl_transfer`. -- Les cas sans transfert exploitable doivent écrire une raison explicite : `fee_instruction_has_no_actual_transfer` ou `fee_instruction_has_only_zero_amount_transfers`. +La suite ne doit pas matérialiser automatiquement des contenus inconnus. La future `demo4` doit seulement afficher, scorer et regrouper les surfaces non encore prises en compte pour permettre une programmation manuelle ou semi-automatique contrôlée. -### Prochaine tranche immédiate +À observer sans promotion automatique : + +- `program_id` inconnus ou non routés ; +- discriminators inconnus sur program id connu ; +- logs Anchor `Instruction: ...` non mappés ; +- `Program data` / Anchor events non mappés ; +- fallback `upstream_git.instruction_match` ; +- layouts accounts/data/inner instructions ; +- signatures samples et statut success/failed. + +### Prochaines tranches recommandées | Priorité | Tranche | Surface | Objectif | |---:|---|---|---| -| 1 | `0.7.57` | `meteora_dlmm` | Full decode + full materialization : `76` instructions IDL, `30` events Anchor, swaps, exact-out, liquidity/bin/position, fees, rewards, admin/config, limit-order events et side effects sans double-count. | -| 2 | `0.7.58` | `meteora_damm_v1` | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. | -| 3 | `0.7.59` | `meteora_damm_v2` | Couverture complète : create/custom pools, swaps, liquidity, dynamic config, fees/admin. | -| 4 | `0.7.60` | `meteora_vault` | Vault deposit/withdraw/fee/accounting ; pas de candle directe. | -| 5 | `0.7.61+` | programmes transversaux | System/SPL/ATA/Compute/Memo/ALT : side effects et contexte, pas de trade direct. | +| 1 | `0.7.58` | `demo4_program_surface_discovery` | UI/queries de découverte des contenus non pris en compte : aucun auto-decode métier, aucune auto-materialization, aucune auto-promotion. | +| 2 | `0.7.59` | `sqlite_db_transaction_merger` | Nouveau module binaire utilisant `kb_lib` pour fusionner plusieurs bases SQLite de corpus existants dans une base output, sans rescanner/backfill les signatures déjà disponibles. | +| 3 | `0.7.60` | `meteora_damm_v1` | Clôture séparée DAMM v1 : pools, swaps, liquidity, lock, fees/admin, coverage et matérialisation complète. | +| 4 | `0.7.61` | `meteora_damm_v2` | Clôture séparée DAMM v2 : create/custom pools, swaps, liquidity, dynamic config, fees/admin. | +| 5 | `0.7.62` | `meteora_vault` | Vault deposit/withdraw/fee/accounting ; pas de candle directe. | -La tranche `0.7.57 meteora_dlmm` doit partir d'une base neuve, réutiliser `fee_event_amounts`, ajouter une validation dédiée `validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql`, et clôturer uniquement si toutes les entrées IDL locales sont couvertes, décodées ou explicitement non observées avec tests synthétiques. +Décision de planification : ne pas fusionner DAMM v1 et DAMM v2 dans une seule tranche. Ils peuvent partager des helpers, des tests et des conventions de matérialisation, mais les `program_id`, IDL, discriminants et corpus doivent rester séparés afin de garder les validations SQL lisibles et de réduire les risques de faux positif. ## 0.7.47-1FE5 — Décision de planification : ne plus viser “tous les events en une session” @@ -62,11 +69,11 @@ Règles de planification : | `https://github.com/all-in-one-blockchain/phoenix-onchain-mm` | Source Phoenix/MM complémentaire. | | `https://docs.vybenetwork.com/docs/available-dexs-amms` | Source externe de découverte DEX/AMM, non vérifiante. | -### Plan révisé `0.7.53+` — une version par `program_id` +### Plan actif post-`0.7.57` — phasage par version -Règle de planification validée après `0.7.52` : **une version cible = un `program_id`**. +Règle active après la clôture DLMM : les tranches `0.7.58` et `0.7.59` sont des tranches d’outillage, puis la règle **une version cible = un `program_id`** reprend à partir de `0.7.60`. -Exceptions : les comptes non-programmes (`platform_config`, token authority, comptes de configuration, comptes de pool, accounts de programme) ne créent pas de version decoder autonome. Ils restent des sources de contexte ou d’enrichissement. `SOLSCAN_ACCOUNT_SOURCES` reste un inventaire de découverte, pas une preuve de support local. +Les comptes non-programmes (`platform_config`, token authority, comptes de configuration, comptes de pool, accounts de programme) ne créent pas de version decoder autonome. `SOLSCAN_ACCOUNT_SOURCES` reste un inventaire de découverte, pas une preuve de support local. | Version cible | Decoder / surface | Program id | Famille | Objectif de clôture | |---|---|---|---|---| @@ -74,87 +81,89 @@ Exceptions : les comptes non-programmes (`platform_config`, token authority, com | `0.7.54` | `pump_fun` | `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` | Pump / launch-bonding | **Clos** : decoder maximal IDL/local, trades directs `buy/sell/buy_exact_sol_in`, v2/exact via `trade_event`, non-trades matérialisés selon contexte, validations Pump.fun propres. | | `0.7.55` | `pump_fees` | `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` | Pump / fee | **Clos** : `29` instructions, `20` events Anchor, fee/reward/admin/lifecycle, `get_fees` decoded-only, failed tx audit-only, aucun trade/candle direct. | | `0.7.56` | `meteora_dbc` | `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` | Meteora / DBC | Clos : `28` instructions et `23` events Anchor couverts, swaps `swap/swap2`, lifecycle/admin/fees, `k_sol_fee_event_amounts`, validations SQL propres. | -| `0.7.57` | `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Meteora / DLMM | Full decode + full materialization : `76` instructions IDL, `30` events Anchor, swaps, bins, positions, liquidity, fees/rewards/admin/limit-order, sans double-count. | -| `0.7.58` | `meteora_damm_v1` | `Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB` | Meteora / DAMM v1 | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. | -| `0.7.59` | `meteora_damm_v2` | `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` | Meteora / DAMM v2 | Couverture complète : create/custom pools, swaps, liquidity, dynamic config, fees/admin. | -| `0.7.60` | `meteora_vault` | `24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi` | Meteora / vault | Vault deposit/withdraw/fee/accounting ; pas de candle directe. | -| `0.7.61` | `system_program` | `11111111111111111111111111111111` | Système Solana | Create/assign/transfer account ; side effects de contexte, pas de trade. | -| `0.7.62` | `spl_token` | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` | SPL Token | Transfer, mint, burn, close account, sync native ; base transversale pour deltas. | -| `0.7.63` | `spl_token_2022` | `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` | Token-2022 | Transfers/extensions Token-2022, mint/burn/close, comptes et side effects. | -| `0.7.64` | `associated_token_account` | `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL` | Système token | Création ATA, rattachement wallet/token/pool. | -| `0.7.65` | `compute_budget` | `ComputeBudget111111111111111111111111111111` | Contexte tx | Budget/prioritization fee ; utile scoring/MEV, pas de trade. | -| `0.7.66` | `memo` | `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr` | Contexte tx | Mémo transactionnel et attribution éventuelle. | -| `0.7.67` | `address_lookup_table` | `AddressLookupTab1e1111111111111111111111111` | Contexte tx | Résolution/diagnostic ALT si nécessaire. | -| `0.7.68` | `mpl_token_metadata` | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | Metadata | Enrichissement token/NFT/mint metadata. | -| `0.7.69` | `mpl_core` | `CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d` | Metadata / asset | Contexte asset si présent dans corpus. | -| `0.7.70` | `bubblegum` | `BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY` | Compressed assets | Audit/contexte assets compressés, pas DEX. | -| `0.7.71` | `raydium_routing` | `routeUGWgWzqBWFcrCfv8tritsqukccJPu3q5GPP3xS` | Router | Route/legs Raydium ; éviter le double-count avec DEX effectifs. | -| `0.7.72` | `jupiter_swap_v6` | `JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4` | Aggregator | Route attribution, legs, no duplicate trade/candle. | -| `0.7.73` | `jupiter_swap_v4` | `JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB` | Legacy aggregator | Audit/route only si corpus encore utile. | -| `0.7.74` | `dflow_aggregator_v4` | `DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH` | Aggregator | Route/intent/orderflow ; pas de double matérialisation. | -| `0.7.75` | `okx_dex` | `6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma` | Aggregator/router | Route attribution ; trades seulement si source exacte non doublonnée. | -| `0.7.76` | `onchain_labs_dex_v2` | `proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u` | Router/DEX candidat | Corpus d’abord ; classifier route vs DEX effectif. | -| `0.7.77` | `titan_router` | `T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT` | Router | Audit route-only sauf preuve de trade direct non doublonné. | -| `0.7.78` | `sanctum_router` | `stkitrT1Uoy18Dk1fTrgPw8W6MVzoCfYoAFT4MLsmhq` | Router | Route/liquid staking context ; pas de candle DEX directe. | -| `0.7.79` | `orca_whirlpools` | `whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc` | DEX avec IDL/source | Swaps, pools, positions, liquidity, fees/rewards. | -| `0.7.80` | `stabble_stable_swap` | `swapNyd8XiQwJ6ianp9snpu4brUqFxadzvHebnAXjJZ` | DEX avec IDL/source | Stable swap ; deltas exacts, liquidity/admin. | -| `0.7.81` | `stabble_weighted_swap` | `swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW` | DEX avec IDL/source | Weighted swap, deltas exacts, liquidity/admin. | -| `0.7.82` | `stabble_clmm` | `6dMXqGZ3ga2dikrYS9ovDXgHGh5RUsb2RTUj6hrQXhk6` | DEX avec IDL/source | CLMM Stabble si corpus utile. | -| `0.7.83` | `bonkswap` | `BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p` | DEX avec IDL/source | Swap/liquidity/non-trade. | -| `0.7.84` | `boop_fun` | `boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4` | Launch/DEX candidat | Launch/swap/migration selon corpus. | -| `0.7.85` | `byreal_clmm` | `REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2` | DEX avec IDL/source | CLMM ; corpus puis matérialisation contrôlée. | -| `0.7.86` | `fusionamm` | `fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9` | DEX avec IDL/source | AMM ; swaps/liquidity si corpus. | -| `0.7.87` | `goosefx_v1` | `GAMMA7meSFWaBXF25oSUgmGRwaW6sCMFLmBNiMSdbHVT` | DEX avec IDL/source | DEX/AMM selon corpus. | -| `0.7.88` | `goosefx_v2` | `GFXsSL5sSaDfNFQUYsHekbWBW1TsFdjDYzACh62tEHxn` | DEX avec IDL/source | DEX/AMM selon corpus. | -| `0.7.89` | `guac_swap` | `Gswppe6ERWKpUTXvRPfXdzHhiCyJvLadVvXGfdpBqcE1` | DEX avec IDL/source | Swap/liquidity si corpus. | -| `0.7.90` | `hylo_exchange` | `HYEXCHtHkBagdStcJCp3xbbb9B7sdMdWXFNj6mdsG4hn` | DEX/source à classifier | Classer DEX/lending/stable selon IDL/corpus. | -| `0.7.91` | `printr` | `T8HsGYv7sMk3kTnyaRqZrbRPuntYzdh12evXBkprint` | Launch/DEX candidat | Corpus d’abord ; surface launch/swap à confirmer. | -| `0.7.92` | `moonit` | `MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG` | Launch/DEX candidat | Launch/migration/swap si prouvé. | -| `0.7.93` | `metadao_amm_v0_5` | `AMMJdEiCCa8mdugg6JPF7gFirmmxisTfDJoSNSUi5zDJ` | DEX avec source | AMM futarchy ; corpus et price semantics. | -| `0.7.94` | `metadao_bid_wall` | `WALL8ucBuUyL46QYxwYJjidaFYhdvxUFrgvBxPshERx` | Order/bid-wall | Order/bid-wall context ; pas de candle directe sans fill exact. | -| `0.7.95` | `metadao_launchpad` | `moontUzsdepotRGe5xsfip7vLPTJnVuafqdUWexVnPM` | Launch surface | Launch/ICO surface ; pas DEX effectif par défaut. | -| `0.7.96` | `vertigo` | `vrTGoBuy5rYSxAfV3jaRJWHH6nN9WK4NRExGxsk1bCJ` | DEX/source | Swap/launch selon corpus. | -| `0.7.97` | `virtuals` | `5U3EU2ubXtK84QcRjWVmYt9RaDyA8gKxdUrPFXmZyaki` | Launch/DEX candidat | Launch/AMM à confirmer. | -| `0.7.98` | `wavebreak` | `waveQX2yP3H1pVU8djGvEHmYg8uamQ84AuyGtpsrXTF` | DEX/source | Corpus et rôle exact. | -| `0.7.99` | `woofi` | `WooFif76YGRNjk1pA8wCsN67aQsD9f9iLsz4NcJ1AVb` | DEX/router | Swap/route selon corpus. | -| `0.7.100` | `pancake_swap` | `HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq` | DEX/source | DEX Solana à confirmer par corpus. | -| `0.7.101` | `gavel` | `srAMMzfVHVAtgSJc8iH6CfKzuWuUTzLHVCE81QU1rgi` | Source upstream | Corpus d’abord ; rôle exact à classer. | -| `0.7.102` | `heaven` | `HEAVENoP2qxoeuF8Dj2oT1GHEnu49U5mJYkdeC8BAX2o` | Launch/DEX candidat | Launch/DEX selon corpus. | -| `0.7.103` | `lifinity_v2` | `2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c` | DEX legacy/actif | Support selon corpus. | -| `0.7.104` | `moonshot` | `MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG` | Source upstream/Solscan | À dédupliquer avec `moonit` si même program id. | -| `0.7.105` | `openbook_v2` | `opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb` | Orderbook | Audit/orderbook complet ; trade/candle seulement si fills exacts. | -| `0.7.106` | `phoenix_v1` | `PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY` | Orderbook | Audit-only complet avant toute matérialisation fill. | -| `0.7.107` | `alphaq` | `ALPHAQmeA7bjrVuccPsYPiCvsi428SNwte66Srvs4pHA` | Solscan/no IDL | Probe par Demo3 + corpus ; décider support ou abandon. | -| `0.7.108` | `aquifer` | `AQU1FRd7papthgdrwPTTq5JacJh8YtwEXaBfKU3bTz45` | Solscan/no IDL | Probe uniquement. | -| `0.7.109` | `bisonfi` | `BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi` | Solscan/no IDL | Probe uniquement. | -| `0.7.110` | `dexlab` | `DSwpgjMvXhtGn6BsbqmacdBZyfLj6jSWf3HJpdJtmg6N` | Solscan/no IDL | Vérifier support partiel / corpus. | -| `0.7.111` | `fluxbeam` | `FLUXubRmkEi2q6K3Y9kBPg9248ggaZVsoSFhtJHSrm1X` | Solscan/no IDL | Vérifier support partiel / corpus. | -| `0.7.112` | `goonfi` | `goonERTdGsjnkZqWuVjs73BZ3Pb9qoCUdBUL17BnS5j` | Solscan/no IDL | Probe. | -| `0.7.113` | `goonfi_v2` | `goonuddtQRrWqqn5nFyczVKaie28f3kDkHWkHtURSLE` | Solscan/no IDL | Probe ; adresse à revérifier si erreur de taille. | -| `0.7.114` | `humidifi` | `9H6tua7jkLhdm3w8BvgpTn5LZNU7g4ZynDmCiNN3q6Rp` | Solscan/no IDL | Probe. | -| `0.7.115` | `obric_v2` | `obriQD1zbpyLz95G5n7nJe6a4DPjpFwa5XYPoNm113y` | Solscan/no IDL | Probe. | -| `0.7.116` | `ondo_global_market` | `XzTT4XB8m7sLD2xi6snefSasaswsKCxx5Tifjondogm` | Solscan IDL | Rôle marché/tokenized assets à confirmer. | -| `0.7.117` | `scorch` | `SCoRcH8c2dpjvcJD6FiPbCSQyQgu3PcUAWj2Xxx3mqn` | Solscan/no IDL | Probe. | -| `0.7.118` | `solfi` | `SoLFiHG9TfgtdUXUjWAxi3LtvYuFyDLVhBWxdMZxyCe` | Solscan/no IDL | Probe. | -| `0.7.119` | `solfi_v2` | `SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF` | Solscan/no IDL | Probe. | -| `0.7.120` | `zerofi` | `ZERor4xhbUycZ6gb9ntrhqscUcZmAbQDjEAtCf4hbZY` | Solscan/no IDL | Probe. | -| `0.7.121` | `zora` | `zoRabwLGd5zXaV7Gxacppw8tcceXEiTrSKyNLSaSTUc` | Solscan/no IDL | Probe, pas de promotion sans corpus. | -| `0.7.122` | `1dex` | `DEXYosS6oEGvk8uCDayvwEZz4qEyDJRf9nFgYCaqPMTm` | Solscan/no IDL | Probe. | -| `0.7.123` | `aldrin_amm` | `AMM55ShdkoGRB5jVYPjWziwk8m5MpwyDgsMWHaMSQWH6` | Legacy/no IDL | Historique/probe. | -| `0.7.124` | `aldrin_amm_v2` | `CURVGoZn8zycx6FXwwevgBTB2gVvdbGTEpvMJDbgs2t4` | Legacy/no IDL | Historique/probe. | -| `0.7.125` | `crema_finance` | `CLMM9tUoggJu2wagPkkqs9eFG4BWhVBZWkP1qv3Sp7tR` | Legacy/no IDL | Historique/probe. | -| `0.7.126` | `cropper_finance` | `CTMAxxk34HjKWxQ3QLZK1HpaLXmBveao3ESePXbiyfzh` | Legacy/no IDL | Historique/probe. | -| `0.7.127` | `cropper_whirlpool` | `H8W3ctz92svYg6mkn1UtGfu2aQr2fnUFHM1RhScEtQDt` | Legacy/no IDL | Historique/probe. | -| `0.7.128` | `mercurial_stable_swap` | `MERLuDFBMmsHnsBPZw2sDQZHvXFMwp8EdjudcU2HKky` | Legacy/no IDL | Historique stable swap ; deltas exacts si support. | -| `0.7.129` | `saber_stable_swap` | `SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ` | Legacy/no IDL | Historique stable swap. | -| `0.7.130` | `saros_amm` | `SSwapUtytfBdBn1b9NUGG6foMVPtcWgpRU32HToDUZr` | Legacy/no IDL | Historique/probe. | -| `0.7.131` | `step_finance_swap` | `SSwpMgqNDsyV7mAgN9ady4bDVu5ySjmmXejXvy2vLt1` | Legacy/no IDL | Historique/probe. | -| `0.7.132` | `stepn_dooar_swap` | `Dooar9JkhdZ7J3LHN3A7YCuoGRUggXhQaG4kijfLGU2j` | Legacy/no IDL | Historique/probe. | -| `0.7.133` | `raydium_amm_v2_legacy` | `RVKd61ztZW9GUwhRbbLoYVRE5Xf1B2tVscKqwZqXgEr` | Raydium legacy/no IDL | Historique Raydium ; corpus d’abord. | -| `0.7.134` | `raydium_amm_v3_legacy` | `27haf8L6oxUeXrHrgEgsexjSY5hbVUWEmvv9Nyxg8vQv` | Raydium legacy/no IDL | Historique Raydium ; ne pas confondre avec CLMM moderne. | -| `0.7.135` | `raydium_pool_v4_json_audit` | aucun `program_id` prouvé par le fichier seul | Audit source annexe | Vérifier `sol-parser-sdk/idls/raydium_pool_v4.json` après les surfaces documentées ; patch AMM v4 si amélioration, sinon clôture no-op. | -| `0.7.136` | cleanup `SOLSCAN_ACCOUNT_SOURCES` | n/a | Nettoyage registry/constants | Retirer doublons/promotions ; les programmes validés deviennent constantes/support matrix, les comptes non-programmes restent contexte ou sont supprimés. | -| `0.7.137` | base neuve multi-programmes | n/a | Validation consolidée | Replay consolidé, coverage global, zéro faux trade/candle, diagnostics bloquants à zéro. | +| `0.7.57` | `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Meteora / DLMM | **Clos** : full decode + full materialization ; `76` instructions IDL, `30` events Anchor, swaps, bins, positions, liquidity, fees/rewards/admin/limit-order, sans double-count. | +| `0.7.58` | `demo4_program_surface_discovery` | n/a | Outillage / discovery | Afficher les contenus non pris en compte : program ids, discriminators, Anchor logs/events, upstream fallbacks, layouts et samples ; aucune matérialisation automatique. | +| `0.7.59` | `sqlite_db_transaction_merger` | n/a | Outillage / consolidation DB | Ajouter un binaire utilisant `kb_lib` pour fusionner plusieurs `.db` de corpus existants vers une base output sans RPC/backfill. | +| `0.7.60` | `meteora_damm_v1` | `Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB` | Meteora / DAMM v1 | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. | +| `0.7.61` | `meteora_damm_v2` | `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` | Meteora / DAMM v2 | Couverture complète : create/custom pools, swaps, liquidity, dynamic config, fees/admin. | +| `0.7.62` | `meteora_vault` | `24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi` | Meteora / vault | Vault deposit/withdraw/fee/accounting ; pas de candle directe. | +| `0.7.63` | `system_program` | `11111111111111111111111111111111` | Système Solana | Create/assign/transfer account ; side effects de contexte, pas de trade. | +| `0.7.64` | `spl_token` | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` | SPL Token | Transfer, mint, burn, close account, sync native ; base transversale pour deltas. | +| `0.7.65` | `spl_token_2022` | `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` | Token-2022 | Transfers/extensions Token-2022, mint/burn/close, comptes et side effects. | +| `0.7.66` | `associated_token_account` | `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL` | Système token | Création ATA, rattachement wallet/token/pool. | +| `0.7.67` | `compute_budget` | `ComputeBudget111111111111111111111111111111` | Contexte tx | Budget/prioritization fee ; utile scoring/MEV, pas de trade. | +| `0.7.68` | `memo` | `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr` | Contexte tx | Mémo transactionnel et attribution éventuelle. | +| `0.7.69` | `address_lookup_table` | `AddressLookupTab1e1111111111111111111111111` | Contexte tx | Résolution/diagnostic ALT si nécessaire. | +| `0.7.70` | `mpl_token_metadata` | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | Metadata | Enrichissement token/NFT/mint metadata. | +| `0.7.71` | `mpl_core` | `CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d` | Metadata / asset | Contexte asset si présent dans corpus. | +| `0.7.72` | `bubblegum` | `BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY` | Compressed assets | Audit/contexte assets compressés, pas DEX. | +| `0.7.73` | `raydium_routing` | `routeUGWgWzqBWFcrCfv8tritsqukccJPu3q5GPP3xS` | Router | Route/legs Raydium ; éviter le double-count avec DEX effectifs. | +| `0.7.74` | `jupiter_swap_v6` | `JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4` | Aggregator | Route attribution, legs, no duplicate trade/candle. | +| `0.7.75` | `jupiter_swap_v4` | `JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB` | Legacy aggregator | Audit/route only si corpus encore utile. | +| `0.7.76` | `dflow_aggregator_v4` | `DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH` | Aggregator | Route/intent/orderflow ; pas de double matérialisation. | +| `0.7.77` | `okx_dex` | `6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma` | Aggregator/router | Route attribution ; trades seulement si source exacte non doublonnée. | +| `0.7.78` | `onchain_labs_dex_v2` | `proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u` | Router/DEX candidat | Corpus d’abord ; classifier route vs DEX effectif. | +| `0.7.79` | `titan_router` | `T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT` | Router | Audit route-only sauf preuve de trade direct non doublonné. | +| `0.7.80` | `sanctum_router` | `stkitrT1Uoy18Dk1fTrgPw8W6MVzoCfYoAFT4MLsmhq` | Router | Route/liquid staking context ; pas de candle DEX directe. | +| `0.7.81` | `orca_whirlpools` | `whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc` | DEX avec IDL/source | Swaps, pools, positions, liquidity, fees/rewards. | +| `0.7.82` | `stabble_stable_swap` | `swapNyd8XiQwJ6ianp9snpu4brUqFxadzvHebnAXjJZ` | DEX avec IDL/source | Stable swap ; deltas exacts, liquidity/admin. | +| `0.7.83` | `stabble_weighted_swap` | `swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW` | DEX avec IDL/source | Weighted swap, deltas exacts, liquidity/admin. | +| `0.7.84` | `stabble_clmm` | `6dMXqGZ3ga2dikrYS9ovDXgHGh5RUsb2RTUj6hrQXhk6` | DEX avec IDL/source | CLMM Stabble si corpus utile. | +| `0.7.85` | `bonkswap` | `BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p` | DEX avec IDL/source | Swap/liquidity/non-trade. | +| `0.7.86` | `boop_fun` | `boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4` | Launch/DEX candidat | Launch/swap/migration selon corpus. | +| `0.7.87` | `byreal_clmm` | `REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2` | DEX avec IDL/source | CLMM ; corpus puis matérialisation contrôlée. | +| `0.7.88` | `fusionamm` | `fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9` | DEX avec IDL/source | AMM ; swaps/liquidity si corpus. | +| `0.7.89` | `goosefx_v1` | `GAMMA7meSFWaBXF25oSUgmGRwaW6sCMFLmBNiMSdbHVT` | DEX avec IDL/source | DEX/AMM selon corpus. | +| `0.7.90` | `goosefx_v2` | `GFXsSL5sSaDfNFQUYsHekbWBW1TsFdjDYzACh62tEHxn` | DEX avec IDL/source | DEX/AMM selon corpus. | +| `0.7.91` | `guac_swap` | `Gswppe6ERWKpUTXvRPfXdzHhiCyJvLadVvXGfdpBqcE1` | DEX avec IDL/source | Swap/liquidity si corpus. | +| `0.7.92` | `hylo_exchange` | `HYEXCHtHkBagdStcJCp3xbbb9B7sdMdWXFNj6mdsG4hn` | DEX/source à classifier | Classer DEX/lending/stable selon IDL/corpus. | +| `0.7.93` | `printr` | `T8HsGYv7sMk3kTnyaRqZrbRPuntYzdh12evXBkprint` | Launch/DEX candidat | Corpus d’abord ; surface launch/swap à confirmer. | +| `0.7.94` | `moonit` | `MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG` | Launch/DEX candidat | Launch/migration/swap si prouvé. | +| `0.7.95` | `metadao_amm_v0_5` | `AMMJdEiCCa8mdugg6JPF7gFirmmxisTfDJoSNSUi5zDJ` | DEX avec source | AMM futarchy ; corpus et price semantics. | +| `0.7.96` | `metadao_bid_wall` | `WALL8ucBuUyL46QYxwYJjidaFYhdvxUFrgvBxPshERx` | Order/bid-wall | Order/bid-wall context ; pas de candle directe sans fill exact. | +| `0.7.97` | `metadao_launchpad` | `moontUzsdepotRGe5xsfip7vLPTJnVuafqdUWexVnPM` | Launch surface | Launch/ICO surface ; pas DEX effectif par défaut. | +| `0.7.98` | `vertigo` | `vrTGoBuy5rYSxAfV3jaRJWHH6nN9WK4NRExGxsk1bCJ` | DEX/source | Swap/launch selon corpus. | +| `0.7.99` | `virtuals` | `5U3EU2ubXtK84QcRjWVmYt9RaDyA8gKxdUrPFXmZyaki` | Launch/DEX candidat | Launch/AMM à confirmer. | +| `0.7.100` | `wavebreak` | `waveQX2yP3H1pVU8djGvEHmYg8uamQ84AuyGtpsrXTF` | DEX/source | Corpus et rôle exact. | +| `0.7.101` | `woofi` | `WooFif76YGRNjk1pA8wCsN67aQsD9f9iLsz4NcJ1AVb` | DEX/router | Swap/route selon corpus. | +| `0.7.102` | `pancake_swap` | `HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq` | DEX/source | DEX Solana à confirmer par corpus. | +| `0.7.103` | `gavel` | `srAMMzfVHVAtgSJc8iH6CfKzuWuUTzLHVCE81QU1rgi` | Source upstream | Corpus d’abord ; rôle exact à classer. | +| `0.7.104` | `heaven` | `HEAVENoP2qxoeuF8Dj2oT1GHEnu49U5mJYkdeC8BAX2o` | Launch/DEX candidat | Launch/DEX selon corpus. | +| `0.7.105` | `lifinity_v2` | `2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c` | DEX legacy/actif | Support selon corpus. | +| `0.7.106` | `moonshot` | `MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG` | Source upstream/Solscan | À dédupliquer avec `moonit` si même program id. | +| `0.7.107` | `openbook_v2` | `opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb` | Orderbook | Audit/orderbook complet ; trade/candle seulement si fills exacts. | +| `0.7.108` | `phoenix_v1` | `PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY` | Orderbook | Audit-only complet avant toute matérialisation fill. | +| `0.7.109` | `alphaq` | `ALPHAQmeA7bjrVuccPsYPiCvsi428SNwte66Srvs4pHA` | Solscan/no IDL | Probe par Demo3 + corpus ; décider support ou abandon. | +| `0.7.110` | `aquifer` | `AQU1FRd7papthgdrwPTTq5JacJh8YtwEXaBfKU3bTz45` | Solscan/no IDL | Probe uniquement. | +| `0.7.111` | `bisonfi` | `BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi` | Solscan/no IDL | Probe uniquement. | +| `0.7.112` | `dexlab` | `DSwpgjMvXhtGn6BsbqmacdBZyfLj6jSWf3HJpdJtmg6N` | Solscan/no IDL | Vérifier support partiel / corpus. | +| `0.7.113` | `fluxbeam` | `FLUXubRmkEi2q6K3Y9kBPg9248ggaZVsoSFhtJHSrm1X` | Solscan/no IDL | Vérifier support partiel / corpus. | +| `0.7.114` | `goonfi` | `goonERTdGsjnkZqWuVjs73BZ3Pb9qoCUdBUL17BnS5j` | Solscan/no IDL | Probe. | +| `0.7.115` | `goonfi_v2` | `goonuddtQRrWqqn5nFyczVKaie28f3kDkHWkHtURSLE` | Solscan/no IDL | Probe ; adresse à revérifier si erreur de taille. | +| `0.7.116` | `humidifi` | `9H6tua7jkLhdm3w8BvgpTn5LZNU7g4ZynDmCiNN3q6Rp` | Solscan/no IDL | Probe. | +| `0.7.117` | `obric_v2` | `obriQD1zbpyLz95G5n7nJe6a4DPjpFwa5XYPoNm113y` | Solscan/no IDL | Probe. | +| `0.7.118` | `ondo_global_market` | `XzTT4XB8m7sLD2xi6snefSasaswsKCxx5Tifjondogm` | Solscan IDL | Rôle marché/tokenized assets à confirmer. | +| `0.7.119` | `scorch` | `SCoRcH8c2dpjvcJD6FiPbCSQyQgu3PcUAWj2Xxx3mqn` | Solscan/no IDL | Probe. | +| `0.7.120` | `solfi` | `SoLFiHG9TfgtdUXUjWAxi3LtvYuFyDLVhBWxdMZxyCe` | Solscan/no IDL | Probe. | +| `0.7.121` | `solfi_v2` | `SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF` | Solscan/no IDL | Probe. | +| `0.7.122` | `zerofi` | `ZERor4xhbUycZ6gb9ntrhqscUcZmAbQDjEAtCf4hbZY` | Solscan/no IDL | Probe. | +| `0.7.123` | `zora` | `zoRabwLGd5zXaV7Gxacppw8tcceXEiTrSKyNLSaSTUc` | Solscan/no IDL | Probe, pas de promotion sans corpus. | +| `0.7.124` | `1dex` | `DEXYosS6oEGvk8uCDayvwEZz4qEyDJRf9nFgYCaqPMTm` | Solscan/no IDL | Probe. | +| `0.7.125` | `aldrin_amm` | `AMM55ShdkoGRB5jVYPjWziwk8m5MpwyDgsMWHaMSQWH6` | Legacy/no IDL | Historique/probe. | +| `0.7.126` | `aldrin_amm_v2` | `CURVGoZn8zycx6FXwwevgBTB2gVvdbGTEpvMJDbgs2t4` | Legacy/no IDL | Historique/probe. | +| `0.7.127` | `crema_finance` | `CLMM9tUoggJu2wagPkkqs9eFG4BWhVBZWkP1qv3Sp7tR` | Legacy/no IDL | Historique/probe. | +| `0.7.128` | `cropper_finance` | `CTMAxxk34HjKWxQ3QLZK1HpaLXmBveao3ESePXbiyfzh` | Legacy/no IDL | Historique/probe. | +| `0.7.129` | `cropper_whirlpool` | `H8W3ctz92svYg6mkn1UtGfu2aQr2fnUFHM1RhScEtQDt` | Legacy/no IDL | Historique/probe. | +| `0.7.130` | `mercurial_stable_swap` | `MERLuDFBMmsHnsBPZw2sDQZHvXFMwp8EdjudcU2HKky` | Legacy/no IDL | Historique stable swap ; deltas exacts si support. | +| `0.7.131` | `saber_stable_swap` | `SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ` | Legacy/no IDL | Historique stable swap. | +| `0.7.132` | `saros_amm` | `SSwapUtytfBdBn1b9NUGG6foMVPtcWgpRU32HToDUZr` | Legacy/no IDL | Historique/probe. | +| `0.7.133` | `step_finance_swap` | `SSwpMgqNDsyV7mAgN9ady4bDVu5ySjmmXejXvy2vLt1` | Legacy/no IDL | Historique/probe. | +| `0.7.134` | `stepn_dooar_swap` | `Dooar9JkhdZ7J3LHN3A7YCuoGRUggXhQaG4kijfLGU2j` | Legacy/no IDL | Historique/probe. | +| `0.7.135` | `raydium_amm_v2_legacy` | `RVKd61ztZW9GUwhRbbLoYVRE5Xf1B2tVscKqwZqXgEr` | Raydium legacy/no IDL | Historique Raydium ; corpus d’abord. | +| `0.7.136` | `raydium_amm_v3_legacy` | `27haf8L6oxUeXrHrgEgsexjSY5hbVUWEmvv9Nyxg8vQv` | Raydium legacy/no IDL | Historique Raydium ; ne pas confondre avec CLMM moderne. | +| `0.7.137` | `raydium_pool_v4_json_audit` | aucun `program_id` prouvé par le fichier seul | Audit source annexe | Vérifier `sol-parser-sdk/idls/raydium_pool_v4.json` après les surfaces documentées ; patch AMM v4 si amélioration, sinon clôture no-op. | +| `0.7.138` | cleanup `SOLSCAN_ACCOUNT_SOURCES` | n/a | Nettoyage registry/constants | Retirer doublons/promotions ; les programmes validés deviennent constantes/support matrix, les comptes non-programmes restent contexte ou sont supprimés. | +| `0.7.139` | base neuve multi-programmes | n/a | Validation consolidée | Replay consolidé, coverage global, zéro faux trade/candle, diagnostics bloquants à zéro. | Ce plan remplace les regroupements larges qui mélangeaient plusieurs DEX ou plusieurs `program_id` dans une seule tranche. `raydium_pool_v4.json` est explicitement repoussé vers la fin : il ne bloque plus `0.7.53`. @@ -1422,130 +1431,108 @@ Objectif : reprendre Raydium Stable comme tranche Raydium dédiée après AMM v4 Réalisé : decoder legacy 1 octet, surface locale `00..0d`, matérialisation lifecycle/liquidity/admin/fee/orderbook selon contexte, swaps `swap_base_in/out` matérialisés uniquement depuis deltas vault exacts (`stable_swap_vault_balance_delta`), transactions failed decoded-only, invariants trade/candle propres. -### 6.085. Versions `0.7.53` à `0.7.137` — phasage par `program_id` -Objectif : reprendre la suite après `0.7.52 raydium_stable_swap` avec une règle stricte : **une version = un `program_id`**. +### 6.085. Versions `0.7.53` à `0.7.139` — phasage actif post-DLMM -Les comptes non-programmes ne créent pas de tranche decoder autonome. `SOLSCAN_ACCOUNT_SOURCES` reste un inventaire de découverte et sera nettoyé après validation des surfaces. +Objectif : maintenir un phasage lisible après la clôture `0.7.57 meteora_dlmm`. -#### Bloc Pump +Décisions actives : -| Version | Decoder / surface | Program id | Objectif | -|---:|---|---|---| -| `0.7.53` | `pump_swap` | `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA` | Clos : `buy/sell/buy_exact_quote_in` depuis sources exactes, non-trades spécialisés, events Anchor audit-only. | -| `0.7.54` | `pump_fun` | `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` | **Clos** : decoder maximal local, trades directs et `trade_event` canonique, non-trades selon contexte, validations propres. | -| `0.7.55` | `pump_fees` | `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` | Couvrir fee accounting/claim/config ; aucun trade/candle direct. | +- `0.7.58` est réservé à `demo4_program_surface_discovery` : affichage et revue de surfaces non prises en compte, sans matérialisation automatique. +- `0.7.59` est réservé au binaire de consolidation de bases SQLite : fusion de corpus transactionnels déjà présents, sans RPC/backfill. +- `0.7.60` reprend le travail DEX avec `meteora_damm_v1`. +- `0.7.61` cible `meteora_damm_v2`. +- `0.7.62` cible `meteora_vault`. +- Après `0.7.62`, le décalage de deux versions est assumé et documenté. -#### Bloc Meteora - -| Version | Decoder / surface | Program id | Objectif | -|---:|---|---|---| -| `0.7.56` | `meteora_dbc` | `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` | Compléter toutes les instructions/events DBC : launch/bonding, swap exploitable, migration, fees/admin/config. | -| `0.7.57` | `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Full decode + full materialization : `76` instructions IDL, `30` events Anchor, swaps, bins, positions, liquidity, fees/rewards/admin/limit-order, sans double-count. | -| `0.7.58` | `meteora_damm_v1` | `Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB` | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. | -| `0.7.59` | `meteora_damm_v2` | `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` | Couverture complète : create/custom pools, swaps, liquidity, dynamic config, fees/admin. | -| `0.7.60` | `meteora_vault` | `24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi` | Vault deposit/withdraw/fee/accounting ; pas de candle directe. | - -#### Bloc programmes système / contexte transactionnel - -| Version | Decoder / surface | Program id | Objectif | -|---:|---|---|---| -| `0.7.61` | `system_program` | `11111111111111111111111111111111` | Create/assign/transfer account ; side effects de contexte, pas de trade. | -| `0.7.62` | `spl_token` | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` | Transfer, mint, burn, close account, sync native ; base transversale pour deltas. | -| `0.7.63` | `spl_token_2022` | `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` | Transfers/extensions Token-2022, mint/burn/close, comptes et side effects. | -| `0.7.64` | `associated_token_account` | `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL` | Création ATA, rattachement wallet/token/pool. | -| `0.7.65` | `compute_budget` | `ComputeBudget111111111111111111111111111111` | Budget/prioritization fee ; utile scoring/MEV, pas de trade. | -| `0.7.66` | `memo` | `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr` | Mémo transactionnel et attribution éventuelle. | -| `0.7.67` | `address_lookup_table` | `AddressLookupTab1e1111111111111111111111111` | Résolution/diagnostic ALT si nécessaire. | -| `0.7.68` | `mpl_token_metadata` | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | Enrichissement token/NFT/mint metadata. | -| `0.7.69` | `mpl_core` | `CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d` | Contexte asset si présent dans corpus. | -| `0.7.70` | `bubblegum` | `BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY` | Audit/contexte assets compressés, pas DEX. | - -#### Bloc routers / aggregators - -| Version | Decoder / surface | Program id | Objectif | -|---:|---|---|---| -| `0.7.71` | `raydium_routing` | `routeUGWgWzqBWFcrCfv8tritsqukccJPu3q5GPP3xS` | Route/legs Raydium ; éviter le double-count avec AMM/CLMM/CPMM/Stable. | -| `0.7.72` | `jupiter_swap_v6` | `JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4` | Route attribution, legs, no duplicate trade/candle. | -| `0.7.73` | `jupiter_swap_v4` | `JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB` | Audit/route only si corpus encore utile. | -| `0.7.74` | `dflow_aggregator_v4` | `DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH` | Route/intent/orderflow ; pas de double matérialisation. | -| `0.7.75` | `okx_dex` | `6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma` | Route attribution ; trades seulement si source exacte non doublonnée. | -| `0.7.76` | `onchain_labs_dex_v2` | `proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u` | Corpus d’abord ; classifier route vs DEX effectif. | -| `0.7.77` | `titan_router` | `T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT` | Audit route-only sauf preuve de trade direct non doublonné. | -| `0.7.78` | `sanctum_router` | `stkitrT1Uoy18Dk1fTrgPw8W6MVzoCfYoAFT4MLsmhq` | Route/liquid staking context ; pas de candle DEX directe. | - -#### Bloc DEX avec IDL/code/source exploitable - -| Version | Decoder / surface | Program id | Objectif | -|---:|---|---|---| -| `0.7.79` | `orca_whirlpools` | `whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc` | Swaps, pools, positions, liquidity, fees/rewards. | -| `0.7.80` | `stabble_stable_swap` | `swapNyd8XiQwJ6ianp9snpu4brUqFxadzvHebnAXjJZ` | Stable swap ; deltas exacts, liquidity/admin. | -| `0.7.81` | `stabble_weighted_swap` | `swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW` | Weighted swap, deltas exacts, liquidity/admin. | -| `0.7.82` | `stabble_clmm` | `6dMXqGZ3ga2dikrYS9ovDXgHGh5RUsb2RTUj6hrQXhk6` | CLMM Stabble si corpus utile. | -| `0.7.83` | `bonkswap` | `BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p` | Swap/liquidity/non-trade. | -| `0.7.84` | `boop_fun` | `boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4` | Launch/swap/migration selon corpus. | -| `0.7.85` | `byreal_clmm` | `REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2` | CLMM ; corpus puis matérialisation contrôlée. | -| `0.7.86` | `fusionamm` | `fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9` | AMM ; swaps/liquidity si corpus. | -| `0.7.87` | `goosefx_v1` | `GAMMA7meSFWaBXF25oSUgmGRwaW6sCMFLmBNiMSdbHVT` | DEX/AMM selon corpus. | -| `0.7.88` | `goosefx_v2` | `GFXsSL5sSaDfNFQUYsHekbWBW1TsFdjDYzACh62tEHxn` | DEX/AMM selon corpus. | -| `0.7.89` | `guac_swap` | `Gswppe6ERWKpUTXvRPfXdzHhiCyJvLadVvXGfdpBqcE1` | Swap/liquidity si corpus. | -| `0.7.90` | `hylo_exchange` | `HYEXCHtHkBagdStcJCp3xbbb9B7sdMdWXFNj6mdsG4hn` | Classer DEX/lending/stable selon IDL/corpus. | -| `0.7.91` | `printr` | `T8HsGYv7sMk3kTnyaRqZrbRPuntYzdh12evXBkprint` | Corpus d’abord ; surface launch/swap à confirmer. | -| `0.7.92` | `moonit` | `MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG` | Launch/migration/swap si prouvé. | -| `0.7.93` | `metadao_amm_v0_5` | `AMMJdEiCCa8mdugg6JPF7gFirmmxisTfDJoSNSUi5zDJ` | AMM futarchy ; corpus et price semantics. | -| `0.7.94` | `metadao_bid_wall` | `WALL8ucBuUyL46QYxwYJjidaFYhdvxUFrgvBxPshERx` | Order/bid-wall context ; pas de candle directe sans fill exact. | -| `0.7.95` | `metadao_launchpad` | `moontUzsdepotRGe5xsfip7vLPTJnVuafqdUWexVnPM` | Launch/ICO surface ; pas DEX effectif par défaut. | -| `0.7.96` | `vertigo` | `vrTGoBuy5rYSxAfV3jaRJWHH6nN9WK4NRExGxsk1bCJ` | Swap/launch selon corpus. | -| `0.7.97` | `virtuals` | `5U3EU2ubXtK84QcRjWVmYt9RaDyA8gKxdUrPFXmZyaki` | Launch/AMM à confirmer. | -| `0.7.98` | `wavebreak` | `waveQX2yP3H1pVU8djGvEHmYg8uamQ84AuyGtpsrXTF` | Corpus et rôle exact. | -| `0.7.99` | `woofi` | `WooFif76YGRNjk1pA8wCsN67aQsD9f9iLsz4NcJ1AVb` | Swap/route selon corpus. | -| `0.7.100` | `pancake_swap` | `HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq` | DEX Solana à confirmer par corpus. | -| `0.7.101` | `gavel` | `srAMMzfVHVAtgSJc8iH6CfKzuWuUTzLHVCE81QU1rgi` | Corpus d’abord ; rôle exact à classer. | -| `0.7.102` | `heaven` | `HEAVENoP2qxoeuF8Dj2oT1GHEnu49U5mJYkdeC8BAX2o` | Launch/DEX selon corpus. | -| `0.7.103` | `lifinity_v2` | `2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c` | DEX legacy/actif à confirmer. | -| `0.7.104` | `moonshot` | `MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG` | À dédupliquer avec `moonit` si même program id. | -| `0.7.105` | `openbook_v2` | `opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb` | Audit/orderbook complet ; trade/candle seulement si fills exacts. | -| `0.7.106` | `phoenix_v1` | `PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY` | Audit-only complet avant toute matérialisation fill. | - -#### Bloc probes / DEX sans IDL fiable / historiques - -| Version | Decoder / surface | Program id | Objectif | -|---:|---|---|---| -| `0.7.107` | `alphaq` | `ALPHAQmeA7bjrVuccPsYPiCvsi428SNwte66Srvs4pHA` | Demo3 + corpus ; décider support ou abandon. | -| `0.7.108` | `aquifer` | `AQU1FRd7papthgdrwPTTq5JacJh8YtwEXaBfKU3bTz45` | Probe uniquement. | -| `0.7.109` | `bisonfi` | `BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi` | Probe uniquement. | -| `0.7.110` | `dexlab` | `DSwpgjMvXhtGn6BsbqmacdBZyfLj6jSWf3HJpdJtmg6N` | Vérifier support partiel / corpus. | -| `0.7.111` | `fluxbeam` | `FLUXubRmkEi2q6K3Y9kBPg9248ggaZVsoSFhtJHSrm1X` | Vérifier support partiel / corpus. | -| `0.7.112` | `goonfi` | `goonERTdGsjnkZqWuVjs73BZ3Pb9qoCUdBUL17BnS5j` | Probe. | -| `0.7.113` | `goonfi_v2` | `goonuddtQRrWqqn5nFyczVKaie28f3kDkHWkHtURSLE` | Probe ; adresse à revérifier si erreur de taille. | -| `0.7.114` | `humidifi` | `9H6tua7jkLhdm3w8BvgpTn5LZNU7g4ZynDmCiNN3q6Rp` | Probe. | -| `0.7.115` | `obric_v2` | `obriQD1zbpyLz95G5n7nJe6a4DPjpFwa5XYPoNm113y` | Probe. | -| `0.7.116` | `ondo_global_market` | `XzTT4XB8m7sLD2xi6snefSasaswsKCxx5Tifjondogm` | Rôle marché/tokenized assets à confirmer. | -| `0.7.117` | `scorch` | `SCoRcH8c2dpjvcJD6FiPbCSQyQgu3PcUAWj2Xxx3mqn` | Probe. | -| `0.7.118` | `solfi` | `SoLFiHG9TfgtdUXUjWAxi3LtvYuFyDLVhBWxdMZxyCe` | Probe. | -| `0.7.119` | `solfi_v2` | `SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF` | Probe. | -| `0.7.120` | `zerofi` | `ZERor4xhbUycZ6gb9ntrhqscUcZmAbQDjEAtCf4hbZY` | Probe. | -| `0.7.121` | `zora` | `zoRabwLGd5zXaV7Gxacppw8tcceXEiTrSKyNLSaSTUc` | Probe, pas de promotion sans corpus. | -| `0.7.122` | `1dex` | `DEXYosS6oEGvk8uCDayvwEZz4qEyDJRf9nFgYCaqPMTm` | Probe. | -| `0.7.123` | `aldrin_amm` | `AMM55ShdkoGRB5jVYPjWziwk8m5MpwyDgsMWHaMSQWH6` | Historique/probe. | -| `0.7.124` | `aldrin_amm_v2` | `CURVGoZn8zycx6FXwwevgBTB2gVvdbGTEpvMJDbgs2t4` | Historique/probe. | -| `0.7.125` | `crema_finance` | `CLMM9tUoggJu2wagPkkqs9eFG4BWhVBZWkP1qv3Sp7tR` | Historique/probe. | -| `0.7.126` | `cropper_finance` | `CTMAxxk34HjKWxQ3QLZK1HpaLXmBveao3ESePXbiyfzh` | Historique/probe. | -| `0.7.127` | `cropper_whirlpool` | `H8W3ctz92svYg6mkn1UtGfu2aQr2fnUFHM1RhScEtQDt` | Historique/probe. | -| `0.7.128` | `mercurial_stable_swap` | `MERLuDFBMmsHnsBPZw2sDQZHvXFMwp8EdjudcU2HKky` | Historique stable swap ; deltas exacts si support. | -| `0.7.129` | `saber_stable_swap` | `SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ` | Historique stable swap. | -| `0.7.130` | `saros_amm` | `SSwapUtytfBdBn1b9NUGG6foMVPtcWgpRU32HToDUZr` | Historique/probe. | -| `0.7.131` | `step_finance_swap` | `SSwpMgqNDsyV7mAgN9ady4bDVu5ySjmmXejXvy2vLt1` | Historique/probe. | -| `0.7.132` | `stepn_dooar_swap` | `Dooar9JkhdZ7J3LHN3A7YCuoGRUggXhQaG4kijfLGU2j` | Historique/probe. | -| `0.7.133` | `raydium_amm_v2_legacy` | `RVKd61ztZW9GUwhRbbLoYVRE5Xf1B2tVscKqwZqXgEr` | Historique Raydium ; corpus d’abord. | -| `0.7.134` | `raydium_amm_v3_legacy` | `27haf8L6oxUeXrHrgEgsexjSY5hbVUWEmvv9Nyxg8vQv` | Historique Raydium ; ne pas confondre avec CLMM moderne. | -| `0.7.135` | `raydium_pool_v4_json_audit` | aucun `program_id` prouvé par le fichier seul | Vérifier `sol-parser-sdk/idls/raydium_pool_v4.json` après les surfaces documentées ; patch AMM v4 si amélioration, sinon clôture no-op. | - -#### Nettoyage / consolidation - -| Version | Scope | Objectif | -|---:|---|---| -| `0.7.136` | cleanup `SOLSCAN_ACCOUNT_SOURCES` | Retirer les doublons/promotions : les vrais programmes validés deviennent constantes + support matrix ; les comptes non-programmes restent contexte ou sont supprimés. | -| `0.7.137` | base neuve multi-programmes | Replay consolidé, coverage global, zéro faux trade/candle, diagnostics bloquants à zéro. | +| Version cible | Decoder / surface | Program id | Famille | Objectif de clôture | +|---|---|---|---|---| +| `0.7.53` | `pump_swap` | `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA` | Pump / AMM | **Clos** : `buy/sell/buy_exact_quote_in` matérialisés seulement depuis sources exactes ; events Anchor audit-only ; tests synthétiques IDL ; SQL global. | +| `0.7.54` | `pump_fun` | `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` | Pump / launch-bonding | **Clos** : decoder maximal IDL/local, trades directs `buy/sell/buy_exact_sol_in`, v2/exact via `trade_event`, non-trades matérialisés selon contexte, validations Pump.fun propres. | +| `0.7.55` | `pump_fees` | `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` | Pump / fee | **Clos** : `29` instructions, `20` events Anchor, fee/reward/admin/lifecycle, `get_fees` decoded-only, failed tx audit-only, aucun trade/candle direct. | +| `0.7.56` | `meteora_dbc` | `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` | Meteora / DBC | Clos : `28` instructions et `23` events Anchor couverts, swaps `swap/swap2`, lifecycle/admin/fees, `k_sol_fee_event_amounts`, validations SQL propres. | +| `0.7.57` | `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Meteora / DLMM | **Clos** : full decode + full materialization ; `76` instructions IDL, `30` events Anchor, swaps, bins, positions, liquidity, fees/rewards/admin/limit-order, sans double-count. | +| `0.7.58` | `demo4_program_surface_discovery` | n/a | Outillage / discovery | Afficher les contenus non pris en compte : program ids, discriminators, Anchor logs/events, upstream fallbacks, layouts et samples ; aucune matérialisation automatique. | +| `0.7.59` | `sqlite_db_transaction_merger` | n/a | Outillage / consolidation DB | Ajouter un binaire utilisant `kb_lib` pour fusionner plusieurs `.db` de corpus existants vers une base output sans RPC/backfill. | +| `0.7.60` | `meteora_damm_v1` | `Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB` | Meteora / DAMM v1 | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. | +| `0.7.61` | `meteora_damm_v2` | `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` | Meteora / DAMM v2 | Couverture complète : create/custom pools, swaps, liquidity, dynamic config, fees/admin. | +| `0.7.62` | `meteora_vault` | `24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi` | Meteora / vault | Vault deposit/withdraw/fee/accounting ; pas de candle directe. | +| `0.7.63` | `system_program` | `11111111111111111111111111111111` | Système Solana | Create/assign/transfer account ; side effects de contexte, pas de trade. | +| `0.7.64` | `spl_token` | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` | SPL Token | Transfer, mint, burn, close account, sync native ; base transversale pour deltas. | +| `0.7.65` | `spl_token_2022` | `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` | Token-2022 | Transfers/extensions Token-2022, mint/burn/close, comptes et side effects. | +| `0.7.66` | `associated_token_account` | `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL` | Système token | Création ATA, rattachement wallet/token/pool. | +| `0.7.67` | `compute_budget` | `ComputeBudget111111111111111111111111111111` | Contexte tx | Budget/prioritization fee ; utile scoring/MEV, pas de trade. | +| `0.7.68` | `memo` | `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr` | Contexte tx | Mémo transactionnel et attribution éventuelle. | +| `0.7.69` | `address_lookup_table` | `AddressLookupTab1e1111111111111111111111111` | Contexte tx | Résolution/diagnostic ALT si nécessaire. | +| `0.7.70` | `mpl_token_metadata` | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | Metadata | Enrichissement token/NFT/mint metadata. | +| `0.7.71` | `mpl_core` | `CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d` | Metadata / asset | Contexte asset si présent dans corpus. | +| `0.7.72` | `bubblegum` | `BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY` | Compressed assets | Audit/contexte assets compressés, pas DEX. | +| `0.7.73` | `raydium_routing` | `routeUGWgWzqBWFcrCfv8tritsqukccJPu3q5GPP3xS` | Router | Route/legs Raydium ; éviter le double-count avec DEX effectifs. | +| `0.7.74` | `jupiter_swap_v6` | `JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4` | Aggregator | Route attribution, legs, no duplicate trade/candle. | +| `0.7.75` | `jupiter_swap_v4` | `JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB` | Legacy aggregator | Audit/route only si corpus encore utile. | +| `0.7.76` | `dflow_aggregator_v4` | `DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH` | Aggregator | Route/intent/orderflow ; pas de double matérialisation. | +| `0.7.77` | `okx_dex` | `6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma` | Aggregator/router | Route attribution ; trades seulement si source exacte non doublonnée. | +| `0.7.78` | `onchain_labs_dex_v2` | `proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u` | Router/DEX candidat | Corpus d’abord ; classifier route vs DEX effectif. | +| `0.7.79` | `titan_router` | `T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT` | Router | Audit route-only sauf preuve de trade direct non doublonné. | +| `0.7.80` | `sanctum_router` | `stkitrT1Uoy18Dk1fTrgPw8W6MVzoCfYoAFT4MLsmhq` | Router | Route/liquid staking context ; pas de candle DEX directe. | +| `0.7.81` | `orca_whirlpools` | `whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc` | DEX avec IDL/source | Swaps, pools, positions, liquidity, fees/rewards. | +| `0.7.82` | `stabble_stable_swap` | `swapNyd8XiQwJ6ianp9snpu4brUqFxadzvHebnAXjJZ` | DEX avec IDL/source | Stable swap ; deltas exacts, liquidity/admin. | +| `0.7.83` | `stabble_weighted_swap` | `swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW` | DEX avec IDL/source | Weighted swap, deltas exacts, liquidity/admin. | +| `0.7.84` | `stabble_clmm` | `6dMXqGZ3ga2dikrYS9ovDXgHGh5RUsb2RTUj6hrQXhk6` | DEX avec IDL/source | CLMM Stabble si corpus utile. | +| `0.7.85` | `bonkswap` | `BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p` | DEX avec IDL/source | Swap/liquidity/non-trade. | +| `0.7.86` | `boop_fun` | `boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4` | Launch/DEX candidat | Launch/swap/migration selon corpus. | +| `0.7.87` | `byreal_clmm` | `REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2` | DEX avec IDL/source | CLMM ; corpus puis matérialisation contrôlée. | +| `0.7.88` | `fusionamm` | `fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9` | DEX avec IDL/source | AMM ; swaps/liquidity si corpus. | +| `0.7.89` | `goosefx_v1` | `GAMMA7meSFWaBXF25oSUgmGRwaW6sCMFLmBNiMSdbHVT` | DEX avec IDL/source | DEX/AMM selon corpus. | +| `0.7.90` | `goosefx_v2` | `GFXsSL5sSaDfNFQUYsHekbWBW1TsFdjDYzACh62tEHxn` | DEX avec IDL/source | DEX/AMM selon corpus. | +| `0.7.91` | `guac_swap` | `Gswppe6ERWKpUTXvRPfXdzHhiCyJvLadVvXGfdpBqcE1` | DEX avec IDL/source | Swap/liquidity si corpus. | +| `0.7.92` | `hylo_exchange` | `HYEXCHtHkBagdStcJCp3xbbb9B7sdMdWXFNj6mdsG4hn` | DEX/source à classifier | Classer DEX/lending/stable selon IDL/corpus. | +| `0.7.93` | `printr` | `T8HsGYv7sMk3kTnyaRqZrbRPuntYzdh12evXBkprint` | Launch/DEX candidat | Corpus d’abord ; surface launch/swap à confirmer. | +| `0.7.94` | `moonit` | `MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG` | Launch/DEX candidat | Launch/migration/swap si prouvé. | +| `0.7.95` | `metadao_amm_v0_5` | `AMMJdEiCCa8mdugg6JPF7gFirmmxisTfDJoSNSUi5zDJ` | DEX avec source | AMM futarchy ; corpus et price semantics. | +| `0.7.96` | `metadao_bid_wall` | `WALL8ucBuUyL46QYxwYJjidaFYhdvxUFrgvBxPshERx` | Order/bid-wall | Order/bid-wall context ; pas de candle directe sans fill exact. | +| `0.7.97` | `metadao_launchpad` | `moontUzsdepotRGe5xsfip7vLPTJnVuafqdUWexVnPM` | Launch surface | Launch/ICO surface ; pas DEX effectif par défaut. | +| `0.7.98` | `vertigo` | `vrTGoBuy5rYSxAfV3jaRJWHH6nN9WK4NRExGxsk1bCJ` | DEX/source | Swap/launch selon corpus. | +| `0.7.99` | `virtuals` | `5U3EU2ubXtK84QcRjWVmYt9RaDyA8gKxdUrPFXmZyaki` | Launch/DEX candidat | Launch/AMM à confirmer. | +| `0.7.100` | `wavebreak` | `waveQX2yP3H1pVU8djGvEHmYg8uamQ84AuyGtpsrXTF` | DEX/source | Corpus et rôle exact. | +| `0.7.101` | `woofi` | `WooFif76YGRNjk1pA8wCsN67aQsD9f9iLsz4NcJ1AVb` | DEX/router | Swap/route selon corpus. | +| `0.7.102` | `pancake_swap` | `HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq` | DEX/source | DEX Solana à confirmer par corpus. | +| `0.7.103` | `gavel` | `srAMMzfVHVAtgSJc8iH6CfKzuWuUTzLHVCE81QU1rgi` | Source upstream | Corpus d’abord ; rôle exact à classer. | +| `0.7.104` | `heaven` | `HEAVENoP2qxoeuF8Dj2oT1GHEnu49U5mJYkdeC8BAX2o` | Launch/DEX candidat | Launch/DEX selon corpus. | +| `0.7.105` | `lifinity_v2` | `2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c` | DEX legacy/actif | Support selon corpus. | +| `0.7.106` | `moonshot` | `MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG` | Source upstream/Solscan | À dédupliquer avec `moonit` si même program id. | +| `0.7.107` | `openbook_v2` | `opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb` | Orderbook | Audit/orderbook complet ; trade/candle seulement si fills exacts. | +| `0.7.108` | `phoenix_v1` | `PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY` | Orderbook | Audit-only complet avant toute matérialisation fill. | +| `0.7.109` | `alphaq` | `ALPHAQmeA7bjrVuccPsYPiCvsi428SNwte66Srvs4pHA` | Solscan/no IDL | Probe par Demo3 + corpus ; décider support ou abandon. | +| `0.7.110` | `aquifer` | `AQU1FRd7papthgdrwPTTq5JacJh8YtwEXaBfKU3bTz45` | Solscan/no IDL | Probe uniquement. | +| `0.7.111` | `bisonfi` | `BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi` | Solscan/no IDL | Probe uniquement. | +| `0.7.112` | `dexlab` | `DSwpgjMvXhtGn6BsbqmacdBZyfLj6jSWf3HJpdJtmg6N` | Solscan/no IDL | Vérifier support partiel / corpus. | +| `0.7.113` | `fluxbeam` | `FLUXubRmkEi2q6K3Y9kBPg9248ggaZVsoSFhtJHSrm1X` | Solscan/no IDL | Vérifier support partiel / corpus. | +| `0.7.114` | `goonfi` | `goonERTdGsjnkZqWuVjs73BZ3Pb9qoCUdBUL17BnS5j` | Solscan/no IDL | Probe. | +| `0.7.115` | `goonfi_v2` | `goonuddtQRrWqqn5nFyczVKaie28f3kDkHWkHtURSLE` | Solscan/no IDL | Probe ; adresse à revérifier si erreur de taille. | +| `0.7.116` | `humidifi` | `9H6tua7jkLhdm3w8BvgpTn5LZNU7g4ZynDmCiNN3q6Rp` | Solscan/no IDL | Probe. | +| `0.7.117` | `obric_v2` | `obriQD1zbpyLz95G5n7nJe6a4DPjpFwa5XYPoNm113y` | Solscan/no IDL | Probe. | +| `0.7.118` | `ondo_global_market` | `XzTT4XB8m7sLD2xi6snefSasaswsKCxx5Tifjondogm` | Solscan IDL | Rôle marché/tokenized assets à confirmer. | +| `0.7.119` | `scorch` | `SCoRcH8c2dpjvcJD6FiPbCSQyQgu3PcUAWj2Xxx3mqn` | Solscan/no IDL | Probe. | +| `0.7.120` | `solfi` | `SoLFiHG9TfgtdUXUjWAxi3LtvYuFyDLVhBWxdMZxyCe` | Solscan/no IDL | Probe. | +| `0.7.121` | `solfi_v2` | `SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF` | Solscan/no IDL | Probe. | +| `0.7.122` | `zerofi` | `ZERor4xhbUycZ6gb9ntrhqscUcZmAbQDjEAtCf4hbZY` | Solscan/no IDL | Probe. | +| `0.7.123` | `zora` | `zoRabwLGd5zXaV7Gxacppw8tcceXEiTrSKyNLSaSTUc` | Solscan/no IDL | Probe, pas de promotion sans corpus. | +| `0.7.124` | `1dex` | `DEXYosS6oEGvk8uCDayvwEZz4qEyDJRf9nFgYCaqPMTm` | Solscan/no IDL | Probe. | +| `0.7.125` | `aldrin_amm` | `AMM55ShdkoGRB5jVYPjWziwk8m5MpwyDgsMWHaMSQWH6` | Legacy/no IDL | Historique/probe. | +| `0.7.126` | `aldrin_amm_v2` | `CURVGoZn8zycx6FXwwevgBTB2gVvdbGTEpvMJDbgs2t4` | Legacy/no IDL | Historique/probe. | +| `0.7.127` | `crema_finance` | `CLMM9tUoggJu2wagPkkqs9eFG4BWhVBZWkP1qv3Sp7tR` | Legacy/no IDL | Historique/probe. | +| `0.7.128` | `cropper_finance` | `CTMAxxk34HjKWxQ3QLZK1HpaLXmBveao3ESePXbiyfzh` | Legacy/no IDL | Historique/probe. | +| `0.7.129` | `cropper_whirlpool` | `H8W3ctz92svYg6mkn1UtGfu2aQr2fnUFHM1RhScEtQDt` | Legacy/no IDL | Historique/probe. | +| `0.7.130` | `mercurial_stable_swap` | `MERLuDFBMmsHnsBPZw2sDQZHvXFMwp8EdjudcU2HKky` | Legacy/no IDL | Historique stable swap ; deltas exacts si support. | +| `0.7.131` | `saber_stable_swap` | `SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ` | Legacy/no IDL | Historique stable swap. | +| `0.7.132` | `saros_amm` | `SSwapUtytfBdBn1b9NUGG6foMVPtcWgpRU32HToDUZr` | Legacy/no IDL | Historique/probe. | +| `0.7.133` | `step_finance_swap` | `SSwpMgqNDsyV7mAgN9ady4bDVu5ySjmmXejXvy2vLt1` | Legacy/no IDL | Historique/probe. | +| `0.7.134` | `stepn_dooar_swap` | `Dooar9JkhdZ7J3LHN3A7YCuoGRUggXhQaG4kijfLGU2j` | Legacy/no IDL | Historique/probe. | +| `0.7.135` | `raydium_amm_v2_legacy` | `RVKd61ztZW9GUwhRbbLoYVRE5Xf1B2tVscKqwZqXgEr` | Raydium legacy/no IDL | Historique Raydium ; corpus d’abord. | +| `0.7.136` | `raydium_amm_v3_legacy` | `27haf8L6oxUeXrHrgEgsexjSY5hbVUWEmvv9Nyxg8vQv` | Raydium legacy/no IDL | Historique Raydium ; ne pas confondre avec CLMM moderne. | +| `0.7.137` | `raydium_pool_v4_json_audit` | aucun `program_id` prouvé par le fichier seul | Audit source annexe | Vérifier `sol-parser-sdk/idls/raydium_pool_v4.json` après les surfaces documentées ; patch AMM v4 si amélioration, sinon clôture no-op. | +| `0.7.138` | cleanup `SOLSCAN_ACCOUNT_SOURCES` | n/a | Nettoyage registry/constants | Retirer doublons/promotions ; les programmes validés deviennent constantes/support matrix, les comptes non-programmes restent contexte ou sont supprimés. | +| `0.7.139` | base neuve multi-programmes | n/a | Validation consolidée | Replay consolidé, coverage global, zéro faux trade/candle, diagnostics bloquants à zéro. | Garde-fous constants : pas de faux trade, pas de fausse candle, pas de `program_id` fictif, pas de promotion sans corpus transactionnel local, pas de double matérialisation router/leg, pas de logique métier DEX profonde dans `kb_demo_app`. @@ -1737,8 +1724,8 @@ Préconditions considérées acquises avant cette reprise : Ordre de travail recommandé pour la suite : 1. `0.7.44` : ledger de décodage/replay et skip sûr — acquis ; -2. `0.7.45` : `meteora_dlmm` — clos ; -3. `0.7.46` : `meteora_damm_v1` — clos côté corpus local ; +2. `0.7.45` : `meteora_dlmm` historique — clos ; +3. `0.7.46` : `meteora_damm_v1` historique — clos côté corpus local ; 4. `0.7.47` : Upstream Git Registry / DEX discovery preparation — acquis ; 5. `0.7.48-pre` : event coverage + DB model checkpoint — clos ; 6. `0.7.48` : `raydium_cpmm` — clos ; @@ -1746,10 +1733,16 @@ Ordre de travail recommandé pour la suite : 8. `0.7.50-pre-r2` : `raydium_launchpad` clos + re-vérification CPMM/CLMM ; 9. `0.7.51` : `raydium_amm_v4` — clos ; 10. `0.7.52` : `raydium_stable_swap` — clos ; -11. `0.7.53` : `pump_swap` / `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA` — clos ; -12. `0.7.54` : `pump_fun` / `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` ; -13. `0.7.55` : `pump_fees` / `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` ; -14. `0.7.56+` : appliquer le phasage strict “une version = un `program_id`” défini en section `6.085`. +11. `0.7.53` : `pump_swap` — clos ; +12. `0.7.54` : `pump_fun` — clos ; +13. `0.7.55` : `pump_fees` — clos ; +14. `0.7.56` : `meteora_dbc` — clos ; +15. `0.7.57` : `meteora_dlmm` — clos ; +16. `0.7.58` : `demo4_program_surface_discovery` — à faire ; +17. `0.7.59` : `sqlite_db_transaction_merger` — à faire ; +18. `0.7.60` : `meteora_damm_v1` — prochaine tranche DEX ; +19. `0.7.61` : `meteora_damm_v2` — après DAMM v1 ; +20. `0.7.62+` : appliquer le phasage actif défini en section `6.085`. Garde-fous constants : diff --git a/docs/DEX_DECODER_MATRIX.md b/docs/DEX_DECODER_MATRIX.md index 26e79f7..63a9987 100644 --- a/docs/DEX_DECODER_MATRIX.md +++ b/docs/DEX_DECODER_MATRIX.md @@ -1,9 +1,15 @@ -# DEX Decoder Matrix — `khadhroony-bobobot` `0.7.56 meteora_dbc closed` +# DEX Decoder Matrix — `khadhroony-bobobot` `0.7.57 meteora_dlmm closed` +## Note `0.7.57 closed` — Meteora DLMM clôturé et suite discovery + +La tranche `0.7.57` ferme `meteora_dlmm` / `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`. Elle remplace la couverture partielle historique `0.7.45` par un decoder local maximal aligné sur l'IDL locale, avec correction du discriminant `75c73e67068e1fcb` en `initialize_preset_parameter_v2`. Les résidus observés utiles ne restent plus en decoded-only. + +La prochaine étape n'est pas un nouveau decoder direct, mais `0.7.58 demo4_program_surface_discovery` : affichage et scoring des contenus inconnus/non pris en compte, sans auto-materialization ni auto-promotion. + ## Note `0.7.56 closed` — Meteora DBC clôturé, DLMM ensuite La tranche `0.7.56` ferme `meteora_dbc` / `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` depuis l'IDL locale `idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json`. @@ -81,7 +87,7 @@ Cette matrice complète `kb_lib/src/dex_support_matrix.rs`. Elle documente **ce | 8 | `pump_fun` | `supported / 0.7.54 closed` | Surface launch/bonding/migration Pump.fun couverte localement ; trades directs et `trade_event` canonique validés. | Ne rouvrir que pour bug prouvé ou changement externe. | | 9 | `pump_fees` | `supported / 0.7.55 closed` | Surface fee/config/accounting couverte localement : `29` instructions, `20` events Anchor, fee/reward/admin/lifecycle, tests synthétiques Anchor IDL non observés, failed tx audit-only. | Aucun trade/candle direct ; conserver les deux discriminators Solscan hors IDL comme futures surfaces non observées. | | 10 | `meteora_dbc` | `supported / closed 0.7.56` | Decoder local maximal : `28` instructions, `23` events Anchor, swaps `swap/swap2`, lifecycle/admin/fees, `k_sol_fee_event_amounts`, validation SQL propre. | Ne pas rouvrir sauf bug prouvé ; préserver la policy fee parent+legs et la recovery allowlistée. | -| 11 | `meteora_dlmm` | `next / 0.7.57 full decode` | Couverture partielle historique validée en `0.7.45`; IDL locale `76` instructions / `30` events Anchor. | Reprendre depuis base neuve : full decode, full materialization fiable, fees/rewards via `fee_event_amounts`, no double-count avec events Anchor. | +| 11 | `meteora_dlmm` | `supported / closed 0.7.57` | Decoder local maximal : `76` instructions IDL, `30` events Anchor, `12` accounts, correction `initialize_preset_parameter_v2`, swaps/exact-out, liquidity, bins, positions, lifecycle, fees/rewards, admin/config et orderbook. | Ne pas rouvrir sauf bug prouvé ; `swap_event/swap2_evt` restent lifecycle `swap_log`, pas trade ; préserver recovery fee/reward allowlistée. | | 12 | `meteora_damm_v1` | `supported / 0.7.58 parity` | Couverture `0.7.46` : swap, create_pool, add/remove liquidity, claim_fee, create_lock_escrow, lock_liquidity. | Vérifier les surfaces upstream non observées ; améliorer rattachement pool/pair pour remove_liquidity non matérialisés ; revalidation stricte. | | 13 | `meteora_damm_v2` | `partial / 0.7.59 planned` | `swap`, `instruction_audit`, registry/discriminants et corpus Demo3 existent. | Couvrir tous les events Carbon/source : create pool, liquidity, fees, dynamic config, admin ; déterminer actionability des swaps ; matérialiser si montants fiables. | | 14 | `phoenix_v1` | `audit-only / 0.7.60 planned` | Decoder local audit-only ; `log_audit`, order place/cancel, withdraw ; parsing strict `0x0f`; events `Reduce`, `Place`, `TimeInForce` observés ; `trade_count=0`. | Terminer tous les events Git : `Fill`, `FillSummary`, `Fee`, `Evict`, `ExpiredOrder`, etc. ; ajouter counts/flags audit ; seulement ensuite étudier trade materialization. | @@ -346,8 +352,14 @@ La tranche a été validée sur base SQLite dédiée : tous les discriminants `0 Validation finale : `446` tests, clippy OK, `480 replayed`, `264 trades`, `1 liquidity`, `122 lifecycle`, `1056 candles`, `89` parents fee DBC, `96` fee amount legs, invariants SQL propres. -## 0.7.57 — Meteora DLMM prochain +## 0.7.57 — Meteora DLMM clôturé -| Decoder | Program id | Statut | Source locale | Couverture attendue | Décision métier | +| Decoder | Program id | Statut | Source locale | Couverture finale | Décision métier | |---|---|---|---|---|---| -| `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | `next / full decode + full materialization` | `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json` | `76` instructions IDL + `30` events Anchor IDL | Swaps/exact-out, liquidity/bin/position, fees/rewards/admin/config, limit order events ; no duplicate trade/candle ; `fee_event_amounts` obligatoire pour fees. | +| `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | `supported / closed 0.7.57` | `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json` | `76` instructions IDL, `30` events Anchor, `12` accounts, `initialize_preset_parameter_v2` ajouté depuis corpus local | Les 6 swaps instructionnels produisent seuls les trades/candles ; Anchor `swap_event/swap2_evt` = lifecycle `swap_log`; liquidity/bin/position/lifecycle/admin/orderbook/fee/reward matérialisés selon cible ; failed tx audit-only. | + +Validation finale : `460` tests, clippy OK, `769 replayed`, `106 trades`, `664 liquidity`, `1107 lifecycle`, `424 candles`, `8062` instruction observations, catalogue `169/218/218`. Checks SQL bloquants propres. + +### 0.7.57 pre.001 — Meteora DLMM + +`meteora_dlmm` passe de tranche `next` à tranche ouverte `pre.001` : l’IDL locale complète est inventoriée, tous les discriminants instruction/event sont classifiés localement, et les familles non prouvées restent decoded-only/audit-safe avec `skip*Reason`. diff --git a/docs/DEX_EVENT_COVERAGE_MATRIX.md b/docs/DEX_EVENT_COVERAGE_MATRIX.md index c976644..c663601 100644 --- a/docs/DEX_EVENT_COVERAGE_MATRIX.md +++ b/docs/DEX_EVENT_COVERAGE_MATRIX.md @@ -1,6 +1,6 @@ -# DEX Event Coverage Matrix — `khadhroony-bobobot` `0.7.54 pump_fun closed` +# DEX Event Coverage Matrix — `khadhroony-bobobot` `0.7.57 meteora_dlmm closed` Cette matrice complète `docs/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. @@ -326,17 +326,28 @@ Programme : `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN`. Source locale priorit Validation finale DBC : `89` parents fee, `96` legs fee amount, aucun parent scalaire sans leg, aucun leg orphelin, aucun decoded event local sans coverage, aucun failed tx matérialisé, aucun multi-target, aucun non-swap vers trade/candle. -## 0.7.57 — Meteora DLMM à reprendre +## `0.7.57` — `meteora_dlmm` final coverage -Programme : `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`. Source locale : `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json`. +Programme : `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`. Source locale : `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json`. Surface finale : `76` instructions IDL, `30` events Anchor, `12` accounts, plus correction locale `initialize_preset_parameter_v2` (`75c73e67068e1fcb`) observée dans le corpus. -| Groupe | Entrées IDL | Famille | Cible DB attendue | Décision à valider | +Validation finale : `460` tests, clippy OK, `769` replayed, `106` trades, `664` liquidity, `1107` lifecycle, `424` candle upserts, `8062` instruction observations. Les checks decoded sans coverage, successful non-materialized, failed tx materialized, multi-target, non-swap trade/candle, fee scalar sans leg, orphan fee legs et coverage duplicates sont vides. + +| Famille | Entrées DLMM | Statut `0.7.57` | Cible DB | Justification / règle | |---|---|---|---|---| -| swaps | `swap`, `swap2`, `swap_exact_out*`, `swap_with_price_impact*`, `Swap`, `Swap2Evt` | swap | `k_sol_trade_events` + candles | Montants exécutés uniquement ; éviter double-count instruction/event. | -| pools / bins | `initialize_*lb_pair*`, `initialize_bin_array*`, `close_bin_array`, `LbPairCreate` | lifecycle | `k_sol_pool_lifecycle_events`, catalog/pair | Catalog si comptes pool/mints/config fiables. | -| liquidity / positions | `add_liquidity*`, `remove_liquidity*`, `remove_all_liquidity`, `rebalance_liquidity`, position create/close/update events | liquidity/lifecycle | `k_sol_liquidity_events`, `k_sol_pool_lifecycle_events` | Montants x/y/bin/liquidity fiables ; sinon skip reason. | -| fees | `claim_fee*`, `withdraw_protocol_fee`, `zap_protocol_fee`, `ClaimFee*`, `CompositionFee` | fee | `k_sol_fee_events` + `k_sol_fee_event_amounts` | Utiliser le socle fee parent+legs ; policy recovery explicite, non globale. | -| rewards | `initialize_reward`, `fund_reward`, `claim_reward*`, `withdraw_ineligible_reward`, reward events | reward | `k_sol_reward_events` | Montant/mint/récompense fiable uniquement. | -| admin/config | fee parameters, pair status, activation, operator, token badge, preset parameter | admin_config | `k_sol_pool_admin_events` ou decoded-only | Matérialiser si acteur/compte cible fiable. | -| limit/order events | place/cancel/close limit order events | orderbook/audit | `k_sol_orderbook_events` si modèle fiable | Pas de candle directe sans fill exact. | +| `swap` | `swap`, `swap2`, `swap_exact_out`, `swap_exact_out2`, `swap_with_price_impact`, `swap_with_price_impact2` | `materialized` | `k_sol_trade_events` + candles | Seules ces instructions produisent trade/candle direct quand montants/mints/pool sont fiables. | +| `swap_log` | `swap_event`, `swap2_evt` | `materialized` | `k_sol_pool_lifecycle_events` | Logs Anchor informatifs matérialisés sans double-count trade/candle. | +| `pool_create` | `create_pool`, `lb_pair_create_event`, `initialize_*_lb_pair*`, `initialize_permission_lb_pair` | `materialized` si observé, sinon `upstream_git_mapped_unverified` | `k_sol_pool_lifecycle_events` | Pool lifecycle ; pas de trade direct. | +| `liquidity_add` | `add_liquidity*`, `add_liquidity_event` | `materialized` | `k_sol_liquidity_events` | Montants/side effects liquidity ; aucun trade/candle. | +| `liquidity_remove` | `remove_liquidity*`, `remove_all_liquidity`, `remove_liquidity_event` | `materialized` | `k_sol_liquidity_events` | Retrait liquidity ; failed tx audit-only. | +| `liquidity_change` | `rebalance_liquidity`, `rebalancing_event` | `materialized` | `k_sol_liquidity_events` | Changement de liquidité/bin ; pas de trade. | +| `position_open` | `initialize_position*`, `position_create_event` | `materialized` | `k_sol_pool_lifecycle_events` | Position/bin lifecycle, pas liquidity artificielle quand l'opération est structurelle. | +| `position_close` | `close_position*`, `position_close_event` | `materialized` | `k_sol_pool_lifecycle_events` | Fermeture position lifecycle. | +| `position_update` | `increase_position_length*`, `decrease_position_length*`, `update_position_operator*`, position update events | `materialized` si observé | `k_sol_pool_lifecycle_events` | Mise à jour de position ; pas admin générique. | +| `bin/oracle lifecycle` | `initialize_bin_array*`, `close_bin_array`, `go_to_a_bin*`, `increase_oracle_length`, `migrate_bin_array` | `materialized` si observé | `k_sol_pool_lifecycle_events` | `close_bin_array` a `16/14` car `2` failed tx `Custom 6015`, comportement attendu. | +| `fee` | `claim_fee*`, `claim_fee*_event`, `composition_fee_event`, `withdraw_protocol_fee`, `zap_protocol_fee` | `materialized` | `k_sol_fee_events` + `k_sol_fee_event_amounts` | Recovery inner SPL transfer strictement allowlistée ; legs multi-mint conservés sans agrégation artificielle. | +| `reward` | `claim_reward*`, `claim_reward*_event`, `fund_reward*`, `initialize_reward*`, `withdraw_ineligible_reward*` | `materialized` si observé | `k_sol_reward_events` | Claims/funding avec amounts récupérés quand transfert réel ; `initialize_reward*` sans amount est normal. | +| `admin/config` | `update_base_fee_parameters`, `fee_parameter_update_event`, `set_pair_status*`, `set_activation_point`, `set_pre_activation*`, preset/token badge/operator/config, `initialize_preset_parameter_v2` | `materialized` si observé | `k_sol_pool_admin_events` | Admin/config non financier ; pas trade/candle. | +| `orderbook` | `place_limit_order*`, `cancel_limit_order*`, `close_limit_order*` | `materialized` | `k_sol_orderbook_events` | Limit/orderbook events sans trade/candle. | +| `account metadata` | `lb_pair`, `bin_array`, `position_v2`, `oracle`, `operator`, `token_badge`, etc. | `upstream_git_mapped_unverified` / non observé | `k_sol_dex_decoded_events_only` | Accounts IDL listés pour coverage, pas de matérialisation métier directe. | +| `instruction_audit` | résidu inconnu | `closed` | `k_sol_pool_admin_events` seulement si observé | Résidu final observé = `0`; `75c73e67068e1fcb` a été promu en `initialize_preset_parameter_v2`. | diff --git a/docs/VALIDATION_STATUS_0_7_57_FINAL.md b/docs/VALIDATION_STATUS_0_7_57_FINAL.md new file mode 100644 index 0000000..9606f56 --- /dev/null +++ b/docs/VALIDATION_STATUS_0_7_57_FINAL.md @@ -0,0 +1,73 @@ +# Validation status — 0.7.57 Meteora DLMM final + +## Build + +```text +cargo test -p kb_lib -> 460 passed / 0 failed +cargo clippy -p kb_lib --all-targets -- -D warnings -> OK +``` + +## Replay + +Recommended replay settings used: + +```text +skipDexDecode=no +forceDexDecode=yes +deferInstructionObservations=yes +``` + +Final replay summary: + +```text +769 replayed +0 decode skipped +769 ledger upserts +646 unsafe ledger rows +106 trades +664 liquidity +1107 lifecycle +0 tokenAccount +424 candle upserts +instructionObservations = 8062 +resetDeleted = 9898 +catalog = 169 tokens / 218 pools / 218 pairs +``` + +## Final SQL gates + +| Gate | Result | +|---|---:| +| Upstream fallback for local DLMM coverage | empty | +| Local `instruction_audit` observed | `0` | +| Decoded DLMM without coverage | empty | +| Successful non-materialized without explicit skip/policy | empty | +| Failed transaction business materialization | empty | +| Multi-target materialization | empty | +| Non-swap trade/candle safety | empty | +| Fee parent scalar without amount leg | empty | +| Orphan fee amount legs | empty | +| Reward/fee collision | clean separation | +| Limit/orderbook trade/candle double-count | empty | +| Logical duplicate coverage rows | empty | + +## Final materialization counters + +```text +trade events = 106 +liquidity events = 664 +lifecycle events = 1107 +candles = 424 +``` + +## Important decisions + +- `swap_event` and `swap2_evt` are materialized as lifecycle `swap_log`, not as trades. +- `initialize_preset_parameter_v2` was added from corpus evidence for discriminator `75c73e67068e1fcb`. +- `close_bin_array` has `16` observations but only `14` materializations because `2` transactions failed with `Custom 6015`. +- `initialize_reward` and `initialize_reward_event` remain without amounts because they are configuration/init events, not transfers. +- No observed useful DLMM event remains decoded-only. + +## Closure decision + +`0.7.57 meteora_dlmm` is considered closed for the validated corpus. Future changes require new corpus evidence. diff --git a/docs/prompts/PROMPT_0_7_58_DEMO4_PROGRAM_SURFACE_DISCOVERY.md b/docs/prompts/PROMPT_0_7_58_DEMO4_PROGRAM_SURFACE_DISCOVERY.md new file mode 100644 index 0000000..cc1fa27 --- /dev/null +++ b/docs/prompts/PROMPT_0_7_58_DEMO4_PROGRAM_SURFACE_DISCOVERY.md @@ -0,0 +1,245 @@ + + +# Prompt — 0.7.58 Demo4 Program Surface Discovery + +## Contexte + +Nous reprenons le workspace Rust/Tauri `khadhroony-bobobot` après la clôture de `0.7.57 meteora_dlmm`. + +État validé : + +```text +0.7.57 meteora_dlmm clos +cargo test -p kb_lib -> 460 passed +cargo clippy -p kb_lib --all-targets -- -D warnings -> OK +replay -> 769 replayed, 106 trades, 664 liquidity, 1107 lifecycle, 424 candles +instructionObservations = 8062 +catalogue = 169 tokens / 218 pools / 218 pairs +``` + +Contraintes projet à respecter : + +- Rust 2024. +- Code async-first. +- Pas de `?`, `unwrap`, `expect` dans le code applicatif. +- Pas de `anyhow`, pas de `thiserror`. +- Pas de `mod.rs`. +- Pas de `pub mod` ; utiliser `mod` privé + `pub use`. +- Imports uniquement pour les traits quand c’est possible. +- `#![deny(unreachable_pub)]` et `#![warn(missing_docs)]` doivent rester propres. +- Tests offline. +- Les nouvelles requêtes DB doivent mettre à jour les re-exports dans `kb_lib/src/db.rs` puis `kb_lib/src/lib.rs`. +- Les structs DB doivent aller dans `kb_lib/src/db/entities/*.rs` et `kb_lib/src/db/dtos/*.rs`, pas dans les fichiers de requêtes. + +## Objectif + +Ajouter `Demo4` dans `kb_demo_app` et les fonctions de lecture associées dans `kb_lib` pour afficher les surfaces de programmes non encore prises en compte par le système. + +Cette fonctionnalité est strictement un cockpit de découverte et de revue. + +Elle ne doit pas : + +- matérialiser automatiquement des contenus inconnus ; +- promouvoir automatiquement des discriminators inconnus dans les décodeurs officiels ; +- marquer automatiquement un `program_id` comme supporté ; +- créer automatiquement des lignes `trade`, `candle`, `fee`, `liquidity`, `reward`, `admin` ou `orderbook` depuis un contenu inconnu. + +Elle doit : + +- afficher les surfaces inconnues ou non supportées ; +- les regrouper de manière exploitable ; +- montrer les preuves et signatures samples ; +- aider à produire des prompts/patchs futurs validés manuellement. + +## Comportement attendu côté utilisateur + +Créer une nouvelle page ou fenêtre, selon le pattern existant : + +```text +kb_demo_app/src/demo4.html +kb_demo_app/src/demo4.ts +``` + +ou l’équivalent si l’application utilise un autre routage. + +Demo4 doit proposer des vues pour : + +1. discriminators inconnus sur des decoders connus ; +2. `program_id` connus contenant des instructions non mappées par le decoder spécialisé ; +3. `upstream_git.instruction_match` groupés par `upstreamDecoderCode`, `upstreamEntryName`, `upstreamDiscriminatorHex` ; +4. `program_id` observés dans les transactions mais absents de la matrice de support ; +5. logs Anchor `Program log: Instruction: ...` non mappés localement ; +6. events Anchor `Program data:` non mappés localement ; +7. layouts de comptes récurrents sur instructions inconnues ; +8. répartition success/failed pour chaque surface candidate. + +Filtres souhaités : + +```text +program_id +candidate_decoder_code +discriminator_hex +instruction_name / nom log Anchor +upstream decoder +upstream entry name +success only / failed only / both +minimum observation count +minimum tx count +from slot / to slot +signature contains +show only unknown +show only upstream fallback +show only high-confidence candidates +``` + +Colonnes souhaitées : + +```text +candidate_kind +program_id +candidate_decoder +known/unknown status +discriminator_hex +log_instruction_name +anchor_event_discriminator +account_count +layout_hash +observed_count +tx_count +success_count +failed_count +first_slot +last_slot +sample_signature +confidence_score +confidence_reason +status +``` + +Le panneau de détail doit afficher : + +```text +sample signatures +accounts_json +data_json / data prefix +logs de transaction +inner instructions +parsed_json si disponible +decoded_payload_json si disponible +entrées upstream registry correspondantes +preuve de hash Anchor si un nom d’instruction est disponible +``` + +## Design `kb_lib` + +Ajouter des requêtes read-only et des DTOs. Fichiers suggérés : + +```text +kb_lib/src/program_surface_discovery.rs +kb_lib/src/db/dtos/program_surface_candidate_summary.rs +kb_lib/src/db/dtos/program_surface_candidate_sample.rs +kb_lib/src/db/queries/program_surface_discovery.rs +``` + +Si un statut persistant est nécessaire, ajouter : + +```text +kb_lib/src/db/entities/program_surface_candidate.rs +kb_lib/src/db/queries/program_surface_candidates.rs +``` + +Ne pas placer les DTO/entities dans les fichiers de requêtes. + +Statuts candidats suggérés : + +```text +new +reviewed +accepted +rejected +promoted +ignored +``` + +Si une table persistante est ajoutée, la migration doit être explicite, idempotente et testée. + +## Scoring de découverte + +Le scoring est indicatif uniquement. Il ne doit jamais déclencher une matérialisation. + +Signaux de confiance possibles : + +| Signal | Effet | +|---|---:| +| `program_id` connu, discriminator inconnu | +20 | +| log Anchor `Instruction: X` trouvé | +30 | +| `sha256("global:x")` correspond au discriminator | +50 | +| observations success répétées | +10 | +| account count/layout stable | +10 | +| inner instruction family cohérente avec l’action | +10 | +| uniquement des transactions failed | -20 | +| nom très générique comme `swap`, `claim`, `initialize` sans autre preuve | -15 | +| `program_id` inconnu et observation unique | -25 | + +## Helper Anchor + +Ajouter un helper qui normalise les noms d’instructions Anchor depuis les logs : + +```text +Program log: Instruction: InitializePresetParameterV2 +-> initialize_preset_parameter_v2 +-> sha256("global:initialize_preset_parameter_v2")[0..8] +``` + +Ce helper doit seulement produire une preuve/candidate record. Il ne doit pas modifier les mappings de decoder. + +## Export Markdown + +Demo4 doit permettre d’exporter un groupe candidat en Markdown pour une future session d’implémentation. + +L’export doit inclure : + +```text +candidate decoder +program_id +discriminator_hex +candidate name +confidence score +preuves/evidence +sample signatures +account layout +logs +inner instruction summary +proposed target table éventuelle +warning explicite : non implémenté, non matérialisé +``` + +## Tests + +Ajouter des tests offline pour : + +- normalisation des noms Anchor depuis logs ; +- calcul de discriminators Anchor pour des noms samples ; +- groupement par program/discriminator/layout ; +- ordre de score ; +- sérialisation/désérialisation des DTOs si utilisés ; +- validation des paramètres de commandes UI. + +Aucun test ne doit dépendre de RPC ou du réseau. + +## Validation + +```bash +cargo fmt +cargo test -p kb_lib program_surface +cargo test -p kb_lib +cargo clippy -p kb_lib --all-targets -- -D warnings +``` + +Pour Tauri/frontend, lancer la commande de build/test existante du projet si elle existe. + +## Résultat attendu + +L’utilisateur peut ouvrir Demo4 et voir immédiatement les surfaces unsupported/upstream/unknown présentes dans la base SQLite courante, sans rescanner la blockchain. + +Demo4 est un cockpit de découverte uniquement. Il ne change pas la matérialisation métier. diff --git a/docs/prompts/PROMPT_0_7_58_demo4_program_surface_discovery.md b/docs/prompts/PROMPT_0_7_58_demo4_program_surface_discovery.md new file mode 100644 index 0000000..f93fefb --- /dev/null +++ b/docs/prompts/PROMPT_0_7_58_demo4_program_surface_discovery.md @@ -0,0 +1,245 @@ + + +# Prompt — 0.7.58 Demo4 Program Surface Discovery + +## Contexte + +Nous reprenons le workspace Rust/Tauri `khadhroony-bobobot` après la clôture de `0.7.57 meteora_dlmm`. + +État validé : + +```text +0.7.57 meteora_dlmm clos +cargo test -p kb_lib -> 460 passed +cargo clippy -p kb_lib --all-targets -- -D warnings -> OK +replay -> 769 replayed, 106 trades, 664 liquidity, 1107 lifecycle, 424 candles +instructionObservations = 8062 +catalogue = 169 tokens / 218 pools / 218 pairs +``` + +Contraintes projet à respecter : + +- Rust 2024. +- Code async-first. +- Pas de `?`, `unwrap`, `expect` dans le code applicatif. +- Pas de `anyhow`, pas de `thiserror`. +- Pas de `mod.rs`. +- Pas de `pub mod` ; utiliser `mod` privé + `pub use`. +- Imports uniquement pour les traits quand c’est possible. +- `#![deny(unreachable_pub)]` et `#![warn(missing_docs)]` doivent rester propres. +- Tests offline. +- Les nouvelles requêtes DB doivent mettre à jour les re-exports dans `kb_lib/src/db.rs` puis `kb_lib/src/lib.rs`. +- Les structs DB doivent aller dans `kb_lib/src/db/entities/*.rs` et `kb_lib/src/db/dtos/*.rs`, pas dans les fichiers de requêtes. + +## Objectif + +Ajouter `Demo4` dans `kb_demo_app` et les fonctions de lecture associées dans `kb_lib` pour afficher les surfaces de programmes non encore prises en compte par le système. + +Cette fonctionnalité est strictement un cockpit de découverte et de revue. + +Elle ne doit pas : + +- matérialiser automatiquement des contenus inconnus ; +- promouvoir automatiquement des discriminators inconnus dans les décodeurs officiels ; +- marquer automatiquement un `program_id` comme supporté ; +- créer automatiquement des lignes `trade`, `candle`, `fee`, `liquidity`, `reward`, `admin` ou `orderbook` depuis un contenu inconnu. + +Elle doit : + +- afficher les surfaces inconnues ou non supportées ; +- les regrouper de manière exploitable ; +- montrer les preuves et signatures samples ; +- aider à produire des prompts/patchs futurs validés manuellement. + +## Comportement attendu côté utilisateur + +Créer une nouvelle page ou fenêtre, selon le pattern existant : + +```text +kb_demo_app/src/demo4.html +kb_demo_app/src/demo4.ts +``` + +ou l’équivalent si l’application utilise un autre routage. + +Demo4 doit proposer des vues pour : + +1. discriminators inconnus sur des decoders connus ; +2. `program_id` connus contenant des instructions non mappées par le decoder spécialisé ; +3. `upstream_git.instruction_match` groupés par `upstreamDecoderCode`, `upstreamEntryName`, `upstreamDiscriminatorHex` ; +4. `program_id` observés dans les transactions mais absents de la matrice de support ; +5. logs Anchor `Program log: Instruction: ...` non mappés localement ; +6. events Anchor `Program data:` non mappés localement ; +7. layouts de comptes récurrents sur instructions inconnues ; +8. répartition success/failed pour chaque surface candidate. + +Filtres souhaités : + +```text +program_id +candidate_decoder_code +discriminator_hex +instruction_name / nom log Anchor +upstream decoder +upstream entry name +success only / failed only / both +minimum observation count +minimum tx count +from slot / to slot +signature contains +show only unknown +show only upstream fallback +show only high-confidence candidates +``` + +Colonnes souhaitées : + +```text +candidate_kind +program_id +candidate_decoder +known/unknown status +discriminator_hex +log_instruction_name +anchor_event_discriminator +account_count +layout_hash +observed_count +tx_count +success_count +failed_count +first_slot +last_slot +sample_signature +confidence_score +confidence_reason +status +``` + +Le panneau de détail doit afficher : + +```text +sample signatures +accounts_json +data_json / data prefix +logs de transaction +inner instructions +parsed_json si disponible +decoded_payload_json si disponible +entrées upstream registry correspondantes +preuve de hash Anchor si un nom d’instruction est disponible +``` + +## Design `kb_lib` + +Ajouter des requêtes read-only et des DTOs. Fichiers suggérés : + +```text +kb_lib/src/program_surface_discovery.rs +kb_lib/src/db/dtos/program_surface_candidate_summary.rs +kb_lib/src/db/dtos/program_surface_candidate_sample.rs +kb_lib/src/db/queries/program_surface_discovery.rs +``` + +Si un statut persistant est nécessaire, ajouter : + +```text +kb_lib/src/db/entities/program_surface_candidate.rs +kb_lib/src/db/queries/program_surface_candidates.rs +``` + +Ne pas placer les DTO/entities dans les fichiers de requêtes. + +Statuts candidats suggérés : + +```text +new +reviewed +accepted +rejected +promoted +ignored +``` + +Si une table persistante est ajoutée, la migration doit être explicite, idempotente et testée. + +## Scoring de découverte + +Le scoring est indicatif uniquement. Il ne doit jamais déclencher une matérialisation. + +Signaux de confiance possibles : + +| Signal | Effet | +|---|---:| +| `program_id` connu, discriminator inconnu | +20 | +| log Anchor `Instruction: X` trouvé | +30 | +| `sha256("global:x")` correspond au discriminator | +50 | +| observations success répétées | +10 | +| account count/layout stable | +10 | +| inner instruction family cohérente avec l’action | +10 | +| uniquement des transactions failed | -20 | +| nom très générique comme `swap`, `claim`, `initialize` sans autre preuve | -15 | +| `program_id` inconnu et observation unique | -25 | + +## Helper Anchor + +Ajouter un helper qui normalise les noms d’instructions Anchor depuis les logs : + +```text +Program log: Instruction: InitializePresetParameterV2 +-> initialize_preset_parameter_v2 +-> sha256("global:initialize_preset_parameter_v2")[0..8] +``` + +Ce helper doit seulement produire une preuve/candidate record. Il ne doit pas modifier les mappings de decoder. + +## Export Markdown + +Demo4 doit permettre d’exporter un groupe candidat en Markdown pour une future session d’implémentation. + +L’export doit inclure : + +```text +candidate decoder +program_id +discriminator_hex +candidate name +confidence score +preuves/evidence +sample signatures +account layout +logs +inner instruction summary +proposed target table éventuelle +warning explicite : non implémenté, non matérialisé +``` + +## Tests + +Ajouter des tests offline pour : + +- normalisation des noms Anchor depuis logs ; +- calcul de discriminators Anchor pour des noms samples ; +- groupement par program/discriminator/layout ; +- ordre de score ; +- sérialisation/désérialisation des DTOs si utilisés ; +- validation des paramètres de commandes UI. + +Aucun test ne doit dépendre de RPC ou du réseau. + +## Validation + +```bash +cargo fmt +cargo test -p kb_lib program_surface +cargo test -p kb_lib +cargo clippy -p kb_lib --all-targets -- -D warnings +``` + +Pour Tauri/frontend, lancer la commande de build/test existante du projet si elle existe. + +## Résultat attendu + +L’utilisateur peut ouvrir Demo4 et voir immédiatement les surfaces unsupported/upstream/unknown présentes dans la base SQLite courante, sans rescanner la blockchain. + +Demo4 est un cockpit de découverte uniquement. Il ne change pas la matérialisation métier. diff --git a/docs/prompts/PROMPT_0_7_59_SQLITE_DB_TRANSACTION_MERGER_BINARY.md b/docs/prompts/PROMPT_0_7_59_SQLITE_DB_TRANSACTION_MERGER_BINARY.md new file mode 100644 index 0000000..7385dc4 --- /dev/null +++ b/docs/prompts/PROMPT_0_7_59_SQLITE_DB_TRANSACTION_MERGER_BINARY.md @@ -0,0 +1,249 @@ + + +# Prompt — 0.7.59 Binaire de fusion de bases SQLite transactionnelles + +## Contexte + +Nous reprenons le workspace Rust `khadhroony-bobobot` après la clôture de `0.7.57 meteora_dlmm` et après/ou en parallèle de `0.7.58 demo4_program_surface_discovery`. + +L’utilisateur possède plusieurs bases SQLite dédiées construites pendant les tranches Raydium, Pump et Meteora. L’objectif est d’éviter de rescanner/backfiller des signatures déjà présentes dans ces bases. + +## Objectif + +Ajouter un module binaire qui utilise `kb_lib` pour fusionner des données de corpus transactionnel depuis plusieurs fichiers SQLite `.db` vers une base SQLite de sortie unique. + +Le merger doit copier suffisamment de données brutes transaction/instruction pour permettre un replay local dans la base consolidée, sans refaire les appels RPC. + +Ce binaire est un outil de consolidation de corpus. Ce n’est pas un scanner blockchain. + +## Forme recommandée + +Créer un nouveau package workspace : + +```text +kb_tools/ + Cargo.toml + src/bin/kb_db_merge.rs +``` + +Le binaire doit dépendre de `kb_lib` et réutiliser autant que possible les fonctions de schéma/setup/query existantes. + +Alternative acceptable si le workspace préfère ce pattern : + +```text +kb_lib/src/bin/kb_db_merge.rs +``` + +Préférer `kb_tools` si cela évite de mélanger du code CLI-only dans `kb_lib`. + +## Interface CLI + +Commande suggérée : + +```bash +cargo run -p kb_tools --bin kb_db_merge -- \ + --output ./merged_corpus.db \ + --input ./raydium.db \ + --input ./pump.db \ + --input ./meteora.db \ + --mode raw-corpus \ + --dry-run +``` + +Options requises ou utiles : + +```text +--output +--input répétable +--mode +--dry-run optionnel +--replace-output optionnel ; sinon refuser si output existe +--source-label optionnel ou inféré depuis le nom de fichier +--limit optionnel pour tests +``` + +Modes initiaux : + +```text +raw-corpus +full-copy-safe +signatures-only +``` + +## Mode `raw-corpus` + +Mode par défaut recommandé. + +Copier les données brutes canonique nécessaires au replay local sans RPC : + +```text +k_sol_chain_transactions +k_sol_chain_instructions +``` + +Si le schéma contient d’autres tables brutes/contextuelles strictement nécessaires, les inspecter avant de les ajouter. Ne copier que les tables sûres, sans duplication sémantique. + +La base output doit ensuite pouvoir exécuter le replay/décodage local depuis les transactions persistées. + +## Mode `full-copy-safe` + +Mode optionnel pour plus tard. + +Copier aussi des lignes décodées/matérialisées seulement si les foreign keys, versions de decoder et versions de schéma sont cohérentes. + +Ne pas l’implémenter comme mode par défaut. Il est plus dangereux car les lignes matérialisées dépendent de la version du code. + +## Mode `signatures-only` + +Copier uniquement les signatures/slots dans une table de staging afin qu’un outil futur décide quoi importer/rejouer. + +## Politique de déduplication + +Identité primaire : + +```text +signature +``` + +Règles : + +- Une même signature dans plusieurs inputs doit produire une seule transaction dans l’output. +- Si `meta_json`, `transaction_json`, `slot` ou `err_json` diffèrent pour une même signature, enregistrer un conflit ; ne pas écraser silencieusement. +- Préférer la ligne brute la plus complète uniquement si le conflit est explicable et journalisé. +- Conserver la provenance dans des tables d’audit de merge. + +Tables suggérées : + +```text +k_sol_db_merge_sources +k_sol_db_merge_transactions +k_sol_db_merge_conflicts +``` + +Champs source suggérés : + +```text +source_id +source_path +source_label +source_schema_version +source_created_at +input_transaction_count +copied_transaction_count +skipped_duplicate_count +conflict_count +``` + +Pour chaque signature copiée : + +```text +signature +slot +source_id +source_transaction_id +copied_at +copy_status +conflict_status +``` + +## Règles de sécurité + +- Ne pas appeler RPC. +- Ne pas backfiller. +- Ne pas décoder DEX pendant le merge. +- Ne pas matérialiser trades/candles pendant le merge. +- Ne pas supposer une compatibilité de schéma sans inspection. +- Ne pas utiliser `ATTACH` avec des chemins non validés/échappés. +- Pas de `?`, `unwrap`, `expect` dans le code applicatif. +- Les erreurs doivent être explicites, typées projet, et lisibles. +- Le mode dry-run doit indiquer exactement ce qui serait copié, ignoré ou marqué conflictuel. + +## Compatibilité de schéma + +Le binaire doit inspecter `sqlite_master` et `PRAGMA table_info(...)` dans chaque DB input. + +Tables requises pour `raw-corpus` : + +```text +k_sol_chain_transactions +k_sol_chain_instructions +``` + +Vérifier les colonnes requises par nom. Si une colonne optionnelle manque, l’indiquer clairement et continuer seulement si la copie reste suffisante pour un replay local fiable. + +Si le schéma est incompatible, échouer proprement avec un résumé court. + +## Stratégie de copie + +Algorithme recommandé : + +1. Ouvrir/créer la base output et initialiser le schéma courant via `kb_lib`. +2. Pour chaque input DB : + - inspecter le schéma ; + - enregistrer la source ; + - itérer les transactions par slot/signature ; + - si la signature est absente de l’output, insérer la transaction et ses instructions enfants ; + - si la signature existe, comparer les champs canoniques et ignorer ou enregistrer un conflit ; + - préserver le mapping `source_transaction_id -> output_transaction_id` pour les instructions. +3. Commit par batches. +4. Imprimer un résumé final. + +La taille de batch doit être configurable ou conservatrice. + +## SQL de validation + +Ajouter : + +```text +validation_sql/SQL_VALIDATION_DB_MERGE_0_7_59.sql +``` + +Checks minimaux : + +```sql +-- duplicate signatures in output should be empty +SELECT signature, COUNT(*) +FROM k_sol_chain_transactions +GROUP BY signature +HAVING COUNT(*) > 1; + +-- orphan instructions should be empty +SELECT ins.id +FROM k_sol_chain_instructions ins +LEFT JOIN k_sol_chain_transactions tx ON tx.id = ins.transaction_id +WHERE tx.id IS NULL; + +-- conflicts summary +SELECT * +FROM k_sol_db_merge_conflicts +ORDER BY id DESC +LIMIT 100; +``` + +## Tests + +Ajouter des tests offline avec bases SQLite temporaires : + +1. merge d’une DB vers un output vide ; +2. merge de deux DBs avec signatures disjointes ; +3. merge de deux DBs avec signature identique et contenu identique ; +4. signature dupliquée avec conflit slot/meta et ligne conflict enregistrée ; +5. instructions recopiées avec le nouveau `transaction_id` output ; +6. dry-run sans écriture des lignes output ; +7. table requise manquante -> erreur propre. + +## Documentation + +Mettre à jour : + +```text +README.md +ROADMAP.md +CHANGELOG.md +validation_sql/SQL_VALIDATION_DB_MERGE_0_7_59.sql +docs/reports/SQLITE_DB_TRANSACTION_MERGER_0_7_59.md +``` + +## Résultat attendu + +L’utilisateur peut consolider ses anciennes bases Raydium/Pump/Meteora dans une base output et relancer un replay local depuis les lignes brutes existantes, sans refaire les backfills RPC de signatures. diff --git a/docs/prompts/PROMPT_0_7_59_sqlite_db_transaction_merger_binary.md b/docs/prompts/PROMPT_0_7_59_sqlite_db_transaction_merger_binary.md new file mode 100644 index 0000000..8a7ed6b --- /dev/null +++ b/docs/prompts/PROMPT_0_7_59_sqlite_db_transaction_merger_binary.md @@ -0,0 +1,249 @@ + + +# Prompt — 0.7.59 Binaire de fusion de bases SQLite transactionnelles + +## Contexte + +Nous reprenons le workspace Rust `khadhroony-bobobot` après la clôture de `0.7.57 meteora_dlmm` et après/ou en parallèle de `0.7.58 demo4_program_surface_discovery`. + +L’utilisateur possède plusieurs bases SQLite dédiées construites pendant les tranches Raydium, Pump et Meteora. L’objectif est d’éviter de rescanner/backfiller des signatures déjà présentes dans ces bases. + +## Objectif + +Ajouter un module binaire qui utilise `kb_lib` pour fusionner des données de corpus transactionnel depuis plusieurs fichiers SQLite `.db` vers une base SQLite de sortie unique. + +Le merger doit copier suffisamment de données brutes transaction/instruction pour permettre un replay local dans la base consolidée, sans refaire les appels RPC. + +Ce binaire est un outil de consolidation de corpus. Ce n’est pas un scanner blockchain. + +## Forme recommandée + +Créer un nouveau package workspace : + +```text +kb_tools/ + Cargo.toml + src/bin/kb_db_merge.rs +``` + +Le binaire doit dépendre de `kb_lib` et réutiliser autant que possible les fonctions de schéma/setup/query existantes. + +Alternative acceptable si le workspace préfère ce pattern : + +```text +kb_lib/src/bin/kb_db_merge.rs +``` + +Préférer `kb_tools` si cela évite de mélanger du code CLI-only dans `kb_lib`. + +## Interface CLI + +Commande suggérée : + +```bash +cargo run -p kb_tools --bin kb_db_merge -- \ + --output ./merged_corpus.db \ + --input ./raydium.db \ + --input ./pump.db \ + --input ./meteora.db \ + --mode raw-corpus \ + --dry-run +``` + +Options requises ou utiles : + +```text +--output +--input répétable +--mode +--dry-run optionnel +--replace-output optionnel ; sinon refuser si output existe +--source-label optionnel ou inféré depuis le nom de fichier +--limit optionnel pour tests +``` + +Modes initiaux : + +```text +raw-corpus +full-copy-safe +signatures-only +``` + +## Mode `raw-corpus` + +Mode par défaut recommandé. + +Copier les données brutes canonique nécessaires au replay local sans RPC : + +```text +k_sol_chain_transactions +k_sol_chain_instructions +``` + +Si le schéma contient d’autres tables brutes/contextuelles strictement nécessaires, les inspecter avant de les ajouter. Ne copier que les tables sûres, sans duplication sémantique. + +La base output doit ensuite pouvoir exécuter le replay/décodage local depuis les transactions persistées. + +## Mode `full-copy-safe` + +Mode optionnel pour plus tard. + +Copier aussi des lignes décodées/matérialisées seulement si les foreign keys, versions de decoder et versions de schéma sont cohérentes. + +Ne pas l’implémenter comme mode par défaut. Il est plus dangereux car les lignes matérialisées dépendent de la version du code. + +## Mode `signatures-only` + +Copier uniquement les signatures/slots dans une table de staging afin qu’un outil futur décide quoi importer/rejouer. + +## Politique de déduplication + +Identité primaire : + +```text +signature +``` + +Règles : + +- Une même signature dans plusieurs inputs doit produire une seule transaction dans l’output. +- Si `meta_json`, `transaction_json`, `slot` ou `err_json` diffèrent pour une même signature, enregistrer un conflit ; ne pas écraser silencieusement. +- Préférer la ligne brute la plus complète uniquement si le conflit est explicable et journalisé. +- Conserver la provenance dans des tables d’audit de merge. + +Tables suggérées : + +```text +k_sol_db_merge_sources +k_sol_db_merge_transactions +k_sol_db_merge_conflicts +``` + +Champs source suggérés : + +```text +source_id +source_path +source_label +source_schema_version +source_created_at +input_transaction_count +copied_transaction_count +skipped_duplicate_count +conflict_count +``` + +Pour chaque signature copiée : + +```text +signature +slot +source_id +source_transaction_id +copied_at +copy_status +conflict_status +``` + +## Règles de sécurité + +- Ne pas appeler RPC. +- Ne pas backfiller. +- Ne pas décoder DEX pendant le merge. +- Ne pas matérialiser trades/candles pendant le merge. +- Ne pas supposer une compatibilité de schéma sans inspection. +- Ne pas utiliser `ATTACH` avec des chemins non validés/échappés. +- Pas de `?`, `unwrap`, `expect` dans le code applicatif. +- Les erreurs doivent être explicites, typées projet, et lisibles. +- Le mode dry-run doit indiquer exactement ce qui serait copié, ignoré ou marqué conflictuel. + +## Compatibilité de schéma + +Le binaire doit inspecter `sqlite_master` et `PRAGMA table_info(...)` dans chaque DB input. + +Tables requises pour `raw-corpus` : + +```text +k_sol_chain_transactions +k_sol_chain_instructions +``` + +Vérifier les colonnes requises par nom. Si une colonne optionnelle manque, l’indiquer clairement et continuer seulement si la copie reste suffisante pour un replay local fiable. + +Si le schéma est incompatible, échouer proprement avec un résumé court. + +## Stratégie de copie + +Algorithme recommandé : + +1. Ouvrir/créer la base output et initialiser le schéma courant via `kb_lib`. +2. Pour chaque input DB : + - inspecter le schéma ; + - enregistrer la source ; + - itérer les transactions par slot/signature ; + - si la signature est absente de l’output, insérer la transaction et ses instructions enfants ; + - si la signature existe, comparer les champs canoniques et ignorer ou enregistrer un conflit ; + - préserver le mapping `source_transaction_id -> output_transaction_id` pour les instructions. +3. Commit par batches. +4. Imprimer un résumé final. + +La taille de batch doit être configurable ou conservatrice. + +## SQL de validation + +Ajouter : + +```text +validation_sql/SQL_VALIDATION_DB_MERGE_0_7_59.sql +``` + +Checks minimaux : + +```sql +-- duplicate signatures in output should be empty +SELECT signature, COUNT(*) +FROM k_sol_chain_transactions +GROUP BY signature +HAVING COUNT(*) > 1; + +-- orphan instructions should be empty +SELECT ins.id +FROM k_sol_chain_instructions ins +LEFT JOIN k_sol_chain_transactions tx ON tx.id = ins.transaction_id +WHERE tx.id IS NULL; + +-- conflicts summary +SELECT * +FROM k_sol_db_merge_conflicts +ORDER BY id DESC +LIMIT 100; +``` + +## Tests + +Ajouter des tests offline avec bases SQLite temporaires : + +1. merge d’une DB vers un output vide ; +2. merge de deux DBs avec signatures disjointes ; +3. merge de deux DBs avec signature identique et contenu identique ; +4. signature dupliquée avec conflit slot/meta et ligne conflict enregistrée ; +5. instructions recopiées avec le nouveau `transaction_id` output ; +6. dry-run sans écriture des lignes output ; +7. table requise manquante -> erreur propre. + +## Documentation + +Mettre à jour : + +```text +README.md +ROADMAP.md +CHANGELOG.md +validation_sql/SQL_VALIDATION_DB_MERGE_0_7_59.sql +docs/reports/SQLITE_DB_TRANSACTION_MERGER_0_7_59.md +``` + +## Résultat attendu + +L’utilisateur peut consolider ses anciennes bases Raydium/Pump/Meteora dans une base output et relancer un replay local depuis les lignes brutes existantes, sans refaire les backfills RPC de signatures. diff --git a/docs/prompts/PROMPT_0_7_60_METEORA_DAMM_NEXT_DEX.md b/docs/prompts/PROMPT_0_7_60_METEORA_DAMM_NEXT_DEX.md new file mode 100644 index 0000000..cdd9294 --- /dev/null +++ b/docs/prompts/PROMPT_0_7_60_METEORA_DAMM_NEXT_DEX.md @@ -0,0 +1,198 @@ + + +# Prompt — 0.7.60 Meteora DAMM v1 puis DAMM v2 + +## Décision + +Ne pas fusionner Meteora DAMM v1 et Meteora DAMM v2 dans une seule tranche d’implémentation. + +Utiliser des versions séparées : + +```text +0.7.60 -> meteora_damm_v1 +0.7.61 -> meteora_damm_v2 +``` + +Raison : + +- `program_id` différents ; +- surfaces IDL/discriminators différentes ; +- corpus historiques et besoins de validation différents ; +- clôture SQL plus lisible ; +- risque réduit de faux positifs cross-version ; +- rollback plus simple si une version contient une hypothèse incorrecte. + +Partage autorisé : + +- helpers communs privés ; +- patterns communs de récupération fee/reward amounts ; +- conventions communes de coverage/tests ; +- structure commune de rapport ; +- template SQL commun. + +Ne pas partager la classification de decoder d’une manière qui masque les discriminators version-spécifiques ou les layouts propres à chaque programme. + +## Contexte + +`0.7.57 meteora_dlmm` est clos. Les tranches intermédiaires planifiées sont : + +```text +0.7.58 -> demo4_program_surface_discovery +0.7.59 -> sqlite_db_transaction_merger +``` + +La prochaine tranche DEX doit reprendre la famille Meteora après DLMM/DBC, en commençant par DAMM v1. + +Surfaces Meteora connues : + +```text +meteora_dbc -> clos 0.7.56 +meteora_dlmm -> clos 0.7.57 +meteora_damm_v1 -> 0.7.60 +meteora_damm_v2 -> 0.7.61 +meteora_vault -> plus tard, surface séparée +``` + +## Cible 0.7.60 — meteora_damm_v1 + +Program id depuis la roadmap : + +```text +Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB +``` + +Objectif principal : + +```text +Full decode + full materialization pour toutes les instructions/events DAMM v1 disponibles depuis les IDL locales et les sources upstream. +``` + +Familles attendues : + +```text +swap +pool_create +add_liquidity +remove_liquidity +lock/unlock liquidity si présent +fee/admin/config +lifecycle/migration si présent +``` + +Règles : + +- Pas de decoded-only pour une entrée utile observée si elle peut être matérialisée sûrement. +- Ne pas créer de trade/candle depuis des non-swaps. +- Ne pas matérialiser les transactions failed. +- Ne pas agréger artificiellement les fees multi-mint. +- Utiliser `k_sol_fee_event_amounts` pour les legs fee fiables. +- Utiliser la récupération inner SPL transfer seulement avec allowlist explicite et tests. +- Conserver toutes les entrées IDL/upstream non observées dans la coverage avec `0/0` et proof status clair. + +## Sources à vérifier + +Inspecter d’abord le répertoire local : + +```text +idls/ +``` + +Puis comparer avec : + +```text +Carbon decoders +Pinax/substreams-solana-idls +0xfnzero sol-parser-sdk / solana-streamer +ancien code local et rapports historiques du projet +samples Solscan / backfills Demo3 +``` + +Ne pas considérer les anciens résultats `0.7.36` ou `0.7.46` comme clôture finale. Revalider depuis le schéma courant et les règles actuelles de matérialisation. + +## Plan de travail + +1. Créer ou rafraîchir les entrées coverage `meteora_damm_v1`. +2. Vérifier le `program_id` et la surface IDL locale. +3. Étendre l’enum/local classifier des discriminators. +4. Ajouter le naming dans `instruction_observation_index`. +5. Décoder toutes les instructions et tous les events Anchor disponibles. +6. Matérialiser swaps, liquidity, lifecycle, fee, reward/admin/orderbook si applicable. +7. Ajouter des tests synthétiques pour chaque instruction/event IDL, y compris non observé. +8. Ajouter le SQL de validation. +9. Rejouer sur base fraîche avec : + +```text +skipDexDecode=no +forceDexDecode=yes +deferInstructionObservations=yes +``` + +10. Clore seulement si les gates SQL sont propres. + +## Gates SQL obligatoires + +Reprendre le pattern DLMM : + +```text +fallback upstream pour entrées localement couvertes -> vide +instruction_name null/blank -> vide +decoded sans coverage -> vide +successful non-materialized without skip/policy -> vide +failed tx materialization -> vide +multi-target materialization -> vide +non-swap trade/candle -> vide +fee scalar parent without leg -> vide +orphan fee legs -> vide +coverage logical duplicates -> vide +watchlist sans backlog meteora_damm_v1 +``` + +## Cible 0.7.61 — meteora_damm_v2 + +Program id depuis la roadmap : + +```text +cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG +``` + +N’ouvrir cette tranche qu’après clôture DAMM v1. + +La tranche v2 peut réutiliser le template et des helpers privés, mais doit conserver ses propres éléments : + +```text +decoder code +coverage entries +instruction enum +event mapping +synthetic tests +SQL validation +rapport +``` + +## Livrables attendus pour 0.7.60 + +Fichiers probables : + +```text +kb_lib/src/dex/meteora_damm_v1.rs +kb_lib/src/dex_event_coverage.rs +kb_lib/src/dex_event_classification.rs +kb_lib/src/instruction_observation_index.rs +kb_lib/src/non_trade_event_materialization.rs +validation_sql/SQL_VALIDATION_METEORA_DAMM_V1_0_7_60.sql +docs/reports/METEORA_DAMM_V1_EVENT_COVERAGE_REPORT.md +``` + +Mettre à jour les re-exports si des modules ou requêtes sont ajoutés : + +```text +kb_lib/src/dex.rs +kb_lib/src/lib.rs +kb_lib/src/db.rs +``` + +## Note finale + +Si la découverte corpus montre que DAMM v1 et v2 partagent une logique basse-niveau précise, créer des helpers privés communs. + +Ne pas fusionner les deux protocoles dans un seul decoder, une seule coverage ou un seul rapport de clôture. diff --git a/docs/prompts/PROMPT_0_7_60_meteora_damm_next_dex.md b/docs/prompts/PROMPT_0_7_60_meteora_damm_next_dex.md new file mode 100644 index 0000000..117f5ce --- /dev/null +++ b/docs/prompts/PROMPT_0_7_60_meteora_damm_next_dex.md @@ -0,0 +1,198 @@ + + +# Prompt — 0.7.60 Meteora DAMM v1 puis DAMM v2 + +## Décision + +Ne pas fusionner Meteora DAMM v1 et Meteora DAMM v2 dans une seule tranche d’implémentation. + +Utiliser des versions séparées : + +```text +0.7.60 -> meteora_damm_v1 +0.7.61 -> meteora_damm_v2 +``` + +Raison : + +- `program_id` différents ; +- surfaces IDL/discriminators différentes ; +- corpus historiques et besoins de validation différents ; +- clôture SQL plus lisible ; +- risque réduit de faux positifs cross-version ; +- rollback plus simple si une version contient une hypothèse incorrecte. + +Partage autorisé : + +- helpers communs privés ; +- patterns communs de récupération fee/reward amounts ; +- conventions communes de coverage/tests ; +- structure commune de rapport ; +- template SQL commun. + +Ne pas partager la classification de decoder d’une manière qui masque les discriminators version-spécifiques ou les layouts propres à chaque programme. + +## Contexte + +`0.7.57 meteora_dlmm` est clos. Les tranches intermédiaires planifiées sont : + +```text +0.7.58 -> demo4_program_surface_discovery +0.7.59 -> sqlite_db_transaction_merger +``` + +La prochaine tranche DEX doit reprendre la famille Meteora après DLMM/DBC, en commençant par DAMM v1. + +Surfaces Meteora connues : + +```text +meteora_dbc -> clos 0.7.56 +meteora_dlmm -> clos 0.7.57 +meteora_damm_v1 -> 0.7.60 +meteora_damm_v2 -> 0.7.61 +meteora_vault -> plus tard, surface séparée +``` + +## Cible 0.7.60 — meteora_damm_v1 + +Program id depuis la roadmap : + +```text +Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB +``` + +Objectif principal : + +```text +Full decode + full materialization pour toutes les instructions/events DAMM v1 disponibles depuis les IDL locales et les sources upstream. +``` + +Familles attendues : + +```text +swap +pool_create +add_liquidity +remove_liquidity +lock/unlock liquidity si présent +fee/admin/config +lifecycle/migration si présent +``` + +Règles : + +- Pas de decoded-only pour une entrée utile observée si elle peut être matérialisée sûrement. +- Ne pas créer de trade/candle depuis des non-swaps. +- Ne pas matérialiser les transactions failed. +- Ne pas agréger artificiellement les fees multi-mint. +- Utiliser `k_sol_fee_event_amounts` pour les legs fee fiables. +- Utiliser la récupération inner SPL transfer seulement avec allowlist explicite et tests. +- Conserver toutes les entrées IDL/upstream non observées dans la coverage avec `0/0` et proof status clair. + +## Sources à vérifier + +Inspecter d’abord le répertoire local : + +```text +idls/ +``` + +Puis comparer avec : + +```text +Carbon decoders +Pinax/substreams-solana-idls +0xfnzero sol-parser-sdk / solana-streamer +ancien code local et rapports historiques du projet +samples Solscan / backfills Demo3 +``` + +Ne pas considérer les anciens résultats `0.7.36` ou `0.7.46` comme clôture finale. Revalider depuis le schéma courant et les règles actuelles de matérialisation. + +## Plan de travail + +1. Créer ou rafraîchir les entrées coverage `meteora_damm_v1`. +2. Vérifier le `program_id` et la surface IDL locale. +3. Étendre l’enum/local classifier des discriminators. +4. Ajouter le naming dans `instruction_observation_index`. +5. Décoder toutes les instructions et tous les events Anchor disponibles. +6. Matérialiser swaps, liquidity, lifecycle, fee, reward/admin/orderbook si applicable. +7. Ajouter des tests synthétiques pour chaque instruction/event IDL, y compris non observé. +8. Ajouter le SQL de validation. +9. Rejouer sur base fraîche avec : + +```text +skipDexDecode=no +forceDexDecode=yes +deferInstructionObservations=yes +``` + +10. Clore seulement si les gates SQL sont propres. + +## Gates SQL obligatoires + +Reprendre le pattern DLMM : + +```text +fallback upstream pour entrées localement couvertes -> vide +instruction_name null/blank -> vide +decoded sans coverage -> vide +successful non-materialized without skip/policy -> vide +failed tx materialization -> vide +multi-target materialization -> vide +non-swap trade/candle -> vide +fee scalar parent without leg -> vide +orphan fee legs -> vide +coverage logical duplicates -> vide +watchlist sans backlog meteora_damm_v1 +``` + +## Cible 0.7.61 — meteora_damm_v2 + +Program id depuis la roadmap : + +```text +cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG +``` + +N’ouvrir cette tranche qu’après clôture DAMM v1. + +La tranche v2 peut réutiliser le template et des helpers privés, mais doit conserver ses propres éléments : + +```text +decoder code +coverage entries +instruction enum +event mapping +synthetic tests +SQL validation +rapport +``` + +## Livrables attendus pour 0.7.60 + +Fichiers probables : + +```text +kb_lib/src/dex/meteora_damm_v1.rs +kb_lib/src/dex_event_coverage.rs +kb_lib/src/dex_event_classification.rs +kb_lib/src/instruction_observation_index.rs +kb_lib/src/non_trade_event_materialization.rs +validation_sql/SQL_VALIDATION_METEORA_DAMM_V1_0_7_60.sql +docs/reports/METEORA_DAMM_V1_EVENT_COVERAGE_REPORT.md +``` + +Mettre à jour les re-exports si des modules ou requêtes sont ajoutés : + +```text +kb_lib/src/dex.rs +kb_lib/src/lib.rs +kb_lib/src/db.rs +``` + +## Note finale + +Si la découverte corpus montre que DAMM v1 et v2 partagent une logique basse-niveau précise, créer des helpers privés communs. + +Ne pas fusionner les deux protocoles dans un seul decoder, une seule coverage ou un seul rapport de clôture. diff --git a/docs/reports/METEORA_DLMM_EVENT_COVERAGE_REPORT.md b/docs/reports/METEORA_DLMM_EVENT_COVERAGE_REPORT.md new file mode 100644 index 0000000..ded828c --- /dev/null +++ b/docs/reports/METEORA_DLMM_EVENT_COVERAGE_REPORT.md @@ -0,0 +1,182 @@ +# Meteora DLMM event coverage report — 0.7.57 final + +## Scope + +Version `0.7.57` closes `meteora_dlmm` from the local IDL and the dedicated local corpus. + +Target program: + +```text +LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo +``` + +Local IDL: + +```text +idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json +``` + +Final inventoried surface: + +```text +76 IDL instructions +30 Anchor events +12 accounts +``` + +The local corpus additionally identified discriminator `75c73e67068e1fcb` as `initialize_preset_parameter_v2`. This discriminator was not present in the local IDL under that spelling, but the transaction logs contain `Instruction: InitializePresetParameterV2`, the account layout is stable, and the instruction creates a 36-byte DLMM-owned preset parameter account through the system program. + +## Final validation summary + +```text +cargo test -p kb_lib -> 460 passed / 0 failed +cargo clippy -p kb_lib --all-targets -- -D warnings -> OK +769 replayed +0 decode skipped +769 ledger upserts +646 unsafe ledger rows +106 trades +664 liquidity +1107 lifecycle +0 tokenAccount +424 candle upserts +instructionObservations = 8062 +resetDeleted = 9898 +catalog = 169 tokens / 218 pools / 218 pairs +``` + +## Closure checks + +All blocking checks are clean: + +```text +upstream_git fallback for locally covered meteora_dlmm entries -> empty +local instruction_audit observed -> 0 +DLMM decoded events without coverage -> empty +successful non-materialized DLMM without explicit skip/policy -> empty +failed transaction business materialization -> empty +multi-target materialization -> empty +non-swap trade/candle safety -> empty +fee parent scalar without fee amount leg -> empty +orphan fee amount legs -> empty +limit/orderbook trade/candle double-count -> empty +logical duplicate coverage rows -> empty +``` + +The only coverage difference left is explained and non-blocking: + +```text +close_bin_array observed 16 / materialized 14 +14 successful transactions -> lifecycle materialized +2 failed transactions Custom 6015 -> decoded/audit only, no business materialization +``` + +## Materialization policy + +### Trades and candles + +Only instruction-level DLMM swaps can produce trades/candles: + +```text +meteora_dlmm.swap +meteora_dlmm.swap2 +meteora_dlmm.swap_exact_out +meteora_dlmm.swap_exact_out2 +meteora_dlmm.swap_with_price_impact +meteora_dlmm.swap_with_price_impact2 +``` + +Anchor `swap_event` and `swap2_evt` are materialized as lifecycle `swap_log` rows and never as trades/candles. This prevents double-counting when an instruction swap and an Anchor log describe the same user-visible swap. + +### Liquidity and lifecycle + +The following families are materialized when transactions are successful and context is reliable: + +```text +add_liquidity* +remove_liquidity* +remove_all_liquidity +rebalance_liquidity +initialize_bin_array* +close_bin_array +go_to_a_bin* +initialize_position* +close_position* +increase_position_length* +decrease_position_length* +update_position_operator* +lb_pair_create_event +position_create_event +position_close_event +``` + +Position and bin lifecycle events are routed to `k_sol_pool_lifecycle_events` when the operation is structural rather than a direct liquidity amount delta. + +### Fees + +Fee parents are written to `k_sol_fee_events`. Amount legs are written to `k_sol_fee_event_amounts`. + +Final observed amount-leg summary: + +```text +claim_fee 64 parents / 64 legs +claim_fee2 63 parents / 88 legs +claim_fee_event 127 parents / 187 legs +claim_fee2_event 78 parents / 118 legs +composition_fee_event 51 parents / 63 legs +withdraw_protocol_fee 14 parents / 20 legs +zap_protocol_fee 13 parents / 13 legs +``` + +The inner SPL transfer recovery is explicitly allowlisted for DLMM fee event kinds. It is not inherited globally by future decoders. + +### Rewards + +Reward parents are written to `k_sol_reward_events`. Amounts are recovered only when a reliable inner SPL transfer or decoded amount is present. + +Final observed scalar amount summary: + +```text +claim_reward 15 parents / 1 scalar amount +claim_reward2 19 parents / 10 scalar amounts +claim_reward_event 34 parents / 25 scalar amounts +claim_reward2_event 19 parents / 10 scalar amounts +fund_reward 10 parents / 10 scalar amounts +fund_reward_event 10 parents / 10 scalar amounts +initialize_reward 9 parents / 0 amount +initialize_reward_event 9 parents / 0 amount +``` + +`initialize_reward` and `initialize_reward_event` remain without amount because they are configuration/initialization events, not executed reward transfers. No amount is fabricated from limits, bounds or configuration fields. + +### Admin/config and orderbook + +Admin/config events materialize to `k_sol_pool_admin_events`. Limit-order operations materialize to `k_sol_orderbook_events` and never to trade/candle tables. + +Important promoted local corpus entry: + +```text +75c73e67068e1fcb -> meteora_dlmm.initialize_preset_parameter_v2 -> k_sol_pool_admin_events +``` + +## Remaining non-observed surfaces + +Non-observed IDL/account rows remain in coverage with `observed_count = 0` and `materialized_count = 0`. They are not blockers. + +Examples: + +```text +update_reward_duration +update_reward_funder +withdraw_ineligible_reward +migrate_position +account metadata rows +for_idl_type_generation_do_not_call +anchor_self_cpi_log registry row +``` + +## Decision + +`0.7.57 meteora_dlmm` is closed. Do not add further DLMM Rust patches unless a new corpus proves a successful transaction that is decoded but not materialized and lacks an explicit skip/policy reason. + +Next recommended work is not another DLMM patch: implement `0.7.58 demo4_program_surface_discovery` to display unknown or unsupported program surfaces without automatic materialization or automatic decoder promotion. diff --git a/kb_demo_app/package.json b/kb_demo_app/package.json index 09aa2d8..cc470a7 100644 --- a/kb_demo_app/package.json +++ b/kb_demo_app/package.json @@ -1,7 +1,7 @@ { "name": "kb-demo-app", "private": true, - "version": "0.7.56", + "version": "0.7.57", "type": "module", "scripts": { "dev": "vite", diff --git a/kb_demo_app/tauri.conf.json b/kb_demo_app/tauri.conf.json index 1564bcc..71380c8 100644 --- a/kb_demo_app/tauri.conf.json +++ b/kb_demo_app/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "kb-demo-app", - "version": "0.7.56", + "version": "0.7.57", "identifier": "com.sasedev.kb-demo-app", "build": { "beforeDevCommand": "npm run dev", diff --git a/kb_lib/src/dex/meteora_dlmm.rs b/kb_lib/src/dex/meteora_dlmm.rs index e32b348..6b337b1 100644 --- a/kb_lib/src/dex/meteora_dlmm.rs +++ b/kb_lib/src/dex/meteora_dlmm.rs @@ -6,20 +6,225 @@ //! decoded events when the projected instruction or transaction logs expose //! clear DLMM create/swap hints. -const DLMM_DISCRIMINATOR_CLAIM_FEE2: [u8; 8] = [0x70, 0xbf, 0x65, 0xab, 0x1c, 0x90, 0x7f, 0xbb]; +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY: [u8; 8] = [0xb5, 0x9d, 0x59, 0x43, 0x8f, 0xb6, 0x34, 0x48]; -const DLMM_DISCRIMINATOR_CLOSE_POSITION_IF_EMPTY: [u8; 8] = - [0x3b, 0x7c, 0xd4, 0x76, 0x5b, 0x98, 0x6e, 0x9d]; +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY2: [u8; 8] = [0xe4, 0xa2, 0x4e, 0x1c, 0x46, 0xdb, 0x74, 0x73]; + +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY: [u8; 8] = + [0x07, 0x03, 0x96, 0x7f, 0x94, 0x28, 0x3d, 0xc8]; const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY2: [u8; 8] = [0x03, 0xdd, 0x95, 0xda, 0x6f, 0x8d, 0x76, 0xd5]; +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY_ONE_SIDE: [u8; 8] = + [0x29, 0x05, 0xee, 0xaf, 0x64, 0xe1, 0x06, 0xcd]; + const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_WEIGHT: [u8; 8] = [0x1c, 0x8c, 0xee, 0x63, 0xe7, 0xa2, 0x15, 0x95]; +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_WEIGHT2: [u8; 8] = + [0xd1, 0x3b, 0x3f, 0x5b, 0x6f, 0xc8, 0x99, 0xe4]; + +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_ONE_SIDE: [u8; 8] = + [0x5e, 0x9b, 0x67, 0x97, 0x46, 0x5f, 0xdc, 0xa5]; + +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_ONE_SIDE_PRECISE: [u8; 8] = + [0xa1, 0xc2, 0x67, 0x54, 0xab, 0x47, 0xfa, 0x9a]; + +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_ONE_SIDE_PRECISE2: [u8; 8] = + [0x21, 0x33, 0xa3, 0xc9, 0x75, 0x62, 0x7d, 0xe7]; + +const DLMM_DISCRIMINATOR_CANCEL_LIMIT_ORDER: [u8; 8] = + [0x84, 0x9c, 0x84, 0x1f, 0x43, 0x28, 0xe8, 0x61]; + +const DLMM_DISCRIMINATOR_CLAIM_FEE: [u8; 8] = [0xa9, 0x20, 0x4f, 0x89, 0x88, 0xe8, 0x46, 0x89]; + +const DLMM_DISCRIMINATOR_CLAIM_FEE2: [u8; 8] = [0x70, 0xbf, 0x65, 0xab, 0x1c, 0x90, 0x7f, 0xbb]; + +const DLMM_DISCRIMINATOR_CLAIM_REWARD: [u8; 8] = [0x95, 0x5f, 0xb5, 0xf2, 0x5e, 0x5a, 0x9e, 0xa2]; + +const DLMM_DISCRIMINATOR_CLAIM_REWARD2: [u8; 8] = [0xbe, 0x03, 0x7f, 0x77, 0xb2, 0x57, 0x9d, 0xb7]; + +const DLMM_DISCRIMINATOR_CLOSE_BIN_ARRAY: [u8; 8] = + [0x44, 0xae, 0x58, 0x50, 0xb5, 0xcc, 0x13, 0xe0]; + +const DLMM_DISCRIMINATOR_CLOSE_CLAIM_FEE_OPERATOR_ACCOUNT: [u8; 8] = + [0xb8, 0xd5, 0x58, 0x1f, 0xb3, 0x65, 0x82, 0x24]; + +const DLMM_DISCRIMINATOR_CLOSE_LIMIT_ORDER_IF_EMPTY: [u8; 8] = + [0x39, 0x7c, 0x24, 0x9b, 0x7e, 0xf9, 0x5d, 0xab]; + +const DLMM_DISCRIMINATOR_CLOSE_OPERATOR_ACCOUNT: [u8; 8] = + [0xab, 0x09, 0xd5, 0x4a, 0x78, 0x17, 0x03, 0x1d]; + +const DLMM_DISCRIMINATOR_CLOSE_POSITION: [u8; 8] = [0x7b, 0x86, 0x51, 0x00, 0x31, 0x44, 0x62, 0x62]; + +const DLMM_DISCRIMINATOR_CLOSE_POSITION2: [u8; 8] = + [0xae, 0x5a, 0x23, 0x73, 0xba, 0x28, 0x93, 0xe2]; + +const DLMM_DISCRIMINATOR_CLOSE_POSITION_IF_EMPTY: [u8; 8] = + [0x3b, 0x7c, 0xd4, 0x76, 0x5b, 0x98, 0x6e, 0x9d]; + +const DLMM_DISCRIMINATOR_CLOSE_PRESET_PARAMETER: [u8; 8] = + [0x04, 0x94, 0x91, 0x64, 0x86, 0x1a, 0xb5, 0x3d]; + +const DLMM_DISCRIMINATOR_CLOSE_PRESET_PARAMETER2: [u8; 8] = + [0x27, 0x19, 0x5f, 0x6b, 0x74, 0x11, 0x73, 0x1c]; + +const DLMM_DISCRIMINATOR_CLOSE_TOKEN_BADGE: [u8; 8] = + [0x6c, 0x92, 0x56, 0x6e, 0xb3, 0xfe, 0x0a, 0x68]; + +const DLMM_DISCRIMINATOR_CREATE_OPERATOR_ACCOUNT: [u8; 8] = + [0xdd, 0x40, 0xf6, 0x95, 0xf0, 0x99, 0xe5, 0xa3]; + +const DLMM_DISCRIMINATOR_DECREASE_POSITION_LENGTH: [u8; 8] = + [0xc2, 0xdb, 0x88, 0x20, 0x19, 0x60, 0x69, 0x25]; + +const DLMM_DISCRIMINATOR_FOR_IDL_TYPE_GENERATION_DO_NOT_CALL: [u8; 8] = + [0xb4, 0x69, 0x45, 0x50, 0x5f, 0x32, 0x49, 0x6c]; + +const DLMM_DISCRIMINATOR_FUND_REWARD: [u8; 8] = [0xbc, 0x32, 0xf9, 0xa5, 0x5d, 0x97, 0x26, 0x3f]; + +const DLMM_DISCRIMINATOR_GO_TO_A_BIN: [u8; 8] = [0x92, 0x48, 0xae, 0xe0, 0x28, 0xfd, 0x54, 0xae]; + +const DLMM_DISCRIMINATOR_INCREASE_ORACLE_LENGTH: [u8; 8] = + [0xbe, 0x3d, 0x7d, 0x57, 0x67, 0x4f, 0x9e, 0xad]; + +const DLMM_DISCRIMINATOR_INCREASE_POSITION_LENGTH: [u8; 8] = + [0x50, 0x53, 0x75, 0xd3, 0x42, 0x0d, 0x21, 0x95]; + +const DLMM_DISCRIMINATOR_INCREASE_POSITION_LENGTH2: [u8; 8] = + [0xff, 0xd2, 0xcc, 0x47, 0x73, 0x89, 0xe1, 0x71]; + +const DLMM_DISCRIMINATOR_INITIALIZE_BIN_ARRAY: [u8; 8] = + [0x23, 0x56, 0x13, 0xb9, 0x4e, 0xd4, 0x4b, 0xd3]; + +const DLMM_DISCRIMINATOR_INITIALIZE_BIN_ARRAY_BITMAP_EXTENSION: [u8; 8] = + [0x2f, 0x9d, 0xe2, 0xb4, 0x0c, 0xf0, 0x21, 0x47]; + +const DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR: [u8; 8] = + [0x2e, 0x27, 0x29, 0x87, 0x6f, 0xb7, 0xc8, 0x40]; + +const DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR2: [u8; 8] = + [0xf3, 0x49, 0x81, 0x7e, 0x33, 0x13, 0xf1, 0x6b]; + +const DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR: [u8; 8] = + [0x2d, 0x9a, 0xed, 0xd2, 0xdd, 0x0f, 0xa6, 0x5c]; + +const DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR2: [u8; 8] = + [0x49, 0x3b, 0x24, 0x78, 0xed, 0x53, 0x6c, 0xc6]; + +const DLMM_DISCRIMINATOR_INITIALIZE_PERMISSION_LB_PAIR: [u8; 8] = + [0x6c, 0x66, 0xd5, 0x55, 0xfb, 0x03, 0x35, 0x15]; + +const DLMM_DISCRIMINATOR_INITIALIZE_POSITION: [u8; 8] = + [0xdb, 0xc0, 0xea, 0x47, 0xbe, 0xbf, 0x66, 0x50]; + +const DLMM_DISCRIMINATOR_INITIALIZE_POSITION2: [u8; 8] = + [0x8f, 0x13, 0xf2, 0x91, 0xd5, 0x0f, 0x68, 0x73]; + +const DLMM_DISCRIMINATOR_INITIALIZE_POSITION_BY_OPERATOR: [u8; 8] = + [0xfb, 0xbd, 0xbe, 0xf4, 0x75, 0xfe, 0x23, 0x94]; + +const DLMM_DISCRIMINATOR_INITIALIZE_POSITION_PDA: [u8; 8] = + [0x2e, 0x52, 0x7d, 0x92, 0x55, 0x8d, 0xe4, 0x99]; + +const DLMM_DISCRIMINATOR_INITIALIZE_PRESET_PARAMETER: [u8; 8] = + [0x42, 0xbc, 0x47, 0xd3, 0x62, 0x6d, 0x0e, 0xba]; + +const DLMM_DISCRIMINATOR_INITIALIZE_PRESET_PARAMETER_V2: [u8; 8] = + [0x75, 0xc7, 0x3e, 0x67, 0x06, 0x8e, 0x1f, 0xcb]; + +const DLMM_DISCRIMINATOR_INITIALIZE_REWARD: [u8; 8] = + [0x5f, 0x87, 0xc0, 0xc4, 0xf2, 0x81, 0xe6, 0x44]; + +const DLMM_DISCRIMINATOR_INITIALIZE_TOKEN_BADGE: [u8; 8] = + [0xfd, 0x4d, 0xcd, 0x5f, 0x1b, 0xe0, 0x59, 0xdf]; + +const DLMM_DISCRIMINATOR_PLACE_LIMIT_ORDER: [u8; 8] = + [0x6c, 0xb0, 0x21, 0xba, 0x92, 0xe5, 0x01, 0xc5]; + +const DLMM_DISCRIMINATOR_REBALANCE_LIQUIDITY: [u8; 8] = + [0x5c, 0x04, 0xb0, 0xc1, 0x77, 0xb9, 0x53, 0x09]; + +const DLMM_DISCRIMINATOR_REMOVE_ALL_LIQUIDITY: [u8; 8] = + [0x0a, 0x33, 0x3d, 0x23, 0x70, 0x69, 0x18, 0x55]; + +const DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY: [u8; 8] = + [0x50, 0x55, 0xd1, 0x48, 0x18, 0xce, 0xb1, 0x6c]; + +const DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY2: [u8; 8] = + [0xe6, 0xd7, 0x52, 0x7f, 0xf1, 0x65, 0xe3, 0x92]; + +const DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY_BY_RANGE: [u8; 8] = + [0x1a, 0x52, 0x66, 0x98, 0xf0, 0x4a, 0x69, 0x1a]; + const DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY_BY_RANGE2: [u8; 8] = [0xcc, 0x02, 0xc3, 0x91, 0x35, 0x91, 0x91, 0xcd]; +const DLMM_DISCRIMINATOR_SET_ACTIVATION_POINT: [u8; 8] = + [0x5b, 0xf9, 0x0f, 0xa5, 0x1a, 0x81, 0xfe, 0x7d]; + +const DLMM_DISCRIMINATOR_SET_PAIR_STATUS: [u8; 8] = + [0x43, 0xf8, 0xe7, 0x89, 0x9a, 0x95, 0xd9, 0xae]; + +const DLMM_DISCRIMINATOR_SET_PAIR_STATUS_PERMISSIONLESS: [u8; 8] = + [0x4e, 0x3b, 0x98, 0xd3, 0x46, 0xb7, 0x2e, 0xd0]; + +const DLMM_DISCRIMINATOR_SET_PERMISSIONLESS_OPERATION_BITS: [u8; 8] = + [0x54, 0x3a, 0xcb, 0x8b, 0xa3, 0x51, 0xbe, 0xba]; + +const DLMM_DISCRIMINATOR_SET_PRE_ACTIVATION_DURATION: [u8; 8] = + [0xa5, 0x3d, 0xc9, 0xf4, 0x82, 0x9f, 0x16, 0x64]; + +const DLMM_DISCRIMINATOR_SET_PRE_ACTIVATION_SWAP_ADDRESS: [u8; 8] = + [0x39, 0x8b, 0x2f, 0x7b, 0xd8, 0x50, 0xdf, 0x0a]; + +const DLMM_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8]; + +const DLMM_DISCRIMINATOR_SWAP2: [u8; 8] = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88]; + +const DLMM_DISCRIMINATOR_SWAP_EXACT_OUT: [u8; 8] = [0xfa, 0x49, 0x65, 0x21, 0x26, 0xcf, 0x4b, 0xb8]; + +const DLMM_DISCRIMINATOR_SWAP_EXACT_OUT2: [u8; 8] = + [0x2b, 0xd7, 0xf7, 0x84, 0x89, 0x3c, 0xf3, 0x51]; + +const DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT: [u8; 8] = + [0x38, 0xad, 0xe6, 0xd0, 0xad, 0xe4, 0x9c, 0xcd]; + +const DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT2: [u8; 8] = + [0x4a, 0x62, 0xc0, 0xd6, 0xb1, 0x33, 0x4b, 0x33]; + +const DLMM_DISCRIMINATOR_UPDATE_BASE_FEE_PARAMETERS: [u8; 8] = + [0x4b, 0xa8, 0xdf, 0xa1, 0x10, 0xc3, 0x03, 0x2f]; + +const DLMM_DISCRIMINATOR_UPDATE_DYNAMIC_FEE_PARAMETERS: [u8; 8] = + [0x5c, 0xa1, 0x2e, 0xf6, 0xff, 0xbd, 0x16, 0x16]; + +const DLMM_DISCRIMINATOR_UPDATE_FEES_AND_REWARD2: [u8; 8] = + [0x20, 0x8e, 0xb8, 0x9a, 0x67, 0x41, 0xb8, 0x58]; + +const DLMM_DISCRIMINATOR_UPDATE_FEES_AND_REWARDS: [u8; 8] = + [0x9a, 0xe6, 0xfa, 0x0d, 0xec, 0xd1, 0x4b, 0xdf]; + +const DLMM_DISCRIMINATOR_UPDATE_POSITION_OPERATOR: [u8; 8] = + [0xca, 0xb8, 0x67, 0x8f, 0xb4, 0xbf, 0x74, 0xd9]; + +const DLMM_DISCRIMINATOR_UPDATE_REWARD_DURATION: [u8; 8] = + [0x8a, 0xae, 0xc4, 0xa9, 0xd5, 0xeb, 0xfe, 0x6b]; + +const DLMM_DISCRIMINATOR_UPDATE_REWARD_FUNDER: [u8; 8] = + [0xd3, 0x1c, 0x30, 0x20, 0xd7, 0xa0, 0x23, 0x17]; + +const DLMM_DISCRIMINATOR_WITHDRAW_INELIGIBLE_REWARD: [u8; 8] = + [0x94, 0xce, 0x2a, 0xc3, 0xf7, 0x31, 0x67, 0x08]; + +const DLMM_DISCRIMINATOR_WITHDRAW_PROTOCOL_FEE: [u8; 8] = + [0x9e, 0xc9, 0x9e, 0xbd, 0x21, 0x5d, 0xa2, 0x67]; + +const DLMM_DISCRIMINATOR_ZAP_PROTOCOL_FEE: [u8; 8] = + [0xd5, 0x9b, 0xbb, 0x22, 0x38, 0xb6, 0x5b, 0xf0]; + const DLMM_ANCHOR_SELF_CPI_LOG_SELECTOR: [u8; 8] = [0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d]; const DLMM_EVENT_DISCRIMINATOR_SWAP: [u8; 8] = [0x51, 0x6c, 0xe3, 0xbe, 0xcd, 0xd0, 0x0a, 0xc4]; @@ -42,6 +247,36 @@ const DLMM_EVENT_DISCRIMINATOR_CLAIM_FEE: [u8; 8] = const DLMM_EVENT_DISCRIMINATOR_CLAIM_REWARD: [u8; 8] = [0x94, 0x74, 0x86, 0xcc, 0x16, 0xab, 0x55, 0x5f]; +const DLMM_EVENT_DISCRIMINATOR_CLAIM_FEE2: [u8; 8] = + [0xe8, 0xab, 0xf2, 0x61, 0x3a, 0x4d, 0x23, 0x2d]; + +const DLMM_EVENT_DISCRIMINATOR_CLAIM_REWARD2: [u8; 8] = + [0x1b, 0x8f, 0xf4, 0x21, 0x50, 0x2b, 0x6e, 0x92]; + +const DLMM_EVENT_DISCRIMINATOR_COMPOSITION_FEE: [u8; 8] = + [0x80, 0x97, 0x7b, 0x6a, 0x11, 0x66, 0x71, 0x8e]; + +const DLMM_EVENT_DISCRIMINATOR_INITIALIZE_REWARD: [u8; 8] = + [0xd3, 0x99, 0x58, 0x3e, 0x95, 0x3c, 0xb1, 0x46]; + +const DLMM_EVENT_DISCRIMINATOR_WITHDRAW_INELIGIBLE_REWARD: [u8; 8] = + [0xe7, 0xbd, 0x41, 0x95, 0x66, 0xd7, 0x9a, 0xf4]; + +const DLMM_EVENT_DISCRIMINATOR_PLACE_LIMIT_ORDER_EVT: [u8; 8] = + [0x2b, 0x4f, 0x1b, 0xa9, 0xf4, 0x1c, 0xe1, 0x3f]; + +const DLMM_EVENT_DISCRIMINATOR_CANCEL_LIMIT_ORDER_EVT: [u8; 8] = + [0x83, 0xea, 0xc2, 0x85, 0x09, 0x0e, 0xbd, 0xd1]; + +const DLMM_EVENT_DISCRIMINATOR_CLOSE_LIMIT_ORDER_EVT: [u8; 8] = + [0x8e, 0x87, 0x08, 0x4c, 0x5c, 0x3f, 0x76, 0x53]; + +const DLMM_EVENT_DISCRIMINATOR_FEE_PARAMETER_UPDATE: [u8; 8] = + [0x30, 0x4c, 0xf1, 0x75, 0x90, 0xd7, 0xf2, 0x2c]; + +const DLMM_EVENT_DISCRIMINATOR_DYNAMIC_FEE_PARAMETER_UPDATE: [u8; 8] = + [0x58, 0x58, 0xb2, 0x87, 0xc2, 0x92, 0x5b, 0xf3]; + const DLMM_EVENT_DISCRIMINATOR_FUND_REWARD: [u8; 8] = [0xf6, 0xe4, 0x3a, 0x82, 0x91, 0xaa, 0x4f, 0xcc]; @@ -51,41 +286,6 @@ const DLMM_EVENT_DISCRIMINATOR_POSITION_CREATE: [u8; 8] = const DLMM_EVENT_DISCRIMINATOR_POSITION_CLOSE: [u8; 8] = [0xff, 0xc4, 0x10, 0x6b, 0x1c, 0xca, 0x35, 0x80]; -const DLMM_DISCRIMINATOR_INITIALIZE_BIN_ARRAY: [u8; 8] = - [0x23, 0x56, 0x13, 0xb9, 0x4e, 0xd4, 0x4b, 0xd3]; - -const DLMM_DISCRIMINATOR_INITIALIZE_POSITION: [u8; 8] = - [0xdb, 0xc0, 0xea, 0x47, 0xbe, 0xbf, 0x66, 0x50]; - -const DLMM_DISCRIMINATOR_ADD_LIQUIDITY: [u8; 8] = [0xb5, 0x9d, 0x59, 0x43, 0x8f, 0xb6, 0x34, 0x48]; - -const DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY: [u8; 8] = - [0x50, 0x55, 0xd1, 0x48, 0x18, 0xce, 0xb1, 0x6c]; - -const DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR: [u8; 8] = - [0x2d, 0x9a, 0xed, 0xd2, 0xdd, 0x0f, 0xa6, 0x5c]; - -const DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR2: [u8; 8] = - [0x49, 0x3b, 0x24, 0x78, 0xed, 0x53, 0x6c, 0xc6]; - -const DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR: [u8; 8] = - [0x2e, 0x27, 0x29, 0x87, 0x6f, 0xb7, 0xc8, 0x40]; - -const DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR2: [u8; 8] = - [0xf3, 0x49, 0x81, 0x7e, 0x33, 0x13, 0xf1, 0x6b]; - -const DLMM_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8]; - -const DLMM_DISCRIMINATOR_SWAP2: [u8; 8] = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88]; - -const DLMM_DISCRIMINATOR_SWAP_EXACT_OUT: [u8; 8] = [0xfa, 0x49, 0x65, 0x21, 0x26, 0xcf, 0x4b, 0xb8]; - -const DLMM_DISCRIMINATOR_SWAP_EXACT_OUT2: [u8; 8] = - [0x2b, 0xd7, 0xf7, 0x84, 0x89, 0x3c, 0xf3, 0x51]; - -const DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT: [u8; 8] = - [0x38, 0xad, 0xe6, 0xd0, 0xad, 0xe4, 0x9c, 0xcd]; - /// Decoded Meteora DLMM create-pool event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MeteoraDlmmCreatePoolDecoded { @@ -120,6 +320,8 @@ pub struct MeteoraDlmmSwapDecoded { pub signature: std::string::String, /// Program id. pub program_id: std::string::String, + /// Normalized decoded event kind. + pub event_kind: std::string::String, /// Trade side relative to normalized base, when inferable. pub trade_side: crate::SwapTradeSide, /// Optional DLMM pair/pool account. @@ -267,89 +469,273 @@ enum MeteoraDlmmInstructionKind { Swap, LiquidityAdd, LiquidityRemove, + LiquidityChange, PositionOpen, PositionClose, + PositionUpdate, PoolLifecycle, Fee, + Reward, + Admin, + Orderbook, Unknown, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MeteoraDlmmInstructionName { - InitializeLbPair, - InitializeLbPair2, + AddLiquidity, + AddLiquidity2, + AddLiquidityByStrategy, + AddLiquidityByStrategy2, + AddLiquidityByStrategyOneSide, + AddLiquidityByWeight, + AddLiquidityByWeight2, + AddLiquidityOneSide, + AddLiquidityOneSidePrecise, + AddLiquidityOneSidePrecise2, + CancelLimitOrder, + ClaimFee, + ClaimFee2, + ClaimReward, + ClaimReward2, + CloseBinArray, + CloseClaimFeeOperatorAccount, + CloseLimitOrderIfEmpty, + CloseOperatorAccount, + ClosePosition, + ClosePosition2, + ClosePositionIfEmpty, + ClosePresetParameter, + ClosePresetParameter2, + CloseTokenBadge, + CreateOperatorAccount, + DecreasePositionLength, + ForIdlTypeGenerationDoNotCall, + FundReward, + GoToABin, + IncreaseOracleLength, + IncreasePositionLength, + IncreasePositionLength2, + InitializeBinArray, + InitializeBinArrayBitmapExtension, InitializeCustomizablePermissionlessLbPair, InitializeCustomizablePermissionlessLbPair2, + InitializeLbPair, + InitializeLbPair2, + InitializePermissionLbPair, + InitializePosition, + InitializePosition2, + InitializePositionByOperator, + InitializePositionPda, + InitializePresetParameter, + InitializePresetParameterV2, + InitializeReward, + InitializeTokenBadge, + PlaceLimitOrder, + RebalanceLiquidity, + RemoveAllLiquidity, + RemoveLiquidity, + RemoveLiquidity2, + RemoveLiquidityByRange, + RemoveLiquidityByRange2, + SetActivationPoint, + SetPairStatus, + SetPairStatusPermissionless, + SetPermissionlessOperationBits, + SetPreActivationDuration, + SetPreActivationSwapAddress, Swap, Swap2, SwapExactOut, SwapExactOut2, SwapWithPriceImpact, - InitializeBinArray, - AddLiquidity, - AddLiquidityByStrategy2, - AddLiquidityByWeight, - RemoveLiquidity, - RemoveLiquidityByRange2, - ClaimFee2, - InitializePosition, - ClosePositionIfEmpty, + SwapWithPriceImpact2, + UpdateBaseFeeParameters, + UpdateDynamicFeeParameters, + UpdateFeesAndReward2, + UpdateFeesAndRewards, + UpdatePositionOperator, + UpdateRewardDuration, + UpdateRewardFunder, + WithdrawIneligibleReward, + WithdrawProtocolFee, + ZapProtocolFee, Unknown, } impl MeteoraDlmmInstructionName { fn as_str(&self) -> &'static str { match self { - Self::InitializeLbPair => return "initialize_lb_pair", - Self::InitializeLbPair2 => return "initialize_lb_pair2", + Self::AddLiquidity => return "add_liquidity", + Self::AddLiquidity2 => return "add_liquidity2", + Self::AddLiquidityByStrategy => return "add_liquidity_by_strategy", + Self::AddLiquidityByStrategy2 => return "add_liquidity_by_strategy2", + Self::AddLiquidityByStrategyOneSide => return "add_liquidity_by_strategy_one_side", + Self::AddLiquidityByWeight => return "add_liquidity_by_weight", + Self::AddLiquidityByWeight2 => return "add_liquidity_by_weight2", + Self::AddLiquidityOneSide => return "add_liquidity_one_side", + Self::AddLiquidityOneSidePrecise => return "add_liquidity_one_side_precise", + Self::AddLiquidityOneSidePrecise2 => return "add_liquidity_one_side_precise2", + Self::CancelLimitOrder => return "cancel_limit_order", + Self::ClaimFee => return "claim_fee", + Self::ClaimFee2 => return "claim_fee2", + Self::ClaimReward => return "claim_reward", + Self::ClaimReward2 => return "claim_reward2", + Self::CloseBinArray => return "close_bin_array", + Self::CloseClaimFeeOperatorAccount => return "close_claim_fee_operator_account", + Self::CloseLimitOrderIfEmpty => return "close_limit_order_if_empty", + Self::CloseOperatorAccount => return "close_operator_account", + Self::ClosePosition => return "close_position", + Self::ClosePosition2 => return "close_position2", + Self::ClosePositionIfEmpty => return "close_position_if_empty", + Self::ClosePresetParameter => return "close_preset_parameter", + Self::ClosePresetParameter2 => return "close_preset_parameter2", + Self::CloseTokenBadge => return "close_token_badge", + Self::CreateOperatorAccount => return "create_operator_account", + Self::DecreasePositionLength => return "decrease_position_length", + Self::ForIdlTypeGenerationDoNotCall => return "for_idl_type_generation_do_not_call", + Self::FundReward => return "fund_reward", + Self::GoToABin => return "go_to_a_bin", + Self::IncreaseOracleLength => return "increase_oracle_length", + Self::IncreasePositionLength => return "increase_position_length", + Self::IncreasePositionLength2 => return "increase_position_length2", + Self::InitializeBinArray => return "initialize_bin_array", + Self::InitializeBinArrayBitmapExtension => { + return "initialize_bin_array_bitmap_extension"; + }, Self::InitializeCustomizablePermissionlessLbPair => { return "initialize_customizable_permissionless_lb_pair"; }, Self::InitializeCustomizablePermissionlessLbPair2 => { return "initialize_customizable_permissionless_lb_pair2"; }, + Self::InitializeLbPair => return "initialize_lb_pair", + Self::InitializeLbPair2 => return "initialize_lb_pair2", + Self::InitializePermissionLbPair => return "initialize_permission_lb_pair", + Self::InitializePosition => return "initialize_position", + Self::InitializePosition2 => return "initialize_position2", + Self::InitializePositionByOperator => return "initialize_position_by_operator", + Self::InitializePositionPda => return "initialize_position_pda", + Self::InitializePresetParameter => return "initialize_preset_parameter", + Self::InitializePresetParameterV2 => return "initialize_preset_parameter_v2", + Self::InitializeReward => return "initialize_reward", + Self::InitializeTokenBadge => return "initialize_token_badge", + Self::PlaceLimitOrder => return "place_limit_order", + Self::RebalanceLiquidity => return "rebalance_liquidity", + Self::RemoveAllLiquidity => return "remove_all_liquidity", + Self::RemoveLiquidity => return "remove_liquidity", + Self::RemoveLiquidity2 => return "remove_liquidity2", + Self::RemoveLiquidityByRange => return "remove_liquidity_by_range", + Self::RemoveLiquidityByRange2 => return "remove_liquidity_by_range2", + Self::SetActivationPoint => return "set_activation_point", + Self::SetPairStatus => return "set_pair_status", + Self::SetPairStatusPermissionless => return "set_pair_status_permissionless", + Self::SetPermissionlessOperationBits => return "set_permissionless_operation_bits", + Self::SetPreActivationDuration => return "set_pre_activation_duration", + Self::SetPreActivationSwapAddress => return "set_pre_activation_swap_address", Self::Swap => return "swap", Self::Swap2 => return "swap2", Self::SwapExactOut => return "swap_exact_out", Self::SwapExactOut2 => return "swap_exact_out2", Self::SwapWithPriceImpact => return "swap_with_price_impact", - Self::InitializeBinArray => return "initialize_bin_array", - Self::AddLiquidity => return "add_liquidity", - Self::AddLiquidityByStrategy2 => return "add_liquidity_by_strategy2", - Self::AddLiquidityByWeight => return "add_liquidity_by_weight", - Self::RemoveLiquidity => return "remove_liquidity", - Self::RemoveLiquidityByRange2 => return "remove_liquidity_by_range2", - Self::ClaimFee2 => return "claim_fee2", - Self::InitializePosition => return "initialize_position", - Self::ClosePositionIfEmpty => return "close_position_if_empty", + Self::SwapWithPriceImpact2 => return "swap_with_price_impact2", + Self::UpdateBaseFeeParameters => return "update_base_fee_parameters", + Self::UpdateDynamicFeeParameters => return "update_dynamic_fee_parameters", + Self::UpdateFeesAndReward2 => return "update_fees_and_reward2", + Self::UpdateFeesAndRewards => return "update_fees_and_rewards", + Self::UpdatePositionOperator => return "update_position_operator", + Self::UpdateRewardDuration => return "update_reward_duration", + Self::UpdateRewardFunder => return "update_reward_funder", + Self::WithdrawIneligibleReward => return "withdraw_ineligible_reward", + Self::WithdrawProtocolFee => return "withdraw_protocol_fee", + Self::ZapProtocolFee => return "zap_protocol_fee", Self::Unknown => return "unknown", } } fn kind(&self) -> MeteoraDlmmInstructionKind { match self { - Self::InitializeLbPair - | Self::InitializeLbPair2 - | Self::InitializeCustomizablePermissionlessLbPair - | Self::InitializeCustomizablePermissionlessLbPair2 => { - return MeteoraDlmmInstructionKind::CreatePool; + Self::AddLiquidity + | Self::AddLiquidity2 + | Self::AddLiquidityByStrategy + | Self::AddLiquidityByStrategy2 + | Self::AddLiquidityByStrategyOneSide + | Self::AddLiquidityByWeight + | Self::AddLiquidityByWeight2 + | Self::AddLiquidityOneSide + | Self::AddLiquidityOneSidePrecise + | Self::AddLiquidityOneSidePrecise2 => return MeteoraDlmmInstructionKind::LiquidityAdd, + Self::CancelLimitOrder | Self::CloseLimitOrderIfEmpty | Self::PlaceLimitOrder => { + return MeteoraDlmmInstructionKind::Orderbook; }, + Self::ClaimFee | Self::ClaimFee2 | Self::WithdrawProtocolFee | Self::ZapProtocolFee => { + return MeteoraDlmmInstructionKind::Fee; + }, + Self::ClaimReward + | Self::ClaimReward2 + | Self::FundReward + | Self::InitializeReward + | Self::WithdrawIneligibleReward => return MeteoraDlmmInstructionKind::Reward, + Self::CloseBinArray + | Self::GoToABin + | Self::IncreaseOracleLength + | Self::InitializeBinArray + | Self::InitializeBinArrayBitmapExtension => { + return MeteoraDlmmInstructionKind::PoolLifecycle; + }, + Self::CloseClaimFeeOperatorAccount + | Self::CloseOperatorAccount + | Self::ClosePresetParameter + | Self::ClosePresetParameter2 + | Self::CloseTokenBadge + | Self::CreateOperatorAccount + | Self::InitializePresetParameter + | Self::InitializePresetParameterV2 + | Self::InitializeTokenBadge + | Self::SetActivationPoint + | Self::SetPairStatus + | Self::SetPairStatusPermissionless + | Self::SetPermissionlessOperationBits + | Self::SetPreActivationDuration + | Self::SetPreActivationSwapAddress + | Self::UpdateBaseFeeParameters + | Self::UpdateDynamicFeeParameters + | Self::UpdateFeesAndReward2 + | Self::UpdateFeesAndRewards + | Self::UpdatePositionOperator + | Self::UpdateRewardDuration + | Self::UpdateRewardFunder => return MeteoraDlmmInstructionKind::Admin, + Self::ClosePosition | Self::ClosePosition2 | Self::ClosePositionIfEmpty => { + return MeteoraDlmmInstructionKind::PositionClose; + }, + Self::DecreasePositionLength + | Self::IncreasePositionLength + | Self::IncreasePositionLength2 => return MeteoraDlmmInstructionKind::PositionUpdate, + Self::ForIdlTypeGenerationDoNotCall => { + return MeteoraDlmmInstructionKind::PoolLifecycle; + }, + Self::InitializeCustomizablePermissionlessLbPair + | Self::InitializeCustomizablePermissionlessLbPair2 + | Self::InitializeLbPair + | Self::InitializeLbPair2 + | Self::InitializePermissionLbPair => return MeteoraDlmmInstructionKind::CreatePool, + Self::InitializePosition + | Self::InitializePosition2 + | Self::InitializePositionByOperator + | Self::InitializePositionPda => return MeteoraDlmmInstructionKind::PositionOpen, + Self::RebalanceLiquidity => return MeteoraDlmmInstructionKind::LiquidityChange, + Self::RemoveAllLiquidity + | Self::RemoveLiquidity + | Self::RemoveLiquidity2 + | Self::RemoveLiquidityByRange + | Self::RemoveLiquidityByRange2 => return MeteoraDlmmInstructionKind::LiquidityRemove, Self::Swap | Self::Swap2 | Self::SwapExactOut | Self::SwapExactOut2 - | Self::SwapWithPriceImpact => return MeteoraDlmmInstructionKind::Swap, - Self::AddLiquidity | Self::AddLiquidityByStrategy2 | Self::AddLiquidityByWeight => { - return MeteoraDlmmInstructionKind::LiquidityAdd; - }, - Self::RemoveLiquidity | Self::RemoveLiquidityByRange2 => { - return MeteoraDlmmInstructionKind::LiquidityRemove; - }, - Self::InitializePosition => return MeteoraDlmmInstructionKind::PositionOpen, - Self::ClosePositionIfEmpty => return MeteoraDlmmInstructionKind::PositionClose, - Self::InitializeBinArray => return MeteoraDlmmInstructionKind::PoolLifecycle, + | Self::SwapWithPriceImpact + | Self::SwapWithPriceImpact2 => return MeteoraDlmmInstructionKind::Swap, Self::Unknown => return MeteoraDlmmInstructionKind::Unknown, - Self::ClaimFee2 => return MeteoraDlmmInstructionKind::Fee, } } } @@ -428,6 +814,7 @@ impl MeteoraDlmmDecoder { program_id, instruction, data.as_slice(), + instructions, ); if let Some(anchor_event) = anchor_event { decoded_events.push(anchor_event); @@ -441,7 +828,9 @@ impl MeteoraDlmmDecoder { &log_messages, ); let instruction_kind = instruction_name.kind(); - if instruction_kind == MeteoraDlmmInstructionKind::Unknown { + if instruction_kind == MeteoraDlmmInstructionKind::Unknown + || instruction_name == MeteoraDlmmInstructionName::ForIdlTypeGenerationDoNotCall + { continue; } if instruction_kind == MeteoraDlmmInstructionKind::PositionClose { @@ -496,13 +885,10 @@ impl MeteoraDlmmDecoder { resolve_dlmm_token_x_mint(instruction_name, parsed_json.as_ref(), &accounts); let token_b_mint = resolve_dlmm_token_y_mint(instruction_name, parsed_json.as_ref(), &accounts); - if pool_account.is_none() { - continue; - } let config_account = resolve_dlmm_config_account(instruction_name, parsed_json.as_ref(), &accounts); if instruction_kind == MeteoraDlmmInstructionKind::CreatePool { - if token_a_mint.is_none() || token_b_mint.is_none() { + if pool_account.is_none() || token_a_mint.is_none() || token_b_mint.is_none() { continue; } let payload_json = serde_json::json!({ @@ -543,9 +929,10 @@ impl MeteoraDlmmDecoder { continue; } if instruction_kind == MeteoraDlmmInstructionKind::Swap { - if token_a_mint.is_none() || token_b_mint.is_none() { + if pool_account.is_none() || token_a_mint.is_none() || token_b_mint.is_none() { continue; } + let event_kind = format!("meteora_dlmm.{}", instruction_name.as_str()); let reserve_x_account = resolve_dlmm_reserve_x_account( instruction_name, parsed_json.as_ref(), @@ -579,12 +966,14 @@ impl MeteoraDlmmDecoder { extract_json_string_field(anchor_swap_event.as_ref(), "protocolFeeRaw"); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", - "eventKind": "swap", + "eventKind": instruction_name.as_str(), "decodedInstructionName": instruction_name.as_str(), "dataDiscriminatorHex": instruction_data .as_ref() .and_then(|data| return first_8_bytes_hex(data.as_slice())), "classifiedInstructionKind": "swap", + "skipTradeReason": serde_json::Value::Null, + "skipCandleReason": serde_json::Value::Null, "signature": transaction.signature, "instructionId": instruction_id, "parentInstructionId": instruction.parent_instruction_id, @@ -614,6 +1003,7 @@ impl MeteoraDlmmDecoder { instruction_id, signature: transaction.signature.clone(), program_id: program_id.to_string(), + event_kind, trade_side, pool_account, token_a_mint, @@ -629,7 +1019,9 @@ impl MeteoraDlmmDecoder { } if instruction_kind == MeteoraDlmmInstructionKind::LiquidityAdd || instruction_kind == MeteoraDlmmInstructionKind::LiquidityRemove + || instruction_kind == MeteoraDlmmInstructionKind::LiquidityChange || instruction_kind == MeteoraDlmmInstructionKind::PositionOpen + || instruction_kind == MeteoraDlmmInstructionKind::PositionUpdate { let event_kind = format!("meteora_dlmm.{}", instruction_name.as_str()); let actor_wallet = @@ -717,6 +1109,7 @@ impl MeteoraDlmmDecoder { "baseAmountRaw": base_amount_raw, "quoteAmountRaw": quote_amount_raw, "liquidityAmountRaw": liquidity_amount_raw, + "materializationSource": "dlmm_instruction_liquidity_materialization", "rangeFromBinId": range_from_bin_id, "rangeToBinId": range_to_bin_id, "rangeBpsToRemove": range_bps_to_remove, @@ -797,7 +1190,8 @@ impl MeteoraDlmmDecoder { "poolAccount": pool_account, "actorWallet": actor_wallet, "feeTokenMint": fee_token_mint, - "feeAmountRaw": fee_amount_raw + "feeAmountRaw": fee_amount_raw, + "materializationSource": "dlmm_instruction_fee_materialization" }); decoded_events.push(crate::MeteoraDlmmDecodedEvent::Fee( crate::MeteoraDlmmFeeDecoded { @@ -815,6 +1209,117 @@ impl MeteoraDlmmDecoder { )); continue; } + if instruction_kind == MeteoraDlmmInstructionKind::Reward { + let event_kind = format!("meteora_dlmm.{}", instruction_name.as_str()); + let actor_wallet = + resolve_dlmm_actor_wallet(instruction_name, parsed_json.as_ref(), &accounts); + let reward_token_mint = resolve_dlmm_reward_token_mint( + instruction_name, + parsed_json.as_ref(), + &accounts, + ); + let reward_amount_raw = extract_amount_string_by_candidate_keys( + parsed_json.as_ref(), + &[ + "rewardAmountRaw", + "reward_amount_raw", + "rewardAmount", + "reward_amount", + "totalReward", + "total_reward", + "amount", + ], + ); + let reward_index = extract_amount_string_by_candidate_keys( + parsed_json.as_ref(), + &["rewardIndex", "reward_index", "index"], + ); + let payload_json = serde_json::json!({ + "decoder": "meteora_dlmm", + "eventKind": instruction_name.as_str(), + "decodedInstructionName": instruction_name.as_str(), + "dataDiscriminatorHex": instruction_data + .as_ref() + .and_then(|data| return first_8_bytes_hex(data.as_slice())), + "classifiedInstructionKind": crate::classify_dex_event_lifecycle_kind_code(event_kind.as_str()), + "signature": transaction.signature, + "instructionId": instruction_id, + "parentInstructionId": instruction.parent_instruction_id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "stackHeight": instruction.stack_height, + "accounts": accounts, + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "actorWallet": actor_wallet, + "rewardIndex": reward_index, + "rewardTokenMint": reward_token_mint, + "rewardAmountRaw": reward_amount_raw, + "materializationSource": "dlmm_instruction_reward_materialization" + }); + decoded_events.push(crate::MeteoraDlmmDecodedEvent::Reward( + crate::MeteoraDlmmRewardDecoded { + transaction_id, + instruction_id, + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + actor_wallet, + reward_token_mint, + reward_amount_raw, + payload_json, + }, + )); + continue; + } + if instruction_kind == MeteoraDlmmInstructionKind::Admin + || instruction_kind == MeteoraDlmmInstructionKind::Orderbook + { + let event_kind = format!("meteora_dlmm.{}", instruction_name.as_str()); + let actor_wallet = + resolve_dlmm_actor_wallet(instruction_name, parsed_json.as_ref(), &accounts); + let payload_json = serde_json::json!({ + "decoder": "meteora_dlmm", + "eventKind": instruction_name.as_str(), + "decodedInstructionName": instruction_name.as_str(), + "dataDiscriminatorHex": instruction_data + .as_ref() + .and_then(|data| return first_8_bytes_hex(data.as_slice())), + "classifiedInstructionKind": crate::classify_dex_event_lifecycle_kind_code(event_kind.as_str()), + "signature": transaction.signature, + "instructionId": instruction_id, + "parentInstructionId": instruction.parent_instruction_id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "stackHeight": instruction.stack_height, + "accounts": accounts, + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "tokenAMint": token_a_mint, + "tokenBMint": token_b_mint, + "actorWallet": actor_wallet, + "materializationTarget": if instruction_kind == MeteoraDlmmInstructionKind::Orderbook { "orderbook" } else { "pool_admin" }, + "skipTradeReason": "dlmm_instruction_not_a_verified_fill", + "skipCandleReason": "dlmm_instruction_not_a_verified_fill" + }); + decoded_events.push(crate::MeteoraDlmmDecodedEvent::PoolLifecycle( + crate::MeteoraDlmmPoolLifecycleDecoded { + transaction_id, + instruction_id, + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + token_a_mint, + token_b_mint, + payload_json, + }, + )); + continue; + } if instruction_kind == MeteoraDlmmInstructionKind::PoolLifecycle { let event_kind = format!("meteora_dlmm.{}", instruction_name.as_str()); let payload_json = serde_json::json!({ @@ -864,6 +1369,7 @@ fn decode_anchor_non_swap_event( program_id: &str, instruction: &crate::ChainInstructionDto, data: &[u8], + instructions: &[crate::ChainInstructionDto], ) -> std::option::Option { if !is_dlmm_anchor_self_cpi_log(data) { return None; @@ -893,6 +1399,7 @@ fn decode_anchor_non_swap_event( "meteora_dlmm.add_liquidity_event", "add_liquidity_event", "1f5e7d5ae3343dba", + instructions, ); } if event_discriminator == DLMM_EVENT_DISCRIMINATOR_REMOVE_LIQUIDITY { @@ -906,6 +1413,7 @@ fn decode_anchor_non_swap_event( "meteora_dlmm.remove_liquidity_event", "remove_liquidity_event", "74f461e8671f983a", + instructions, ); } if event_discriminator == DLMM_EVENT_DISCRIMINATOR_CLAIM_FEE { @@ -916,6 +1424,22 @@ fn decode_anchor_non_swap_event( program_id, instruction, data, + "claim_fee_event", + "4b7a9a308c4a7ba3", + instructions, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_CLAIM_FEE2 { + return decode_anchor_claim_fee_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "claim_fee2_event", + "e8abf2613a4d232d", + instructions, ); } if event_discriminator == DLMM_EVENT_DISCRIMINATOR_CLAIM_REWARD { @@ -926,6 +1450,22 @@ fn decode_anchor_non_swap_event( program_id, instruction, data, + "claim_reward_event", + "947486cc16ab555f", + instructions, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_CLAIM_REWARD2 { + return decode_anchor_claim_reward_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "claim_reward2_event", + "1b8ff421502b6e92", + instructions, ); } if event_discriminator == DLMM_EVENT_DISCRIMINATOR_FUND_REWARD { @@ -936,6 +1476,7 @@ fn decode_anchor_non_swap_event( program_id, instruction, data, + instructions, ); } if event_discriminator == DLMM_EVENT_DISCRIMINATOR_POSITION_CREATE { @@ -946,6 +1487,7 @@ fn decode_anchor_non_swap_event( program_id, instruction, data, + instructions, ); } if event_discriminator == DLMM_EVENT_DISCRIMINATOR_POSITION_CLOSE { @@ -956,11 +1498,496 @@ fn decode_anchor_non_swap_event( program_id, instruction, data, + instructions, ); } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_COMPOSITION_FEE { + return decode_anchor_generic_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "80977b6a1166718e", + "composition_fee_event", + instructions, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_INITIALIZE_REWARD { + return decode_anchor_generic_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "d399583e953cb146", + "initialize_reward_event", + instructions, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_WITHDRAW_INELIGIBLE_REWARD { + return decode_anchor_generic_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "e7bd419566d79af4", + "withdraw_ineligible_reward_event", + instructions, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_PLACE_LIMIT_ORDER_EVT { + return decode_anchor_generic_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "2b4f1ba9f41ce13f", + "place_limit_order_evt", + instructions, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_CANCEL_LIMIT_ORDER_EVT { + return decode_anchor_generic_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "83eac285090ebdd1", + "cancel_limit_order_evt", + instructions, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_CLOSE_LIMIT_ORDER_EVT { + return decode_anchor_generic_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "8e87084c5c3f7653", + "close_limit_order_evt", + instructions, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_FEE_PARAMETER_UPDATE { + return decode_anchor_generic_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "304cf17590d7f22c", + "fee_parameter_update_event", + instructions, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_DYNAMIC_FEE_PARAMETER_UPDATE { + return decode_anchor_generic_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "5858b287c2925bf3", + "dynamic_fee_parameter_update_event", + instructions, + ); + } + let event_discriminator_hex = match first_8_bytes_hex(&event_discriminator) { + Some(event_discriminator_hex) => event_discriminator_hex, + None => return None, + }; + let anchor_event_name = + match dlmm_anchor_event_name_from_discriminator_hex(event_discriminator_hex.as_str()) { + Some(anchor_event_name) => anchor_event_name, + None => return None, + }; + return decode_anchor_generic_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + event_discriminator_hex.as_str(), + anchor_event_name, + instructions, + ); +} + +fn dlmm_anchor_event_name_from_discriminator_hex( + event_discriminator_hex: &str, +) -> std::option::Option<&'static str> { + match event_discriminator_hex { + "1f5e7d5ae3343dba" => return Some("add_liquidity_event"), + "83eac285090ebdd1" => return Some("cancel_limit_order_evt"), + "4b7a9a308c4a7ba3" => return Some("claim_fee_event"), + "e8abf2613a4d232d" => return Some("claim_fee2_event"), + "947486cc16ab555f" => return Some("claim_reward_event"), + "1b8ff421502b6e92" => return Some("claim_reward2_event"), + "8e87084c5c3f7653" => return Some("close_limit_order_evt"), + "80977b6a1166718e" => return Some("composition_fee_event"), + "3476eb55aca90f80" => return Some("decrease_position_length_event"), + "5858b287c2925bf3" => return Some("dynamic_fee_parameter_update_event"), + "304cf17590d7f22c" => return Some("fee_parameter_update_event"), + "f6e43a8291aa4fcc" => return Some("fund_reward_event"), + "3b8a4c448a83b043" => return Some("go_to_a_bin_event"), + "63f91179a69ccfd7" => return Some("increase_observation_event"), + "9def2acc1e38df2e" => return Some("increase_position_length_event"), + "d399583e953cb146" => return Some("initialize_reward_event"), + "b94afc7d1bd7bc6f" => return Some("lb_pair_create_event"), + "2b4f1ba9f41ce13f" => return Some("place_limit_order_evt"), + "ffc4106b1cca3580" => return Some("position_close_event"), + "908efc549d352579" => return Some("position_create_event"), + "006d75b33d5bc7c8" => return Some("rebalancing_event"), + "74f461e8671f983a" => return Some("remove_liquidity_event"), + "c3e593f51d7d30a8" => return Some("set_position_permissionless_operation_bits_evt"), + "516ce3becdd00ac4" => return Some("swap_event"), + "2e7452d7941b544d" => return Some("swap2_evt"), + "85d642e0400c07bf" => return Some("update_position_lock_release_point_event"), + "277330ccf62f4239" => return Some("update_position_operator_event"), + "dff5e099311da3ac" => return Some("update_reward_duration_event"), + "e0b2ae4afca555b4" => return Some("update_reward_funder_event"), + "e7bd419566d79af4" => return Some("withdraw_ineligible_reward_event"), + _ => return None, + } +} + +fn decode_anchor_generic_event( + transaction_id: i64, + instruction_id: i64, + signature: &str, + program_id: &str, + instruction: &crate::ChainInstructionDto, + data: &[u8], + event_discriminator_hex: &str, + anchor_event_name: &str, + instructions: &[crate::ChainInstructionDto], +) -> std::option::Option { + let event_kind = format!("meteora_dlmm.{}", anchor_event_name); + let parent = find_parent_dlmm_instruction(instruction, instructions); + let parent_name = parent_dlmm_instruction_name(parent); + let parent_accounts = parent_dlmm_accounts(parent); + let pool_account = + match resolve_anchor_parent_pool_account(parent_name, parent_accounts.as_slice()) { + Some(pool_account) => Some(pool_account), + None => extract_anchor_pool_account_from_payload(data), + }; + let actor_wallet = extract_anchor_actor_from_payload(data); + let token_x_mint = resolve_anchor_parent_token_x_mint(parent_name, parent_accounts.as_slice()); + let token_y_mint = resolve_anchor_parent_token_y_mint(parent_name, parent_accounts.as_slice()); + let reward_mint = resolve_anchor_parent_reward_mint(parent_name, parent_accounts.as_slice()); + let payload_json = serde_json::json!({ + "decoder": "meteora_dlmm", + "eventKind": anchor_event_name, + "decodedInstructionName": "anchor_self_cpi_log", + "classifiedInstructionKind": crate::classify_dex_event_lifecycle_kind_code(event_kind.as_str()), + "signature": signature, + "instructionId": instruction_id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "parentInstructionId": instruction.parent_instruction_id, + "stackHeight": instruction.stack_height, + "anchorSelfCpiLog": true, + "anchorEventName": anchor_event_name, + "anchorEventDiscriminatorHex": event_discriminator_hex, + "anchorEventPayloadSize": data.len().saturating_sub(8), + "proofStatus": "local_idl_discriminator_observed_synthetic_materialization", + "poolAccount": pool_account, + "actorWallet": actor_wallet, + "tokenXMint": token_x_mint, + "tokenYMint": token_y_mint, + "rewardTokenMint": reward_mint, + "materializationSource": "dlmm_anchor_generic_materialization", + "skipTradeReason": "dlmm_anchor_event_not_a_verified_instruction_fill", + "skipCandleReason": "dlmm_anchor_event_not_a_verified_instruction_fill" + }); + if crate::is_dex_fee_event_kind(event_kind.as_str()) { + return Some(crate::MeteoraDlmmDecodedEvent::Fee(crate::MeteoraDlmmFeeDecoded { + transaction_id, + instruction_id, + signature: signature.to_string(), + program_id: program_id.to_string(), + event_kind, + pool_account, + actor_wallet, + fee_token_mint: token_x_mint, + fee_amount_raw: None, + payload_json, + })); + } + if crate::is_dex_reward_event_kind(event_kind.as_str()) { + return Some(crate::MeteoraDlmmDecodedEvent::Reward(crate::MeteoraDlmmRewardDecoded { + transaction_id, + instruction_id, + signature: signature.to_string(), + program_id: program_id.to_string(), + event_kind, + pool_account, + actor_wallet, + reward_token_mint: reward_mint, + reward_amount_raw: None, + payload_json, + })); + } + if crate::is_dex_liquidity_event_kind(event_kind.as_str()) { + return Some(crate::MeteoraDlmmDecodedEvent::Liquidity( + crate::MeteoraDlmmLiquidityDecoded { + transaction_id, + instruction_id, + signature: signature.to_string(), + program_id: program_id.to_string(), + event_kind, + pool_account, + token_a_mint: token_x_mint, + token_b_mint: token_y_mint, + actor_wallet, + base_amount_raw: None, + quote_amount_raw: None, + liquidity_amount_raw: None, + payload_json, + }, + )); + } + return Some(crate::MeteoraDlmmDecodedEvent::PoolLifecycle( + crate::MeteoraDlmmPoolLifecycleDecoded { + transaction_id, + instruction_id, + signature: signature.to_string(), + program_id: program_id.to_string(), + event_kind, + pool_account, + token_a_mint: token_x_mint, + token_b_mint: token_y_mint, + payload_json, + }, + )); +} + +fn extract_anchor_pool_account_from_payload( + data: &[u8], +) -> std::option::Option { + if data.len() < 48 { + return None; + } + return read_pubkey_string(data, 16); +} + +fn extract_anchor_actor_from_payload(data: &[u8]) -> std::option::Option { + if data.len() < 80 { + return None; + } + return read_pubkey_string(data, 48); +} + +fn find_parent_dlmm_instruction<'a>( + instruction: &crate::ChainInstructionDto, + instructions: &'a [crate::ChainInstructionDto], +) -> std::option::Option<&'a crate::ChainInstructionDto> { + if let Some(parent_instruction_id) = instruction.parent_instruction_id { + for candidate in instructions { + if candidate.id == Some(parent_instruction_id) { + return Some(candidate); + } + } + } + for candidate in instructions { + if candidate.inner_instruction_index.is_some() { + continue; + } + if candidate.instruction_index != instruction.instruction_index { + continue; + } + if candidate.program_id.as_deref() != Some(crate::METEORA_DLMM_PROGRAM_ID) { + continue; + } + return Some(candidate); + } return None; } +fn parent_dlmm_instruction_name( + parent: std::option::Option<&crate::ChainInstructionDto>, +) -> MeteoraDlmmInstructionName { + let parent = match parent { + Some(parent) => parent, + None => return MeteoraDlmmInstructionName::Unknown, + }; + let parent_data_result = decode_instruction_data_json(parent.data_json.as_ref()); + let parent_data = match parent_data_result { + Ok(parent_data) => parent_data, + Err(_) => None, + }; + return classify_instruction_name( + None, + parent.parsed_type.as_deref(), + parent_data.as_deref(), + &[], + ); +} + +fn parent_dlmm_accounts( + parent: std::option::Option<&crate::ChainInstructionDto>, +) -> std::vec::Vec { + let parent = match parent { + Some(parent) => parent, + None => return std::vec::Vec::new(), + }; + let parsed = parse_accounts_json(parent.accounts_json.as_str()); + match parsed { + Ok(parsed) => return parsed, + Err(_) => return std::vec::Vec::new(), + } +} + +fn resolve_anchor_parent_token_x_mint( + parent_name: MeteoraDlmmInstructionName, + accounts: &[std::string::String], +) -> std::option::Option { + match parent_name { + MeteoraDlmmInstructionName::ClaimFee => return extract_account(accounts, 9), + MeteoraDlmmInstructionName::ClaimFee2 => return extract_account(accounts, 7), + MeteoraDlmmInstructionName::WithdrawProtocolFee => return extract_account(accounts, 3), + MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidity2 + | MeteoraDlmmInstructionName::RemoveLiquidity + | MeteoraDlmmInstructionName::RemoveLiquidity2 => return extract_account(accounts, 7), + MeteoraDlmmInstructionName::Swap2 + | MeteoraDlmmInstructionName::SwapExactOut2 + | MeteoraDlmmInstructionName::SwapWithPriceImpact2 => return extract_account(accounts, 6), + _ => return None, + } +} + +fn resolve_anchor_parent_token_y_mint( + parent_name: MeteoraDlmmInstructionName, + accounts: &[std::string::String], +) -> std::option::Option { + match parent_name { + MeteoraDlmmInstructionName::ClaimFee => return extract_account(accounts, 10), + MeteoraDlmmInstructionName::ClaimFee2 => return extract_account(accounts, 8), + MeteoraDlmmInstructionName::WithdrawProtocolFee => return extract_account(accounts, 4), + MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidity2 + | MeteoraDlmmInstructionName::RemoveLiquidity + | MeteoraDlmmInstructionName::RemoveLiquidity2 => return extract_account(accounts, 8), + MeteoraDlmmInstructionName::Swap2 + | MeteoraDlmmInstructionName::SwapExactOut2 + | MeteoraDlmmInstructionName::SwapWithPriceImpact2 => return extract_account(accounts, 7), + _ => return None, + } +} + +fn resolve_anchor_parent_reward_mint( + parent_name: MeteoraDlmmInstructionName, + accounts: &[std::string::String], +) -> std::option::Option { + match parent_name { + MeteoraDlmmInstructionName::ClaimReward => return extract_account(accounts, 6), + MeteoraDlmmInstructionName::ClaimReward2 => return extract_account(accounts, 4), + MeteoraDlmmInstructionName::FundReward + | MeteoraDlmmInstructionName::InitializeReward + | MeteoraDlmmInstructionName::WithdrawIneligibleReward => { + return extract_account(accounts, 2); + }, + _ => return None, + } +} + +fn resolve_anchor_parent_pool_account( + parent_name: MeteoraDlmmInstructionName, + accounts: &[std::string::String], +) -> std::option::Option { + match parent_name { + MeteoraDlmmInstructionName::ClosePosition => return extract_account(accounts, 1), + MeteoraDlmmInstructionName::InitializePosition + | MeteoraDlmmInstructionName::InitializePosition2 + | MeteoraDlmmInstructionName::InitializePositionByOperator + | MeteoraDlmmInstructionName::InitializePositionPda => return extract_account(accounts, 2), + _ => return extract_account(accounts, 0), + } +} + +fn amount_is_non_zero(amount: &std::option::Option) -> bool { + let amount = match amount { + Some(amount) => amount.trim(), + None => return false, + }; + if amount.is_empty() { + return false; + } + let parsed = amount.parse::(); + match parsed { + Ok(parsed) => return parsed > 0, + Err(_) => return true, + } +} + +fn build_fee_amount_leg( + token_mint: &std::option::Option, + amount_raw: &std::option::Option, + amount_source: &str, + source_account: std::option::Option, + destination_account: std::option::Option, +) -> std::option::Option { + if !amount_is_non_zero(amount_raw) { + return None; + } + let token_mint = match token_mint { + Some(token_mint) => token_mint, + None => return None, + }; + let amount_raw = match amount_raw { + Some(amount_raw) => amount_raw, + None => return None, + }; + let mut object = serde_json::Map::new(); + object.insert("tokenMint".to_string(), serde_json::Value::String(token_mint.clone())); + object.insert("amountRaw".to_string(), serde_json::Value::String(amount_raw.clone())); + object.insert("amountSource".to_string(), serde_json::Value::String(amount_source.to_string())); + if let Some(source_account) = source_account { + object.insert("sourceAccount".to_string(), serde_json::Value::String(source_account)); + } + if let Some(destination_account) = destination_account { + object.insert( + "destinationAccount".to_string(), + serde_json::Value::String(destination_account), + ); + } + return Some(serde_json::Value::Object(object)); +} + +fn build_dual_fee_amount_legs( + token_x_mint: &std::option::Option, + fee_x: &std::option::Option, + token_y_mint: &std::option::Option, + fee_y: &std::option::Option, + amount_source: &str, +) -> serde_json::Value { + let mut legs = std::vec::Vec::new(); + let x_leg = build_fee_amount_leg(token_x_mint, fee_x, amount_source, None, None); + if let Some(x_leg) = x_leg { + legs.push(x_leg); + } + let y_leg = build_fee_amount_leg(token_y_mint, fee_y, amount_source, None, None); + if let Some(y_leg) = y_leg { + legs.push(y_leg); + } + return serde_json::Value::Array(legs); +} + fn decode_anchor_lb_pair_create_event( transaction_id: i64, instruction_id: i64, @@ -1026,6 +2053,7 @@ fn decode_anchor_liquidity_event( event_kind_text: &str, anchor_event_name: &str, event_discriminator_hex: &str, + instructions: &[crate::ChainInstructionDto], ) -> std::option::Option { if data.len() < 132 { return None; @@ -1036,6 +2064,11 @@ fn decode_anchor_liquidity_event( let amount_x = read_u64_string(data, 112); let amount_y = read_u64_string(data, 120); let active_bin_id = read_i32_string(data, 128); + let parent = find_parent_dlmm_instruction(instruction, instructions); + let parent_name = parent_dlmm_instruction_name(parent); + let parent_accounts = parent_dlmm_accounts(parent); + let token_x_mint = resolve_anchor_parent_token_x_mint(parent_name, parent_accounts.as_slice()); + let token_y_mint = resolve_anchor_parent_token_y_mint(parent_name, parent_accounts.as_slice()); let event_kind = event_kind_text.to_string(); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", @@ -1058,10 +2091,13 @@ fn decode_anchor_liquidity_event( "from": from, "actorWallet": from, "position": position, + "tokenAMint": token_x_mint, + "tokenBMint": token_y_mint, "amounts": [amount_x.clone(), amount_y.clone()], "baseAmountRaw": amount_x, "quoteAmountRaw": amount_y, - "activeBinId": active_bin_id + "activeBinId": active_bin_id, + "materializationSource": "anchor_liquidity_event_amounts_with_parent_accounts" }); return Some(crate::MeteoraDlmmDecodedEvent::Liquidity(crate::MeteoraDlmmLiquidityDecoded { transaction_id, @@ -1070,8 +2106,8 @@ fn decode_anchor_liquidity_event( program_id: program_id.to_string(), event_kind, pool_account: lb_pair, - token_a_mint: None, - token_b_mint: None, + token_a_mint: token_x_mint, + token_b_mint: token_y_mint, actor_wallet: from, base_amount_raw: amount_x, quote_amount_raw: amount_y, @@ -1087,6 +2123,9 @@ fn decode_anchor_claim_fee_event( program_id: &str, instruction: &crate::ChainInstructionDto, data: &[u8], + anchor_event_name: &str, + event_discriminator_hex: &str, + instructions: &[crate::ChainInstructionDto], ) -> std::option::Option { if data.len() < 128 { return None; @@ -1096,10 +2135,22 @@ fn decode_anchor_claim_fee_event( let owner = read_pubkey_string(data, 80); let fee_x = read_u64_string(data, 112); let fee_y = read_u64_string(data, 120); - let event_kind = "meteora_dlmm.claim_fee_event".to_string(); + let parent = find_parent_dlmm_instruction(instruction, instructions); + let parent_name = parent_dlmm_instruction_name(parent); + let parent_accounts = parent_dlmm_accounts(parent); + let token_x_mint = resolve_anchor_parent_token_x_mint(parent_name, parent_accounts.as_slice()); + let token_y_mint = resolve_anchor_parent_token_y_mint(parent_name, parent_accounts.as_slice()); + let fee_amount_legs = build_dual_fee_amount_legs( + &token_x_mint, + &fee_x, + &token_y_mint, + &fee_y, + "anchor_claim_fee_event_amounts", + ); + let event_kind = format!("meteora_dlmm.{}", anchor_event_name); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", - "eventKind": "claim_fee_event", + "eventKind": anchor_event_name, "decodedInstructionName": "anchor_self_cpi_log", "classifiedInstructionKind": crate::classify_dex_event_lifecycle_kind_code(event_kind.as_str()), "signature": signature, @@ -1109,8 +2160,8 @@ fn decode_anchor_claim_fee_event( "parentInstructionId": instruction.parent_instruction_id, "stackHeight": instruction.stack_height, "anchorSelfCpiLog": true, - "anchorEventName": "claim_fee_event", - "anchorEventDiscriminatorHex": "4b7a9a308c4a7ba3", + "anchorEventName": anchor_event_name, + "anchorEventDiscriminatorHex": event_discriminator_hex, "anchorEventPayloadSize": data.len().saturating_sub(8), "proofStatus": "upstream_git_layout_unverified", "lbPair": lb_pair, @@ -1118,8 +2169,12 @@ fn decode_anchor_claim_fee_event( "position": position, "owner": owner, "actorWallet": owner, + "tokenXMint": token_x_mint, + "tokenYMint": token_y_mint, "feeXRaw": fee_x, - "feeYRaw": fee_y + "feeYRaw": fee_y, + "feeAmountLegs": fee_amount_legs, + "materializationSource": "anchor_claim_fee_event_amounts_with_parent_accounts" }); return Some(crate::MeteoraDlmmDecodedEvent::Fee(crate::MeteoraDlmmFeeDecoded { transaction_id, @@ -1142,6 +2197,9 @@ fn decode_anchor_claim_reward_event( program_id: &str, instruction: &crate::ChainInstructionDto, data: &[u8], + anchor_event_name: &str, + event_discriminator_hex: &str, + instructions: &[crate::ChainInstructionDto], ) -> std::option::Option { if data.len() < 128 { return None; @@ -1151,10 +2209,14 @@ fn decode_anchor_claim_reward_event( let owner = read_pubkey_string(data, 80); let reward_index = read_u64_string(data, 112); let total_reward = read_u64_string(data, 120); - let event_kind = "meteora_dlmm.claim_reward_event".to_string(); + let parent = find_parent_dlmm_instruction(instruction, instructions); + let parent_name = parent_dlmm_instruction_name(parent); + let parent_accounts = parent_dlmm_accounts(parent); + let reward_mint = resolve_anchor_parent_reward_mint(parent_name, parent_accounts.as_slice()); + let event_kind = format!("meteora_dlmm.{}", anchor_event_name); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", - "eventKind": "claim_reward_event", + "eventKind": anchor_event_name, "decodedInstructionName": "anchor_self_cpi_log", "classifiedInstructionKind": crate::classify_dex_event_lifecycle_kind_code(event_kind.as_str()), "signature": signature, @@ -1164,8 +2226,8 @@ fn decode_anchor_claim_reward_event( "parentInstructionId": instruction.parent_instruction_id, "stackHeight": instruction.stack_height, "anchorSelfCpiLog": true, - "anchorEventName": "claim_reward_event", - "anchorEventDiscriminatorHex": "947486cc16ab555f", + "anchorEventName": anchor_event_name, + "anchorEventDiscriminatorHex": event_discriminator_hex, "anchorEventPayloadSize": data.len().saturating_sub(8), "proofStatus": "upstream_git_layout_unverified", "lbPair": lb_pair, @@ -1174,8 +2236,10 @@ fn decode_anchor_claim_reward_event( "owner": owner, "actorWallet": owner, "rewardIndex": reward_index, + "rewardTokenMint": reward_mint, "totalRewardRaw": total_reward, - "rewardAmountRaw": total_reward + "rewardAmountRaw": total_reward, + "materializationSource": "anchor_claim_reward_event_amount_with_parent_accounts" }); return Some(crate::MeteoraDlmmDecodedEvent::Reward(crate::MeteoraDlmmRewardDecoded { transaction_id, @@ -1185,7 +2249,7 @@ fn decode_anchor_claim_reward_event( event_kind, pool_account: lb_pair, actor_wallet: owner, - reward_token_mint: None, + reward_token_mint: reward_mint, reward_amount_raw: total_reward, payload_json, })); @@ -1198,6 +2262,7 @@ fn decode_anchor_fund_reward_event( program_id: &str, instruction: &crate::ChainInstructionDto, data: &[u8], + instructions: &[crate::ChainInstructionDto], ) -> std::option::Option { if data.len() < 96 { return None; @@ -1206,6 +2271,10 @@ fn decode_anchor_fund_reward_event( let funder = read_pubkey_string(data, 48); let reward_index = read_u64_string(data, 80); let amount = read_u64_string(data, 88); + let parent = find_parent_dlmm_instruction(instruction, instructions); + let parent_name = parent_dlmm_instruction_name(parent); + let parent_accounts = parent_dlmm_accounts(parent); + let reward_mint = resolve_anchor_parent_reward_mint(parent_name, parent_accounts.as_slice()); let event_kind = "meteora_dlmm.fund_reward_event".to_string(); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", @@ -1228,8 +2297,10 @@ fn decode_anchor_fund_reward_event( "funder": funder, "actorWallet": funder, "rewardIndex": reward_index, + "rewardTokenMint": reward_mint, "amount": amount, - "rewardAmountRaw": amount + "rewardAmountRaw": amount, + "materializationSource": "anchor_fund_reward_event_amount_with_parent_accounts" }); return Some(crate::MeteoraDlmmDecodedEvent::Reward(crate::MeteoraDlmmRewardDecoded { transaction_id, @@ -1239,7 +2310,7 @@ fn decode_anchor_fund_reward_event( event_kind, pool_account: lb_pair, actor_wallet: funder, - reward_token_mint: None, + reward_token_mint: reward_mint, reward_amount_raw: amount, payload_json, })); @@ -1252,6 +2323,7 @@ fn decode_anchor_position_create_event( program_id: &str, instruction: &crate::ChainInstructionDto, data: &[u8], + instructions: &[crate::ChainInstructionDto], ) -> std::option::Option { if data.len() < 112 { return None; @@ -1259,6 +2331,11 @@ fn decode_anchor_position_create_event( let lb_pair = read_pubkey_string(data, 16); let position = read_pubkey_string(data, 48); let owner = read_pubkey_string(data, 80); + let parent = find_parent_dlmm_instruction(instruction, instructions); + let parent_name = parent_dlmm_instruction_name(parent); + let parent_accounts = parent_dlmm_accounts(parent); + let parent_pool_account = + resolve_anchor_parent_pool_account(parent_name, parent_accounts.as_slice()); let event_kind = "meteora_dlmm.position_create_event".to_string(); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", @@ -1280,7 +2357,9 @@ fn decode_anchor_position_create_event( "poolAccount": lb_pair, "position": position, "owner": owner, - "actorWallet": owner + "actorWallet": owner, + "parentPoolAccount": parent_pool_account, + "materializationSource": "anchor_position_create_event" }); return Some(crate::MeteoraDlmmDecodedEvent::Liquidity(crate::MeteoraDlmmLiquidityDecoded { transaction_id, @@ -1306,12 +2385,18 @@ fn decode_anchor_position_close_event( program_id: &str, instruction: &crate::ChainInstructionDto, data: &[u8], + instructions: &[crate::ChainInstructionDto], ) -> std::option::Option { if data.len() < 80 { return None; } let position = read_pubkey_string(data, 16); let owner = read_pubkey_string(data, 48); + let parent = find_parent_dlmm_instruction(instruction, instructions); + let parent_name = parent_dlmm_instruction_name(parent); + let parent_accounts = parent_dlmm_accounts(parent); + let parent_pool_account = + resolve_anchor_parent_pool_account(parent_name, parent_accounts.as_slice()); let event_kind = "meteora_dlmm.position_close_event".to_string(); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", @@ -1331,7 +2416,9 @@ fn decode_anchor_position_close_event( "proofStatus": "upstream_git_layout_unverified", "position": position, "owner": owner, - "actorWallet": owner + "actorWallet": owner, + "parentPoolAccount": parent_pool_account, + "materializationSource": "anchor_position_create_event" }); return Some(crate::MeteoraDlmmDecodedEvent::Liquidity(crate::MeteoraDlmmLiquidityDecoded { transaction_id, @@ -1339,7 +2426,7 @@ fn decode_anchor_position_close_event( signature: signature.to_string(), program_id: program_id.to_string(), event_kind, - pool_account: None, + pool_account: parent_pool_account, token_a_mint: None, token_b_mint: None, actor_wallet: owner, @@ -1690,6 +2777,150 @@ fn extract_json_string_field( return None; } +fn classify_instruction_name_from_normalized_text(normalized: &str) -> MeteoraDlmmInstructionName { + match normalized { + "add_liquidity" => return MeteoraDlmmInstructionName::AddLiquidity, + "add_liquidity2" => return MeteoraDlmmInstructionName::AddLiquidity2, + "add_liquidity_by_strategy" => return MeteoraDlmmInstructionName::AddLiquidityByStrategy, + "add_liquidity_by_strategy2" => return MeteoraDlmmInstructionName::AddLiquidityByStrategy2, + "add_liquidity_by_strategy_one_side" => { + return MeteoraDlmmInstructionName::AddLiquidityByStrategyOneSide; + }, + "add_liquidity_by_weight" => return MeteoraDlmmInstructionName::AddLiquidityByWeight, + "add_liquidity_by_weight2" => return MeteoraDlmmInstructionName::AddLiquidityByWeight2, + "add_liquidity_one_side" => return MeteoraDlmmInstructionName::AddLiquidityOneSide, + "add_liquidity_one_side_precise" => { + return MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise; + }, + "add_liquidity_one_side_precise2" => { + return MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise2; + }, + "cancel_limit_order" => return MeteoraDlmmInstructionName::CancelLimitOrder, + "claim_fee" => return MeteoraDlmmInstructionName::ClaimFee, + "claim_fee2" => return MeteoraDlmmInstructionName::ClaimFee2, + "claim_reward" => return MeteoraDlmmInstructionName::ClaimReward, + "claim_reward2" => return MeteoraDlmmInstructionName::ClaimReward2, + "close_bin_array" => return MeteoraDlmmInstructionName::CloseBinArray, + "close_claim_fee_operator_account" => { + return MeteoraDlmmInstructionName::CloseClaimFeeOperatorAccount; + }, + "close_limit_order_if_empty" => return MeteoraDlmmInstructionName::CloseLimitOrderIfEmpty, + "close_operator_account" => return MeteoraDlmmInstructionName::CloseOperatorAccount, + "close_position" => return MeteoraDlmmInstructionName::ClosePosition, + "close_position2" => return MeteoraDlmmInstructionName::ClosePosition2, + "close_position_if_empty" => return MeteoraDlmmInstructionName::ClosePositionIfEmpty, + "close_preset_parameter" => return MeteoraDlmmInstructionName::ClosePresetParameter, + "close_preset_parameter2" => return MeteoraDlmmInstructionName::ClosePresetParameter2, + "close_token_badge" => return MeteoraDlmmInstructionName::CloseTokenBadge, + "create_operator_account" => return MeteoraDlmmInstructionName::CreateOperatorAccount, + "decrease_position_length" => return MeteoraDlmmInstructionName::DecreasePositionLength, + "for_idl_type_generation_do_not_call" => { + return MeteoraDlmmInstructionName::ForIdlTypeGenerationDoNotCall; + }, + "fund_reward" => return MeteoraDlmmInstructionName::FundReward, + "go_to_a_bin" => return MeteoraDlmmInstructionName::GoToABin, + "increase_oracle_length" => return MeteoraDlmmInstructionName::IncreaseOracleLength, + "increase_position_length" => return MeteoraDlmmInstructionName::IncreasePositionLength, + "increase_position_length2" => return MeteoraDlmmInstructionName::IncreasePositionLength2, + "initialize_bin_array" => return MeteoraDlmmInstructionName::InitializeBinArray, + "initialize_bin_array_bitmap_extension" => { + return MeteoraDlmmInstructionName::InitializeBinArrayBitmapExtension; + }, + "initialize_customizable_permissionless_lb_pair" => { + return MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair; + }, + "initialize_customizable_permissionless_lb_pair2" => { + return MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2; + }, + "initialize_lb_pair" => return MeteoraDlmmInstructionName::InitializeLbPair, + "initialize_lb_pair2" => return MeteoraDlmmInstructionName::InitializeLbPair2, + "initialize_permission_lb_pair" => { + return MeteoraDlmmInstructionName::InitializePermissionLbPair; + }, + "initialize_position" => return MeteoraDlmmInstructionName::InitializePosition, + "initialize_position2" => return MeteoraDlmmInstructionName::InitializePosition2, + "initialize_position_by_operator" => { + return MeteoraDlmmInstructionName::InitializePositionByOperator; + }, + "initialize_position_pda" => return MeteoraDlmmInstructionName::InitializePositionPda, + "initialize_preset_parameter" => { + return MeteoraDlmmInstructionName::InitializePresetParameter; + }, + "initialize_preset_parameter_v2" => { + return MeteoraDlmmInstructionName::InitializePresetParameterV2; + }, + "initialize_reward" => return MeteoraDlmmInstructionName::InitializeReward, + "initialize_token_badge" => return MeteoraDlmmInstructionName::InitializeTokenBadge, + "place_limit_order" => return MeteoraDlmmInstructionName::PlaceLimitOrder, + "rebalance_liquidity" => return MeteoraDlmmInstructionName::RebalanceLiquidity, + "remove_all_liquidity" => return MeteoraDlmmInstructionName::RemoveAllLiquidity, + "remove_liquidity" => return MeteoraDlmmInstructionName::RemoveLiquidity, + "remove_liquidity2" => return MeteoraDlmmInstructionName::RemoveLiquidity2, + "remove_liquidity_by_range" => return MeteoraDlmmInstructionName::RemoveLiquidityByRange, + "remove_liquidity_by_range2" => return MeteoraDlmmInstructionName::RemoveLiquidityByRange2, + "set_activation_point" => return MeteoraDlmmInstructionName::SetActivationPoint, + "set_pair_status" => return MeteoraDlmmInstructionName::SetPairStatus, + "set_pair_status_permissionless" => { + return MeteoraDlmmInstructionName::SetPairStatusPermissionless; + }, + "set_permissionless_operation_bits" => { + return MeteoraDlmmInstructionName::SetPermissionlessOperationBits; + }, + "set_pre_activation_duration" => { + return MeteoraDlmmInstructionName::SetPreActivationDuration; + }, + "set_pre_activation_swap_address" => { + return MeteoraDlmmInstructionName::SetPreActivationSwapAddress; + }, + "swap" => return MeteoraDlmmInstructionName::Swap, + "swap2" => return MeteoraDlmmInstructionName::Swap2, + "swap_exact_out" => return MeteoraDlmmInstructionName::SwapExactOut, + "swap_exact_out2" => return MeteoraDlmmInstructionName::SwapExactOut2, + "swap_with_price_impact" => return MeteoraDlmmInstructionName::SwapWithPriceImpact, + "swap_with_price_impact2" => return MeteoraDlmmInstructionName::SwapWithPriceImpact2, + "update_base_fee_parameters" => return MeteoraDlmmInstructionName::UpdateBaseFeeParameters, + "update_dynamic_fee_parameters" => { + return MeteoraDlmmInstructionName::UpdateDynamicFeeParameters; + }, + "update_fees_and_reward2" => return MeteoraDlmmInstructionName::UpdateFeesAndReward2, + "update_fees_and_rewards" => return MeteoraDlmmInstructionName::UpdateFeesAndRewards, + "update_position_operator" => return MeteoraDlmmInstructionName::UpdatePositionOperator, + "update_reward_duration" => return MeteoraDlmmInstructionName::UpdateRewardDuration, + "update_reward_funder" => return MeteoraDlmmInstructionName::UpdateRewardFunder, + "withdraw_ineligible_reward" => { + return MeteoraDlmmInstructionName::WithdrawIneligibleReward; + }, + "withdraw_protocol_fee" => return MeteoraDlmmInstructionName::WithdrawProtocolFee, + "zap_protocol_fee" => return MeteoraDlmmInstructionName::ZapProtocolFee, + _ => return MeteoraDlmmInstructionName::Unknown, + } +} + +fn normalize_dlmm_instruction_text(value: &str) -> std::string::String { + let mut normalized = std::string::String::new(); + let mut previous_was_separator = false; + for character in value.chars() { + if character == '_' || character == '-' || character == ' ' { + if !previous_was_separator && !normalized.is_empty() { + normalized.push('_'); + } + previous_was_separator = true; + continue; + } + if character.is_ascii_uppercase() { + if !previous_was_separator && !normalized.is_empty() { + normalized.push('_'); + } + normalized.push(character.to_ascii_lowercase()); + previous_was_separator = false; + continue; + } + normalized.push(character.to_ascii_lowercase()); + previous_was_separator = false; + } + return normalized.trim_matches('_').to_string(); +} + fn classify_instruction_name( parsed_json: std::option::Option<&serde_json::Value>, parsed_type: std::option::Option<&str>, @@ -1703,66 +2934,37 @@ fn classify_instruction_name( if instruction_data.is_some() { return MeteoraDlmmInstructionName::Unknown; } - if contains_create_pool_hint(parsed_type) { - return MeteoraDlmmInstructionName::InitializeLbPair; - } - if contains_swap_hint(parsed_type) { - return MeteoraDlmmInstructionName::Swap; - } - if contains_add_liquidity_hint(parsed_type) { - return MeteoraDlmmInstructionName::AddLiquidity; - } - if contains_remove_liquidity_hint(parsed_type) { - return MeteoraDlmmInstructionName::RemoveLiquidity; - } - if contains_initialize_position_hint(parsed_type) { - return MeteoraDlmmInstructionName::InitializePosition; - } - if contains_initialize_bin_array_hint(parsed_type) { - return MeteoraDlmmInstructionName::InitializeBinArray; - } - if parsed_type.is_some() { - return MeteoraDlmmInstructionName::Unknown; + if let Some(parsed_type) = parsed_type { + let normalized = normalize_dlmm_instruction_text(parsed_type); + let parsed_name = classify_instruction_name_from_normalized_text(normalized.as_str()); + if parsed_name != MeteoraDlmmInstructionName::Unknown { + return parsed_name; + } } if let Some(parsed_json) = parsed_json { - if contains_create_pool_hint_in_value(parsed_json) { - return MeteoraDlmmInstructionName::InitializeLbPair; + if let Some(parsed_name) = extract_string_by_candidate_keys( + Some(parsed_json), + &["type", "instruction", "instructionName", "instruction_name", "name"], + ) { + let normalized = normalize_dlmm_instruction_text(parsed_name.as_str()); + let parsed_name = classify_instruction_name_from_normalized_text(normalized.as_str()); + if parsed_name != MeteoraDlmmInstructionName::Unknown { + return parsed_name; + } } - if contains_swap_hint_in_value(parsed_json) { - return MeteoraDlmmInstructionName::Swap; - } - if contains_add_liquidity_hint_in_value(parsed_json) { - return MeteoraDlmmInstructionName::AddLiquidity; - } - if contains_remove_liquidity_hint_in_value(parsed_json) { - return MeteoraDlmmInstructionName::RemoveLiquidity; - } - if contains_initialize_position_hint_in_value(parsed_json) { - return MeteoraDlmmInstructionName::InitializePosition; - } - if contains_initialize_bin_array_hint_in_value(parsed_json) { - return MeteoraDlmmInstructionName::InitializeBinArray; - } - return MeteoraDlmmInstructionName::Unknown; } for log_message in log_messages { - if contains_create_pool_hint(Some(log_message.as_str())) { - return MeteoraDlmmInstructionName::InitializeLbPair; + if !log_message.contains("Instruction:") { + continue; } - if contains_swap_hint(Some(log_message.as_str())) { - return MeteoraDlmmInstructionName::Swap; - } - if contains_add_liquidity_hint(Some(log_message.as_str())) { - return MeteoraDlmmInstructionName::AddLiquidity; - } - if contains_remove_liquidity_hint(Some(log_message.as_str())) { - return MeteoraDlmmInstructionName::RemoveLiquidity; - } - if contains_initialize_position_hint(Some(log_message.as_str())) { - return MeteoraDlmmInstructionName::InitializePosition; - } - if contains_initialize_bin_array_hint(Some(log_message.as_str())) { - return MeteoraDlmmInstructionName::InitializeBinArray; + let instruction_text = match log_message.split("Instruction:").nth(1) { + Some(value) => value.trim(), + None => continue, + }; + let normalized = normalize_dlmm_instruction_text(instruction_text); + let log_name = classify_instruction_name_from_normalized_text(normalized.as_str()); + if log_name != MeteoraDlmmInstructionName::Unknown { + return log_name; } } return MeteoraDlmmInstructionName::Unknown; @@ -1778,21 +2980,111 @@ fn classify_instruction_name_from_data( if instruction_data.len() < 8 { return MeteoraDlmmInstructionName::Unknown; } - let discriminator = [ - instruction_data[0], - instruction_data[1], - instruction_data[2], - instruction_data[3], - instruction_data[4], - instruction_data[5], - instruction_data[6], - instruction_data[7], - ]; - if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR { - return MeteoraDlmmInstructionName::InitializeLbPair; + let discriminator = read_8_bytes(instruction_data, 0); + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY { + return MeteoraDlmmInstructionName::AddLiquidity; } - if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR2 { - return MeteoraDlmmInstructionName::InitializeLbPair2; + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY2 { + return MeteoraDlmmInstructionName::AddLiquidity2; + } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY { + return MeteoraDlmmInstructionName::AddLiquidityByStrategy; + } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY2 { + return MeteoraDlmmInstructionName::AddLiquidityByStrategy2; + } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY_ONE_SIDE { + return MeteoraDlmmInstructionName::AddLiquidityByStrategyOneSide; + } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_WEIGHT { + return MeteoraDlmmInstructionName::AddLiquidityByWeight; + } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_WEIGHT2 { + return MeteoraDlmmInstructionName::AddLiquidityByWeight2; + } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_ONE_SIDE { + return MeteoraDlmmInstructionName::AddLiquidityOneSide; + } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_ONE_SIDE_PRECISE { + return MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise; + } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_ONE_SIDE_PRECISE2 { + return MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise2; + } + if discriminator == DLMM_DISCRIMINATOR_CANCEL_LIMIT_ORDER { + return MeteoraDlmmInstructionName::CancelLimitOrder; + } + if discriminator == DLMM_DISCRIMINATOR_CLAIM_FEE { + return MeteoraDlmmInstructionName::ClaimFee; + } + if discriminator == DLMM_DISCRIMINATOR_CLAIM_FEE2 { + return MeteoraDlmmInstructionName::ClaimFee2; + } + if discriminator == DLMM_DISCRIMINATOR_CLAIM_REWARD { + return MeteoraDlmmInstructionName::ClaimReward; + } + if discriminator == DLMM_DISCRIMINATOR_CLAIM_REWARD2 { + return MeteoraDlmmInstructionName::ClaimReward2; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_BIN_ARRAY { + return MeteoraDlmmInstructionName::CloseBinArray; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_CLAIM_FEE_OPERATOR_ACCOUNT { + return MeteoraDlmmInstructionName::CloseClaimFeeOperatorAccount; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_LIMIT_ORDER_IF_EMPTY { + return MeteoraDlmmInstructionName::CloseLimitOrderIfEmpty; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_OPERATOR_ACCOUNT { + return MeteoraDlmmInstructionName::CloseOperatorAccount; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_POSITION { + return MeteoraDlmmInstructionName::ClosePosition; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_POSITION2 { + return MeteoraDlmmInstructionName::ClosePosition2; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_POSITION_IF_EMPTY { + return MeteoraDlmmInstructionName::ClosePositionIfEmpty; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_PRESET_PARAMETER { + return MeteoraDlmmInstructionName::ClosePresetParameter; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_PRESET_PARAMETER2 { + return MeteoraDlmmInstructionName::ClosePresetParameter2; + } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_TOKEN_BADGE { + return MeteoraDlmmInstructionName::CloseTokenBadge; + } + if discriminator == DLMM_DISCRIMINATOR_CREATE_OPERATOR_ACCOUNT { + return MeteoraDlmmInstructionName::CreateOperatorAccount; + } + if discriminator == DLMM_DISCRIMINATOR_DECREASE_POSITION_LENGTH { + return MeteoraDlmmInstructionName::DecreasePositionLength; + } + if discriminator == DLMM_DISCRIMINATOR_FOR_IDL_TYPE_GENERATION_DO_NOT_CALL { + return MeteoraDlmmInstructionName::ForIdlTypeGenerationDoNotCall; + } + if discriminator == DLMM_DISCRIMINATOR_FUND_REWARD { + return MeteoraDlmmInstructionName::FundReward; + } + if discriminator == DLMM_DISCRIMINATOR_GO_TO_A_BIN { + return MeteoraDlmmInstructionName::GoToABin; + } + if discriminator == DLMM_DISCRIMINATOR_INCREASE_ORACLE_LENGTH { + return MeteoraDlmmInstructionName::IncreaseOracleLength; + } + if discriminator == DLMM_DISCRIMINATOR_INCREASE_POSITION_LENGTH { + return MeteoraDlmmInstructionName::IncreasePositionLength; + } + if discriminator == DLMM_DISCRIMINATOR_INCREASE_POSITION_LENGTH2 { + return MeteoraDlmmInstructionName::IncreasePositionLength2; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_BIN_ARRAY { + return MeteoraDlmmInstructionName::InitializeBinArray; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_BIN_ARRAY_BITMAP_EXTENSION { + return MeteoraDlmmInstructionName::InitializeBinArrayBitmapExtension; } if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR { return MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair; @@ -1800,6 +3092,78 @@ fn classify_instruction_name_from_data( if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR2 { return MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2; } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR { + return MeteoraDlmmInstructionName::InitializeLbPair; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR2 { + return MeteoraDlmmInstructionName::InitializeLbPair2; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_PERMISSION_LB_PAIR { + return MeteoraDlmmInstructionName::InitializePermissionLbPair; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_POSITION { + return MeteoraDlmmInstructionName::InitializePosition; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_POSITION2 { + return MeteoraDlmmInstructionName::InitializePosition2; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_POSITION_BY_OPERATOR { + return MeteoraDlmmInstructionName::InitializePositionByOperator; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_POSITION_PDA { + return MeteoraDlmmInstructionName::InitializePositionPda; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_PRESET_PARAMETER { + return MeteoraDlmmInstructionName::InitializePresetParameter; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_PRESET_PARAMETER_V2 { + return MeteoraDlmmInstructionName::InitializePresetParameterV2; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_REWARD { + return MeteoraDlmmInstructionName::InitializeReward; + } + if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_TOKEN_BADGE { + return MeteoraDlmmInstructionName::InitializeTokenBadge; + } + if discriminator == DLMM_DISCRIMINATOR_PLACE_LIMIT_ORDER { + return MeteoraDlmmInstructionName::PlaceLimitOrder; + } + if discriminator == DLMM_DISCRIMINATOR_REBALANCE_LIQUIDITY { + return MeteoraDlmmInstructionName::RebalanceLiquidity; + } + if discriminator == DLMM_DISCRIMINATOR_REMOVE_ALL_LIQUIDITY { + return MeteoraDlmmInstructionName::RemoveAllLiquidity; + } + if discriminator == DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY { + return MeteoraDlmmInstructionName::RemoveLiquidity; + } + if discriminator == DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY2 { + return MeteoraDlmmInstructionName::RemoveLiquidity2; + } + if discriminator == DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY_BY_RANGE { + return MeteoraDlmmInstructionName::RemoveLiquidityByRange; + } + if discriminator == DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY_BY_RANGE2 { + return MeteoraDlmmInstructionName::RemoveLiquidityByRange2; + } + if discriminator == DLMM_DISCRIMINATOR_SET_ACTIVATION_POINT { + return MeteoraDlmmInstructionName::SetActivationPoint; + } + if discriminator == DLMM_DISCRIMINATOR_SET_PAIR_STATUS { + return MeteoraDlmmInstructionName::SetPairStatus; + } + if discriminator == DLMM_DISCRIMINATOR_SET_PAIR_STATUS_PERMISSIONLESS { + return MeteoraDlmmInstructionName::SetPairStatusPermissionless; + } + if discriminator == DLMM_DISCRIMINATOR_SET_PERMISSIONLESS_OPERATION_BITS { + return MeteoraDlmmInstructionName::SetPermissionlessOperationBits; + } + if discriminator == DLMM_DISCRIMINATOR_SET_PRE_ACTIVATION_DURATION { + return MeteoraDlmmInstructionName::SetPreActivationDuration; + } + if discriminator == DLMM_DISCRIMINATOR_SET_PRE_ACTIVATION_SWAP_ADDRESS { + return MeteoraDlmmInstructionName::SetPreActivationSwapAddress; + } if discriminator == DLMM_DISCRIMINATOR_SWAP { return MeteoraDlmmInstructionName::Swap; } @@ -1815,32 +3179,38 @@ fn classify_instruction_name_from_data( if discriminator == DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT { return MeteoraDlmmInstructionName::SwapWithPriceImpact; } - if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_BIN_ARRAY { - return MeteoraDlmmInstructionName::InitializeBinArray; + if discriminator == DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT2 { + return MeteoraDlmmInstructionName::SwapWithPriceImpact2; } - if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY { - return MeteoraDlmmInstructionName::AddLiquidity; + if discriminator == DLMM_DISCRIMINATOR_UPDATE_BASE_FEE_PARAMETERS { + return MeteoraDlmmInstructionName::UpdateBaseFeeParameters; } - if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY2 { - return MeteoraDlmmInstructionName::AddLiquidityByStrategy2; + if discriminator == DLMM_DISCRIMINATOR_UPDATE_DYNAMIC_FEE_PARAMETERS { + return MeteoraDlmmInstructionName::UpdateDynamicFeeParameters; } - if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_WEIGHT { - return MeteoraDlmmInstructionName::AddLiquidityByWeight; + if discriminator == DLMM_DISCRIMINATOR_UPDATE_FEES_AND_REWARD2 { + return MeteoraDlmmInstructionName::UpdateFeesAndReward2; } - if discriminator == DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY { - return MeteoraDlmmInstructionName::RemoveLiquidity; + if discriminator == DLMM_DISCRIMINATOR_UPDATE_FEES_AND_REWARDS { + return MeteoraDlmmInstructionName::UpdateFeesAndRewards; } - if discriminator == DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY_BY_RANGE2 { - return MeteoraDlmmInstructionName::RemoveLiquidityByRange2; + if discriminator == DLMM_DISCRIMINATOR_UPDATE_POSITION_OPERATOR { + return MeteoraDlmmInstructionName::UpdatePositionOperator; } - if discriminator == DLMM_DISCRIMINATOR_CLAIM_FEE2 { - return MeteoraDlmmInstructionName::ClaimFee2; + if discriminator == DLMM_DISCRIMINATOR_UPDATE_REWARD_DURATION { + return MeteoraDlmmInstructionName::UpdateRewardDuration; } - if discriminator == DLMM_DISCRIMINATOR_CLOSE_POSITION_IF_EMPTY { - return MeteoraDlmmInstructionName::ClosePositionIfEmpty; + if discriminator == DLMM_DISCRIMINATOR_UPDATE_REWARD_FUNDER { + return MeteoraDlmmInstructionName::UpdateRewardFunder; } - if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_POSITION { - return MeteoraDlmmInstructionName::InitializePosition; + if discriminator == DLMM_DISCRIMINATOR_WITHDRAW_INELIGIBLE_REWARD { + return MeteoraDlmmInstructionName::WithdrawIneligibleReward; + } + if discriminator == DLMM_DISCRIMINATOR_WITHDRAW_PROTOCOL_FEE { + return MeteoraDlmmInstructionName::WithdrawProtocolFee; + } + if discriminator == DLMM_DISCRIMINATOR_ZAP_PROTOCOL_FEE { + return MeteoraDlmmInstructionName::ZapProtocolFee; } return MeteoraDlmmInstructionName::Unknown; } @@ -1866,6 +3236,9 @@ fn resolve_dlmm_pool_account( return parsed_value; } match instruction_name { + MeteoraDlmmInstructionName::InitializePermissionLbPair => { + return extract_account(accounts, 1); + }, MeteoraDlmmInstructionName::InitializeLbPair | MeteoraDlmmInstructionName::InitializeLbPair2 | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair @@ -1875,28 +3248,55 @@ fn resolve_dlmm_pool_account( | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 | MeteoraDlmmInstructionName::SwapWithPriceImpact - | MeteoraDlmmInstructionName::InitializeBinArray => { - return extract_account(accounts, 0); - }, + | MeteoraDlmmInstructionName::SwapWithPriceImpact2 + | MeteoraDlmmInstructionName::InitializeBinArray + | MeteoraDlmmInstructionName::InitializeBinArrayBitmapExtension + | MeteoraDlmmInstructionName::CloseBinArray + | MeteoraDlmmInstructionName::GoToABin + | MeteoraDlmmInstructionName::IncreaseOracleLength + | MeteoraDlmmInstructionName::UpdateFeesAndRewards + | MeteoraDlmmInstructionName::UpdateFeesAndReward2 + | MeteoraDlmmInstructionName::UpdateBaseFeeParameters + | MeteoraDlmmInstructionName::UpdateDynamicFeeParameters + | MeteoraDlmmInstructionName::SetPairStatus + | MeteoraDlmmInstructionName::SetPairStatusPermissionless + | MeteoraDlmmInstructionName::SetActivationPoint + | MeteoraDlmmInstructionName::SetPreActivationDuration + | MeteoraDlmmInstructionName::SetPreActivationSwapAddress + | MeteoraDlmmInstructionName::SetPermissionlessOperationBits + | MeteoraDlmmInstructionName::UpdateRewardDuration + | MeteoraDlmmInstructionName::UpdateRewardFunder + | MeteoraDlmmInstructionName::FundReward + | MeteoraDlmmInstructionName::InitializeReward + | MeteoraDlmmInstructionName::ClaimReward + | MeteoraDlmmInstructionName::ClaimReward2 + | MeteoraDlmmInstructionName::WithdrawIneligibleReward + | MeteoraDlmmInstructionName::ClaimFee + | MeteoraDlmmInstructionName::ClaimFee2 + | MeteoraDlmmInstructionName::WithdrawProtocolFee => return extract_account(accounts, 0), MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidity2 + | MeteoraDlmmInstructionName::AddLiquidityByStrategy | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::AddLiquidityByStrategyOneSide | MeteoraDlmmInstructionName::AddLiquidityByWeight + | MeteoraDlmmInstructionName::AddLiquidityByWeight2 + | MeteoraDlmmInstructionName::AddLiquidityOneSide + | MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise + | MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise2 + | MeteoraDlmmInstructionName::RemoveAllLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity - | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { - return extract_account(accounts, 1); - }, - MeteoraDlmmInstructionName::InitializePosition => { - return extract_account(accounts, 2); - }, - MeteoraDlmmInstructionName::ClaimFee2 => { - return extract_account(accounts, 0); - }, - MeteoraDlmmInstructionName::ClosePositionIfEmpty => { - return None; - }, - MeteoraDlmmInstructionName::Unknown => { - return None; - }, + | MeteoraDlmmInstructionName::RemoveLiquidity2 + | MeteoraDlmmInstructionName::RemoveLiquidityByRange + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 + | MeteoraDlmmInstructionName::RebalanceLiquidity + | MeteoraDlmmInstructionName::ClosePosition + | MeteoraDlmmInstructionName::ClosePosition2 => return extract_account(accounts, 1), + MeteoraDlmmInstructionName::InitializePosition + | MeteoraDlmmInstructionName::InitializePosition2 => return extract_account(accounts, 2), + MeteoraDlmmInstructionName::InitializePositionByOperator + | MeteoraDlmmInstructionName::InitializePositionPda => return extract_account(accounts, 3), + _ => return None, } } @@ -1923,6 +3323,9 @@ fn resolve_dlmm_token_x_mint( return parsed_value; } match instruction_name { + MeteoraDlmmInstructionName::InitializePermissionLbPair => { + return extract_account(accounts, 3); + }, MeteoraDlmmInstructionName::InitializeLbPair | MeteoraDlmmInstructionName::InitializeLbPair2 | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair @@ -1933,21 +3336,29 @@ fn resolve_dlmm_token_x_mint( | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 - | MeteoraDlmmInstructionName::SwapWithPriceImpact => { - return extract_account(accounts, 6); - }, + | MeteoraDlmmInstructionName::SwapWithPriceImpact + | MeteoraDlmmInstructionName::SwapWithPriceImpact2 => return extract_account(accounts, 6), MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidity2 + | MeteoraDlmmInstructionName::AddLiquidityByStrategy | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::AddLiquidityByStrategyOneSide | MeteoraDlmmInstructionName::AddLiquidityByWeight + | MeteoraDlmmInstructionName::AddLiquidityByWeight2 + | MeteoraDlmmInstructionName::AddLiquidityOneSide + | MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise + | MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise2 + | MeteoraDlmmInstructionName::RemoveAllLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity - | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { - return extract_account(accounts, 7); - }, - MeteoraDlmmInstructionName::ClaimFee2 - | MeteoraDlmmInstructionName::ClosePositionIfEmpty - | MeteoraDlmmInstructionName::InitializeBinArray - | MeteoraDlmmInstructionName::InitializePosition - | MeteoraDlmmInstructionName::Unknown => return None, + | MeteoraDlmmInstructionName::RemoveLiquidity2 + | MeteoraDlmmInstructionName::RemoveLiquidityByRange + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 + | MeteoraDlmmInstructionName::RebalanceLiquidity => return extract_account(accounts, 7), + MeteoraDlmmInstructionName::ClaimFee => return extract_account(accounts, 9), + MeteoraDlmmInstructionName::ClaimFee2 => return extract_account(accounts, 7), + MeteoraDlmmInstructionName::WithdrawProtocolFee => return extract_account(accounts, 3), + MeteoraDlmmInstructionName::ZapProtocolFee => return extract_account(accounts, 2), + _ => return None, } } @@ -1974,6 +3385,9 @@ fn resolve_dlmm_token_y_mint( return parsed_value; } match instruction_name { + MeteoraDlmmInstructionName::InitializePermissionLbPair => { + return extract_account(accounts, 4); + }, MeteoraDlmmInstructionName::InitializeLbPair | MeteoraDlmmInstructionName::InitializeLbPair2 | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair @@ -1984,21 +3398,28 @@ fn resolve_dlmm_token_y_mint( | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 - | MeteoraDlmmInstructionName::SwapWithPriceImpact => { - return extract_account(accounts, 7); - }, + | MeteoraDlmmInstructionName::SwapWithPriceImpact + | MeteoraDlmmInstructionName::SwapWithPriceImpact2 => return extract_account(accounts, 7), MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidity2 + | MeteoraDlmmInstructionName::AddLiquidityByStrategy | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::AddLiquidityByStrategyOneSide | MeteoraDlmmInstructionName::AddLiquidityByWeight + | MeteoraDlmmInstructionName::AddLiquidityByWeight2 + | MeteoraDlmmInstructionName::AddLiquidityOneSide + | MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise + | MeteoraDlmmInstructionName::AddLiquidityOneSidePrecise2 + | MeteoraDlmmInstructionName::RemoveAllLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity - | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { - return extract_account(accounts, 8); - }, - MeteoraDlmmInstructionName::ClaimFee2 - | MeteoraDlmmInstructionName::ClosePositionIfEmpty - | MeteoraDlmmInstructionName::InitializeBinArray - | MeteoraDlmmInstructionName::InitializePosition - | MeteoraDlmmInstructionName::Unknown => return None, + | MeteoraDlmmInstructionName::RemoveLiquidity2 + | MeteoraDlmmInstructionName::RemoveLiquidityByRange + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 + | MeteoraDlmmInstructionName::RebalanceLiquidity => return extract_account(accounts, 8), + MeteoraDlmmInstructionName::ClaimFee => return extract_account(accounts, 10), + MeteoraDlmmInstructionName::ClaimFee2 => return extract_account(accounts, 8), + MeteoraDlmmInstructionName::WithdrawProtocolFee => return extract_account(accounts, 4), + _ => return None, } } @@ -2019,7 +3440,8 @@ fn resolve_dlmm_reserve_x_account( | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 - | MeteoraDlmmInstructionName::SwapWithPriceImpact => { + | MeteoraDlmmInstructionName::SwapWithPriceImpact + | MeteoraDlmmInstructionName::SwapWithPriceImpact2 => { return extract_account(accounts, 2); }, _ => return None, @@ -2043,7 +3465,8 @@ fn resolve_dlmm_reserve_y_account( | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 - | MeteoraDlmmInstructionName::SwapWithPriceImpact => { + | MeteoraDlmmInstructionName::SwapWithPriceImpact + | MeteoraDlmmInstructionName::SwapWithPriceImpact2 => { return extract_account(accounts, 3); }, _ => return None, @@ -2067,7 +3490,8 @@ fn resolve_dlmm_user_token_in_account( | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 - | MeteoraDlmmInstructionName::SwapWithPriceImpact => { + | MeteoraDlmmInstructionName::SwapWithPriceImpact + | MeteoraDlmmInstructionName::SwapWithPriceImpact2 => { return extract_account(accounts, 4); }, _ => return None, @@ -2096,6 +3520,7 @@ fn resolve_dlmm_user_token_out_account( | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapWithPriceImpact + | MeteoraDlmmInstructionName::SwapWithPriceImpact2 | MeteoraDlmmInstructionName::SwapExactOut2 => { return extract_account(accounts, 5); }, @@ -2163,6 +3588,38 @@ fn resolve_dlmm_actor_wallet( } } +fn resolve_dlmm_reward_token_mint( + instruction_name: MeteoraDlmmInstructionName, + parsed_json: std::option::Option<&serde_json::Value>, + accounts: &[std::string::String], +) -> std::option::Option { + let parsed_value = extract_string_by_candidate_keys( + parsed_json, + &[ + "rewardTokenMint", + "reward_token_mint", + "rewardMint", + "reward_mint", + "tokenMint", + "token_mint", + "mint", + ], + ); + if parsed_value.is_some() { + return parsed_value; + } + match instruction_name { + MeteoraDlmmInstructionName::InitializeReward + | MeteoraDlmmInstructionName::FundReward + | MeteoraDlmmInstructionName::WithdrawIneligibleReward => { + return extract_account(accounts, 2); + }, + MeteoraDlmmInstructionName::ClaimReward => return extract_account(accounts, 6), + MeteoraDlmmInstructionName::ClaimReward2 => return extract_account(accounts, 4), + _ => return None, + } +} + fn read_i32_string_from_instruction_data( instruction_name: MeteoraDlmmInstructionName, instruction_data: std::option::Option<&[u8]>, @@ -2311,151 +3768,6 @@ fn extract_number_by_candidate_keys( return None; } -fn contains_create_pool_hint(value: std::option::Option<&str>) -> bool { - let value = match value { - Some(value) => value.to_ascii_lowercase(), - None => return false, - }; - if value.contains("initializelbpair") { - return true; - } - if value.contains("initialize_lb_pair") { - return true; - } - if value.contains("initializecustomizablepermissionlesslbpair") { - return true; - } - if value.contains("initialize_customizable_permissionless_lb_pair") { - return true; - } - if value.contains("initializepermissionlbpair") { - return true; - } - if value.contains("initialize_permission_lb_pair") { - return true; - } - if value.contains("create_lb_pair") { - return true; - } - return false; -} - -fn contains_swap_hint(value: std::option::Option<&str>) -> bool { - let value = match value { - Some(value) => value.to_ascii_lowercase(), - None => return false, - }; - if value.contains("swap") { - return true; - } - return false; -} - -fn contains_add_liquidity_hint(value: std::option::Option<&str>) -> bool { - let value = match value { - Some(value) => value.to_ascii_lowercase(), - None => return false, - }; - if value.contains("addliquidity") { - return true; - } - if value.contains("add_liquidity") { - return true; - } - return false; -} - -fn contains_remove_liquidity_hint(value: std::option::Option<&str>) -> bool { - let value = match value { - Some(value) => value.to_ascii_lowercase(), - None => return false, - }; - if value.contains("removeliquidity") { - return true; - } - if value.contains("remove_liquidity") { - return true; - } - return false; -} - -fn contains_initialize_position_hint(value: std::option::Option<&str>) -> bool { - let value = match value { - Some(value) => value.to_ascii_lowercase(), - None => return false, - }; - if value.contains("initializeposition") { - return true; - } - if value.contains("initialize_position") { - return true; - } - return false; -} - -fn contains_initialize_bin_array_hint(value: std::option::Option<&str>) -> bool { - let value = match value { - Some(value) => value.to_ascii_lowercase(), - None => return false, - }; - if value.contains("initializebinarray") { - return true; - } - if value.contains("initialize_bin_array") { - return true; - } - return false; -} - -fn contains_create_pool_hint_in_value(value: &serde_json::Value) -> bool { - return contains_string_hint_in_value(value, contains_create_pool_hint); -} - -fn contains_swap_hint_in_value(value: &serde_json::Value) -> bool { - return contains_string_hint_in_value(value, contains_swap_hint); -} - -fn contains_add_liquidity_hint_in_value(value: &serde_json::Value) -> bool { - return contains_string_hint_in_value(value, contains_add_liquidity_hint); -} - -fn contains_remove_liquidity_hint_in_value(value: &serde_json::Value) -> bool { - return contains_string_hint_in_value(value, contains_remove_liquidity_hint); -} - -fn contains_initialize_position_hint_in_value(value: &serde_json::Value) -> bool { - return contains_string_hint_in_value(value, contains_initialize_position_hint); -} - -fn contains_initialize_bin_array_hint_in_value(value: &serde_json::Value) -> bool { - return contains_string_hint_in_value(value, contains_initialize_bin_array_hint); -} - -fn contains_string_hint_in_value( - value: &serde_json::Value, - predicate: fn(std::option::Option<&str>) -> bool, -) -> bool { - match value { - serde_json::Value::String(text) => return predicate(Some(text.as_str())), - serde_json::Value::Array(values) => { - for nested in values { - if contains_string_hint_in_value(nested, predicate) { - return true; - } - } - }, - serde_json::Value::Object(object) => { - for nested in object.values() { - if contains_string_hint_in_value(nested, predicate) { - return true; - } - } - }, - _ => {}, - } - return false; -} - fn infer_trade_side(parsed_json: std::option::Option<&serde_json::Value>) -> crate::SwapTradeSide { let side = extract_string_by_candidate_keys( parsed_json, @@ -3174,4 +4486,497 @@ mod tests { super::classify_instruction_name(None, None, Some(&instruction_data), &log_messages); assert_eq!(name, super::MeteoraDlmmInstructionName::Unknown); } + + #[test] + fn meteora_dlmm_classifies_observed_initialize_preset_parameter_v2_discriminator() { + let instruction_data = + [0x75, 0xc7, 0x3e, 0x67, 0x06, 0x8e, 0x1f, 0xcb, 0x64, 0x00, 0x10, 0x27]; + let log_messages = + vec!["Program log: Instruction: InitializePresetParameterV2".to_string()]; + let name = + super::classify_instruction_name(None, None, Some(&instruction_data), &log_messages); + assert_eq!(name, super::MeteoraDlmmInstructionName::InitializePresetParameterV2); + assert_eq!(name.as_str(), "initialize_preset_parameter_v2"); + assert_eq!(name.kind(), super::MeteoraDlmmInstructionKind::Admin); + } + + #[test] + fn meteora_dlmm_all_local_idl_instruction_discriminators_are_classified() { + let cases: &[(&str, [u8; 8])] = &[ + ("add_liquidity", [0xb5, 0x9d, 0x59, 0x43, 0x8f, 0xb6, 0x34, 0x48]), + ("add_liquidity2", [0xe4, 0xa2, 0x4e, 0x1c, 0x46, 0xdb, 0x74, 0x73]), + ("add_liquidity_by_strategy", [0x07, 0x03, 0x96, 0x7f, 0x94, 0x28, 0x3d, 0xc8]), + ("add_liquidity_by_strategy2", [0x03, 0xdd, 0x95, 0xda, 0x6f, 0x8d, 0x76, 0xd5]), + ( + "add_liquidity_by_strategy_one_side", + [0x29, 0x05, 0xee, 0xaf, 0x64, 0xe1, 0x06, 0xcd], + ), + ("add_liquidity_by_weight", [0x1c, 0x8c, 0xee, 0x63, 0xe7, 0xa2, 0x15, 0x95]), + ("add_liquidity_by_weight2", [0xd1, 0x3b, 0x3f, 0x5b, 0x6f, 0xc8, 0x99, 0xe4]), + ("add_liquidity_one_side", [0x5e, 0x9b, 0x67, 0x97, 0x46, 0x5f, 0xdc, 0xa5]), + ( + "add_liquidity_one_side_precise", + [0xa1, 0xc2, 0x67, 0x54, 0xab, 0x47, 0xfa, 0x9a], + ), + ( + "add_liquidity_one_side_precise2", + [0x21, 0x33, 0xa3, 0xc9, 0x75, 0x62, 0x7d, 0xe7], + ), + ("cancel_limit_order", [0x84, 0x9c, 0x84, 0x1f, 0x43, 0x28, 0xe8, 0x61]), + ("claim_fee", [0xa9, 0x20, 0x4f, 0x89, 0x88, 0xe8, 0x46, 0x89]), + ("claim_fee2", [0x70, 0xbf, 0x65, 0xab, 0x1c, 0x90, 0x7f, 0xbb]), + ("claim_reward", [0x95, 0x5f, 0xb5, 0xf2, 0x5e, 0x5a, 0x9e, 0xa2]), + ("claim_reward2", [0xbe, 0x03, 0x7f, 0x77, 0xb2, 0x57, 0x9d, 0xb7]), + ("close_bin_array", [0x44, 0xae, 0x58, 0x50, 0xb5, 0xcc, 0x13, 0xe0]), + ( + "close_claim_fee_operator_account", + [0xb8, 0xd5, 0x58, 0x1f, 0xb3, 0x65, 0x82, 0x24], + ), + ("close_limit_order_if_empty", [0x39, 0x7c, 0x24, 0x9b, 0x7e, 0xf9, 0x5d, 0xab]), + ("close_operator_account", [0xab, 0x09, 0xd5, 0x4a, 0x78, 0x17, 0x03, 0x1d]), + ("close_position", [0x7b, 0x86, 0x51, 0x00, 0x31, 0x44, 0x62, 0x62]), + ("close_position2", [0xae, 0x5a, 0x23, 0x73, 0xba, 0x28, 0x93, 0xe2]), + ("close_position_if_empty", [0x3b, 0x7c, 0xd4, 0x76, 0x5b, 0x98, 0x6e, 0x9d]), + ("close_preset_parameter", [0x04, 0x94, 0x91, 0x64, 0x86, 0x1a, 0xb5, 0x3d]), + ("close_preset_parameter2", [0x27, 0x19, 0x5f, 0x6b, 0x74, 0x11, 0x73, 0x1c]), + ("close_token_badge", [0x6c, 0x92, 0x56, 0x6e, 0xb3, 0xfe, 0x0a, 0x68]), + ("create_operator_account", [0xdd, 0x40, 0xf6, 0x95, 0xf0, 0x99, 0xe5, 0xa3]), + ("decrease_position_length", [0xc2, 0xdb, 0x88, 0x20, 0x19, 0x60, 0x69, 0x25]), + ( + "for_idl_type_generation_do_not_call", + [0xb4, 0x69, 0x45, 0x50, 0x5f, 0x32, 0x49, 0x6c], + ), + ("fund_reward", [0xbc, 0x32, 0xf9, 0xa5, 0x5d, 0x97, 0x26, 0x3f]), + ("go_to_a_bin", [0x92, 0x48, 0xae, 0xe0, 0x28, 0xfd, 0x54, 0xae]), + ("increase_oracle_length", [0xbe, 0x3d, 0x7d, 0x57, 0x67, 0x4f, 0x9e, 0xad]), + ("increase_position_length", [0x50, 0x53, 0x75, 0xd3, 0x42, 0x0d, 0x21, 0x95]), + ("increase_position_length2", [0xff, 0xd2, 0xcc, 0x47, 0x73, 0x89, 0xe1, 0x71]), + ("initialize_bin_array", [0x23, 0x56, 0x13, 0xb9, 0x4e, 0xd4, 0x4b, 0xd3]), + ( + "initialize_bin_array_bitmap_extension", + [0x2f, 0x9d, 0xe2, 0xb4, 0x0c, 0xf0, 0x21, 0x47], + ), + ( + "initialize_customizable_permissionless_lb_pair", + [0x2e, 0x27, 0x29, 0x87, 0x6f, 0xb7, 0xc8, 0x40], + ), + ( + "initialize_customizable_permissionless_lb_pair2", + [0xf3, 0x49, 0x81, 0x7e, 0x33, 0x13, 0xf1, 0x6b], + ), + ("initialize_lb_pair", [0x2d, 0x9a, 0xed, 0xd2, 0xdd, 0x0f, 0xa6, 0x5c]), + ("initialize_lb_pair2", [0x49, 0x3b, 0x24, 0x78, 0xed, 0x53, 0x6c, 0xc6]), + ( + "initialize_permission_lb_pair", + [0x6c, 0x66, 0xd5, 0x55, 0xfb, 0x03, 0x35, 0x15], + ), + ("initialize_position", [0xdb, 0xc0, 0xea, 0x47, 0xbe, 0xbf, 0x66, 0x50]), + ("initialize_position2", [0x8f, 0x13, 0xf2, 0x91, 0xd5, 0x0f, 0x68, 0x73]), + ( + "initialize_position_by_operator", + [0xfb, 0xbd, 0xbe, 0xf4, 0x75, 0xfe, 0x23, 0x94], + ), + ("initialize_position_pda", [0x2e, 0x52, 0x7d, 0x92, 0x55, 0x8d, 0xe4, 0x99]), + ("initialize_preset_parameter", [0x42, 0xbc, 0x47, 0xd3, 0x62, 0x6d, 0x0e, 0xba]), + ("initialize_reward", [0x5f, 0x87, 0xc0, 0xc4, 0xf2, 0x81, 0xe6, 0x44]), + ("initialize_token_badge", [0xfd, 0x4d, 0xcd, 0x5f, 0x1b, 0xe0, 0x59, 0xdf]), + ("place_limit_order", [0x6c, 0xb0, 0x21, 0xba, 0x92, 0xe5, 0x01, 0xc5]), + ("rebalance_liquidity", [0x5c, 0x04, 0xb0, 0xc1, 0x77, 0xb9, 0x53, 0x09]), + ("remove_all_liquidity", [0x0a, 0x33, 0x3d, 0x23, 0x70, 0x69, 0x18, 0x55]), + ("remove_liquidity", [0x50, 0x55, 0xd1, 0x48, 0x18, 0xce, 0xb1, 0x6c]), + ("remove_liquidity2", [0xe6, 0xd7, 0x52, 0x7f, 0xf1, 0x65, 0xe3, 0x92]), + ("remove_liquidity_by_range", [0x1a, 0x52, 0x66, 0x98, 0xf0, 0x4a, 0x69, 0x1a]), + ("remove_liquidity_by_range2", [0xcc, 0x02, 0xc3, 0x91, 0x35, 0x91, 0x91, 0xcd]), + ("set_activation_point", [0x5b, 0xf9, 0x0f, 0xa5, 0x1a, 0x81, 0xfe, 0x7d]), + ("set_pair_status", [0x43, 0xf8, 0xe7, 0x89, 0x9a, 0x95, 0xd9, 0xae]), + ( + "set_pair_status_permissionless", + [0x4e, 0x3b, 0x98, 0xd3, 0x46, 0xb7, 0x2e, 0xd0], + ), + ( + "set_permissionless_operation_bits", + [0x54, 0x3a, 0xcb, 0x8b, 0xa3, 0x51, 0xbe, 0xba], + ), + ("set_pre_activation_duration", [0xa5, 0x3d, 0xc9, 0xf4, 0x82, 0x9f, 0x16, 0x64]), + ( + "set_pre_activation_swap_address", + [0x39, 0x8b, 0x2f, 0x7b, 0xd8, 0x50, 0xdf, 0x0a], + ), + ("swap", [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8]), + ("swap2", [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88]), + ("swap_exact_out", [0xfa, 0x49, 0x65, 0x21, 0x26, 0xcf, 0x4b, 0xb8]), + ("swap_exact_out2", [0x2b, 0xd7, 0xf7, 0x84, 0x89, 0x3c, 0xf3, 0x51]), + ("swap_with_price_impact", [0x38, 0xad, 0xe6, 0xd0, 0xad, 0xe4, 0x9c, 0xcd]), + ("swap_with_price_impact2", [0x4a, 0x62, 0xc0, 0xd6, 0xb1, 0x33, 0x4b, 0x33]), + ("update_base_fee_parameters", [0x4b, 0xa8, 0xdf, 0xa1, 0x10, 0xc3, 0x03, 0x2f]), + ( + "update_dynamic_fee_parameters", + [0x5c, 0xa1, 0x2e, 0xf6, 0xff, 0xbd, 0x16, 0x16], + ), + ("update_fees_and_reward2", [0x20, 0x8e, 0xb8, 0x9a, 0x67, 0x41, 0xb8, 0x58]), + ("update_fees_and_rewards", [0x9a, 0xe6, 0xfa, 0x0d, 0xec, 0xd1, 0x4b, 0xdf]), + ("update_position_operator", [0xca, 0xb8, 0x67, 0x8f, 0xb4, 0xbf, 0x74, 0xd9]), + ("update_reward_duration", [0x8a, 0xae, 0xc4, 0xa9, 0xd5, 0xeb, 0xfe, 0x6b]), + ("update_reward_funder", [0xd3, 0x1c, 0x30, 0x20, 0xd7, 0xa0, 0x23, 0x17]), + ("withdraw_ineligible_reward", [0x94, 0xce, 0x2a, 0xc3, 0xf7, 0x31, 0x67, 0x08]), + ("withdraw_protocol_fee", [0x9e, 0xc9, 0x9e, 0xbd, 0x21, 0x5d, 0xa2, 0x67]), + ("zap_protocol_fee", [0xd5, 0x9b, 0xbb, 0x22, 0x38, 0xb6, 0x5b, 0xf0]), + ]; + assert_eq!(cases.len(), 76); + for (expected_name, discriminator) in cases { + let decoded_name = super::classify_instruction_name_from_data(Some(discriminator)); + assert_eq!(decoded_name.as_str(), *expected_name); + assert_ne!(decoded_name.kind(), super::MeteoraDlmmInstructionKind::Unknown); + } + } + + #[test] + fn meteora_dlmm_all_local_idl_anchor_event_discriminators_are_named() { + let cases: &[(&str, &str)] = &[ + ("add_liquidity_event", "1f5e7d5ae3343dba"), + ("cancel_limit_order_evt", "83eac285090ebdd1"), + ("claim_fee_event", "4b7a9a308c4a7ba3"), + ("claim_fee2_event", "e8abf2613a4d232d"), + ("claim_reward_event", "947486cc16ab555f"), + ("claim_reward2_event", "1b8ff421502b6e92"), + ("close_limit_order_evt", "8e87084c5c3f7653"), + ("composition_fee_event", "80977b6a1166718e"), + ("decrease_position_length_event", "3476eb55aca90f80"), + ("dynamic_fee_parameter_update_event", "5858b287c2925bf3"), + ("fee_parameter_update_event", "304cf17590d7f22c"), + ("fund_reward_event", "f6e43a8291aa4fcc"), + ("go_to_a_bin_event", "3b8a4c448a83b043"), + ("increase_observation_event", "63f91179a69ccfd7"), + ("increase_position_length_event", "9def2acc1e38df2e"), + ("initialize_reward_event", "d399583e953cb146"), + ("lb_pair_create_event", "b94afc7d1bd7bc6f"), + ("place_limit_order_evt", "2b4f1ba9f41ce13f"), + ("position_close_event", "ffc4106b1cca3580"), + ("position_create_event", "908efc549d352579"), + ("rebalancing_event", "006d75b33d5bc7c8"), + ("remove_liquidity_event", "74f461e8671f983a"), + ("set_position_permissionless_operation_bits_evt", "c3e593f51d7d30a8"), + ("swap_event", "516ce3becdd00ac4"), + ("swap2_evt", "2e7452d7941b544d"), + ("update_position_lock_release_point_event", "85d642e0400c07bf"), + ("update_position_operator_event", "277330ccf62f4239"), + ("update_reward_duration_event", "dff5e099311da3ac"), + ("update_reward_funder_event", "e0b2ae4afca555b4"), + ("withdraw_ineligible_reward_event", "e7bd419566d79af4"), + ]; + assert_eq!(cases.len(), 30); + for (expected_name, discriminator_hex) in cases { + let decoded_name = + super::dlmm_anchor_event_name_from_discriminator_hex(discriminator_hex); + assert_eq!(decoded_name, Some(*expected_name)); + } + } + + fn synthetic_data_json(bytes: &[u8]) -> std::string::String { + return serde_json::Value::String(bs58::encode(bytes).into_string()).to_string(); + } + + fn synthetic_instruction_data(discriminator: [u8; 8]) -> std::vec::Vec { + let mut data = std::vec::Vec::new(); + data.extend_from_slice(&discriminator); + data.push(0_u8); + return data; + } + + fn append_synthetic_pubkey(data: &mut std::vec::Vec, seed: u8) { + data.extend_from_slice(&[seed; 32]); + } + + fn append_u64(data: &mut std::vec::Vec, amount: u64) { + data.extend_from_slice(&amount.to_le_bytes()); + } + + fn synthetic_claim_fee_event_data( + event_discriminator: [u8; 8], + fee_x: u64, + fee_y: u64, + ) -> std::vec::Vec { + let mut data = std::vec::Vec::new(); + data.extend_from_slice(&super::DLMM_ANCHOR_SELF_CPI_LOG_SELECTOR); + data.extend_from_slice(&event_discriminator); + append_synthetic_pubkey(&mut data, 1); + append_synthetic_pubkey(&mut data, 2); + append_synthetic_pubkey(&mut data, 3); + append_u64(&mut data, fee_x); + append_u64(&mut data, fee_y); + return data; + } + + fn synthetic_claim_reward_event_data( + event_discriminator: [u8; 8], + reward_index: u64, + reward_amount: u64, + ) -> std::vec::Vec { + let mut data = std::vec::Vec::new(); + data.extend_from_slice(&super::DLMM_ANCHOR_SELF_CPI_LOG_SELECTOR); + data.extend_from_slice(&event_discriminator); + append_synthetic_pubkey(&mut data, 4); + append_synthetic_pubkey(&mut data, 5); + append_synthetic_pubkey(&mut data, 6); + append_u64(&mut data, reward_index); + append_u64(&mut data, reward_amount); + return data; + } + + fn synthetic_transaction(signature: &str, transaction_id: i64) -> crate::ChainTransactionDto { + let mut transaction = crate::ChainTransactionDto::new( + signature.to_string(), + Some(888157), + Some(1779400057), + Some("helius_primary_http".to_string()), + Some("0".to_string()), + None, + None, + serde_json::json!({ + "slot": 888157, + "meta": { "logMessages": [] }, + "transaction": { "message": { "instructions": [] } } + }) + .to_string(), + ); + transaction.id = Some(transaction_id); + return transaction; + } + + #[test] + fn meteora_dlmm_claim_fee_anchor_event_materializes_dual_fee_legs() { + let decoder = crate::MeteoraDlmmDecoder::new(); + let transaction = synthetic_transaction("sig-meteora-dlmm-claim-fee-anchor", 701); + let mut parent = crate::ChainInstructionDto::new( + 701, + None, + 0, + None, + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora_dlmm".to_string()), + Some(1), + serde_json::json!([ + "LbPairFee111", + "Position111", + "ReserveX111", + "ReserveY111", + "Sender111", + "ReserveX111", + "ReserveY111", + "UserTokenX111", + "UserTokenY111", + "TokenXMint111", + "TokenYMint111" + ]) + .to_string(), + Some(synthetic_data_json(&synthetic_instruction_data( + super::DLMM_DISCRIMINATOR_CLAIM_FEE, + ))), + Some("claim_fee".to_string()), + None, + ); + parent.id = Some(702); + let event_data = + synthetic_claim_fee_event_data(super::DLMM_EVENT_DISCRIMINATOR_CLAIM_FEE, 11, 22); + let mut child = crate::ChainInstructionDto::new( + 701, + Some(702), + 0, + Some(0), + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora_dlmm".to_string()), + Some(2), + serde_json::json!([]).to_string(), + Some(synthetic_data_json(&event_data)), + None, + None, + ); + child.id = Some(703); + let decoded_result = decoder.decode_transaction(&transaction, &[parent, child]); + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => panic!("decode must succeed: {}", error), + }; + let mut found = false; + for event in decoded.iter() { + if let crate::MeteoraDlmmDecodedEvent::Fee(fee_event) = event { + if fee_event.event_kind != "meteora_dlmm.claim_fee_event" { + continue; + } + found = true; + assert!(fee_event.payload_json.get("skipFeeReason").is_none()); + let legs = fee_event.payload_json.get("feeAmountLegs"); + let legs = match legs.and_then(serde_json::Value::as_array) { + Some(legs) => legs, + None => panic!("expected feeAmountLegs"), + }; + assert_eq!(legs.len(), 2); + assert_eq!( + legs[0].get("tokenMint").and_then(serde_json::Value::as_str), + Some("TokenXMint111") + ); + assert_eq!( + legs[0].get("amountRaw").and_then(serde_json::Value::as_str), + Some("11") + ); + assert_eq!( + legs[1].get("tokenMint").and_then(serde_json::Value::as_str), + Some("TokenYMint111") + ); + assert_eq!( + legs[1].get("amountRaw").and_then(serde_json::Value::as_str), + Some("22") + ); + } + } + assert!(found); + } + + #[test] + fn meteora_dlmm_claim_reward_anchor_event_materializes_reward_mint() { + let decoder = crate::MeteoraDlmmDecoder::new(); + let transaction = synthetic_transaction("sig-meteora-dlmm-claim-reward-anchor", 704); + let mut parent = crate::ChainInstructionDto::new( + 704, + None, + 1, + None, + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora_dlmm".to_string()), + Some(1), + serde_json::json!([ + "LbPairReward111", + "Position111", + "BinArray111", + "Sender111", + "ReserveReward111", + "UserReward111", + "RewardMint111" + ]) + .to_string(), + Some(synthetic_data_json(&synthetic_instruction_data( + super::DLMM_DISCRIMINATOR_CLAIM_REWARD, + ))), + Some("claim_reward".to_string()), + None, + ); + parent.id = Some(705); + let event_data = + synthetic_claim_reward_event_data(super::DLMM_EVENT_DISCRIMINATOR_CLAIM_REWARD, 2, 333); + let mut child = crate::ChainInstructionDto::new( + 704, + Some(705), + 1, + Some(0), + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora_dlmm".to_string()), + Some(2), + serde_json::json!([]).to_string(), + Some(synthetic_data_json(&event_data)), + None, + None, + ); + child.id = Some(706); + let decoded_result = decoder.decode_transaction(&transaction, &[parent, child]); + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => panic!("decode must succeed: {}", error), + }; + let mut found = false; + for event in decoded.iter() { + if let crate::MeteoraDlmmDecodedEvent::Reward(reward_event) = event { + if reward_event.event_kind != "meteora_dlmm.claim_reward_event" { + continue; + } + found = true; + assert!(reward_event.payload_json.get("skipRewardReason").is_none()); + assert_eq!(reward_event.reward_token_mint, Some("RewardMint111".to_string())); + assert_eq!(reward_event.reward_amount_raw, Some("333".to_string())); + } + } + assert!(found); + } + + #[test] + fn meteora_dlmm_place_limit_order_instruction_materializes_orderbook_target() { + let decoder = crate::MeteoraDlmmDecoder::new(); + let transaction = synthetic_transaction("sig-meteora-dlmm-place-limit-order", 707); + let mut instruction = crate::ChainInstructionDto::new( + 707, + None, + 2, + None, + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora_dlmm".to_string()), + Some(1), + serde_json::json!([ + "LbPairOrder111", + "BinArrayBitmap111", + "Reserve111", + "TokenMint111", + "LimitOrder111", + "Payer111", + "Owner111", + "TokenProgram111", + "Sender111" + ]) + .to_string(), + Some(synthetic_data_json(&synthetic_instruction_data( + super::DLMM_DISCRIMINATOR_PLACE_LIMIT_ORDER, + ))), + Some("place_limit_order".to_string()), + None, + ); + instruction.id = Some(708); + let decoded_result = decoder.decode_transaction(&transaction, &[instruction]); + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => panic!("decode must succeed: {}", error), + }; + assert_eq!(decoded.len(), 1); + match &decoded[0] { + crate::MeteoraDlmmDecodedEvent::PoolLifecycle(event) => { + assert_eq!(event.event_kind, "meteora_dlmm.place_limit_order"); + assert_eq!(event.payload_json.get("skipOrderbookReason"), None); + assert_eq!( + event + .payload_json + .get("materializationTarget") + .and_then(serde_json::Value::as_str), + Some("orderbook"), + ); + assert!(event.payload_json.get("skipTradeReason").is_some()); + assert!(event.payload_json.get("skipCandleReason").is_some()); + }, + _ => panic!("expected materialized orderbook lifecycle event"), + } + } + + #[test] + fn meteora_dlmm_idl_generation_sentinel_does_not_emit_decoded_only_event() { + let decoder = crate::MeteoraDlmmDecoder::new(); + let transaction = synthetic_transaction("sig-meteora-dlmm-idl-sentinel", 709); + let mut instruction = crate::ChainInstructionDto::new( + 709, + None, + 3, + None, + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora_dlmm".to_string()), + Some(1), + serde_json::json!([]).to_string(), + Some(synthetic_data_json(&synthetic_instruction_data( + super::DLMM_DISCRIMINATOR_FOR_IDL_TYPE_GENERATION_DO_NOT_CALL, + ))), + Some("for_idl_type_generation_do_not_call".to_string()), + None, + ); + instruction.id = Some(710); + let decoded_result = decoder.decode_transaction(&transaction, &[instruction]); + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => panic!("decode must succeed: {}", error), + }; + assert_eq!(decoded.len(), 0); + } } diff --git a/kb_lib/src/dex_decode.rs b/kb_lib/src/dex_decode.rs index 62966f3..5f57348 100644 --- a/kb_lib/src/dex_decode.rs +++ b/kb_lib/src/dex_decode.rs @@ -1126,7 +1126,7 @@ impl DexDecodeService { event.instruction_id, "meteora_dlmm", event.program_id.clone(), - "meteora_dlmm.swap", + event.event_kind.as_str(), event.pool_account.clone(), None, event.token_a_mint.clone(), @@ -4273,6 +4273,18 @@ fn candidate_meteora_audit_pool_account( return accounts.get(index).cloned(); } +fn is_meteora_dlmm_instruction_swap_event_kind(event_kind: &str) -> bool { + return matches!( + event_kind, + "meteora_dlmm.swap" + | "meteora_dlmm.swap2" + | "meteora_dlmm.swap_exact_out" + | "meteora_dlmm.swap_exact_out2" + | "meteora_dlmm.swap_with_price_impact" + | "meteora_dlmm.swap_with_price_impact2" + ); +} + fn is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap( protocol_name: &str, instruction: &crate::ChainInstructionDto, @@ -4294,7 +4306,7 @@ fn is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap( } for decoded_event in decoded_events { if decoded_event.protocol_name == "meteora_dlmm" - && decoded_event.event_kind == "meteora_dlmm.swap" + && is_meteora_dlmm_instruction_swap_event_kind(decoded_event.event_kind.as_str()) { return true; } diff --git a/kb_lib/src/dex_detection_route.rs b/kb_lib/src/dex_detection_route.rs index 6a780f5..34f4d13 100644 --- a/kb_lib/src/dex_detection_route.rs +++ b/kb_lib/src/dex_detection_route.rs @@ -170,7 +170,12 @@ pub(crate) fn dex_detection_route( ("meteora_dlmm", "meteora_dlmm.create_pool") => { return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDlmmPool); }, - ("meteora_dlmm", "meteora_dlmm.swap") => { + ("meteora_dlmm", "meteora_dlmm.swap") + | ("meteora_dlmm", "meteora_dlmm.swap2") + | ("meteora_dlmm", "meteora_dlmm.swap_exact_out") + | ("meteora_dlmm", "meteora_dlmm.swap_exact_out2") + | ("meteora_dlmm", "meteora_dlmm.swap_with_price_impact") + | ("meteora_dlmm", "meteora_dlmm.swap_with_price_impact2") => { return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDlmmPool); }, ("meteora_damm_v1", "meteora_damm_v1.create_pool") => { diff --git a/kb_lib/src/dex_event_classification.rs b/kb_lib/src/dex_event_classification.rs index d36786d..6a15caf 100644 --- a/kb_lib/src/dex_event_classification.rs +++ b/kb_lib/src/dex_event_classification.rs @@ -323,6 +323,9 @@ pub fn is_dex_informational_event_kind(event_kind: &str) -> bool { /// Returns true when the event kind represents a swap-like event. pub fn is_dex_trade_event_kind(event_kind: &str) -> bool { + if is_meteora_dlmm_anchor_swap_log_event_kind(event_kind) { + return false; + } if event_kind == "raydium_launchpad.trade_event" { return true; } @@ -376,8 +379,126 @@ pub fn is_dex_candle_candidate_event_kind(event_kind: &str) -> bool { return is_dex_trade_event_kind(event_kind); } +fn is_meteora_dlmm_anchor_swap_log_event_kind(event_kind: &str) -> bool { + return matches!(event_kind, "meteora_dlmm.swap_event" | "meteora_dlmm.swap2_evt"); +} + +fn is_meteora_dlmm_position_lifecycle_event_kind(event_kind: &str) -> bool { + if !event_kind.starts_with("meteora_dlmm.") { + return false; + } + return matches!( + event_kind, + "meteora_dlmm.initialize_position" + | "meteora_dlmm.initialize_position2" + | "meteora_dlmm.initialize_position_by_operator" + | "meteora_dlmm.initialize_position_pda" + | "meteora_dlmm.position_create_event" + | "meteora_dlmm.close_position" + | "meteora_dlmm.close_position2" + | "meteora_dlmm.close_position_if_empty" + | "meteora_dlmm.position_close_event" + | "meteora_dlmm.decrease_position_length" + | "meteora_dlmm.decrease_position_length_event" + | "meteora_dlmm.increase_position_length" + | "meteora_dlmm.increase_position_length2" + | "meteora_dlmm.increase_position_length_event" + | "meteora_dlmm.update_position_operator" + | "meteora_dlmm.update_position_operator_event" + | "meteora_dlmm.update_position_lock_release_point_event" + | "meteora_dlmm.set_position_permissionless_operation_bits_evt" + ); +} + +fn is_meteora_dlmm_pool_lifecycle_event_kind(event_kind: &str) -> bool { + if !event_kind.starts_with("meteora_dlmm.") { + return false; + } + if is_meteora_dlmm_position_lifecycle_event_kind(event_kind) { + return true; + } + return matches!( + event_kind, + "meteora_dlmm.create_pool" + | "meteora_dlmm.swap_event" + | "meteora_dlmm.swap2_evt" + | "meteora_dlmm.lb_pair_create_event" + | "meteora_dlmm.initialize_lb_pair" + | "meteora_dlmm.initialize_lb_pair2" + | "meteora_dlmm.initialize_permission_lb_pair" + | "meteora_dlmm.initialize_customizable_permissionless_lb_pair" + | "meteora_dlmm.initialize_customizable_permissionless_lb_pair2" + | "meteora_dlmm.initialize_bin_array" + | "meteora_dlmm.initialize_bin_array_bitmap_extension" + | "meteora_dlmm.close_bin_array" + | "meteora_dlmm.go_to_a_bin" + | "meteora_dlmm.go_to_a_bin_event" + | "meteora_dlmm.increase_oracle_length" + | "meteora_dlmm.increase_observation_event" + | "meteora_dlmm.migrate_bin_array" + | "meteora_dlmm.migrate_position" + ); +} + +fn is_meteora_dlmm_admin_event_kind(event_kind: &str) -> bool { + if !event_kind.starts_with("meteora_dlmm.") { + return false; + } + if is_meteora_dlmm_pool_lifecycle_event_kind(event_kind) { + return false; + } + return matches!( + event_kind, + "meteora_dlmm.instruction_audit" + | "meteora_dlmm.close_claim_fee_operator_account" + | "meteora_dlmm.close_claim_protocol_fee_operator" + | "meteora_dlmm.close_operator_account" + | "meteora_dlmm.close_preset_parameter" + | "meteora_dlmm.close_preset_parameter2" + | "meteora_dlmm.close_token_badge" + | "meteora_dlmm.create_claim_protocol_fee_operator" + | "meteora_dlmm.create_operator_account" + | "meteora_dlmm.dynamic_fee_parameter_update_event" + | "meteora_dlmm.fee_parameter_update_event" + | "meteora_dlmm.initialize_preset_parameter" + | "meteora_dlmm.initialize_preset_parameter2" + | "meteora_dlmm.initialize_preset_parameter_v2" + | "meteora_dlmm.initialize_token_badge" + | "meteora_dlmm.set_activation_point" + | "meteora_dlmm.set_pair_status" + | "meteora_dlmm.set_pair_status_permissionless" + | "meteora_dlmm.set_permissionless_operation_bits" + | "meteora_dlmm.set_pre_activation_duration" + | "meteora_dlmm.set_pre_activation_swap_address" + | "meteora_dlmm.update_base_fee_parameters" + | "meteora_dlmm.update_dynamic_fee_parameters" + | "meteora_dlmm.update_fees_and_reward2" + | "meteora_dlmm.update_fees_and_rewards" + | "meteora_dlmm.update_reward_duration" + | "meteora_dlmm.update_reward_duration_event" + | "meteora_dlmm.update_reward_funder" + | "meteora_dlmm.update_reward_funder_event" + ); +} + /// Returns true for liquidity lifecycle changes that must not become candles. pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool { + if event_kind.starts_with("meteora_dlmm.") + && (event_kind.contains("withdraw_protocol_fee") + || event_kind.contains("withdraw_ineligible_reward")) + { + return false; + } + if is_meteora_dlmm_position_lifecycle_event_kind(event_kind) { + return false; + } + if event_kind.starts_with("meteora_dlmm.") + && (event_kind == "meteora_dlmm.rebalance_liquidity" + || event_kind == "meteora_dlmm.rebalancing_event" + || event_kind == "meteora_dlmm.remove_all_liquidity") + { + return true; + } if event_kind.starts_with("meteora_dbc.") && (event_kind.contains("fee") || event_kind.contains("surplus") @@ -461,6 +582,15 @@ pub fn is_dex_liquidity_add_event_kind(event_kind: &str) -> bool { /// Returns true for liquidity remove-like DEX events. pub fn is_dex_liquidity_remove_event_kind(event_kind: &str) -> bool { + if event_kind.starts_with("meteora_dlmm.") + && (event_kind.contains("withdraw_protocol_fee") + || event_kind.contains("withdraw_ineligible_reward")) + { + return false; + } + if event_kind == "meteora_dlmm.remove_all_liquidity" { + return true; + } if event_kind.contains(".withdraw_pnl") { return false; } @@ -509,6 +639,17 @@ pub fn is_dex_position_close_event_kind(event_kind: &str) -> bool { /// Returns true for fee collection events. pub fn is_dex_fee_event_kind(event_kind: &str) -> bool { + if event_kind.starts_with("meteora_dlmm.") && event_kind.contains("claim_fee_operator") { + return false; + } + if event_kind.starts_with("meteora_dlmm.") + && (event_kind.contains("claim_fee") + || event_kind.contains("composition_fee") + || event_kind.contains("withdraw_protocol_fee") + || event_kind.contains("zap_protocol_fee")) + { + return true; + } if event_kind.starts_with("meteora_dbc.") && (event_kind.contains("fee") || event_kind.contains("surplus") @@ -580,6 +721,13 @@ pub fn is_dex_fee_event_kind(event_kind: &str) -> bool { /// Returns true for reward or incentive events. pub fn is_dex_reward_event_kind(event_kind: &str) -> bool { + if event_kind.starts_with("meteora_dlmm.") + && (event_kind.contains("update_fees_and_reward") + || event_kind.contains("update_reward_duration") + || event_kind.contains("update_reward_funder")) + { + return false; + } if event_kind.starts_with("pump_fees.") && (event_kind.contains("social_fee_pda_claimed") || event_kind.contains("claim_social_fee_pda")) @@ -633,6 +781,15 @@ pub fn is_dex_orderbook_event_kind(event_kind: &str) -> bool { if event_kind.contains(".open_limit_order") { return true; } + if event_kind.contains(".place_limit_order") { + return true; + } + if event_kind.contains(".cancel_limit_order") { + return true; + } + if event_kind.contains("limit_order_evt") { + return true; + } if event_kind.contains(".increase_limit_order") { return true; } @@ -650,6 +807,9 @@ pub fn is_dex_orderbook_event_kind(event_kind: &str) -> bool { /// Returns true for pool, pair, launch, mint, burn or migration lifecycle events. pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool { + if is_meteora_dlmm_pool_lifecycle_event_kind(event_kind) { + return true; + } if event_kind.starts_with("pump_fees.") && (event_kind.contains("create_fee_sharing_config") || event_kind.contains("create_social_fee_pda") @@ -772,6 +932,25 @@ pub fn is_dex_migration_event_kind(event_kind: &str) -> bool { /// Returns true for pool creation or initialization events. pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool { + if event_kind.starts_with("meteora_dlmm.") { + if event_kind.contains(".initialize_position") { + return false; + } + if event_kind.contains(".initialize_bin_array") { + return true; + } + if event_kind.contains(".initialize_customizable_permissionless_lb_pair") + || event_kind.contains(".initialize_permission_lb_pair") + || event_kind.contains(".initialize_lb_pair") + || event_kind.contains(".lb_pair_create") + || event_kind.contains(".create_pool") + { + return true; + } + if event_kind.contains(".initialize") { + return false; + } + } if event_kind == "meteora_dbc.evt_initialize_pool_event" || event_kind.contains(".initialize_virtual_pool") { @@ -859,6 +1038,12 @@ pub fn is_dex_token_account_close_event_kind(event_kind: &str) -> bool { /// Returns true for admin, configuration or permission changes. pub fn is_dex_admin_event_kind(event_kind: &str) -> bool { + if is_meteora_dlmm_pool_lifecycle_event_kind(event_kind) { + return false; + } + if is_meteora_dlmm_admin_event_kind(event_kind) { + return true; + } if event_kind.starts_with("meteora_dbc.") && (event_kind.contains("config") || event_kind.contains("operator") @@ -1391,6 +1576,42 @@ mod tests { ); } + #[test] + fn classifies_meteora_dlmm_fee_reward_admin_without_multi_target_overlap() { + assert!(super::is_dex_fee_event_kind("meteora_dlmm.withdraw_protocol_fee")); + assert!(!super::is_dex_liquidity_event_kind("meteora_dlmm.withdraw_protocol_fee")); + assert!(!super::is_dex_liquidity_remove_event_kind("meteora_dlmm.withdraw_protocol_fee")); + assert!(!super::is_dex_fee_event_kind("meteora_dlmm.close_claim_fee_operator_account")); + assert!(super::is_dex_admin_event_kind("meteora_dlmm.close_claim_fee_operator_account")); + assert!(super::is_dex_reward_event_kind("meteora_dlmm.initialize_reward")); + assert!(!super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.initialize_reward")); + assert!(!super::is_dex_reward_event_kind("meteora_dlmm.update_fees_and_rewards")); + assert!(super::is_dex_admin_event_kind("meteora_dlmm.update_fees_and_rewards")); + assert!(!super::is_dex_reward_event_kind("meteora_dlmm.update_reward_duration_event")); + assert!(super::is_dex_admin_event_kind("meteora_dlmm.update_reward_duration_event")); + assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.position_create_event")); + assert!(!super::is_dex_liquidity_event_kind("meteora_dlmm.position_create_event")); + assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.increase_position_length")); + assert!(!super::is_dex_liquidity_event_kind("meteora_dlmm.increase_position_length")); + assert!(!super::is_dex_admin_event_kind("meteora_dlmm.update_position_operator")); + assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.close_bin_array")); + assert!(super::is_dex_admin_event_kind("meteora_dlmm.create_operator_account")); + assert!(super::is_dex_admin_event_kind( + "meteora_dlmm.initialize_preset_parameter_v2" + )); + assert!(super::is_dex_admin_event_kind("meteora_dlmm.instruction_audit")); + assert!(!super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.instruction_audit")); + assert!(!super::is_dex_fee_event_kind("meteora_dlmm.instruction_audit")); + assert!(!super::is_dex_reward_event_kind("meteora_dlmm.instruction_audit")); + assert!(super::is_dex_liquidity_event_kind("meteora_dlmm.rebalancing_event")); + assert!(super::is_dex_liquidity_event_kind("meteora_dlmm.remove_all_liquidity")); + assert!(!super::is_dex_trade_event_kind("meteora_dlmm.swap_event")); + assert!(!super::is_dex_candle_candidate_event_kind("meteora_dlmm.swap_event")); + assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.swap_event")); + assert!(!super::is_dex_trade_event_kind("meteora_dlmm.swap2_evt")); + assert!(!super::is_dex_candle_candidate_event_kind("meteora_dlmm.swap2_evt")); + assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.swap2_evt")); + } #[test] fn enriched_payload_keeps_existing_fields() { let payload_json = serde_json::json!({ @@ -1514,6 +1735,10 @@ mod tests { super::classify_dex_event_category_code("meteora_dlmm.remove_liquidity"), "liquidity" ); + assert_eq!( + super::classify_dex_event_category_code("meteora_dlmm.initialize_position"), + "pool_lifecycle" + ); assert_eq!( super::classify_dex_event_lifecycle_kind_code("meteora_dlmm.initialize_position"), "position_open" diff --git a/kb_lib/src/dex_event_coverage.rs b/kb_lib/src/dex_event_coverage.rs index e17b471..a67609f 100644 --- a/kb_lib/src/dex_event_coverage.rs +++ b/kb_lib/src/dex_event_coverage.rs @@ -65,26 +65,29 @@ impl DexEventCoverageService { Err(error) => return Err(error), } } + let mut supplemental_entries = std::vec::Vec::new(); if decoder_code.as_deref().is_none() || decoder_code.as_deref() == Some("meteora_dbc") { - let supplemental_entries = local_meteora_dbc_registry_entries(); - for entry in &supplemental_entries { - let coverage_entry = build_coverage_entry_from_upstream(entry); - let upsert_result = crate::query_dex_event_coverage_entries_upsert( - self.database.as_ref(), - &coverage_entry, - ) - .await; - match upsert_result { - Ok(_) => upserted_entry_count += 1, - Err(error) => return Err(error), - } - } - return Ok(( - search_result.entries.len() + supplemental_entries.len(), - upserted_entry_count, - )); + supplemental_entries.extend(local_meteora_dbc_registry_entries()); } - return Ok((search_result.entries.len(), upserted_entry_count)); + if decoder_code.as_deref().is_none() || decoder_code.as_deref() == Some("meteora_dlmm") { + supplemental_entries.extend(local_meteora_dlmm_registry_entries()); + } + for entry in &supplemental_entries { + let coverage_entry = build_coverage_entry_from_upstream(entry); + let upsert_result = crate::query_dex_event_coverage_entries_upsert( + self.database.as_ref(), + &coverage_entry, + ) + .await; + match upsert_result { + Ok(_) => upserted_entry_count += 1, + Err(error) => return Err(error), + } + } + return Ok(( + search_result.entries.len() + supplemental_entries.len(), + upserted_entry_count, + )); } async fn ensure_upstream_registry_rows_if_needed( @@ -123,6 +126,11 @@ impl DexEventCoverageService { if let Err(error) = meteora_dbc_duplicate_cleanup_result { return Err(error); } + let meteora_dlmm_duplicate_cleanup_result = + self.cleanup_duplicate_meteora_dlmm_logical_coverage_rows(&decoder_code).await; + if let Err(error) = meteora_dlmm_duplicate_cleanup_result { + return Err(error); + } let refreshed_entry_count = match &decoder_code { Some(decoder_code) => { let refresh_result = @@ -188,6 +196,11 @@ impl DexEventCoverageService { if let Err(error) = meteora_dbc_duplicate_cleanup_result { return Err(error); } + let meteora_dlmm_duplicate_cleanup_result = + self.cleanup_duplicate_meteora_dlmm_logical_coverage_rows(&decoder_code).await; + if let Err(error) = meteora_dlmm_duplicate_cleanup_result { + return Err(error); + } let refreshed_entry_count = match &decoder_code { Some(decoder_code) => { let refresh_result = @@ -351,6 +364,50 @@ WHERE decoder_code = 'meteora_dbc' }, } } + + async fn cleanup_duplicate_meteora_dlmm_logical_coverage_rows( + &self, + decoder_code: &std::option::Option, + ) -> Result { + if let Some(decoder_code) = decoder_code { + if decoder_code != "meteora_dlmm" { + return Ok(0); + } + } + match self.database.connection() { + crate::DatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +DELETE FROM k_sol_dex_event_coverage_entries +WHERE decoder_code = 'meteora_dlmm' + AND id NOT IN ( + SELECT MIN(id) + FROM k_sol_dex_event_coverage_entries + WHERE decoder_code = 'meteora_dlmm' + GROUP BY + decoder_code, + COALESCE(program_id, ''), + entry_kind, + entry_name, + COALESCE(discriminator_hex, ''), + COALESCE(local_event_kind, '') + ) + "#, + ) + .execute(pool) + .await; + match query_result { + Ok(query_result) => return Ok(query_result.rows_affected()), + Err(error) => { + return Err(crate::Error::Db(format!( + "cannot delete duplicate Meteora DLMM logical coverage rows on sqlite: {}", + error + ))); + }, + } + }, + } + } } fn build_coverage_entry_from_upstream( @@ -729,6 +786,762 @@ fn add_local_meteora_dbc_entry( }); } +fn local_meteora_dlmm_registry_entries() -> std::vec::Vec { + let mut entries = std::vec::Vec::new(); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "instruction_audit", + None, + ); + add_local_meteora_dlmm_entry(&mut entries, crate::ENTRY_KIND_EVENT, "create_pool", None); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "anchor_self_cpi_log", + Some("e445a52e51cb9a1d"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity", + Some("b59d59438fb63448"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity2", + Some("e4a24e1c46db7473"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity_by_strategy", + Some("0703967f94283dc8"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity_by_strategy2", + Some("03dd95da6f8d76d5"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity_by_strategy_one_side", + Some("2905eeaf64e106cd"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity_by_weight", + Some("1c8cee63e7a21595"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity_by_weight2", + Some("d13b3f5b6fc899e4"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity_one_side", + Some("5e9b6797465fdca5"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity_one_side_precise", + Some("a1c26754ab47fa9a"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "add_liquidity_one_side_precise2", + Some("2133a3c975627de7"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "cancel_limit_order", + Some("849c841f4328e861"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "claim_fee", + Some("a9204f8988e84689"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "claim_fee2", + Some("70bf65ab1c907fbb"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "claim_reward", + Some("955fb5f25e5a9ea2"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "claim_reward2", + Some("be037f77b2579db7"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_bin_array", + Some("44ae5850b5cc13e0"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_claim_fee_operator_account", + Some("b8d5581fb3658224"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_limit_order_if_empty", + Some("397c249b7ef95dab"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_operator_account", + Some("ab09d54a7817031d"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_position", + Some("7b86510031446262"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_position2", + Some("ae5a2373ba2893e2"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_position_if_empty", + Some("3b7cd4765b986e9d"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_preset_parameter", + Some("04949164861ab53d"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_preset_parameter2", + Some("27195f6b7411731c"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "close_token_badge", + Some("6c92566eb3fe0a68"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "create_operator_account", + Some("dd40f695f099e5a3"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "decrease_position_length", + Some("c2db882019606925"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "for_idl_type_generation_do_not_call", + Some("b46945505f32496c"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "fund_reward", + Some("bc32f9a55d97263f"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "go_to_a_bin", + Some("9248aee028fd54ae"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "increase_oracle_length", + Some("be3d7d57674f9ead"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "increase_position_length", + Some("505375d3420d2195"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "increase_position_length2", + Some("ffd2cc477389e171"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_bin_array", + Some("235613b94ed44bd3"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_bin_array_bitmap_extension", + Some("2f9de2b40cf02147"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_customizable_permissionless_lb_pair", + Some("2e2729876fb7c840"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_customizable_permissionless_lb_pair2", + Some("f349817e3313f16b"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_lb_pair", + Some("2d9aedd2dd0fa65c"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_lb_pair2", + Some("493b2478ed536cc6"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_permission_lb_pair", + Some("6c66d555fb033515"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_position", + Some("dbc0ea47bebf6650"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_position2", + Some("8f13f291d50f6873"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_position_by_operator", + Some("fbbdbef475fe2394"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_position_pda", + Some("2e527d92558de499"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_preset_parameter", + Some("42bc47d3626d0eba"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_preset_parameter_v2", + Some("75c73e67068e1fcb"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_reward", + Some("5f87c0c4f281e644"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "initialize_token_badge", + Some("fd4dcd5f1be059df"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "place_limit_order", + Some("6cb021ba92e501c5"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "rebalance_liquidity", + Some("5c04b0c177b95309"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "remove_all_liquidity", + Some("0a333d2370691855"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "remove_liquidity", + Some("5055d14818ceb16c"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "remove_liquidity2", + Some("e6d7527ff165e392"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "remove_liquidity_by_range", + Some("1a526698f04a691a"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "remove_liquidity_by_range2", + Some("cc02c391359191cd"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "set_activation_point", + Some("5bf90fa51a81fe7d"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "set_pair_status", + Some("43f8e7899a95d9ae"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "set_pair_status_permissionless", + Some("4e3b98d346b72ed0"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "set_permissionless_operation_bits", + Some("543acb8ba351beba"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "set_pre_activation_duration", + Some("a53dc9f4829f1664"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "set_pre_activation_swap_address", + Some("398b2f7bd850df0a"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "swap", + Some("f8c69e91e17587c8"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "swap2", + Some("414b3f4ceb5b5b88"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "swap_exact_out", + Some("fa49652126cf4bb8"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "swap_exact_out2", + Some("2bd7f784893cf351"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "swap_with_price_impact", + Some("38ade6d0ade49ccd"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "swap_with_price_impact2", + Some("4a62c0d6b1334b33"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "update_base_fee_parameters", + Some("4ba8dfa110c3032f"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "update_dynamic_fee_parameters", + Some("5ca12ef6ffbd1616"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "update_fees_and_reward2", + Some("208eb89a6741b858"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "update_fees_and_rewards", + Some("9ae6fa0decd14bdf"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "update_position_operator", + Some("cab8678fb4bf74d9"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "update_reward_duration", + Some("8aaec4a9d5ebfe6b"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "update_reward_funder", + Some("d31c3020d7a02317"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "withdraw_ineligible_reward", + Some("94ce2ac3f7316708"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "withdraw_protocol_fee", + Some("9ec99ebd215da267"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_INSTRUCTION, + "zap_protocol_fee", + Some("d59bbb2238b65bf0"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "add_liquidity_event", + Some("1f5e7d5ae3343dba"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "cancel_limit_order_evt", + Some("83eac285090ebdd1"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "claim_fee_event", + Some("4b7a9a308c4a7ba3"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "claim_fee2_event", + Some("e8abf2613a4d232d"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "claim_reward_event", + Some("947486cc16ab555f"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "claim_reward2_event", + Some("1b8ff421502b6e92"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "close_limit_order_evt", + Some("8e87084c5c3f7653"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "composition_fee_event", + Some("80977b6a1166718e"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "decrease_position_length_event", + Some("3476eb55aca90f80"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "dynamic_fee_parameter_update_event", + Some("5858b287c2925bf3"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "fee_parameter_update_event", + Some("304cf17590d7f22c"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "fund_reward_event", + Some("f6e43a8291aa4fcc"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "go_to_a_bin_event", + Some("3b8a4c448a83b043"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "increase_observation_event", + Some("63f91179a69ccfd7"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "increase_position_length_event", + Some("9def2acc1e38df2e"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "initialize_reward_event", + Some("d399583e953cb146"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "lb_pair_create_event", + Some("b94afc7d1bd7bc6f"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "place_limit_order_evt", + Some("2b4f1ba9f41ce13f"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "position_close_event", + Some("ffc4106b1cca3580"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "position_create_event", + Some("908efc549d352579"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "rebalancing_event", + Some("006d75b33d5bc7c8"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "remove_liquidity_event", + Some("74f461e8671f983a"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "set_position_permissionless_operation_bits_evt", + Some("c3e593f51d7d30a8"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "swap_event", + Some("516ce3becdd00ac4"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "swap2_evt", + Some("2e7452d7941b544d"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "update_position_lock_release_point_event", + Some("85d642e0400c07bf"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "update_position_operator_event", + Some("277330ccf62f4239"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "update_reward_duration_event", + Some("dff5e099311da3ac"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "update_reward_funder_event", + Some("e0b2ae4afca555b4"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_EVENT, + "withdraw_ineligible_reward_event", + Some("e7bd419566d79af4"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "bin_array", + Some("5c8e5cdc059446b5"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "bin_array_bitmap_extension", + Some("506f7c7137ed1205"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "claim_fee_operator", + Some("a630865622c8bc96"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "dummy_zc_account", + Some("5e6bee50d030b408"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "lb_pair", + Some("210b3162b565b10d"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "limit_order", + Some("89b7d45b731d8de3"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "operator", + Some("db1fbc91458bcc75"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "oracle", + Some("8bc283b38cb3e5f4"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "position_v2", + Some("75b0d4c7f5b485b6"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "preset_parameter", + Some("f23ef422b5703aaa"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "preset_parameter2", + Some("abec9473a271deae"), + ); + add_local_meteora_dlmm_entry( + &mut entries, + crate::ENTRY_KIND_ACCOUNT, + "token_badge", + Some("74dbcce5f974ff96"), + ); + return entries; +} + +fn add_local_meteora_dlmm_entry( + entries: &mut std::vec::Vec, + entry_kind: &str, + entry_name: &str, + discriminator_hex: std::option::Option<&str>, +) { + entries.push(crate::UpstreamRegistryEntryDto { + source_repo: Some("local_idl".to_string()), + source_path: Some( + "idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json".to_string(), + ), + decoder_code: "meteora_dlmm".to_string(), + program_id: Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + program_family: "meteora".to_string(), + surface_kind: "dlmm".to_string(), + entry_kind: entry_kind.to_string(), + entry_name: entry_name.to_string(), + discriminator_hex: discriminator_hex.map(|value| return value.to_string()), + discriminator_len: discriminator_hex.map(|_| return 8_u16), + proof_status: crate::PROOF_STATUS_UPSTREAM_GIT_MAPPED_UNVERIFIED.to_string(), + notes: "supplemental local Meteora DLMM IDL coverage row".to_string(), + }); +} + fn infer_expected_db_target_for_entry( decoder_code: &str, entry_name: &str, @@ -747,6 +1560,9 @@ fn infer_expected_db_target_for_entry( if decoder_code == "meteora_dbc" { return infer_meteora_dbc_expected_db_target(entry_name, entry_kind); } + if decoder_code == "meteora_dlmm" { + return infer_meteora_dlmm_expected_db_target(entry_name, entry_kind); + } if decoder_code == "raydium_cpmm" && (entry_name == "swap_event" || entry_name == "anchor_idl_instruction") { @@ -1520,9 +2336,201 @@ fn infer_event_family_for_entry( if decoder_code == "meteora_dbc" { return infer_meteora_dbc_event_family(entry_name, entry_kind); } + if decoder_code == "meteora_dlmm" { + return infer_meteora_dlmm_event_family(entry_name, entry_kind); + } return infer_event_family(entry_name, entry_kind); } +fn infer_meteora_dlmm_expected_db_target( + entry_name: &str, + entry_kind: &str, +) -> std::option::Option { + if entry_kind == crate::ENTRY_KIND_PROGRAM || entry_kind == crate::ENTRY_KIND_ACCOUNT { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string()); + } + let family = infer_meteora_dlmm_event_family(entry_name, entry_kind); + let family = match family { + Some(family) => family, + None => { + return Some( + crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string(), + ); + }, + }; + if family == "swap_log" { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string()); + } + if family == "swap" { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string()); + } + if family == "pool_create" || family == "pool_lifecycle" { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string()); + } + if family == "liquidity" + || family == "liquidity_add" + || family == "liquidity_remove" + || family == "liquidity_change" + { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS.to_string()); + } + if family == "position_open" || family == "position_close" || family == "position_update" { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string()); + } + if family == "fee" { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_FEE_EVENTS.to_string()); + } + if family == "reward" { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_REWARD_EVENTS.to_string()); + } + if family == "admin_config" || family == "admin_audit" { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string()); + } + if family == "order_place" || family == "order_cancel" || family == "order_close" { + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_ORDERBOOK_EVENTS.to_string()); + } + return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string()); +} + +fn infer_meteora_dlmm_event_family( + entry_name: &str, + entry_kind: &str, +) -> std::option::Option { + if entry_kind == crate::ENTRY_KIND_ACCOUNT || entry_kind == crate::ENTRY_KIND_PROGRAM { + return Some("metadata".to_string()); + } + if entry_name == "instruction_audit" { + return Some("admin_audit".to_string()); + } + if entry_name == "migrate_position" { + return Some("position_update".to_string()); + } + if matches!( + entry_name, + "swap" + | "swap2" + | "swap_exact_out" + | "swap_exact_out2" + | "swap_with_price_impact" + | "swap_with_price_impact2" + ) { + return Some("swap".to_string()); + } + if matches!(entry_name, "swap_event" | "swap2_evt") { + return Some("swap_log".to_string()); + } + if matches!( + entry_name, + "create_pool" + | "initialize_lb_pair" + | "initialize_lb_pair2" + | "initialize_permission_lb_pair" + | "initialize_customizable_permissionless_lb_pair" + | "initialize_customizable_permissionless_lb_pair2" + | "lb_pair_create_event" + ) { + return Some("pool_create".to_string()); + } + if entry_name.starts_with("add_liquidity") || entry_name == "add_liquidity_event" { + return Some("liquidity_add".to_string()); + } + if entry_name.starts_with("remove_liquidity") + || entry_name == "remove_all_liquidity" + || entry_name == "remove_liquidity_event" + { + return Some("liquidity_remove".to_string()); + } + if entry_name == "rebalance_liquidity" || entry_name == "rebalancing_event" { + return Some("liquidity_change".to_string()); + } + if entry_name.starts_with("initialize_position") || entry_name == "position_create_event" { + return Some("position_open".to_string()); + } + if entry_name.starts_with("close_position") || entry_name == "position_close_event" { + return Some("position_close".to_string()); + } + if entry_name.contains("position") + && (entry_name.contains("operator") + || entry_name.contains("length") + || entry_name.contains("lock_release_point") + || entry_name.contains("permissionless")) + { + return Some("position_update".to_string()); + } + if matches!( + entry_name, + "close_claim_fee_operator_account" + | "close_claim_protocol_fee_operator" + | "create_claim_protocol_fee_operator" + | "update_fees_and_reward2" + | "update_fees_and_rewards" + | "update_reward_duration" + | "update_reward_duration_event" + | "update_reward_funder" + | "update_reward_funder_event" + ) { + return Some("admin_config".to_string()); + } + if entry_name.contains("claim_fee") + || entry_name == "withdraw_protocol_fee" + || entry_name == "zap_protocol_fee" + || entry_name == "composition_fee_event" + { + return Some("fee".to_string()); + } + if entry_name.contains("reward") + || entry_name == "withdraw_ineligible_reward" + || entry_name == "withdraw_ineligible_reward_event" + { + return Some("reward".to_string()); + } + if entry_name.contains("limit_order") { + if entry_name.starts_with("place") { + return Some("order_place".to_string()); + } + if entry_name.starts_with("cancel") { + return Some("order_cancel".to_string()); + } + return Some("order_close".to_string()); + } + if entry_name.contains("fee_parameter") + || entry_name.contains("dynamic_fee") + || entry_name.contains("base_fee") + || entry_name.contains("pair_status") + || entry_name.contains("activation") + || entry_name.contains("operator") + || entry_name.contains("preset_parameter") + || entry_name.contains("token_badge") + || entry_name == "set_permissionless_operation_bits" + || entry_name == "update_fees_and_rewards" + || entry_name == "update_fees_and_reward2" + { + return Some("admin_config".to_string()); + } + if entry_name.contains("bin_array") + || entry_name == "go_to_a_bin" + || entry_name == "go_to_a_bin_event" + || entry_name == "increase_observation_event" + || entry_name == "increase_oracle_length" + { + return Some("pool_lifecycle".to_string()); + } + return Some("decoded_only".to_string()); +} + +fn meteora_dlmm_local_event_kind(entry_name: &str) -> std::option::Option { + if entry_name == "program" { + return None; + } + if entry_name == "instruction_audit" { + return Some("meteora_dlmm.instruction_audit".to_string()); + } + if entry_name == "anchor_self_cpi_log" { + return Some("meteora_dlmm.anchor_self_cpi_log".to_string()); + } + return Some(format!("meteora_dlmm.{}", entry_name)); +} + fn infer_meteora_dbc_expected_db_target( entry_name: &str, entry_kind: &str, @@ -2132,6 +3140,9 @@ pub(crate) fn known_local_event_kind( if decoder_code == "meteora_dbc" { return meteora_dbc_local_event_kind(entry_name); } + if decoder_code == "meteora_dlmm" { + return meteora_dlmm_local_event_kind(entry_name); + } if decoder_code == "raydium_amm_v4" { return raydium_amm_v4_local_event_kind(entry_name); } @@ -2743,6 +3754,192 @@ mod tests { })); } + #[test] + fn meteora_dlmm_coverage_targets_materialized_anchor_and_synthetic_events() { + assert_eq!( + super::infer_event_family_for_entry( + "meteora_dlmm", + "create_pool", + crate::ENTRY_KIND_EVENT, + ), + Some("pool_create".to_string()) + ); + assert_eq!( + super::infer_expected_db_target_for_entry( + "meteora_dlmm", + "create_pool", + Some("pool_create"), + crate::ENTRY_KIND_EVENT, + ), + Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string()) + ); + assert_eq!( + super::infer_event_family_for_entry( + "meteora_dlmm", + "composition_fee_event", + crate::ENTRY_KIND_EVENT, + ), + Some("fee".to_string()) + ); + assert_eq!( + super::infer_expected_db_target_for_entry( + "meteora_dlmm", + "composition_fee_event", + Some("fee"), + crate::ENTRY_KIND_EVENT, + ), + Some(crate::DexEventCoverageEntryDto::DB_TARGET_FEE_EVENTS.to_string()) + ); + assert_eq!( + super::infer_event_family_for_entry( + "meteora_dlmm", + "swap_event", + crate::ENTRY_KIND_EVENT, + ), + Some("swap_log".to_string()) + ); + assert_eq!( + super::infer_expected_db_target_for_entry( + "meteora_dlmm", + "swap_event", + Some("swap_log"), + crate::ENTRY_KIND_EVENT, + ), + Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string()) + ); + assert_eq!( + super::infer_expected_db_target_for_entry( + "meteora_dlmm", + "swap2_evt", + Some("swap_log"), + crate::ENTRY_KIND_EVENT, + ), + Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string()) + ); + assert_eq!( + super::infer_event_family_for_entry( + "meteora_dlmm", + "update_fees_and_rewards", + crate::ENTRY_KIND_INSTRUCTION, + ), + Some("admin_config".to_string()) + ); + assert_eq!( + super::infer_expected_db_target_for_entry( + "meteora_dlmm", + "update_fees_and_rewards", + Some("admin_config"), + crate::ENTRY_KIND_INSTRUCTION, + ), + Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string()) + ); + assert_eq!( + super::infer_event_family_for_entry( + "meteora_dlmm", + "update_reward_duration_event", + crate::ENTRY_KIND_EVENT, + ), + Some("admin_config".to_string()) + ); + assert_eq!( + super::infer_event_family_for_entry( + "meteora_dlmm", + "close_claim_fee_operator_account", + crate::ENTRY_KIND_INSTRUCTION, + ), + Some("admin_config".to_string()) + ); + assert_eq!( + super::infer_event_family_for_entry( + "meteora_dlmm", + "initialize_preset_parameter_v2", + crate::ENTRY_KIND_INSTRUCTION, + ), + Some("admin_config".to_string()) + ); + assert_eq!( + super::infer_expected_db_target_for_entry( + "meteora_dlmm", + "initialize_preset_parameter_v2", + Some("admin_config"), + crate::ENTRY_KIND_INSTRUCTION, + ), + Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string()) + ); + assert_eq!( + super::infer_event_family_for_entry( + "meteora_dlmm", + "instruction_audit", + crate::ENTRY_KIND_INSTRUCTION, + ), + Some("admin_audit".to_string()) + ); + assert_eq!( + super::infer_expected_db_target_for_entry( + "meteora_dlmm", + "instruction_audit", + Some("admin_audit"), + crate::ENTRY_KIND_INSTRUCTION, + ), + Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string()) + ); + assert_eq!( + super::infer_event_family_for_entry( + "meteora_dlmm", + "migrate_position", + crate::ENTRY_KIND_INSTRUCTION, + ), + Some("position_update".to_string()) + ); + } + + #[tokio::test] + async fn meteora_dlmm_coverage_sync_removes_duplicate_logical_rows() { + let database = make_database().await; + let service = crate::DexEventCoverageService::new(database.clone()); + let result = service.sync_upstream_registry(Some("meteora_dlmm".to_string())).await; + let result = match result { + Ok(result) => result, + Err(error) => panic!("coverage sync must succeed: {}", error), + }; + assert!(result.upstream_entry_count > 0); + let rows_result = crate::query_dex_event_coverage_entries_list_by_decoder( + database.as_ref(), + "meteora_dlmm", + ) + .await; + let rows = match rows_result { + Ok(rows) => rows, + Err(error) => panic!("coverage rows must load: {}", error), + }; + let mut logical_keys = std::collections::HashSet::new(); + for row in &rows { + let key = format!( + "{}|{}|{}|{}|{}|{}", + row.decoder_code, + row.program_id.clone().unwrap_or_default(), + row.entry_kind, + row.entry_name, + row.discriminator_hex.clone().unwrap_or_default(), + row.local_event_kind.clone().unwrap_or_default() + ); + assert!(logical_keys.insert(key)); + } + assert!(rows.iter().any(|row| return { + row.entry_name == "swap" + && row.discriminator_hex == Some("f8c69e91e17587c8".to_string()) + && row.local_event_kind == Some("meteora_dlmm.swap".to_string()) + })); + assert!(rows.iter().any(|row| return { + row.entry_name == "update_fees_and_rewards" + && row.event_family == Some("admin_config".to_string()) + && row.expected_db_target + == Some( + crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string(), + ) + })); + } + #[tokio::test] async fn refresh_local_counts_auto_syncs_empty_coverage_table() { let database = make_database().await; diff --git a/kb_lib/src/instruction_observation_index.rs b/kb_lib/src/instruction_observation_index.rs index d829f03..c370f21 100644 --- a/kb_lib/src/instruction_observation_index.rs +++ b/kb_lib/src/instruction_observation_index.rs @@ -200,6 +200,90 @@ fn resolve_instruction_name( Some(discriminator_hex) => discriminator_hex, None => return None, }; + if program_id == crate::METEORA_DLMM_PROGRAM_ID || decoder_code == Some("meteora_dlmm") { + let name = match discriminator_hex { + "e445a52e51cb9a1d" => "anchor_self_cpi_log", + "b59d59438fb63448" => "add_liquidity", + "e4a24e1c46db7473" => "add_liquidity2", + "0703967f94283dc8" => "add_liquidity_by_strategy", + "03dd95da6f8d76d5" => "add_liquidity_by_strategy2", + "2905eeaf64e106cd" => "add_liquidity_by_strategy_one_side", + "1c8cee63e7a21595" => "add_liquidity_by_weight", + "d13b3f5b6fc899e4" => "add_liquidity_by_weight2", + "5e9b6797465fdca5" => "add_liquidity_one_side", + "a1c26754ab47fa9a" => "add_liquidity_one_side_precise", + "2133a3c975627de7" => "add_liquidity_one_side_precise2", + "849c841f4328e861" => "cancel_limit_order", + "a9204f8988e84689" => "claim_fee", + "70bf65ab1c907fbb" => "claim_fee2", + "955fb5f25e5a9ea2" => "claim_reward", + "be037f77b2579db7" => "claim_reward2", + "44ae5850b5cc13e0" => "close_bin_array", + "b8d5581fb3658224" => "close_claim_fee_operator_account", + "397c249b7ef95dab" => "close_limit_order_if_empty", + "ab09d54a7817031d" => "close_operator_account", + "7b86510031446262" => "close_position", + "ae5a2373ba2893e2" => "close_position2", + "3b7cd4765b986e9d" => "close_position_if_empty", + "04949164861ab53d" => "close_preset_parameter", + "27195f6b7411731c" => "close_preset_parameter2", + "6c92566eb3fe0a68" => "close_token_badge", + "dd40f695f099e5a3" => "create_operator_account", + "c2db882019606925" => "decrease_position_length", + "b46945505f32496c" => "for_idl_type_generation_do_not_call", + "bc32f9a55d97263f" => "fund_reward", + "9248aee028fd54ae" => "go_to_a_bin", + "be3d7d57674f9ead" => "increase_oracle_length", + "505375d3420d2195" => "increase_position_length", + "ffd2cc477389e171" => "increase_position_length2", + "235613b94ed44bd3" => "initialize_bin_array", + "2f9de2b40cf02147" => "initialize_bin_array_bitmap_extension", + "2e2729876fb7c840" => "initialize_customizable_permissionless_lb_pair", + "f349817e3313f16b" => "initialize_customizable_permissionless_lb_pair2", + "2d9aedd2dd0fa65c" => "initialize_lb_pair", + "493b2478ed536cc6" => "initialize_lb_pair2", + "6c66d555fb033515" => "initialize_permission_lb_pair", + "dbc0ea47bebf6650" => "initialize_position", + "8f13f291d50f6873" => "initialize_position2", + "fbbdbef475fe2394" => "initialize_position_by_operator", + "2e527d92558de499" => "initialize_position_pda", + "42bc47d3626d0eba" => "initialize_preset_parameter", + "75c73e67068e1fcb" => "initialize_preset_parameter_v2", + "5f87c0c4f281e644" => "initialize_reward", + "fd4dcd5f1be059df" => "initialize_token_badge", + "6cb021ba92e501c5" => "place_limit_order", + "5c04b0c177b95309" => "rebalance_liquidity", + "0a333d2370691855" => "remove_all_liquidity", + "5055d14818ceb16c" => "remove_liquidity", + "e6d7527ff165e392" => "remove_liquidity2", + "1a526698f04a691a" => "remove_liquidity_by_range", + "cc02c391359191cd" => "remove_liquidity_by_range2", + "5bf90fa51a81fe7d" => "set_activation_point", + "43f8e7899a95d9ae" => "set_pair_status", + "4e3b98d346b72ed0" => "set_pair_status_permissionless", + "543acb8ba351beba" => "set_permissionless_operation_bits", + "a53dc9f4829f1664" => "set_pre_activation_duration", + "398b2f7bd850df0a" => "set_pre_activation_swap_address", + "f8c69e91e17587c8" => "swap", + "414b3f4ceb5b5b88" => "swap2", + "fa49652126cf4bb8" => "swap_exact_out", + "2bd7f784893cf351" => "swap_exact_out2", + "38ade6d0ade49ccd" => "swap_with_price_impact", + "4a62c0d6b1334b33" => "swap_with_price_impact2", + "4ba8dfa110c3032f" => "update_base_fee_parameters", + "5ca12ef6ffbd1616" => "update_dynamic_fee_parameters", + "208eb89a6741b858" => "update_fees_and_reward2", + "9ae6fa0decd14bdf" => "update_fees_and_rewards", + "cab8678fb4bf74d9" => "update_position_operator", + "8aaec4a9d5ebfe6b" => "update_reward_duration", + "d31c3020d7a02317" => "update_reward_funder", + "94ce2ac3f7316708" => "withdraw_ineligible_reward", + "9ec99ebd215da267" => "withdraw_protocol_fee", + "d59bbb2238b65bf0" => "zap_protocol_fee", + _ => "instruction_audit", + }; + return Some(format!("meteora_dlmm.{}", name)); + } if program_id == crate::METEORA_DBC_PROGRAM_ID || decoder_code == Some("meteora_dbc") { let name = match discriminator_hex { "e445a52e51cb9a1d" => "meteora_dbc.anchor_self_cpi_log", @@ -578,6 +662,43 @@ fn option_i64_key(value: std::option::Option) -> std::string::String { #[cfg(test)] mod tests { + #[test] + fn resolves_meteora_dlmm_instruction_observation_names() { + let anchor_name = super::resolve_instruction_name( + crate::METEORA_DLMM_PROGRAM_ID, + Some("meteora_dlmm"), + Some("e445a52e51cb9a1d"), + ); + assert_eq!(anchor_name.as_deref(), Some("meteora_dlmm.anchor_self_cpi_log")); + let swap_exact_out2_name = super::resolve_instruction_name( + crate::METEORA_DLMM_PROGRAM_ID, + Some("meteora_dlmm"), + Some("2bd7f784893cf351"), + ); + assert_eq!(swap_exact_out2_name.as_deref(), Some("meteora_dlmm.swap_exact_out2")); + let close_position_name = super::resolve_instruction_name( + crate::METEORA_DLMM_PROGRAM_ID, + Some("meteora_dlmm"), + Some("7b86510031446262"), + ); + assert_eq!(close_position_name.as_deref(), Some("meteora_dlmm.close_position")); + let preset_v2_name = super::resolve_instruction_name( + crate::METEORA_DLMM_PROGRAM_ID, + Some("meteora_dlmm"), + Some("75c73e67068e1fcb"), + ); + assert_eq!( + preset_v2_name.as_deref(), + Some("meteora_dlmm.initialize_preset_parameter_v2") + ); + let unknown_name = super::resolve_instruction_name( + crate::METEORA_DLMM_PROGRAM_ID, + Some("meteora_dlmm"), + Some("ffffffffffffffff"), + ); + assert_eq!(unknown_name.as_deref(), Some("meteora_dlmm.instruction_audit")); + } + #[test] fn resolves_meteora_dbc_instruction_observation_names() { let anchor_name = super::resolve_instruction_name( diff --git a/kb_lib/src/non_trade_event_materialization.rs b/kb_lib/src/non_trade_event_materialization.rs index 652bd84..5f0e02c 100644 --- a/kb_lib/src/non_trade_event_materialization.rs +++ b/kb_lib/src/non_trade_event_materialization.rs @@ -71,10 +71,14 @@ fn should_skip_non_trade_event_due_to_explicit_reason( { return true; } + if crate::is_dex_orderbook_event_kind(decoded_event.event_kind.as_str()) + && payload_has_non_empty_text(payload, "skipOrderbookReason") + { + return true; + } return false; } - fn should_attempt_meteora_dbc_explicit_skip_materialization( decoded_event: &crate::DexDecodedEventDto, payload: &serde_json::Value, @@ -121,7 +125,6 @@ fn is_meteora_dbc_instruction_admin_materialization_candidate(event_kind: &str) ); } - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum FeeAmountRecoveryPolicy { Disabled, @@ -151,11 +154,43 @@ fn fee_amount_recovery_policy_for_event_kind(event_kind: &str) -> FeeAmountRecov | "raydium_launchpad.claim_platform_fee_from_vault" | "raydium_launchpad.collect_fee" | "raydium_stable_swap.withdraw_pnl" - | "raydium_stable_swap.withdraw_srm" => FeeAmountRecoveryPolicy::InnerSplTransfer, + | "raydium_stable_swap.withdraw_srm" + | "meteora_dlmm.claim_fee" + | "meteora_dlmm.claim_fee2" + | "meteora_dlmm.claim_fee_event" + | "meteora_dlmm.claim_fee2_event" + | "meteora_dlmm.composition_fee_event" + | "meteora_dlmm.withdraw_protocol_fee" + | "meteora_dlmm.zap_protocol_fee" => FeeAmountRecoveryPolicy::InnerSplTransfer, _ => FeeAmountRecoveryPolicy::Disabled, }; } +fn reward_amount_recovery_policy_for_event_kind(event_kind: &str) -> FeeAmountRecoveryPolicy { + return match event_kind { + "meteora_dlmm.claim_reward" + | "meteora_dlmm.claim_reward2" + | "meteora_dlmm.claim_reward_event" + | "meteora_dlmm.claim_reward2_event" + | "meteora_dlmm.fund_reward" + | "meteora_dlmm.fund_reward_event" + | "meteora_dlmm.initialize_reward" + | "meteora_dlmm.initialize_reward_event" + | "meteora_dlmm.withdraw_ineligible_reward" + | "meteora_dlmm.withdraw_ineligible_reward_event" => { + FeeAmountRecoveryPolicy::InnerSplTransfer + }, + _ => FeeAmountRecoveryPolicy::Disabled, + }; +} + +fn optional_text_is_missing(value: &std::option::Option) -> bool { + match value.as_ref() { + Some(value) => return value.trim().is_empty(), + None => return true, + } +} + fn payload_has_non_empty_text(payload: &serde_json::Value, key: &str) -> bool { let value = match payload.get(key) { Some(value) => value, @@ -240,7 +275,10 @@ impl NonTradeEventMaterializationService { continue; } if should_skip_non_trade_event_due_to_explicit_reason(decoded_event, &payload) - && !should_attempt_meteora_dbc_explicit_skip_materialization(decoded_event, &payload) + && !should_attempt_meteora_dbc_explicit_skip_materialization( + decoded_event, + &payload, + ) { tracing::debug!( event_kind = %decoded_event.event_kind, @@ -534,10 +572,44 @@ impl NonTradeEventMaterializationService { ], ); let mut payload_json = decoded_event.payload_json.clone(); - let mut fee_amount_legs: std::vec::Vec = std::vec::Vec::new(); - if is_meteora_dbc_instruction_fee_materialization_candidate(decoded_event.event_kind.as_str()) { + let mut fee_amount_legs: std::vec::Vec = + fee_amount_legs_from_payload(payload); + if !fee_amount_legs.is_empty() { + match fee_amount_legs.as_slice() { + [first_leg] => { + fee_token_mint = Some(first_leg.token_mint.clone()); + fee_amount_raw = Some(first_leg.amount_raw.clone()); + }, + _ => { + fee_token_mint = None; + fee_amount_raw = None; + }, + } + let updated_payload = + decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs); + payload_json = updated_payload.to_string(); + let update_result = crate::query_dex_decoded_events_update_payload_json_by_id( + self.database.as_ref(), + decoded_event_id, + payload_json.as_str(), + ) + .await; + if let Err(error) = update_result { + return Err(error); + } + } + if fee_amount_legs.is_empty() + && is_meteora_dbc_instruction_fee_materialization_candidate( + decoded_event.event_kind.as_str(), + ) + { let inferred_result = self - .resolve_meteora_dbc_actual_fee_amounts(transaction, transaction_id, decoded_event, payload) + .resolve_meteora_dbc_actual_fee_amounts( + transaction, + transaction_id, + decoded_event, + payload, + ) .await; let inferred = match inferred_result { Ok(inferred) => inferred, @@ -555,7 +627,8 @@ impl NonTradeEventMaterializationService { fee_amount_raw = None; }, } - let updated_payload = decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs); + let updated_payload = + decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs); payload_json = updated_payload.to_string(); let update_result = crate::query_dex_decoded_events_update_payload_json_by_id( self.database.as_ref(), @@ -597,11 +670,14 @@ impl NonTradeEventMaterializationService { return Ok(false); } } - let recovery_policy = fee_amount_recovery_policy_for_event_kind(decoded_event.event_kind.as_str()); + let recovery_policy = + fee_amount_recovery_policy_for_event_kind(decoded_event.event_kind.as_str()); if fee_amount_legs.is_empty() && fee_amount_raw.is_none() && recovery_policy != FeeAmountRecoveryPolicy::Disabled - && !is_meteora_dbc_instruction_fee_materialization_candidate(decoded_event.event_kind.as_str()) + && !is_meteora_dbc_instruction_fee_materialization_candidate( + decoded_event.event_kind.as_str(), + ) { let inferred_result = self .resolve_allowlisted_actual_fee_amounts_from_spl_transfers( @@ -626,7 +702,8 @@ impl NonTradeEventMaterializationService { fee_amount_raw = None; }, } - let updated_payload = decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs); + let updated_payload = + decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs); payload_json = updated_payload.to_string(); let update_result = crate::query_dex_decoded_events_update_payload_json_by_id( self.database.as_ref(), @@ -650,10 +727,11 @@ impl NonTradeEventMaterializationService { Err(error) => return Err(error), }; if let Some(explanation) = explanation { - let updated_payload = decoded_payload_with_unmaterialized_fee_amount_recovery_reason( - payload, - explanation.as_str(), - ); + let updated_payload = + decoded_payload_with_unmaterialized_fee_amount_recovery_reason( + payload, + explanation.as_str(), + ); payload_json = updated_payload.to_string(); let update_result = crate::query_dex_decoded_events_update_payload_json_by_id( self.database.as_ref(), @@ -781,10 +859,13 @@ impl NonTradeEventMaterializationService { Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]), None => None, }; - let instruction_index = extract_first_i64(payload, &["instructionIndex", "instruction_index"]); - let instructions_result = - crate::query_chain_instructions_list_by_transaction_id(self.database.as_ref(), transaction_id) - .await; + let instruction_index = + extract_first_i64(payload, &["instructionIndex", "instruction_index"]); + let instructions_result = crate::query_chain_instructions_list_by_transaction_id( + self.database.as_ref(), + transaction_id, + ) + .await; let instructions = match instructions_result { Ok(instructions) => instructions, Err(error) => return Err(error), @@ -855,10 +936,13 @@ impl NonTradeEventMaterializationService { Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]), None => None, }; - let instruction_index = extract_first_i64(payload, &["instructionIndex", "instruction_index"]); - let instructions_result = - crate::query_chain_instructions_list_by_transaction_id(self.database.as_ref(), transaction_id) - .await; + let instruction_index = + extract_first_i64(payload, &["instructionIndex", "instruction_index"]); + let instructions_result = crate::query_chain_instructions_list_by_transaction_id( + self.database.as_ref(), + transaction_id, + ) + .await; let instructions = match instructions_result { Ok(instructions) => instructions, Err(error) => return Err(error), @@ -929,10 +1013,13 @@ impl NonTradeEventMaterializationService { Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]), None => None, }; - let instruction_index = extract_first_i64(payload, &["instructionIndex", "instruction_index"]); - let instructions_result = - crate::query_chain_instructions_list_by_transaction_id(self.database.as_ref(), transaction_id) - .await; + let instruction_index = + extract_first_i64(payload, &["instructionIndex", "instruction_index"]); + let instructions_result = crate::query_chain_instructions_list_by_transaction_id( + self.database.as_ref(), + transaction_id, + ) + .await; let instructions = match instructions_result { Ok(instructions) => instructions, Err(error) => return Err(error), @@ -988,9 +1075,7 @@ impl NonTradeEventMaterializationService { non_zero_mints.insert(mint); } if non_zero_mints.len() > 1 { - return Ok(Some( - "multi_token_fee_amounts_materialized_as_fee_amount_legs".to_string(), - )); + return Ok(Some("multi_token_fee_amounts_materialized_as_fee_amount_legs".to_string())); } if transfer_count > 0 && non_zero_transfer_count == 0 { return Ok(Some("fee_instruction_has_only_zero_amount_transfers".to_string())); @@ -1001,6 +1086,86 @@ impl NonTradeEventMaterializationService { return Ok(None); } + async fn resolve_allowlisted_actual_reward_amount_from_spl_transfers( + &self, + transaction_id: i64, + decoded_event: &crate::DexDecodedEventDto, + payload: &serde_json::Value, + ) -> Result, crate::Error> { + let parent_instruction_id = match decoded_event.id { + Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]), + None => None, + }; + let instruction_index = + extract_first_i64(payload, &["instructionIndex", "instruction_index"]); + let instructions_result = crate::query_chain_instructions_list_by_transaction_id( + self.database.as_ref(), + transaction_id, + ) + .await; + let instructions = match instructions_result { + Ok(instructions) => instructions, + Err(error) => return Err(error), + }; + let mut amount_legs = std::vec::Vec::new(); + for instruction in &instructions { + if !instruction_is_child_of_decoded_event( + instruction, + parent_instruction_id, + instruction_index, + ) { + continue; + } + let parsed_type = match instruction.parsed_type.as_deref() { + Some(parsed_type) => parsed_type, + None => continue, + }; + if parsed_type != "transferChecked" && parsed_type != "transfer" { + continue; + } + let parsed_json = match instruction.parsed_json.as_deref() { + Some(parsed_json) => parsed_json, + None => continue, + }; + let parsed_value = serde_json::from_str::(parsed_json); + let parsed_value = match parsed_value { + Ok(parsed_value) => parsed_value, + Err(_) => continue, + }; + let amount_raw = extract_spl_transfer_amount_raw(&parsed_value); + let amount_raw = match amount_raw { + Some(amount_raw) => amount_raw, + None => continue, + }; + let amount = amount_raw.parse::(); + let amount = match amount { + Ok(amount) => amount, + Err(_) => continue, + }; + if amount == 0 { + continue; + } + let mint = extract_first_string(&parsed_value, &["mint"]); + let mint = match mint { + Some(mint) => mint, + None => continue, + }; + let source_account = extract_first_string(&parsed_value, &["source"]); + let destination_account = extract_first_string(&parsed_value, &["destination"]); + amount_legs.push(MeteoraDbcActualAmount { + token_mint: mint, + amount_raw, + source: "allowlisted_reward_inner_spl_transfer".to_string(), + source_account, + destination_account, + }); + } + return Ok(single_mint_amount_from_actual_amounts( + amount_legs.as_slice(), + "allowlisted_reward_inner_spl_transfer", + )); + } + async fn resolve_meteora_dbc_unmaterialized_fee_skip_reason( &self, transaction_id: i64, @@ -1011,10 +1176,13 @@ impl NonTradeEventMaterializationService { Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]), None => None, }; - let instruction_index = extract_first_i64(payload, &["instructionIndex", "instruction_index"]); - let instructions_result = - crate::query_chain_instructions_list_by_transaction_id(self.database.as_ref(), transaction_id) - .await; + let instruction_index = + extract_first_i64(payload, &["instructionIndex", "instruction_index"]); + let instructions_result = crate::query_chain_instructions_list_by_transaction_id( + self.database.as_ref(), + transaction_id, + ) + .await; let instructions = match instructions_result { Ok(instructions) => instructions, Err(error) => return Err(error), @@ -1070,9 +1238,7 @@ impl NonTradeEventMaterializationService { non_zero_mints.insert(mint); } if non_zero_mints.len() > 1 { - return Ok(Some( - "multi_token_fee_amounts_require_multiple_fee_rows".to_string(), - )); + return Ok(Some("multi_token_fee_amounts_require_multiple_fee_rows".to_string())); } if transfer_count > 0 && non_zero_transfer_count == 0 { return Ok(Some("fee_instruction_has_only_zero_amount_transfers".to_string())); @@ -1112,11 +1278,11 @@ impl NonTradeEventMaterializationService { "user", ], ); - let reward_token_mint = extract_first_string( + let mut reward_token_mint = extract_first_string( payload, &["rewardTokenMint", "reward_token_mint", "tokenMint", "token_mint", "mint"], ); - let reward_amount_raw = extract_first_amount_string( + let mut reward_amount_raw = extract_first_amount_string( payload, &[ "rewardAmountRaw", @@ -1128,6 +1294,35 @@ impl NonTradeEventMaterializationService { "amount", ], ); + let mut reward_payload_json = decoded_event.payload_json.clone(); + if reward_amount_recovery_policy_for_event_kind(decoded_event.event_kind.as_str()) + == FeeAmountRecoveryPolicy::InnerSplTransfer + && (optional_text_is_missing(&reward_token_mint) + || optional_text_is_missing(&reward_amount_raw)) + { + let recovered_reward_result = self + .resolve_allowlisted_actual_reward_amount_from_spl_transfers( + transaction_id, + decoded_event, + payload, + ) + .await; + let recovered_reward = match recovered_reward_result { + Ok(recovered_reward) => recovered_reward, + Err(error) => return Err(error), + }; + if let Some(recovered_reward) = recovered_reward { + if optional_text_is_missing(&reward_token_mint) { + reward_token_mint = Some(recovered_reward.token_mint.clone()); + } + if optional_text_is_missing(&reward_amount_raw) { + reward_amount_raw = Some(recovered_reward.amount_raw.clone()); + } + reward_payload_json = + decoded_payload_with_materialized_reward_amount(payload, &recovered_reward) + .to_string(); + } + } let dto = crate::RewardEventDto::new( transaction_id, Some(decoded_event_id), @@ -1143,7 +1338,7 @@ impl NonTradeEventMaterializationService { actor_wallet, reward_token_mint, reward_amount_raw, - decoded_event.payload_json.clone(), + reward_payload_json, ); let upsert_result = crate::query_reward_events_upsert(self.database.as_ref(), &dto).await; match upsert_result { @@ -2100,6 +2295,119 @@ struct MeteoraDbcActualAmount { destination_account: std::option::Option, } +fn single_mint_amount_from_actual_amounts( + amount_legs: &[MeteoraDbcActualAmount], + source: &str, +) -> std::option::Option { + let mut token_mint: std::option::Option = None; + let mut amount_total: u128 = 0; + let mut source_account: std::option::Option = None; + let mut destination_account: std::option::Option = None; + for leg in amount_legs { + if leg.token_mint.trim().is_empty() { + continue; + } + let amount = leg.amount_raw.parse::(); + let amount = match amount { + Ok(amount) => amount, + Err(_) => continue, + }; + if amount == 0 { + continue; + } + match token_mint.as_ref() { + Some(existing_mint) => { + if existing_mint != &leg.token_mint { + return None; + } + }, + None => token_mint = Some(leg.token_mint.clone()), + } + let checked_total = amount_total.checked_add(amount); + amount_total = match checked_total { + Some(checked_total) => checked_total, + None => return None, + }; + if source_account.is_none() { + source_account = leg.source_account.clone(); + } + if destination_account.is_none() { + destination_account = leg.destination_account.clone(); + } + } + let token_mint = match token_mint { + Some(token_mint) => token_mint, + None => return None, + }; + if amount_total == 0 { + return None; + } + return Some(MeteoraDbcActualAmount { + token_mint, + amount_raw: amount_total.to_string(), + source: source.to_string(), + source_account, + destination_account, + }); +} + +fn fee_amount_legs_from_payload( + payload: &serde_json::Value, +) -> std::vec::Vec { + let mut legs = std::vec::Vec::new(); + let candidate_arrays = ["feeAmountLegs", "materializedFeeAmountLegs"]; + for candidate_key in candidate_arrays { + let candidate = match payload.get(candidate_key) { + Some(candidate) => candidate, + None => continue, + }; + let array = match candidate.as_array() { + Some(array) => array, + None => continue, + }; + for leg_value in array { + let token_mint = extract_first_string( + leg_value, + &["tokenMint", "token_mint", "feeTokenMint", "fee_token_mint", "mint"], + ); + let amount_raw = extract_first_amount_string( + leg_value, + &["amountRaw", "amount_raw", "feeAmountRaw", "fee_amount_raw", "amount"], + ); + let token_mint = match token_mint { + Some(token_mint) => token_mint, + None => continue, + }; + let amount_raw = match amount_raw { + Some(amount_raw) => amount_raw, + None => continue, + }; + let amount_trimmed = amount_raw.trim(); + if amount_trimmed.is_empty() || amount_trimmed == "0" { + continue; + } + let source = + match extract_first_string(leg_value, &["amountSource", "amount_source", "source"]) + { + Some(source) => source, + None => "decoded_fee_amount_leg".to_string(), + }; + let source_account = + extract_first_string(leg_value, &["sourceAccount", "source_account"]); + let destination_account = + extract_first_string(leg_value, &["destinationAccount", "destination_account"]); + legs.push(MeteoraDbcActualAmount { + token_mint, + amount_raw: amount_trimmed.to_string(), + source, + source_account, + destination_account, + }); + } + } + return legs; +} + fn instruction_is_child_of_decoded_event( instruction: &crate::ChainInstructionDto, parent_instruction_id: std::option::Option, @@ -2147,6 +2455,57 @@ fn extract_spl_transfer_amount_raw( return None; } +fn decoded_payload_with_materialized_reward_amount( + payload: &serde_json::Value, + reward_amount: &MeteoraDbcActualAmount, +) -> serde_json::Value { + let mut object = match payload.clone() { + serde_json::Value::Object(object) => object, + other => { + let mut object = serde_json::Map::new(); + object.insert("rawPayload".to_string(), other); + object + }, + }; + object.insert("skipRewardReason".to_string(), serde_json::Value::String(String::new())); + object.insert("rewardMaterialized".to_string(), serde_json::Value::Bool(true)); + object.insert( + "rewardMaterializationSource".to_string(), + serde_json::Value::String(reward_amount.source.clone()), + ); + object.insert( + "rewardTokenMint".to_string(), + serde_json::Value::String(reward_amount.token_mint.clone()), + ); + object.insert( + "rewardAmountRaw".to_string(), + serde_json::Value::String(reward_amount.amount_raw.clone()), + ); + match reward_amount.source_account.as_ref() { + Some(source_account) => { + object.insert( + "rewardSourceAccount".to_string(), + serde_json::Value::String(source_account.clone()), + ); + }, + None => {}, + } + match reward_amount.destination_account.as_ref() { + Some(destination_account) => { + object.insert( + "rewardDestinationAccount".to_string(), + serde_json::Value::String(destination_account.clone()), + ); + }, + None => {}, + } + object.insert( + "eventActionability".to_string(), + serde_json::Value::String("reward_materialized".to_string()), + ); + return serde_json::Value::Object(object); +} + fn decoded_payload_with_materialized_fee_legs( payload: &serde_json::Value, legs: &[MeteoraDbcActualAmount], @@ -2159,11 +2518,11 @@ fn decoded_payload_with_materialized_fee_legs( object }, }; - object.insert("skipFeeReason".to_string(), serde_json::Value::String(std::string::String::new())); object.insert( - "feeMaterialized".to_string(), - serde_json::Value::Bool(true), + "skipFeeReason".to_string(), + serde_json::Value::String(std::string::String::new()), ); + object.insert("feeMaterialized".to_string(), serde_json::Value::Bool(true)); object.insert( "feeMaterializationSource".to_string(), serde_json::Value::String("fee_event_amounts".to_string()), @@ -2217,18 +2576,9 @@ fn fee_amount_leg_payload_json( "legIndex".to_string(), serde_json::Value::Number(serde_json::Number::from(u64::from(leg_index))), ); - object.insert( - "tokenMint".to_string(), - serde_json::Value::String(leg.token_mint.clone()), - ); - object.insert( - "amountRaw".to_string(), - serde_json::Value::String(leg.amount_raw.clone()), - ); - object.insert( - "amountSource".to_string(), - serde_json::Value::String(leg.source.clone()), - ); + object.insert("tokenMint".to_string(), serde_json::Value::String(leg.token_mint.clone())); + object.insert("amountRaw".to_string(), serde_json::Value::String(leg.amount_raw.clone())); + object.insert("amountSource".to_string(), serde_json::Value::String(leg.source.clone())); match leg.source_account.as_ref() { Some(source_account) => { object.insert( @@ -2250,7 +2600,6 @@ fn fee_amount_leg_payload_json( return serde_json::Value::Object(object).to_string(); } - fn fee_event_amount_leg_from_parent_amount( decoded_event: &crate::DexDecodedEventDto, transaction_id: i64, @@ -2272,7 +2621,11 @@ fn fee_event_amount_leg_from_parent_amount( if fee_amount_raw.trim().is_empty() { return None; } - let payload_json = fee_amount_parent_payload_json(decoded_event, fee_token_mint.as_str(), fee_amount_raw.as_str()); + let payload_json = fee_amount_parent_payload_json( + decoded_event, + fee_token_mint.as_str(), + fee_amount_raw.as_str(), + ); return Some(crate::FeeEventAmountDto::new( 0, transaction_id, @@ -2302,14 +2655,8 @@ fn fee_amount_parent_payload_json( "legIndex".to_string(), serde_json::Value::Number(serde_json::Number::from(0_u64)), ); - object.insert( - "tokenMint".to_string(), - serde_json::Value::String(token_mint.to_string()), - ); - object.insert( - "amountRaw".to_string(), - serde_json::Value::String(amount_raw.to_string()), - ); + object.insert("tokenMint".to_string(), serde_json::Value::String(token_mint.to_string())); + object.insert("amountRaw".to_string(), serde_json::Value::String(amount_raw.to_string())); object.insert( "amountSource".to_string(), serde_json::Value::String("parent_fee_event_amount".to_string()), @@ -2329,14 +2676,8 @@ fn decoded_payload_with_unmaterialized_fee_reason( object }, }; - object.insert( - "skipFeeReason".to_string(), - serde_json::Value::String(reason.to_string()), - ); - object.insert( - "feeMaterialized".to_string(), - serde_json::Value::Bool(false), - ); + object.insert("skipFeeReason".to_string(), serde_json::Value::String(reason.to_string())); + object.insert("feeMaterialized".to_string(), serde_json::Value::Bool(false)); object.insert( "feeMaterializationSource".to_string(), serde_json::Value::String("none".to_string()), @@ -2368,10 +2709,7 @@ fn decoded_payload_with_unmaterialized_fee_amount_recovery_reason( "feeAmountRecoveryReason".to_string(), serde_json::Value::String(reason.to_string()), ); - object.insert( - "feeAmountLegsMaterialized".to_string(), - serde_json::Value::Bool(false), - ); + object.insert("feeAmountLegsMaterialized".to_string(), serde_json::Value::Bool(false)); object.insert( "feeAmountLegCount".to_string(), serde_json::Value::Number(serde_json::Number::from(0_u64)), @@ -2387,7 +2725,8 @@ fn resolve_meteora_dbc_actual_fee_amount_from_lamport_delta( if !decoded_event.event_kind.contains("pool_creation_fee") { return None; } - let transaction_value = serde_json::from_str::(transaction.transaction_json.as_str()); + let transaction_value = + serde_json::from_str::(transaction.transaction_json.as_str()); let transaction_value = match transaction_value { Ok(transaction_value) => transaction_value, Err(_) => return None, @@ -2408,7 +2747,8 @@ fn resolve_meteora_dbc_actual_fee_amount_from_lamport_delta( append_unique_candidate_account(&mut candidate_accounts, decoded_event.pool_account.as_deref()); let payload_pool_account = extract_first_string(payload, &["poolAccount", "pool", "poolState"]); append_unique_candidate_account(&mut candidate_accounts, payload_pool_account.as_deref()); - let payload_related_account = extract_first_string(payload, &["relatedAccount", "receiver", "feeReceiver"]); + let payload_related_account = + extract_first_string(payload, &["relatedAccount", "receiver", "feeReceiver"]); append_unique_candidate_account(&mut candidate_accounts, payload_related_account.as_deref()); if let Some(accounts) = payload.get("accounts").and_then(serde_json::Value::as_array) { for account in accounts { @@ -3325,6 +3665,155 @@ mod tests { super::fee_amount_recovery_policy_for_event_kind("meteora_dbc.claim_trading_fee"), super::FeeAmountRecoveryPolicy::Disabled ); + assert_eq!( + super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_fee"), + super::FeeAmountRecoveryPolicy::InnerSplTransfer + ); + assert_eq!( + super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_fee2"), + super::FeeAmountRecoveryPolicy::InnerSplTransfer + ); + assert_eq!( + super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.composition_fee_event"), + super::FeeAmountRecoveryPolicy::InnerSplTransfer + ); + assert_eq!( + super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.withdraw_protocol_fee"), + super::FeeAmountRecoveryPolicy::InnerSplTransfer + ); + assert_eq!( + super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.zap_protocol_fee"), + super::FeeAmountRecoveryPolicy::InnerSplTransfer + ); + assert_eq!( + super::fee_amount_recovery_policy_for_event_kind( + "meteora_dlmm.close_claim_fee_operator_account" + ), + super::FeeAmountRecoveryPolicy::Disabled + ); + assert_eq!( + super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.swap_event"), + super::FeeAmountRecoveryPolicy::Disabled + ); + } + + #[test] + fn reward_amount_recovery_policy_is_allowlisted_only() { + assert_eq!( + super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_reward"), + super::FeeAmountRecoveryPolicy::InnerSplTransfer + ); + assert_eq!( + super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_reward2"), + super::FeeAmountRecoveryPolicy::InnerSplTransfer + ); + assert_eq!( + super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_reward_event"), + super::FeeAmountRecoveryPolicy::InnerSplTransfer + ); + assert_eq!( + super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.fund_reward"), + super::FeeAmountRecoveryPolicy::InnerSplTransfer + ); + assert_eq!( + super::reward_amount_recovery_policy_for_event_kind( + "meteora_dlmm.update_fees_and_rewards" + ), + super::FeeAmountRecoveryPolicy::Disabled + ); + assert_eq!( + super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.swap_event"), + super::FeeAmountRecoveryPolicy::Disabled + ); + } + + #[test] + fn single_mint_reward_amount_sums_matching_mints_only() { + let legs = vec![ + super::MeteoraDbcActualAmount { + token_mint: "RewardMint111".to_string(), + amount_raw: "7".to_string(), + source: "test".to_string(), + source_account: Some("Source111".to_string()), + destination_account: Some("Destination111".to_string()), + }, + super::MeteoraDbcActualAmount { + token_mint: "RewardMint111".to_string(), + amount_raw: "5".to_string(), + source: "test".to_string(), + source_account: Some("Source222".to_string()), + destination_account: Some("Destination222".to_string()), + }, + ]; + let resolved = super::single_mint_amount_from_actual_amounts( + legs.as_slice(), + "allowlisted_reward_inner_spl_transfer", + ); + let resolved = match resolved { + Some(resolved) => resolved, + None => panic!("expected single mint reward amount"), + }; + assert_eq!(resolved.token_mint, "RewardMint111"); + assert_eq!(resolved.amount_raw, "12"); + assert_eq!(resolved.source, "allowlisted_reward_inner_spl_transfer"); + + let mixed_mint_legs = vec![ + super::MeteoraDbcActualAmount { + token_mint: "RewardMint111".to_string(), + amount_raw: "7".to_string(), + source: "test".to_string(), + source_account: None, + destination_account: None, + }, + super::MeteoraDbcActualAmount { + token_mint: "OtherMint111".to_string(), + amount_raw: "5".to_string(), + source: "test".to_string(), + source_account: None, + destination_account: None, + }, + ]; + assert!( + super::single_mint_amount_from_actual_amounts( + mixed_mint_legs.as_slice(), + "allowlisted_reward_inner_spl_transfer", + ) + .is_none() + ); + } + + #[test] + fn decoded_payload_with_materialized_reward_amount_marks_source() { + let payload = serde_json::json!({ + "eventKind": "meteora_dlmm.claim_reward" + }); + let reward_amount = super::MeteoraDbcActualAmount { + token_mint: "RewardMint111".to_string(), + amount_raw: "42".to_string(), + source: "allowlisted_reward_inner_spl_transfer".to_string(), + source_account: Some("Source111".to_string()), + destination_account: Some("Destination111".to_string()), + }; + let materialized = + super::decoded_payload_with_materialized_reward_amount(&payload, &reward_amount); + assert_eq!( + materialized.get("rewardTokenMint").and_then(serde_json::Value::as_str), + Some("RewardMint111") + ); + assert_eq!( + materialized.get("rewardAmountRaw").and_then(serde_json::Value::as_str), + Some("42") + ); + assert_eq!( + materialized + .get("rewardMaterializationSource") + .and_then(serde_json::Value::as_str), + Some("allowlisted_reward_inner_spl_transfer") + ); + assert_eq!( + materialized.get("eventActionability").and_then(serde_json::Value::as_str), + Some("reward_materialized") + ); } #[test] diff --git a/validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql b/validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql index f159b8e..5f77842 100644 --- a/validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql +++ b/validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql @@ -196,7 +196,9 @@ WHERE de.protocol_name = 'meteora_dlmm' 'meteora_dlmm.swap_exact_out', 'meteora_dlmm.swap_exact_out2', 'meteora_dlmm.swap_with_price_impact', - 'meteora_dlmm.swap_with_price_impact2' + 'meteora_dlmm.swap_with_price_impact2', + 'meteora_dlmm.swap_event', + 'meteora_dlmm.swap2_evt' ) GROUP BY de.event_kind HAVING trade_count > 0 OR candle_count > 0 @@ -268,7 +270,32 @@ GROUP BY de.event_kind HAVING fee_count > 0 OR reward_count > 0 ORDER BY de.event_kind; --- 13. Global watchlist after replay. +-- 13. Limit/orderbook no trade/candle double-count. Target: empty. +SELECT + de.event_kind, + COUNT(DISTINCT te.id) AS trade_count, + COUNT(DISTINCT pc.id) AS candle_count, + COUNT(DISTINCT obe.id) AS orderbook_count, + MIN(tx.signature) AS sample_signature +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 +LEFT JOIN k_sol_pair_candles pc ON pc.pair_id = te.pair_id +LEFT JOIN k_sol_orderbook_events obe ON obe.decoded_event_id = de.id +WHERE de.protocol_name = 'meteora_dlmm' + AND ( + de.event_kind LIKE '%limit_order%' + OR de.event_kind IN ( + 'meteora_dlmm.place_limit_order_evt', + 'meteora_dlmm.cancel_limit_order_evt', + 'meteora_dlmm.close_limit_order_evt' + ) + ) +GROUP BY de.event_kind +HAVING trade_count > 0 OR candle_count > 0 +ORDER BY trade_count DESC, candle_count DESC, de.event_kind; + +-- 14. Global watchlist after replay. SELECT COALESCE(json_extract(de.payload_json, '$.upstreamDecoderCode'), de.protocol_name) AS backlog_decoder, de.event_kind, @@ -283,3 +310,63 @@ WHERE de.protocol_name IN ('upstream_git', 'meteora_dlmm') GROUP BY backlog_decoder, de.event_kind, upstream_entry_name, upstream_discriminator_hex ORDER BY decoded_count DESC, backlog_decoder, de.event_kind LIMIT 100; + +-- 15. Coverage observed > materialized explained by failed transactions. +-- The final accepted residual is close_bin_array: 16 observed / 14 materialized, +-- with 2 failed transactions Custom 6015 and 0 business materialization on failed tx. +SELECT + ce.entry_name, + ce.entry_kind, + ce.event_family, + ce.expected_db_target, + ce.local_event_kind, + ce.discriminator_hex, + ce.observed_count, + ce.materialized_count, + ce.observed_count - ce.materialized_count AS missing_count +FROM k_sol_dex_event_coverage_entries ce +WHERE ce.decoder_code = 'meteora_dlmm' + AND ce.observed_count > ce.materialized_count + AND ce.observed_count > 0 +ORDER BY missing_count DESC, ce.entry_kind, ce.entry_name, ce.discriminator_hex; + +-- 16. close_bin_array success/failed split. +-- Final acceptable result: +-- success rows -> decoded_count = lifecycle_count = 14 +-- failed rows -> decoded_count = 2, lifecycle_count = 0 +SELECT + de.event_kind, + tx.err_json, + COUNT(*) AS decoded_count, + COUNT(DISTINCT ple.id) AS lifecycle_count, + MIN(tx.signature) AS sample_signature +FROM k_sol_dex_decoded_events de +JOIN k_sol_chain_transactions tx + ON tx.id = de.transaction_id +LEFT JOIN k_sol_pool_lifecycle_events ple + ON ple.decoded_event_id = de.id +WHERE de.protocol_name = 'meteora_dlmm' + AND de.event_kind = 'meteora_dlmm.close_bin_array' +GROUP BY de.event_kind, tx.err_json +ORDER BY decoded_count DESC; + +-- 17. Logical duplicate coverage rows. Target: empty. +SELECT + decoder_code, + COALESCE(program_id, '') AS program_id, + entry_kind, + entry_name, + COALESCE(discriminator_hex, '') AS discriminator_hex, + COALESCE(local_event_kind, '') AS local_event_kind, + COUNT(*) AS duplicate_count +FROM k_sol_dex_event_coverage_entries +WHERE decoder_code = 'meteora_dlmm' +GROUP BY + decoder_code, + COALESCE(program_id, ''), + entry_kind, + entry_name, + COALESCE(discriminator_hex, ''), + COALESCE(local_event_kind, '') +HAVING COUNT(*) > 1 +ORDER BY duplicate_count DESC, entry_kind, entry_name, discriminator_hex;