From ffa4acbccb09d671c62e77e7ba8397189b94d3a7 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Fri, 29 May 2026 07:38:24 +0200 Subject: [PATCH] 0.7.45 --- CHANGELOG.md | 1 + Cargo.toml | 2 +- README.md | 64 +- ROADMAP.md | 65 +- kb_demo_app/package.json | 2 +- kb_demo_app/tauri.conf.json | 2 +- kb_lib/src/db.rs | 12 +- kb_lib/src/db/queries.rs | 10 +- kb_lib/src/db/queries/dex_decoded_event.rs | 205 +++ kb_lib/src/dex.rs | 2 + kb_lib/src/dex/meteora_dlmm.rs | 1391 +++++++++++++++++++- kb_lib/src/dex_decode.rs | 162 ++- kb_lib/src/dex_event_classification.rs | 22 + kb_lib/src/lib.rs | 28 +- kb_lib/src/local_pipeline_replay.rs | 121 +- 15 files changed, 1982 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 871db28..1ba616b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,3 +75,4 @@ 0.7.42 - Consolidation famille Raydium : audit conservatoire des instructions Raydium non décodées, décodage CLMM legacy `swap`, cleanup des audits remplacés, classification HTTP `getTransaction` comme requête lourde avec retry/backoff de backfill, mapping des événements non-swap prouvés `raydium_clmm` (`increase_liquidity_v2`, `decrease_liquidity_v2`, `open_position_with_token22_nft`, `close_position`) et `raydium_cpmm` (`initialize`, `withdraw`, `collect_creator_fee`), matérialisation de 25 liquidity events, 1 lifecycle event et 2 fee events sur corpus élargi, conservation des non-swaps AMM v4 legacy en audit. 0.7.43-E5C - Reprise documentaire et normalisation DEX-first : `0.7.43` est conservé comme point de reprise non clos pour le lot Meteora, la suite est redécoupée par DEX/version séparés, le besoin d’un ledger de décodage/replay est acté, les statuts `known` / `observed` / `decoded` / `materialized` / `verified_by_corpus` deviennent obligatoires, et aucun `program_id` ne doit être marqué vérifié sans preuve/corpus reproductible. 0.7.44 - Ledger de décodage/replay DEX : ajout de `k_sol_dex_decode_replay_ledger`, des DTO/entities/queries associés, des re-exports DB/lib, et intégration dans le replay local pour skipper uniquement l’étape de décodage DEX lorsqu’un passage certifié existe pour la même version logique de decoder. Les transactions multi-event ou multi-token restent marquées `unsafe` et sont redécodées sauf option future plus explicite ; le replay continue de reconstruire détection, matérialisation, trades, candles et classifications à partir des events persistés. +0.7.45 - Meteora DLMM normalisation finale : consolidation séparée de `meteora_dlmm` sur corpus dédié, maintien du wrapper Anchor `anchor_self_cpi_log` `e445a52e51cb9a1d`, enrichissement des swaps via `Swap` / `Swap2Evt`, cleanup des audits Anchor CPI swap déjà couverts, ajout des events Carbon/IDL observés et vérifiés par corpus (`lb_pair_create_event`, `add_liquidity_event`, `remove_liquidity_event`, `claim_fee_event`, `position_create_event`, `position_close_event`, `close_position_if_empty`, `remove_liquidity_by_range2`, `add_liquidity_by_strategy2`, `add_liquidity_by_weight`), conservation des deux audits résiduels `e8abf2613a4d232d` en `instruction_audit` faute de mapping Carbon/IDL confirmé, matérialisation locale validée avec `15` liquidity events et `6` lifecycle events sur le corpus DLMM élargi, et version logique replay `dex_decode.v0.7.45.dlmm_add_liquidity_strategies1`. Aucun nouveau `program_id` n’est déclaré vérifié sans preuve/corpus reproductible. diff --git a/Cargo.toml b/Cargo.toml index 1176c59..69bed9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.7.44" +version = "0.7.45" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" diff --git a/README.md b/README.md index 7cd0ce3..3ec6ce9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ `khadhroony-bobobot` est un workspace Rust destiné à la détection, au décodage, à l’analyse et, à terme, au trading semi-automatisé de tokens Solana. -Ce document reflète le point de reprise `0.7.43-E5C`. La version Cargo reste `0.7.43`, mais le lot Meteora ouvert en `0.7.43` n’est pas considéré comme terminé. La priorité immédiate est maintenant de normaliser la roadmap DEX-first, d’ajouter un ledger de décodage/replay pour éviter les rescans inutiles, puis de reprendre les DEX un par un, variante par variante. +Ce document reflète le point de reprise `0.7.43-E5C` et l’état de consolidation atteint après `0.7.45` pour `meteora_dlmm`. La version Cargo a évolué ensuite à `0.7.45` côté `kb_lib`. Le lot Meteora initialement ouvert en bloc a été redécoupé : `meteora_dlmm` est traité séparément, puis la suite reprend `meteora_damm_v1`, `meteora_damm_v2` et `meteora_dbc` un par un. ## 1. Objectif @@ -90,7 +90,7 @@ Les surfaces suivantes existent dans le code, dans la matrice ou dans le corpus | `raydium_cpmm` | DEX effectif consolidé partiellement | Swaps et premiers events non-trade prouvés sur corpus antérieur. | | `raydium_clmm` | DEX effectif consolidé partiellement | Swaps v2/legacy, positions et liquidity events prouvés sur corpus antérieur. | | `raydium_amm_v4` | DEX effectif legacy | Swaps AMM v4 legacy matérialisés ; non-swaps legacy conservés en audit tant que le corpus ne permet pas une promotion fiable. | -| `meteora_dlmm` | DEX effectif à reprendre séparément | Présent et observé ; events non-trade à normaliser. | +| `meteora_dlmm` | DEX effectif consolidé en `0.7.45` | Swaps, Anchor CPI swap events, liquidity, positions, fees et lifecycle principaux validés par corpus local ; deux Anchor CPI audits résiduels `e8abf2613a4d232d` restent volontairement non mappés. | | `meteora_damm_v1` | DEX effectif à reprendre séparément | Swaps présents ; plusieurs events restent en audit ou non actionnables. | | `meteora_damm_v2` | DEX effectif à reprendre séparément | Swaps et create_pool observés ; nombreux audits à traiter. | | `meteora_dbc` | launch/bonding + DEX effectif partiel à reprendre séparément | Gros volume d’audits ; séparer bonding/launch, swap effectif et migration. | @@ -112,6 +112,35 @@ Aucun `program_id`, DEX ou event ne doit être documenté comme vérifié sans p | `materialized` | L’event alimente une table métier dédiée : trade, liquidity, lifecycle, fee, reward, admin, mint/burn, etc. | | `verified_by_corpus` | Validé par requêtes SQL, signatures/corpus reproductibles et invariants de validation. | +### 3.6. État validé de `meteora_dlmm` en `0.7.45` + +La tranche `0.7.45` clôt la normalisation séparée de `meteora_dlmm` sur le corpus DLMM élargi constitué via `Demo3`, backfill par signatures anciennes et backfill par pool address. + +Éléments validés : + +| Famille | Events DLMM couverts | +|---|---| +| Swaps | `swap`, `swap2`, `swap_exact_out` lorsque présents, avec enrichissement `anchorSwapEvent` pour `Swap` / `Swap2Evt`. | +| Création / lifecycle | `create_pool`, `lb_pair_create_event`, `initialize_bin_array`, `initialize_position`. | +| Positions | `position_create_event`, `position_close_event`, `close_position_if_empty`. | +| Liquidité | `add_liquidity_event`, `add_liquidity_by_strategy2`, `add_liquidity_by_weight`, `remove_liquidity_event`, `remove_liquidity`, `remove_liquidity_by_range2`. | +| Fees | `claim_fee_event`, `claim_fee2`. | +| Rewards | Décodeurs Anchor CPI présents pour `claim_reward_event` / `fund_reward_event`, mais non observés dans le corpus final `0.7.45`. | + +Validation locale finale sur la base DLMM dédiée : + +| Indicateur | Valeur observée | +|---|---:| +| transactions rejouées | `3027` | +| trades matérialisés | `530` | +| liquidity events matérialisés | `15` | +| lifecycle events matérialisés | `6` | +| candles upsert | `2120` | +| audits DLMM résiduels | `2` | + +Les deux audits restants sont `e445a52e51cb9a1d + e8abf2613a4d232d`. Ils restent en `meteora_dlmm.instruction_audit`, car aucun mapping Carbon/IDL suffisamment fiable n’a été confirmé. Ils ne bloquent pas la clôture de `0.7.45`. + + ## 4. Matrice DEX : priorité révisée À partir du point de reprise `0.7.43-E5C`, la priorité est : @@ -200,11 +229,11 @@ Le modèle actuel contient déjà notamment : - `k_sol_transaction_classifications` ; - `k_sol_protocol_candidates`. -### 5.2. Prochaine évolution DB : ledger de décodage/replay +### 5.2. Ledger de décodage/replay implémenté en `0.7.44` -Le replay actuel peut retraiter trop largement les transactions déjà connues. La prochaine tranche DB doit ajouter une structure de suivi permettant de ne pas rescanner les events certains, sauf option explicite. +Le replay local dispose maintenant de la table `k_sol_dex_decode_replay_ledger`. Elle permet de ne pas relancer l’étape `DexDecodeService` lorsqu’une transaction a déjà été décodée avec certitude pour la même version logique de decoder. Les étapes de détection, matérialisation non-trade, trades, candles et classifications restent rejouées afin de reconstruire les tables dérivées après reset. -Objectifs : +Objectifs maintenus : - mémoriser qu’une transaction/instruction a déjà été traitée par un decoder donné ; - stocker le statut de décodage : certain, partiel, inconnu, erreur, non-actionnable, multi-token ambigu ; @@ -214,26 +243,23 @@ Objectifs : - ne pas skipper automatiquement les transactions multi-token, multi-pool ou multi-event ambiguës ; - conserver les failed transactions comme traçables mais non actionnables. -Nom de table à décider dans la tranche DB, par exemple : +Table actuelle : -- `k_sol_decode_attempts` ; ou -- `k_sol_replay_decode_ledger`. +- `k_sol_dex_decode_replay_ledger`. -Champs conceptuels attendus : +Champs principaux : | Champ conceptuel | Rôle | |---|---| | `transaction_id` / `signature` | rattachement transactionnel stable | -| `instruction_id` / `instruction_index` | granularité instruction si possible | -| `program_id` | programme effectivement traité | -| `protocol_name` | protocole/DEx résolu | -| `decoder_code` | decoder logique utilisé | +| `decoder_scope` | périmètre logique du decoder, actuellement `dex_decode.local_pipeline` | | `decoder_version` | version logique du decoder | -| `decode_status` | certain, partial, unknown, failed, skipped, ambiguous | -| `certainty` | niveau de confiance machine | -| `event_count` | nombre d’events produits | -| `payload_hash` / `instruction_hash` | détection de changement d’entrée | -| `reason_code` / `error_json` | diagnostic sans panic | +| `decode_status` | `decoded` ou `no_events` dans la première implémentation | +| `certainty` | `sure` ou `unsafe` | +| `event_count` | nombre total d’events persistés | +| `distinct_token_mint_count` | garde-fou multi-token | +| `force_replay_required` | indique que le décodage doit être relancé | +| `status_reason` | diagnostic lisible sans panic | | `created_at` / `updated_at` | audit local | ## 6. Politique de replay @@ -274,7 +300,7 @@ La priorité immédiate après le point de reprise `0.7.43-E5C` est : 1. `0.7.43` : resynchronisation documentaire, normalisation DEX-first et gel du point de reprise ; 2. `0.7.44` : ajout du ledger de décodage/replay et options de replay `force` / skip sûr ; -3. `0.7.45` : reprise séparée de `meteora_dlmm` ; +3. `0.7.45` : reprise séparée de `meteora_dlmm` — clôturée côté corpus DLMM actuel ; 4. `0.7.46` : reprise séparée de `meteora_damm_v1` ; 5. `0.7.47` : reprise séparée de `meteora_damm_v2` ; 6. `0.7.48` : reprise séparée de `meteora_dbc` ; diff --git a/ROADMAP.md b/ROADMAP.md index a446c70..9046b54 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1054,19 +1054,66 @@ Reste à faire plus tard : - descendre le ledger au niveau instruction/program lorsque nécessaire ; - ajouter un hash d’entrée transaction/instruction pour détecter les mutations de payload ; -- exposer l’option `force_decode_replay` dans l’UI si besoin ; +- ajouter des filtres plus fins côté UI pour diagnostiquer les lignes ledger `unsafe` ; - ajouter des diagnostics dédiés dans `local_pipeline_diagnostics`. ### 6.077. Version `0.7.45` — `meteora_dlmm` séparé -Objectif : consolider `meteora_dlmm` comme DEX effectif séparé, avec corpus dédié et events utiles au trading. +Objectif : consolider `meteora_dlmm` comme DEX effectif séparé, avec corpus dédié et events utiles au trading, sans mélanger DAMM v1, DAMM v2 ou DBC. -À faire : +Statut : clos sur le corpus DLMM local élargi. -- vérifier le ou les `program_id` par corpus local, pas seulement par constante ; -- consolider swaps exploitables, add/remove liquidity, positions, lifecycle et audits restants ; -- matérialiser uniquement les events prouvés dans les tables dédiées ; -- conserver tout event incomplet en `instruction_audit` ou non-actionnable ; -- ajouter les compteurs diagnostics par event kind. +Fait : + +- constitution d’un corpus dédié `meteora_dlmm` via `Demo3`, backfill manuel des signatures anciennes du pool `HTvjzsfX3yU6BUodCjZ5vZkUrAxMDTrBs3CJaq43ashR`, puis backfill par pool address ; +- confirmation locale du programme DLMM observé `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` dans les transactions du corpus ; +- traitement du wrapper Anchor `anchor_self_cpi_log` `e445a52e51cb9a1d` ; +- mapping prouvé localement et par IDL/Carbon des Anchor CPI swap events : `516ce3becdd00ac4` -> `Swap`, `2e7452d7941b544d` -> `Swap2Evt` ; +- enrichissement du payload `meteora_dlmm.swap` avec `anchorSwapEvent`, montants et fees CPI décodés ; +- cleanup conservatoire des audits Anchor CPI swap déjà couverts par un swap DLMM matérialisé ; +- ajout des events Anchor CPI non-swap DLMM observés : `lb_pair_create_event`, `add_liquidity_event`, `remove_liquidity_event`, `claim_fee_event`, `claim_reward_event` / `fund_reward_event` côté decoder, `position_create_event`, `position_close_event` ; +- promotion du discriminant direct `claim_fee2` vers `meteora_dlmm.claim_fee2` ; +- promotion de `close_position_if_empty` comme event de lifecycle/position close prouvé localement ; +- promotion de `remove_liquidity_by_range2`, `add_liquidity_by_strategy2` et `add_liquidity_by_weight` selon les layouts Carbon et le corpus local ; +- matérialisation validée des families non-trade dans les tables dédiées, notamment `k_sol_liquidity_events`, `k_sol_pool_lifecycle_events` et `k_sol_fee_events` ; +- maintien du ledger replay avec `effective_event_count`, afin que les `.instruction_audit` informatifs ne rendent pas inutilement les transactions `unsafe` ; +- version logique finale du replay pour la tranche : `dex_decode.v0.7.45.dlmm_add_liquidity_strategies1` ; +- maintien de la règle : aucun nouveau `program_id` n’est vérifié sans corpus. + +Validation locale finale observée sur la base DLMM dédiée : + +| Indicateur | Valeur | +|---|---:| +| transactions rejouées | `3027` | +| trades matérialisés | `530` | +| liquidity events matérialisés | `15` | +| lifecycle events matérialisés | `6` | +| candles upsert | `2120` | +| audits DLMM résiduels | `2` | + +Events DLMM observés après replay : + +- `meteora_dlmm.swap` ; +- `meteora_dlmm.create_pool` ; +- `meteora_dlmm.lb_pair_create_event` ; +- `meteora_dlmm.initialize_bin_array` ; +- `meteora_dlmm.initialize_position` ; +- `meteora_dlmm.position_create_event` ; +- `meteora_dlmm.position_close_event` ; +- `meteora_dlmm.close_position_if_empty` ; +- `meteora_dlmm.add_liquidity_event` ; +- `meteora_dlmm.add_liquidity_by_strategy2` ; +- `meteora_dlmm.add_liquidity_by_weight` ; +- `meteora_dlmm.remove_liquidity_event` ; +- `meteora_dlmm.remove_liquidity` ; +- `meteora_dlmm.remove_liquidity_by_range2` ; +- `meteora_dlmm.claim_fee_event` ; +- `meteora_dlmm.claim_fee2`. + +Limite conservée : + +- `e445a52e51cb9a1d + e8abf2613a4d232d` reste en `meteora_dlmm.instruction_audit` avec `proofStatus = observed_local_corpus_anchor_self_cpi_log`, faute de mapping Carbon/IDL confirmé. Ces deux audits ne sont pas promus et ne bloquent pas la clôture de `0.7.45`. + +Décision : `0.7.45` est clos pour `meteora_dlmm`. La suite immédiate est `0.7.46` sur `meteora_damm_v1` uniquement. ### 6.078. Version `0.7.46` — `meteora_damm_v1` séparé Objectif : reprendre `meteora_damm_v1` sans le mélanger à DAMM v2, DBC ou DLMM. @@ -1400,7 +1447,7 @@ 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 ; -2. `0.7.45` : `meteora_dlmm` ; +2. `0.7.45` : `meteora_dlmm` — clos ; 3. `0.7.46` : `meteora_damm_v1` ; 4. `0.7.47` : `meteora_damm_v2` ; 5. `0.7.48` : `meteora_dbc` ; diff --git a/kb_demo_app/package.json b/kb_demo_app/package.json index 5692dd0..390a0ca 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.44", + "version": "0.7.45", "type": "module", "scripts": { "dev": "vite", diff --git a/kb_demo_app/tauri.conf.json b/kb_demo_app/tauri.conf.json index 349f35d..fcad83a 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.44", + "version": "0.7.45", "identifier": "com.sasedev.kb-demo-app", "build": { "beforeDevCommand": "npm run dev", diff --git a/kb_lib/src/db.rs b/kb_lib/src/db.rs index a43f23a..127ee6b 100644 --- a/kb_lib/src/db.rs +++ b/kb_lib/src/db.rs @@ -21,8 +21,8 @@ pub use dtos::ChainSlotDto; pub use dtos::ChainTransactionDto; pub use dtos::DbMetadataDto; pub use dtos::DbRuntimeEventDto; -pub use dtos::DexDecodedEventDto; pub use dtos::DexDecodeReplayLedgerDto; +pub use dtos::DexDecodedEventDto; pub use dtos::DexDto; pub use dtos::FeeEventDto; pub use dtos::KnownHttpEndpointDto; @@ -88,8 +88,8 @@ pub use entities::ChainSlotEntity; pub use entities::ChainTransactionEntity; pub use entities::DbMetadataEntity; pub use entities::DbRuntimeEventEntity; -pub use entities::DexDecodedEventEntity; pub use entities::DexDecodeReplayLedgerEntity; +pub use entities::DexDecodedEventEntity; pub use entities::DexEntity; pub use entities::FeeEventEntity; pub use entities::KnownHttpEndpointEntity; @@ -142,14 +142,16 @@ pub use queries::query_db_metadatas_list; pub use queries::query_db_metadatas_upsert; pub use queries::query_db_runtime_events_insert; pub use queries::query_db_runtime_events_list_recent; +pub use queries::query_dex_decode_replay_ledger_get_by_signature; +pub use queries::query_dex_decode_replay_ledger_get_by_transaction; +pub use queries::query_dex_decode_replay_ledger_upsert; pub use queries::query_dex_decoded_events_delete_by_key; +pub use queries::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits; +pub use queries::query_dex_decoded_events_delete_related_instruction_audit; pub use queries::query_dex_decoded_events_get_by_key; pub use queries::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint; pub use queries::query_dex_decoded_events_list_by_transaction_id; pub use queries::query_dex_decoded_events_upsert; -pub use queries::query_dex_decode_replay_ledger_get_by_signature; -pub use queries::query_dex_decode_replay_ledger_get_by_transaction; -pub use queries::query_dex_decode_replay_ledger_upsert; pub use queries::query_dexs_get_by_code; pub use queries::query_dexs_list; pub use queries::query_dexs_upsert; diff --git a/kb_lib/src/db/queries.rs b/kb_lib/src/db/queries.rs index 8ab24f5..bba6750 100644 --- a/kb_lib/src/db/queries.rs +++ b/kb_lib/src/db/queries.rs @@ -9,8 +9,8 @@ mod chain_transaction; mod db_metadata; mod db_runtime_event; mod dex; -mod dex_decoded_event; mod dex_decode_replay_ledger; +mod dex_decoded_event; mod fee_event; mod known_http_endpoint; mod known_ws_endpoint; @@ -66,14 +66,16 @@ pub use db_runtime_event::query_db_runtime_events_list_recent; pub use dex::query_dexs_get_by_code; pub use dex::query_dexs_list; pub use dex::query_dexs_upsert; +pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_get_by_signature; +pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_get_by_transaction; +pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_upsert; pub use dex_decoded_event::query_dex_decoded_events_delete_by_key; +pub use dex_decoded_event::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits; +pub use dex_decoded_event::query_dex_decoded_events_delete_related_instruction_audit; pub use dex_decoded_event::query_dex_decoded_events_get_by_key; pub use dex_decoded_event::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint; pub use dex_decoded_event::query_dex_decoded_events_list_by_transaction_id; pub use dex_decoded_event::query_dex_decoded_events_upsert; -pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_get_by_signature; -pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_get_by_transaction; -pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_upsert; pub use fee_event::query_fee_events_get_by_decoded_event_id; pub use fee_event::query_fee_events_list_recent; pub use fee_event::query_fee_events_upsert; diff --git a/kb_lib/src/db/queries/dex_decoded_event.rs b/kb_lib/src/db/queries/dex_decoded_event.rs index f4cdfc5..bda0fd8 100644 --- a/kb_lib/src/db/queries/dex_decoded_event.rs +++ b/kb_lib/src/db/queries/dex_decoded_event.rs @@ -128,6 +128,109 @@ WHERE transaction_id = ? } } +/// Deletes decoded DEX instruction audit rows related to one decoded instruction. +/// +/// This removes an audit row attached to the decoded instruction itself, its direct +/// parent instruction, or its direct child instructions inside the same transaction. +pub async fn query_dex_decoded_events_delete_related_instruction_audit( + database: &crate::Database, + transaction_id: i64, + instruction_id: i64, + audit_event_kind: &str, +) -> Result { + match database.connection() { + crate::DatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +DELETE FROM k_sol_dex_decoded_events +WHERE transaction_id = ? + AND event_kind = ? + AND instruction_id IN ( + SELECT id + FROM k_sol_chain_instructions + WHERE transaction_id = ? + AND id = ? + + UNION + + SELECT parent_instruction_id + FROM k_sol_chain_instructions + WHERE transaction_id = ? + AND id = ? + AND parent_instruction_id IS NOT NULL + + UNION + + SELECT id + FROM k_sol_chain_instructions + WHERE transaction_id = ? + AND parent_instruction_id = ? + ) + "#, + ) + .bind(transaction_id) + .bind(audit_event_kind) + .bind(transaction_id) + .bind(instruction_id) + .bind(transaction_id) + .bind(instruction_id) + .bind(transaction_id) + .bind(instruction_id) + .execute(pool) + .await; + match query_result { + Ok(result) => return Ok(result.rows_affected()), + Err(error) => { + return Err(crate::Error::Db(format!( + "cannot delete related instruction audit events on sqlite: {}", + error + ))); + }, + } + }, + } +} + +/// Deletes Meteora DLMM Anchor self-CPI swap audit rows already covered by decoded swaps. +/// +/// This targets only local-corpus-observed Anchor event discriminators that are +/// decoded into `meteora_dlmm.swap` payload enrichment. It does not delete +/// unrelated DLMM instruction audits. +pub async fn query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits( + database: &crate::Database, + transaction_id: i64, +) -> Result { + match database.connection() { + crate::DatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +DELETE FROM k_sol_dex_decoded_events +WHERE transaction_id = ? + AND protocol_name = 'meteora_dlmm' + AND event_kind = 'meteora_dlmm.instruction_audit' + AND json_extract(payload_json, '$.anchorSelfCpiLog') = 1 + AND json_extract(payload_json, '$.anchorEventDiscriminatorHex') IN ( + '516ce3becdd00ac4', + '2e7452d7941b544d' + ) + "#, + ) + .bind(transaction_id) + .execute(pool) + .await; + match query_result { + Ok(result) => return Ok(result.rows_affected()), + Err(error) => { + return Err(crate::Error::Db(format!( + "cannot delete Meteora DLMM Anchor swap audit events on sqlite: {}", + error + ))); + }, + } + }, + } +} + /// Reads one decoded DEX event by its natural key. pub async fn query_dex_decoded_events_get_by_key( database: &crate::Database, @@ -400,4 +503,106 @@ mod tests { assert_eq!(listed.len(), 1); assert_eq!(listed[0].event_kind, "raydium_amm_v4.initialize2_pool"); } + + #[tokio::test] + async fn related_instruction_audit_delete_removes_parent_audit_for_child_decode() { + let database = make_database().await; + let transaction_dto = crate::ChainTransactionDto::new( + "sig-dex-event-related-audit-test-1".to_string(), + None, + None, + Some("helius_primary_http".to_string()), + Some("0".to_string()), + None, + None, + r#"{"transaction":{"message":{"instructions":[]}}}"#.to_string(), + ); + let transaction_id_result = + crate::query_chain_transactions_upsert(&database, &transaction_dto).await; + let transaction_id = match transaction_id_result { + Ok(transaction_id) => transaction_id, + Err(error) => panic!("transaction upsert must succeed: {}", error), + }; + let parent_instruction_dto = crate::ChainInstructionDto::new( + transaction_id, + None, + 0, + None, + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora-dlmm".to_string()), + Some(1), + r#"["ParentAccount","Pool111"]"#.to_string(), + None, + None, + None, + ); + let parent_instruction_id_result = + crate::query_chain_instructions_insert(&database, &parent_instruction_dto).await; + let parent_instruction_id = match parent_instruction_id_result { + Ok(parent_instruction_id) => parent_instruction_id, + Err(error) => panic!("parent instruction insert must succeed: {}", error), + }; + let child_instruction_dto = crate::ChainInstructionDto::new( + transaction_id, + Some(parent_instruction_id), + 0, + Some(0), + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora-dlmm".to_string()), + Some(2), + r#"["ChildAccount","Pool111"]"#.to_string(), + None, + None, + None, + ); + let child_instruction_id_result = + crate::query_chain_instructions_insert(&database, &child_instruction_dto).await; + let child_instruction_id = match child_instruction_id_result { + Ok(child_instruction_id) => child_instruction_id, + Err(error) => panic!("child instruction insert must succeed: {}", error), + }; + let audit_dto = crate::DexDecodedEventDto::new( + transaction_id, + Some(parent_instruction_id), + "meteora_dlmm".to_string(), + crate::METEORA_DLMM_PROGRAM_ID.to_string(), + "meteora_dlmm.instruction_audit".to_string(), + Some("Pool111".to_string()), + None, + None, + None, + None, + r#"{"audit":true}"#.to_string(), + ); + let audit_upsert_result = + crate::query_dex_decoded_events_upsert(&database, &audit_dto).await; + match audit_upsert_result { + Ok(_) => {}, + Err(error) => panic!("audit upsert must succeed: {}", error), + } + let deleted_result = super::query_dex_decoded_events_delete_related_instruction_audit( + &database, + transaction_id, + child_instruction_id, + "meteora_dlmm.instruction_audit", + ) + .await; + let deleted = match deleted_result { + Ok(deleted) => deleted, + Err(error) => panic!("related audit delete must succeed: {}", error), + }; + assert_eq!(deleted, 1); + let fetched_result = crate::query_dex_decoded_events_get_by_key( + &database, + transaction_id, + Some(parent_instruction_id), + "meteora_dlmm.instruction_audit", + ) + .await; + let fetched = match fetched_result { + Ok(fetched) => fetched, + Err(error) => panic!("audit fetch must succeed: {}", error), + }; + assert!(fetched.is_none()); + } } diff --git a/kb_lib/src/dex.rs b/kb_lib/src/dex.rs index 7a86f3f..cb13279 100644 --- a/kb_lib/src/dex.rs +++ b/kb_lib/src/dex.rs @@ -38,8 +38,10 @@ pub use meteora_dbc::MeteoraDbcSwapDecoded; pub use meteora_dlmm::MeteoraDlmmCreatePoolDecoded; pub use meteora_dlmm::MeteoraDlmmDecodedEvent; pub use meteora_dlmm::MeteoraDlmmDecoder; +pub use meteora_dlmm::MeteoraDlmmFeeDecoded; pub use meteora_dlmm::MeteoraDlmmLiquidityDecoded; pub use meteora_dlmm::MeteoraDlmmPoolLifecycleDecoded; +pub use meteora_dlmm::MeteoraDlmmRewardDecoded; pub use meteora_dlmm::MeteoraDlmmSwapDecoded; pub use orca_whirlpools::OrcaWhirlpoolsCreatePoolDecoded; pub use orca_whirlpools::OrcaWhirlpoolsDecodedEvent; diff --git a/kb_lib/src/dex/meteora_dlmm.rs b/kb_lib/src/dex/meteora_dlmm.rs index f972db7..942af69 100644 --- a/kb_lib/src/dex/meteora_dlmm.rs +++ b/kb_lib/src/dex/meteora_dlmm.rs @@ -8,6 +8,49 @@ const DLMM_DISCRIMINATOR_CLAIM_FEE2: [u8; 8] = [0x70, 0xbf, 0x65, 0xab, 0x1c, 0x90, 0x7f, 0xbb]; +const DLMM_DISCRIMINATOR_CLOSE_POSITION_IF_EMPTY: [u8; 8] = + [0x3b, 0x7c, 0xd4, 0x76, 0x5b, 0x98, 0x6e, 0x9d]; + +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY2: [u8; 8] = + [0x03, 0xdd, 0x95, 0xda, 0x6f, 0x8d, 0x76, 0xd5]; + +const DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_WEIGHT: [u8; 8] = + [0x1c, 0x8c, 0xee, 0x63, 0xe7, 0xa2, 0x15, 0x95]; + +const DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY_BY_RANGE2: [u8; 8] = + [0xcc, 0x02, 0xc3, 0x91, 0x35, 0x91, 0x91, 0xcd]; + +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]; + +const DLMM_EVENT_DISCRIMINATOR_SWAP2_EVT: [u8; 8] = + [0x2e, 0x74, 0x52, 0xd7, 0x94, 0x1b, 0x54, 0x4d]; + +const DLMM_EVENT_DISCRIMINATOR_LB_PAIR_CREATE: [u8; 8] = + [0xb9, 0x4a, 0xfc, 0x7d, 0x1b, 0xd7, 0xbc, 0x6f]; + +const DLMM_EVENT_DISCRIMINATOR_ADD_LIQUIDITY: [u8; 8] = + [0x1f, 0x5e, 0x7d, 0x5a, 0xe3, 0x34, 0x3d, 0xba]; + +const DLMM_EVENT_DISCRIMINATOR_REMOVE_LIQUIDITY: [u8; 8] = + [0x74, 0xf4, 0x61, 0xe8, 0x67, 0x1f, 0x98, 0x3a]; + +const DLMM_EVENT_DISCRIMINATOR_CLAIM_FEE: [u8; 8] = + [0x4b, 0x7a, 0x9a, 0x30, 0x8c, 0x4a, 0x7b, 0xa3]; + +const DLMM_EVENT_DISCRIMINATOR_CLAIM_REWARD: [u8; 8] = + [0x94, 0x74, 0x86, 0xcc, 0x16, 0xab, 0x55, 0x5f]; + +const DLMM_EVENT_DISCRIMINATOR_FUND_REWARD: [u8; 8] = + [0xf6, 0xe4, 0x3a, 0x82, 0x91, 0xaa, 0x4f, 0xcc]; + +const DLMM_EVENT_DISCRIMINATOR_POSITION_CREATE: [u8; 8] = + [0x90, 0x8e, 0xfc, 0x54, 0x9d, 0x35, 0x25, 0x79]; + +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]; @@ -151,6 +194,56 @@ pub struct MeteoraDlmmPoolLifecycleDecoded { pub payload_json: serde_json::Value, } +/// Decoded Meteora DLMM fee collection event. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MeteoraDlmmFeeDecoded { + /// Parent transaction id. + pub transaction_id: i64, + /// Parent instruction id. + pub instruction_id: i64, + /// Transaction signature. + pub signature: std::string::String, + /// Program id. + pub program_id: std::string::String, + /// Normalized decoded event kind. + pub event_kind: std::string::String, + /// Optional DLMM pair/pool account. + pub pool_account: std::option::Option, + /// Optional actor wallet or owner account. + pub actor_wallet: std::option::Option, + /// Optional fee token mint. + pub fee_token_mint: std::option::Option, + /// Optional raw fee amount as decimal text. + pub fee_amount_raw: std::option::Option, + /// Decoded payload. + pub payload_json: serde_json::Value, +} + +/// Decoded Meteora DLMM reward or emission event. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MeteoraDlmmRewardDecoded { + /// Parent transaction id. + pub transaction_id: i64, + /// Parent instruction id. + pub instruction_id: i64, + /// Transaction signature. + pub signature: std::string::String, + /// Program id. + pub program_id: std::string::String, + /// Normalized decoded event kind. + pub event_kind: std::string::String, + /// Optional DLMM pair/pool account. + pub pool_account: std::option::Option, + /// Optional actor wallet, owner or funder account. + pub actor_wallet: std::option::Option, + /// Optional reward token mint. + pub reward_token_mint: std::option::Option, + /// Optional raw reward amount as decimal text. + pub reward_amount_raw: std::option::Option, + /// Decoded payload. + pub payload_json: serde_json::Value, +} + /// Decoded Meteora DLMM event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum MeteoraDlmmDecodedEvent { @@ -162,6 +255,10 @@ pub enum MeteoraDlmmDecodedEvent { Liquidity(MeteoraDlmmLiquidityDecoded), /// DLMM pool lifecycle event that is not the canonical create-pool event. PoolLifecycle(MeteoraDlmmPoolLifecycleDecoded), + /// DLMM fee collection event. + Fee(MeteoraDlmmFeeDecoded), + /// DLMM reward or emission event. + Reward(MeteoraDlmmRewardDecoded), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -171,8 +268,9 @@ enum MeteoraDlmmInstructionKind { LiquidityAdd, LiquidityRemove, PositionOpen, + PositionClose, PoolLifecycle, - Ignore, + Fee, Unknown, } @@ -189,9 +287,13 @@ enum MeteoraDlmmInstructionName { SwapWithPriceImpact, InitializeBinArray, AddLiquidity, + AddLiquidityByStrategy2, + AddLiquidityByWeight, RemoveLiquidity, + RemoveLiquidityByRange2, ClaimFee2, InitializePosition, + ClosePositionIfEmpty, Unknown, } @@ -213,9 +315,13 @@ impl MeteoraDlmmInstructionName { 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::Unknown => return "unknown", } } @@ -233,14 +339,17 @@ impl MeteoraDlmmInstructionName { | Self::SwapExactOut | Self::SwapExactOut2 | Self::SwapWithPriceImpact => return MeteoraDlmmInstructionKind::Swap, - Self::AddLiquidity => return MeteoraDlmmInstructionKind::LiquidityAdd, - Self::RemoveLiquidity => return MeteoraDlmmInstructionKind::LiquidityRemove, + 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::Unknown => return MeteoraDlmmInstructionKind::Unknown, - Self::ClaimFee2 => { - return MeteoraDlmmInstructionKind::Ignore; - }, + Self::ClaimFee2 => return MeteoraDlmmInstructionKind::Fee, } } } @@ -311,6 +420,20 @@ impl MeteoraDlmmDecoder { Ok(instruction_data) => instruction_data, Err(error) => return Err(error), }; + if let Some(data) = instruction_data.as_ref() { + let anchor_event = decode_anchor_non_swap_event( + transaction_id, + instruction_id, + transaction.signature.as_str(), + program_id, + instruction, + data.as_slice(), + ); + if let Some(anchor_event) = anchor_event { + decoded_events.push(anchor_event); + continue; + } + } let instruction_name = classify_instruction_name( parsed_json.as_ref(), instruction.parsed_type.as_deref(), @@ -318,9 +441,53 @@ impl MeteoraDlmmDecoder { &log_messages, ); let instruction_kind = instruction_name.kind(); - if instruction_kind == MeteoraDlmmInstructionKind::Unknown - || instruction_kind == MeteoraDlmmInstructionKind::Ignore - { + if instruction_kind == MeteoraDlmmInstructionKind::Unknown { + continue; + } + if instruction_kind == MeteoraDlmmInstructionKind::PositionClose { + let position_account = extract_account(&accounts, 0); + let actor_wallet = extract_account(&accounts, 1); + let rent_receiver = extract_account(&accounts, 2); + let event_authority = extract_account(&accounts, 3); + let program_account = extract_account(&accounts, 4); + let event_kind = format!("meteora_dlmm.{}", instruction_name.as_str()); + 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, + "proofStatus": "observed_local_corpus_and_known_carbon_layout", + "position": position_account, + "actorWallet": actor_wallet, + "rentReceiver": rent_receiver, + "eventAuthority": event_authority, + "programAccount": program_account + }); + 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: None, + token_a_mint: None, + token_b_mint: None, + payload_json, + }, + )); continue; } let pool_account = @@ -400,6 +567,16 @@ impl MeteoraDlmmDecoder { &accounts, ); let trade_side = infer_trade_side(parsed_json.as_ref()); + let anchor_swap_event = + find_best_anchor_swap_event_for_instruction(instruction, instructions); + let anchor_amount_in_raw = + extract_json_string_field(anchor_swap_event.as_ref(), "amountInRaw"); + let anchor_amount_out_raw = + extract_json_string_field(anchor_swap_event.as_ref(), "amountOutRaw"); + let anchor_fee_raw = + extract_json_string_field(anchor_swap_event.as_ref(), "feeRaw"); + let anchor_protocol_fee_raw = + extract_json_string_field(anchor_swap_event.as_ref(), "protocolFeeRaw"); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", "eventKind": "swap", @@ -424,7 +601,12 @@ impl MeteoraDlmmDecoder { "reserveYAccount": reserve_y_account, "userTokenInAccount": user_token_in_account, "userTokenOutAccount": user_token_out_account, - "tradeSide": format!("{:?}", trade_side) + "tradeSide": format!("{:?}", trade_side), + "amountIn": anchor_amount_in_raw, + "amountOut": anchor_amount_out_raw, + "feeAmountRaw": anchor_fee_raw, + "protocolFeeAmountRaw": anchor_protocol_fee_raw, + "anchorSwapEvent": anchor_swap_event }); decoded_events.push(crate::MeteoraDlmmDecodedEvent::Swap( crate::MeteoraDlmmSwapDecoded { @@ -484,6 +666,33 @@ impl MeteoraDlmmDecoder { "bin_liquidity", ], ); + let range_from_bin_id = read_i32_string_from_instruction_data( + instruction_name, + instruction_data.as_deref(), + 8, + ); + let range_to_bin_id = read_i32_string_from_instruction_data( + instruction_name, + instruction_data.as_deref(), + 12, + ); + let range_bps_to_remove = read_u16_string_from_instruction_data( + instruction_name, + instruction_data.as_deref(), + 16, + ); + let liquidity_position_account = + resolve_dlmm_liquidity_position_account(instruction_name, &accounts); + let bin_array_bitmap_extension = extract_account(&accounts, 2); + let user_token_x_account = extract_account(&accounts, 3); + let user_token_y_account = extract_account(&accounts, 4); + let reserve_x_account = extract_account(&accounts, 5); + let reserve_y_account = extract_account(&accounts, 6); + let event_authority_account = + resolve_dlmm_liquidity_event_authority_account(instruction_name, &accounts); + let program_account = + resolve_dlmm_liquidity_program_account(instruction_name, &accounts); + let proof_status = resolve_dlmm_instruction_proof_status(instruction_name); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", "eventKind": instruction_name.as_str(), @@ -507,7 +716,19 @@ impl MeteoraDlmmDecoder { "actorWallet": actor_wallet, "baseAmountRaw": base_amount_raw, "quoteAmountRaw": quote_amount_raw, - "liquidityAmountRaw": liquidity_amount_raw + "liquidityAmountRaw": liquidity_amount_raw, + "rangeFromBinId": range_from_bin_id, + "rangeToBinId": range_to_bin_id, + "rangeBpsToRemove": range_bps_to_remove, + "positionAccount": liquidity_position_account, + "binArrayBitmapExtension": bin_array_bitmap_extension, + "userTokenXAccount": user_token_x_account, + "userTokenYAccount": user_token_y_account, + "reserveXAccount": reserve_x_account, + "reserveYAccount": reserve_y_account, + "eventAuthority": event_authority_account, + "programAccount": program_account, + "proofStatus": proof_status }); decoded_events.push(crate::MeteoraDlmmDecodedEvent::Liquidity( crate::MeteoraDlmmLiquidityDecoded { @@ -528,6 +749,72 @@ impl MeteoraDlmmDecoder { )); continue; } + if instruction_kind == MeteoraDlmmInstructionKind::Fee { + 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 fee_token_mint = extract_string_by_candidate_keys( + parsed_json.as_ref(), + &[ + "feeTokenMint", + "fee_token_mint", + "tokenMint", + "token_mint", + "mint", + "quoteMint", + "quote_mint", + ], + ); + let fee_amount_raw = extract_amount_string_by_candidate_keys( + parsed_json.as_ref(), + &[ + "feeAmountRaw", + "fee_amount_raw", + "feeAmount", + "fee_amount", + "protocolFeeAmount", + "protocol_fee_amount", + "amount", + ], + ); + 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, + "feeTokenMint": fee_token_mint, + "feeAmountRaw": fee_amount_raw + }); + decoded_events.push(crate::MeteoraDlmmDecodedEvent::Fee( + crate::MeteoraDlmmFeeDecoded { + transaction_id, + instruction_id, + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + actor_wallet, + fee_token_mint, + fee_amount_raw, + payload_json, + }, + )); + continue; + } if instruction_kind == MeteoraDlmmInstructionKind::PoolLifecycle { let event_kind = format!("meteora_dlmm.{}", instruction_name.as_str()); let payload_json = serde_json::json!({ @@ -570,6 +857,839 @@ impl MeteoraDlmmDecoder { } } +fn decode_anchor_non_swap_event( + transaction_id: i64, + instruction_id: i64, + signature: &str, + program_id: &str, + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if !is_dlmm_anchor_self_cpi_log(data) { + return None; + } + if data.len() < 16 { + return None; + } + let event_discriminator = read_8_bytes(data, 8); + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_LB_PAIR_CREATE { + return decode_anchor_lb_pair_create_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_ADD_LIQUIDITY { + return decode_anchor_liquidity_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "meteora_dlmm.add_liquidity_event", + "add_liquidity_event", + "1f5e7d5ae3343dba", + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_REMOVE_LIQUIDITY { + return decode_anchor_liquidity_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + "meteora_dlmm.remove_liquidity_event", + "remove_liquidity_event", + "74f461e8671f983a", + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_CLAIM_FEE { + return decode_anchor_claim_fee_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_CLAIM_REWARD { + return decode_anchor_claim_reward_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_FUND_REWARD { + return decode_anchor_fund_reward_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_POSITION_CREATE { + return decode_anchor_position_create_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + ); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_POSITION_CLOSE { + return decode_anchor_position_close_event( + transaction_id, + instruction_id, + signature, + program_id, + instruction, + data, + ); + } + return None; +} + +fn decode_anchor_lb_pair_create_event( + transaction_id: i64, + instruction_id: i64, + signature: &str, + program_id: &str, + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if data.len() < 114 { + return None; + } + let lb_pair = read_pubkey_string(data, 16); + let bin_step = read_u16_string(data, 48); + let token_x = read_pubkey_string(data, 50); + let token_y = read_pubkey_string(data, 82); + let event_kind = "meteora_dlmm.lb_pair_create_event".to_string(); + let payload_json = serde_json::json!({ + "decoder": "meteora_dlmm", + "eventKind": "lb_pair_create_event", + "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": "lb_pair_create_event", + "anchorEventDiscriminatorHex": "b94afc7d1bd7bc6f", + "anchorEventPayloadSize": data.len().saturating_sub(8), + "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "lbPair": lb_pair, + "poolAccount": lb_pair, + "binStep": bin_step, + "tokenX": token_x, + "tokenY": token_y, + "tokenAMint": token_x, + "tokenBMint": token_y + }); + 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: lb_pair, + token_a_mint: token_x, + token_b_mint: token_y, + payload_json, + }, + )); +} + +fn decode_anchor_liquidity_event( + transaction_id: i64, + instruction_id: i64, + signature: &str, + program_id: &str, + instruction: &crate::ChainInstructionDto, + data: &[u8], + event_kind_text: &str, + anchor_event_name: &str, + event_discriminator_hex: &str, +) -> std::option::Option { + if data.len() < 132 { + return None; + } + let lb_pair = read_pubkey_string(data, 16); + let from = read_pubkey_string(data, 48); + let position = read_pubkey_string(data, 80); + 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 event_kind = event_kind_text.to_string(); + 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": "known_carbon_layout_pending_local_corpus_validation", + "lbPair": lb_pair, + "poolAccount": lb_pair, + "from": from, + "actorWallet": from, + "position": position, + "amounts": [amount_x.clone(), amount_y.clone()], + "baseAmountRaw": amount_x, + "quoteAmountRaw": amount_y, + "activeBinId": active_bin_id + }); + 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: lb_pair, + token_a_mint: None, + token_b_mint: None, + actor_wallet: from, + base_amount_raw: amount_x, + quote_amount_raw: amount_y, + liquidity_amount_raw: None, + payload_json, + })); +} + +fn decode_anchor_claim_fee_event( + transaction_id: i64, + instruction_id: i64, + signature: &str, + program_id: &str, + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if data.len() < 128 { + return None; + } + let lb_pair = read_pubkey_string(data, 16); + let position = read_pubkey_string(data, 48); + 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 payload_json = serde_json::json!({ + "decoder": "meteora_dlmm", + "eventKind": "claim_fee_event", + "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": "claim_fee_event", + "anchorEventDiscriminatorHex": "4b7a9a308c4a7ba3", + "anchorEventPayloadSize": data.len().saturating_sub(8), + "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "lbPair": lb_pair, + "poolAccount": lb_pair, + "position": position, + "owner": owner, + "actorWallet": owner, + "feeXRaw": fee_x, + "feeYRaw": fee_y + }); + 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: lb_pair, + actor_wallet: owner, + fee_token_mint: None, + fee_amount_raw: None, + payload_json, + })); +} + +fn decode_anchor_claim_reward_event( + transaction_id: i64, + instruction_id: i64, + signature: &str, + program_id: &str, + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if data.len() < 128 { + return None; + } + let lb_pair = read_pubkey_string(data, 16); + let position = read_pubkey_string(data, 48); + 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 payload_json = serde_json::json!({ + "decoder": "meteora_dlmm", + "eventKind": "claim_reward_event", + "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": "claim_reward_event", + "anchorEventDiscriminatorHex": "947486cc16ab555f", + "anchorEventPayloadSize": data.len().saturating_sub(8), + "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "lbPair": lb_pair, + "poolAccount": lb_pair, + "position": position, + "owner": owner, + "actorWallet": owner, + "rewardIndex": reward_index, + "totalRewardRaw": total_reward, + "rewardAmountRaw": total_reward + }); + 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: lb_pair, + actor_wallet: owner, + reward_token_mint: None, + reward_amount_raw: total_reward, + payload_json, + })); +} + +fn decode_anchor_fund_reward_event( + transaction_id: i64, + instruction_id: i64, + signature: &str, + program_id: &str, + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if data.len() < 96 { + return None; + } + let lb_pair = read_pubkey_string(data, 16); + let funder = read_pubkey_string(data, 48); + let reward_index = read_u64_string(data, 80); + let amount = read_u64_string(data, 88); + let event_kind = "meteora_dlmm.fund_reward_event".to_string(); + let payload_json = serde_json::json!({ + "decoder": "meteora_dlmm", + "eventKind": "fund_reward_event", + "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": "fund_reward_event", + "anchorEventDiscriminatorHex": "f6e43a8291aa4fcc", + "anchorEventPayloadSize": data.len().saturating_sub(8), + "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "lbPair": lb_pair, + "poolAccount": lb_pair, + "funder": funder, + "actorWallet": funder, + "rewardIndex": reward_index, + "amount": amount, + "rewardAmountRaw": amount + }); + 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: lb_pair, + actor_wallet: funder, + reward_token_mint: None, + reward_amount_raw: amount, + payload_json, + })); +} + +fn decode_anchor_position_create_event( + transaction_id: i64, + instruction_id: i64, + signature: &str, + program_id: &str, + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if data.len() < 112 { + return None; + } + let lb_pair = read_pubkey_string(data, 16); + let position = read_pubkey_string(data, 48); + let owner = read_pubkey_string(data, 80); + let event_kind = "meteora_dlmm.position_create_event".to_string(); + let payload_json = serde_json::json!({ + "decoder": "meteora_dlmm", + "eventKind": "position_create_event", + "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": "position_create_event", + "anchorEventDiscriminatorHex": "908efc549d352579", + "anchorEventPayloadSize": data.len().saturating_sub(8), + "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "lbPair": lb_pair, + "poolAccount": lb_pair, + "position": position, + "owner": owner, + "actorWallet": owner + }); + 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: lb_pair, + token_a_mint: None, + token_b_mint: None, + actor_wallet: owner, + base_amount_raw: None, + quote_amount_raw: None, + liquidity_amount_raw: None, + payload_json, + })); +} + +fn decode_anchor_position_close_event( + transaction_id: i64, + instruction_id: i64, + signature: &str, + program_id: &str, + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if data.len() < 80 { + return None; + } + let position = read_pubkey_string(data, 16); + let owner = read_pubkey_string(data, 48); + let event_kind = "meteora_dlmm.position_close_event".to_string(); + let payload_json = serde_json::json!({ + "decoder": "meteora_dlmm", + "eventKind": "position_close_event", + "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": "position_close_event", + "anchorEventDiscriminatorHex": "ffc4106b1cca3580", + "anchorEventPayloadSize": data.len().saturating_sub(8), + "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "position": position, + "owner": owner, + "actorWallet": owner + }); + 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: None, + token_a_mint: None, + token_b_mint: None, + actor_wallet: owner, + base_amount_raw: None, + quote_amount_raw: None, + liquidity_amount_raw: None, + payload_json, + })); +} + +fn find_best_anchor_swap_event_for_instruction( + instruction: &crate::ChainInstructionDto, + instructions: &[crate::ChainInstructionDto], +) -> std::option::Option { + let instruction_id = match instruction.id { + Some(instruction_id) => instruction_id, + None => return None, + }; + let parent_instruction_id = instruction.parent_instruction_id; + let next_dlmm_business_instruction_id = + find_next_dlmm_business_instruction_id(instruction_id, parent_instruction_id, instructions); + let mut legacy_swap_event = None; + let mut swap2_event = None; + for candidate in instructions { + let candidate_id = match candidate.id { + Some(candidate_id) => candidate_id, + None => continue, + }; + if candidate_id <= instruction_id { + continue; + } + if let Some(next_instruction_id) = next_dlmm_business_instruction_id { + if candidate_id >= next_instruction_id { + continue; + } + } + if candidate.parent_instruction_id != parent_instruction_id { + continue; + } + let candidate_program_id = match candidate.program_id.as_deref() { + Some(candidate_program_id) => candidate_program_id, + None => continue, + }; + if candidate_program_id != crate::METEORA_DLMM_PROGRAM_ID { + continue; + } + let candidate_data_result = decode_instruction_data_json(candidate.data_json.as_ref()); + let candidate_data = match candidate_data_result { + Ok(candidate_data) => candidate_data, + Err(_) => continue, + }; + let candidate_data = match candidate_data { + Some(candidate_data) => candidate_data, + None => continue, + }; + let decoded_event = decode_anchor_swap_event(candidate, candidate_data.as_slice()); + let decoded_event = match decoded_event { + Some(decoded_event) => decoded_event, + None => continue, + }; + let event_name = + decoded_event.get("anchorEventName").and_then(|value| return value.as_str()); + if event_name == Some("swap2_evt") { + swap2_event = Some(decoded_event); + continue; + } + if event_name == Some("swap") { + legacy_swap_event = Some(decoded_event); + continue; + } + } + if swap2_event.is_some() { + return swap2_event; + } + return legacy_swap_event; +} + +fn find_next_dlmm_business_instruction_id( + instruction_id: i64, + parent_instruction_id: std::option::Option, + instructions: &[crate::ChainInstructionDto], +) -> std::option::Option { + let mut next_instruction_id = None; + for candidate in instructions { + let candidate_id = match candidate.id { + Some(candidate_id) => candidate_id, + None => continue, + }; + if candidate_id <= instruction_id { + continue; + } + if candidate.parent_instruction_id != parent_instruction_id { + continue; + } + let candidate_program_id = match candidate.program_id.as_deref() { + Some(candidate_program_id) => candidate_program_id, + None => continue, + }; + if candidate_program_id != crate::METEORA_DLMM_PROGRAM_ID { + continue; + } + let candidate_data_result = decode_instruction_data_json(candidate.data_json.as_ref()); + let candidate_data = match candidate_data_result { + Ok(candidate_data) => candidate_data, + Err(_) => continue, + }; + let candidate_data = match candidate_data { + Some(candidate_data) => candidate_data, + None => continue, + }; + if is_dlmm_anchor_self_cpi_log(candidate_data.as_slice()) { + continue; + } + let candidate_instruction_name = + classify_instruction_name_from_data(Some(candidate_data.as_slice())); + if candidate_instruction_name.kind() == MeteoraDlmmInstructionKind::Unknown { + continue; + } + match next_instruction_id { + Some(existing_id) => { + if candidate_id < existing_id { + next_instruction_id = Some(candidate_id); + } + }, + None => next_instruction_id = Some(candidate_id), + } + } + return next_instruction_id; +} + +fn decode_anchor_swap_event( + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if !is_dlmm_anchor_self_cpi_log(data) { + return None; + } + if data.len() < 16 { + return None; + } + let event_discriminator = read_8_bytes(data, 8); + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_SWAP2_EVT { + return decode_anchor_swap2_evt(instruction, data); + } + if event_discriminator == DLMM_EVENT_DISCRIMINATOR_SWAP { + return decode_anchor_swap(instruction, data); + } + return None; +} + +fn decode_anchor_swap( + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if data.len() < 145 { + return None; + } + let lb_pair = read_pubkey_string(data, 16); + let from = read_pubkey_string(data, 48); + let start_bin_id = read_i32_string(data, 80); + let end_bin_id = read_i32_string(data, 84); + let amount_in = read_u64_string(data, 88); + let amount_out = read_u64_string(data, 96); + let swap_for_y = read_bool(data, 104); + let fee = read_u64_string(data, 105); + let protocol_fee = read_u64_string(data, 113); + let fee_bps = read_u128_string(data, 121); + let host_fee = read_u64_string(data, 137); + return Some(serde_json::json!({ + "anchorEventName": "swap", + "anchorEventDiscriminatorHex": "516ce3becdd00ac4", + "anchorEventPayloadSize": data.len().saturating_sub(8), + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "parentInstructionId": instruction.parent_instruction_id, + "lbPair": lb_pair, + "from": from, + "startBinId": start_bin_id, + "endBinId": end_bin_id, + "amountInRaw": amount_in, + "amountOutRaw": amount_out, + "swapForY": swap_for_y, + "feeRaw": fee, + "protocolFeeRaw": protocol_fee, + "feeBpsRaw": fee_bps, + "hostFeeRaw": host_fee + })); +} + +fn decode_anchor_swap2_evt( + instruction: &crate::ChainInstructionDto, + data: &[u8], +) -> std::option::Option { + if data.len() < 163 { + return None; + } + let lb_pair = read_pubkey_string(data, 16); + let from = read_pubkey_string(data, 48); + let start_bin_id = read_i32_string(data, 80); + let end_bin_id = read_i32_string(data, 84); + let swap_for_y = read_bool(data, 88); + let fee_bps = read_u128_string(data, 89); + let amount_in = read_u64_string(data, 105); + let amount_left = read_u64_string(data, 113); + let amount_out = read_u64_string(data, 121); + let mm_fee = read_u64_string(data, 129); + let protocol_fee = read_u64_string(data, 137); + let limit_order_fee = read_u64_string(data, 145); + let host_fee = read_u64_string(data, 153); + let fees_on_input = read_bool(data, 161); + let fees_on_token_x = read_bool(data, 162); + return Some(serde_json::json!({ + "anchorEventName": "swap2_evt", + "anchorEventDiscriminatorHex": "2e7452d7941b544d", + "anchorEventPayloadSize": data.len().saturating_sub(8), + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "parentInstructionId": instruction.parent_instruction_id, + "lbPair": lb_pair, + "from": from, + "startBinId": start_bin_id, + "endBinId": end_bin_id, + "swapForY": swap_for_y, + "feeBpsRaw": fee_bps, + "amountInRaw": amount_in, + "amountLeftRaw": amount_left, + "amountOutRaw": amount_out, + "feeRaw": mm_fee, + "mmFeeRaw": mm_fee, + "protocolFeeRaw": protocol_fee, + "limitOrderFeeRaw": limit_order_fee, + "hostFeeRaw": host_fee, + "feesOnInput": fees_on_input, + "feesOnTokenX": fees_on_token_x + })); +} + +fn is_dlmm_anchor_self_cpi_log(data: &[u8]) -> bool { + if data.len() < 8 { + return false; + } + let discriminator = read_8_bytes(data, 0); + return discriminator == DLMM_ANCHOR_SELF_CPI_LOG_SELECTOR; +} + +fn read_8_bytes(data: &[u8], offset: usize) -> [u8; 8] { + let mut bytes = [0_u8; 8]; + if data.len() < offset + 8 { + return bytes; + } + let mut index = 0_usize; + while index < 8 { + bytes[index] = data[offset + index]; + index += 1; + } + return bytes; +} + +fn read_pubkey_string(data: &[u8], offset: usize) -> std::option::Option { + if data.len() < offset + 32 { + return None; + } + return Some(bs58::encode(&data[offset..offset + 32]).into_string()); +} + +fn read_i32_string(data: &[u8], offset: usize) -> std::option::Option { + if data.len() < offset + 4 { + return None; + } + let mut bytes = [0_u8; 4]; + let mut index = 0_usize; + while index < 4 { + bytes[index] = data[offset + index]; + index += 1; + } + return Some(i32::from_le_bytes(bytes).to_string()); +} + +fn read_u16_string(data: &[u8], offset: usize) -> std::option::Option { + if data.len() < offset + 2 { + return None; + } + let mut bytes = [0_u8; 2]; + let mut index = 0_usize; + while index < 2 { + bytes[index] = data[offset + index]; + index += 1; + } + return Some(u16::from_le_bytes(bytes).to_string()); +} + +fn read_u64_string(data: &[u8], offset: usize) -> std::option::Option { + if data.len() < offset + 8 { + return None; + } + let mut bytes = [0_u8; 8]; + let mut index = 0_usize; + while index < 8 { + bytes[index] = data[offset + index]; + index += 1; + } + return Some(u64::from_le_bytes(bytes).to_string()); +} + +fn read_u128_string(data: &[u8], offset: usize) -> std::option::Option { + if data.len() < offset + 16 { + return None; + } + let mut bytes = [0_u8; 16]; + let mut index = 0_usize; + while index < 16 { + bytes[index] = data[offset + index]; + index += 1; + } + return Some(u128::from_le_bytes(bytes).to_string()); +} + +fn read_bool(data: &[u8], offset: usize) -> std::option::Option { + if data.len() <= offset { + return None; + } + return Some(data[offset] != 0); +} + +fn extract_json_string_field( + value: std::option::Option<&serde_json::Value>, + key: &str, +) -> std::option::Option { + let value = match value { + Some(value) => value, + None => return None, + }; + let field = match value.get(key) { + Some(field) => field, + None => return None, + }; + if let Some(text) = field.as_str() { + return Some(text.to_string()); + } + if let Some(number) = field.as_u64() { + return Some(number.to_string()); + } + if let Some(number) = field.as_i64() { + return Some(number.to_string()); + } + return None; +} + fn classify_instruction_name( parsed_json: std::option::Option<&serde_json::Value>, parsed_type: std::option::Option<&str>, @@ -701,12 +1821,24 @@ fn classify_instruction_name_from_data( if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY { return MeteoraDlmmInstructionName::AddLiquidity; } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY2 { + return MeteoraDlmmInstructionName::AddLiquidityByStrategy2; + } + if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY_BY_WEIGHT { + return MeteoraDlmmInstructionName::AddLiquidityByWeight; + } if discriminator == DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY { return MeteoraDlmmInstructionName::RemoveLiquidity; } + if discriminator == DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY_BY_RANGE2 { + return MeteoraDlmmInstructionName::RemoveLiquidityByRange2; + } if discriminator == DLMM_DISCRIMINATOR_CLAIM_FEE2 { return MeteoraDlmmInstructionName::ClaimFee2; } + if discriminator == DLMM_DISCRIMINATOR_CLOSE_POSITION_IF_EMPTY { + return MeteoraDlmmInstructionName::ClosePositionIfEmpty; + } if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_POSITION { return MeteoraDlmmInstructionName::InitializePosition; } @@ -746,13 +1878,23 @@ fn resolve_dlmm_pool_account( | MeteoraDlmmInstructionName::InitializeBinArray => { return extract_account(accounts, 0); }, - MeteoraDlmmInstructionName::AddLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity => { + MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::AddLiquidityByWeight + | MeteoraDlmmInstructionName::RemoveLiquidity + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { return extract_account(accounts, 1); }, MeteoraDlmmInstructionName::InitializePosition => { return extract_account(accounts, 2); }, - MeteoraDlmmInstructionName::ClaimFee2 | MeteoraDlmmInstructionName::Unknown => { + MeteoraDlmmInstructionName::ClaimFee2 => { + return extract_account(accounts, 0); + }, + MeteoraDlmmInstructionName::ClosePositionIfEmpty => { + return None; + }, + MeteoraDlmmInstructionName::Unknown => { return None; }, } @@ -794,10 +1936,15 @@ fn resolve_dlmm_token_x_mint( | MeteoraDlmmInstructionName::SwapWithPriceImpact => { return extract_account(accounts, 6); }, - MeteoraDlmmInstructionName::AddLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity => { + MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::AddLiquidityByWeight + | MeteoraDlmmInstructionName::RemoveLiquidity + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { return extract_account(accounts, 7); }, MeteoraDlmmInstructionName::ClaimFee2 + | MeteoraDlmmInstructionName::ClosePositionIfEmpty | MeteoraDlmmInstructionName::InitializeBinArray | MeteoraDlmmInstructionName::InitializePosition | MeteoraDlmmInstructionName::Unknown => return None, @@ -840,10 +1987,15 @@ fn resolve_dlmm_token_y_mint( | MeteoraDlmmInstructionName::SwapWithPriceImpact => { return extract_account(accounts, 7); }, - MeteoraDlmmInstructionName::AddLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity => { + MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::AddLiquidityByWeight + | MeteoraDlmmInstructionName::RemoveLiquidity + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { return extract_account(accounts, 8); }, MeteoraDlmmInstructionName::ClaimFee2 + | MeteoraDlmmInstructionName::ClosePositionIfEmpty | MeteoraDlmmInstructionName::InitializeBinArray | MeteoraDlmmInstructionName::InitializePosition | MeteoraDlmmInstructionName::Unknown => return None, @@ -995,9 +2147,15 @@ fn resolve_dlmm_actor_wallet( return parsed_value; } match instruction_name { - MeteoraDlmmInstructionName::AddLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity => { + MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::RemoveLiquidity + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { return extract_account(accounts, 9); }, + MeteoraDlmmInstructionName::AddLiquidityByWeight => { + return extract_account(accounts, 11); + }, MeteoraDlmmInstructionName::InitializePosition => { return extract_account(accounts, 3); }, @@ -1005,6 +2163,97 @@ fn resolve_dlmm_actor_wallet( } } +fn read_i32_string_from_instruction_data( + instruction_name: MeteoraDlmmInstructionName, + instruction_data: std::option::Option<&[u8]>, + offset: usize, +) -> std::option::Option { + if instruction_name != MeteoraDlmmInstructionName::RemoveLiquidityByRange2 { + return None; + } + let instruction_data = match instruction_data { + Some(instruction_data) => instruction_data, + None => return None, + }; + return read_i32_string(instruction_data, offset); +} + +fn read_u16_string_from_instruction_data( + instruction_name: MeteoraDlmmInstructionName, + instruction_data: std::option::Option<&[u8]>, + offset: usize, +) -> std::option::Option { + if instruction_name != MeteoraDlmmInstructionName::RemoveLiquidityByRange2 { + return None; + } + let instruction_data = match instruction_data { + Some(instruction_data) => instruction_data, + None => return None, + }; + return read_u16_string(instruction_data, offset); +} + +fn resolve_dlmm_liquidity_position_account( + instruction_name: MeteoraDlmmInstructionName, + accounts: &[std::string::String], +) -> std::option::Option { + match instruction_name { + MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::AddLiquidityByWeight + | MeteoraDlmmInstructionName::RemoveLiquidity + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { + return extract_account(accounts, 0); + }, + _ => return None, + } +} + +fn resolve_dlmm_liquidity_event_authority_account( + instruction_name: MeteoraDlmmInstructionName, + accounts: &[std::string::String], +) -> std::option::Option { + match instruction_name { + MeteoraDlmmInstructionName::AddLiquidityByWeight => return extract_account(accounts, 14), + MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::RemoveLiquidity + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { + return extract_account(accounts, 12); + }, + _ => return None, + } +} + +fn resolve_dlmm_liquidity_program_account( + instruction_name: MeteoraDlmmInstructionName, + accounts: &[std::string::String], +) -> std::option::Option { + match instruction_name { + MeteoraDlmmInstructionName::AddLiquidityByWeight => return extract_account(accounts, 15), + MeteoraDlmmInstructionName::AddLiquidity + | MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::RemoveLiquidity + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { + return extract_account(accounts, 13); + }, + _ => return None, + } +} + +fn resolve_dlmm_instruction_proof_status( + instruction_name: MeteoraDlmmInstructionName, +) -> &'static str { + match instruction_name { + MeteoraDlmmInstructionName::AddLiquidityByStrategy2 + | MeteoraDlmmInstructionName::AddLiquidityByWeight + | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { + return "observed_local_corpus_and_known_carbon_layout"; + }, + _ => return "decoded_from_instruction_discriminator_or_local_hint", + } +} + fn extract_amount_string_by_candidate_keys( value: std::option::Option<&serde_json::Value>, candidate_keys: &[&str], @@ -1538,7 +2787,9 @@ mod tests { panic!("unexpected swap event"); }, crate::MeteoraDlmmDecodedEvent::Liquidity(_) - | crate::MeteoraDlmmDecodedEvent::PoolLifecycle(_) => { + | crate::MeteoraDlmmDecodedEvent::PoolLifecycle(_) + | crate::MeteoraDlmmDecodedEvent::Fee(_) + | crate::MeteoraDlmmDecodedEvent::Reward(_) => { panic!("unexpected non-trade event"); }, } @@ -1568,7 +2819,9 @@ mod tests { panic!("unexpected create event"); }, crate::MeteoraDlmmDecodedEvent::Liquidity(_) - | crate::MeteoraDlmmDecodedEvent::PoolLifecycle(_) => { + | crate::MeteoraDlmmDecodedEvent::PoolLifecycle(_) + | crate::MeteoraDlmmDecodedEvent::Fee(_) + | crate::MeteoraDlmmDecodedEvent::Reward(_) => { panic!("unexpected non-trade event"); }, } @@ -1729,7 +2982,9 @@ mod tests { panic!("unexpected create event"); }, crate::MeteoraDlmmDecodedEvent::Liquidity(_) - | crate::MeteoraDlmmDecodedEvent::PoolLifecycle(_) => { + | crate::MeteoraDlmmDecodedEvent::PoolLifecycle(_) + | crate::MeteoraDlmmDecodedEvent::Fee(_) + | crate::MeteoraDlmmDecodedEvent::Reward(_) => { panic!("unexpected non-trade event"); }, } @@ -1813,6 +3068,104 @@ mod tests { } } + #[test] + fn meteora_dlmm_claim_fee2_discriminator_is_decoded_as_fee_event() { + let instruction_data = [0x70, 0xbf, 0x65, 0xab, 0x1c, 0x90, 0x7f, 0xbb, 0x01]; + let name = super::classify_instruction_name_from_data(Some(&instruction_data)); + assert_eq!(name, super::MeteoraDlmmInstructionName::ClaimFee2); + assert_eq!(name.kind(), super::MeteoraDlmmInstructionKind::Fee); + + let decoder = crate::MeteoraDlmmDecoder::new(); + let transaction = make_swap_transaction(); + let mut instruction = crate::ChainInstructionDto::new( + 403, + None, + 0, + None, + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora-dlmm".to_string()), + Some(1), + serde_json::json!([ + "DlmmPairFee111", + "Position111", + "BinArrayLower111", + "BinArrayUpper111", + "Owner111", + "ReserveX111", + "ReserveY111", + "UserTokenX111", + "UserTokenY111" + ]) + .to_string(), + Some("\"2SEn7GSqXFVhr\"".to_string()), + None, + None, + ); + instruction.id = Some(407); + 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::Fee(event) => { + assert_eq!(event.event_kind, "meteora_dlmm.claim_fee2"); + assert_eq!(event.pool_account, Some("DlmmPairFee111".to_string())); + }, + _ => panic!("expected fee event"), + } + } + + #[test] + fn meteora_dlmm_close_position_if_empty_is_pool_lifecycle() { + let instruction_data = [0x3b, 0x7c, 0xd4, 0x76, 0x5b, 0x98, 0x6e, 0x9d, 0x01]; + let name = super::classify_instruction_name_from_data(Some(&instruction_data)); + assert_eq!(name, super::MeteoraDlmmInstructionName::ClosePositionIfEmpty); + assert_eq!(name.kind(), super::MeteoraDlmmInstructionKind::PositionClose); + + let decoder = crate::MeteoraDlmmDecoder::new(); + let transaction = make_swap_transaction(); + let mut instruction = crate::ChainInstructionDto::new( + 403, + None, + 0, + None, + Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), + Some("meteora-dlmm".to_string()), + Some(1), + serde_json::json!([ + "Position111", + "Sender111", + "RentReceiver111", + "EventAuthority111", + crate::METEORA_DLMM_PROGRAM_ID + ]) + .to_string(), + Some("\"kvDj9kL1jgdn\"".to_string()), + None, + None, + ); + instruction.id = Some(408); + 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.close_position_if_empty"); + assert_eq!(event.pool_account, None); + assert_eq!( + event.payload_json.get("position").and_then(serde_json::Value::as_str), + Some("Position111") + ); + }, + _ => panic!("expected pool lifecycle event"), + } + } + #[test] fn meteora_dlmm_unknown_data_discriminator_does_not_fallback_to_global_swap_logs() { let instruction_data = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x01, 0x02, 0x03]; diff --git a/kb_lib/src/dex_decode.rs b/kb_lib/src/dex_decode.rs index 04be686..b1fffc1 100644 --- a/kb_lib/src/dex_decode.rs +++ b/kb_lib/src/dex_decode.rs @@ -2,6 +2,8 @@ //! Persistence-oriented DEX decoding service. +const METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX: &str = "e445a52e51cb9a1d"; + /// DEX decode service. #[derive(Debug, Clone)] pub struct DexDecodeService { @@ -206,7 +208,7 @@ impl DexDecodeService { Err(error) => return Err(error), }; let cleanup_result = self - .delete_replaced_raydium_instruction_audit( + .delete_replaced_instruction_audit( transaction_id, instruction_id, protocol_name, @@ -219,7 +221,7 @@ impl DexDecodeService { return Ok(materialized); } - async fn delete_replaced_raydium_instruction_audit( + async fn delete_replaced_instruction_audit( &self, transaction_id: i64, instruction_id: i64, @@ -229,15 +231,14 @@ impl DexDecodeService { if event_kind.ends_with(".instruction_audit") { return Ok(()); } - let audit_event_kind = match raydium_instruction_audit_event_kind_by_protocol(protocol_name) - { + let audit_event_kind = match instruction_audit_event_kind_by_protocol(protocol_name) { Some(audit_event_kind) => audit_event_kind, None => return Ok(()), }; - let delete_result = crate::query_dex_decoded_events_delete_by_key( + let delete_result = crate::query_dex_decoded_events_delete_related_instruction_audit( self.database.as_ref(), transaction_id, - Some(instruction_id), + instruction_id, audit_event_kind, ) .await; @@ -505,6 +506,42 @@ impl DexDecodeService { ) .await; }, + crate::MeteoraDlmmDecodedEvent::Fee(event) => { + return self + .materialize_named_dex_event( + transaction, + event.transaction_id, + event.instruction_id, + "meteora_dlmm", + event.program_id.clone(), + event.event_kind.as_str(), + event.pool_account.clone(), + None, + None, + None, + None, + event.payload_json.clone(), + ) + .await; + }, + crate::MeteoraDlmmDecodedEvent::Reward(event) => { + return self + .materialize_named_dex_event( + transaction, + event.transaction_id, + event.instruction_id, + "meteora_dlmm", + event.program_id.clone(), + event.event_kind.as_str(), + event.pool_account.clone(), + None, + None, + None, + None, + event.payload_json.clone(), + ) + .await; + }, } } @@ -1167,6 +1204,13 @@ impl DexDecodeService { if decoded_instruction_ids.contains(&instruction_id) { continue; } + if is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap( + audit_spec.protocol_name, + instruction, + decoded_events.as_slice(), + ) { + continue; + } let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str()); let payload = build_meteora_instruction_audit_payload( transaction, @@ -1773,6 +1817,35 @@ fn candidate_meteora_audit_pool_account( return accounts.get(index).cloned(); } +fn is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap( + protocol_name: &str, + instruction: &crate::ChainInstructionDto, + decoded_events: &[crate::DexDecodedEventDto], +) -> bool { + if protocol_name != "meteora_dlmm" { + return false; + } + let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref()); + let data_bytes = instruction_data_bytes_from_base58(data_base58.as_deref()); + let selector_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 0); + if selector_hex.as_deref() != Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX) { + return false; + } + let event_discriminator_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 8); + match event_discriminator_hex.as_deref() { + Some("516ce3becdd00ac4") | Some("2e7452d7941b544d") => {}, + _ => return false, + } + for decoded_event in decoded_events { + if decoded_event.protocol_name == "meteora_dlmm" + && decoded_event.event_kind == "meteora_dlmm.swap" + { + return true; + } + } + return false; +} + fn build_meteora_instruction_audit_payload( transaction: &crate::ChainTransactionDto, instruction: &crate::ChainInstructionDto, @@ -1786,10 +1859,36 @@ fn build_meteora_instruction_audit_payload( None => 0, }; let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref()); - let discriminator_hex = discriminator_hex_from_base58(data_base58.as_deref()); + let data_bytes = instruction_data_bytes_from_base58(data_base58.as_deref()); + let discriminator_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 0); + let anchor_self_cpi_log = + discriminator_hex.as_deref() == Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX); + let anchor_event_discriminator_hex = if anchor_self_cpi_log { + discriminator_hex_from_bytes(data_bytes.as_deref(), 8) + } else { + None + }; + let anchor_event_payload_size = if anchor_self_cpi_log { + match data_bytes.as_ref() { + Some(data_bytes) => data_bytes.len().checked_sub(8), + None => None, + } + } else { + None + }; let data_prefix = data_base58 .as_ref() .map(|value| return value.chars().take(16).collect::()); + let audit_reason = if anchor_self_cpi_log { + "meteora_anchor_self_cpi_log_not_decoded_by_specific_event_decoder" + } else { + "meteora_instruction_not_decoded_by_specific_decoder" + }; + let proof_status = if anchor_self_cpi_log { + "observed_local_corpus_anchor_self_cpi_log" + } else { + "unclassified_local_corpus_instruction" + }; return serde_json::json!({ "decoder": protocol_name, "eventKind": event_kind, @@ -1806,8 +1905,12 @@ fn build_meteora_instruction_audit_payload( "data": data_base58, "dataPrefix": data_prefix, "discriminatorHex": discriminator_hex, - "auditReason": "meteora_instruction_not_decoded_by_specific_decoder", - "proofStatus": "unclassified_local_corpus_instruction", + "anchorSelfCpiLog": anchor_self_cpi_log, + "anchorSelfCpiLogSelectorHex": if anchor_self_cpi_log { Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX) } else { None }, + "anchorEventDiscriminatorHex": anchor_event_discriminator_hex, + "anchorEventPayloadSize": anchor_event_payload_size, + "auditReason": audit_reason, + "proofStatus": proof_status, "tradeCandidate": false, "candleCandidate": false, "nonTradeUseful": false, @@ -1816,13 +1919,14 @@ fn build_meteora_instruction_audit_payload( }); } -fn raydium_instruction_audit_event_kind_by_protocol( +fn instruction_audit_event_kind_by_protocol( protocol_name: &str, ) -> std::option::Option<&'static str> { match protocol_name { "raydium_amm_v4" => return Some("raydium_amm_v4.instruction_audit"), "raydium_clmm" => return Some("raydium_clmm.instruction_audit"), "raydium_cpmm" => return Some("raydium_cpmm.instruction_audit"), + "meteora_dlmm" => return Some("meteora_dlmm.instruction_audit"), _ => return None, } } @@ -1932,21 +2036,28 @@ fn parse_instruction_data_base58( fn discriminator_hex_from_base58( data_base58: std::option::Option<&str>, ) -> std::option::Option { - let data_base58 = match data_base58 { - Some(data_base58) => data_base58, + let bytes = instruction_data_bytes_from_base58(data_base58); + return discriminator_hex_from_bytes(bytes.as_deref(), 0); +} + +fn discriminator_hex_from_bytes( + bytes: std::option::Option<&[u8]>, + offset: usize, +) -> std::option::Option { + let bytes = match bytes { + Some(bytes) => bytes, None => return None, }; - let bytes_result = bs58::decode(data_base58).into_vec(); - let bytes = match bytes_result { - Ok(bytes) => bytes, - Err(_) => return None, - }; - if bytes.len() < 8 { + if bytes.len() < offset + 8 { return None; } let mut text = std::string::String::new(); - for byte in bytes.iter().take(8) { + let mut index = offset; + let end = offset + 8; + while index < end { + let byte = bytes[index]; text.push_str(format!("{byte:02x}").as_str()); + index += 1; } return Some(text); } @@ -3012,4 +3123,17 @@ mod tests { }; assert_eq!(initialize.event_kind, "raydium_cpmm.initialize"); } + + #[test] + fn maps_instruction_audit_event_kind_for_raydium_and_meteora_dlmm_protocols() { + assert_eq!( + super::instruction_audit_event_kind_by_protocol("raydium_clmm"), + Some("raydium_clmm.instruction_audit") + ); + assert_eq!( + super::instruction_audit_event_kind_by_protocol("meteora_dlmm"), + Some("meteora_dlmm.instruction_audit") + ); + assert_eq!(super::instruction_audit_event_kind_by_protocol("unknown"), None); + } } diff --git a/kb_lib/src/dex_event_classification.rs b/kb_lib/src/dex_event_classification.rs index 4c4e828..a7109eb 100644 --- a/kb_lib/src/dex_event_classification.rs +++ b/kb_lib/src/dex_event_classification.rs @@ -335,6 +335,9 @@ pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool { if event_kind.contains(".close_position") { return true; } + if event_kind.contains(".position_close") { + return true; + } return false; } @@ -374,6 +377,9 @@ pub fn is_dex_position_open_event_kind(event_kind: &str) -> bool { if event_kind.contains(".open_position") { return true; } + if event_kind.contains(".position_create") { + return true; + } return false; } @@ -399,6 +405,9 @@ pub fn is_dex_fee_event_kind(event_kind: &str) -> bool { if event_kind.contains("collect_fee") { return true; } + if event_kind.contains("claim_fee") { + return true; + } return false; } @@ -1041,6 +1050,19 @@ mod tests { assert!(!super::decoded_payload_has_trade_amount_or_price_payload(&empty_payload_json)); } + #[test] + fn classifies_dlmm_claim_fee2_as_fee_collection() { + assert_eq!(super::classify_dex_event_category_code("meteora_dlmm.claim_fee2"), "fee"); + assert_eq!( + super::classify_dex_event_lifecycle_kind_code("meteora_dlmm.claim_fee2"), + "fee_collection" + ); + assert_eq!( + super::classify_dex_event_actionability_code("meteora_dlmm.claim_fee2", false, false,), + "non_trade_useful" + ); + } + #[test] fn classifies_dlmm_add_remove_liquidity_and_positions_as_non_trade_useful() { assert_eq!( diff --git a/kb_lib/src/lib.rs b/kb_lib/src/lib.rs index 79ee0b3..6767019 100644 --- a/kb_lib/src/lib.rs +++ b/kb_lib/src/lib.rs @@ -343,14 +343,14 @@ pub use db::DbRuntimeEventDto; pub use db::DbRuntimeEventEntity; /// Runtime event level used by the local database layer. pub use db::DbRuntimeEventLevel; -/// Application-facing decoded DEX event DTO. -pub use db::DexDecodedEventDto; /// Application-facing DEX decode replay ledger DTO. pub use db::DexDecodeReplayLedgerDto; -/// Persisted decoded DEX event row. -pub use db::DexDecodedEventEntity; /// Persisted DEX decode replay ledger row. pub use db::DexDecodeReplayLedgerEntity; +/// Application-facing decoded DEX event DTO. +pub use db::DexDecodedEventDto; +/// Persisted decoded DEX event row. +pub use db::DexDecodedEventEntity; /// Application-facing normalized DEX DTO. pub use db::DexDto; /// Persisted normalized DEX row. @@ -591,8 +591,18 @@ pub use db::query_db_metadatas_upsert; pub use db::query_db_runtime_events_insert; /// Lists recent runtime events ordered from newest to oldest. pub use db::query_db_runtime_events_list_recent; +/// Reads one DEX decode replay ledger row by signature and decoder identity. +pub use db::query_dex_decode_replay_ledger_get_by_signature; +/// Reads one DEX decode replay ledger row by transaction and decoder identity. +pub use db::query_dex_decode_replay_ledger_get_by_transaction; +/// Inserts or updates one DEX decode replay ledger row. +pub use db::query_dex_decode_replay_ledger_upsert; /// Deletes one decoded DEX event row by its natural key. pub use db::query_dex_decoded_events_delete_by_key; +/// Deletes Meteora DLMM Anchor self-CPI swap audit rows already covered by decoded swaps. +pub use db::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits; +/// Deletes decoded DEX instruction audit rows related to one decoded instruction. +pub use db::query_dex_decoded_events_delete_related_instruction_audit; /// Reads one decoded DEX event by its natural key. pub use db::query_dex_decoded_events_get_by_key; /// Returns the latest Pump.fun create payload associated with a token mint. @@ -601,12 +611,6 @@ pub use db::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint; pub use db::query_dex_decoded_events_list_by_transaction_id; /// Inserts or updates one decoded DEX event row. pub use db::query_dex_decoded_events_upsert; -/// Reads one DEX decode replay ledger row by signature and decoder identity. -pub use db::query_dex_decode_replay_ledger_get_by_signature; -/// Reads one DEX decode replay ledger row by transaction and decoder identity. -pub use db::query_dex_decode_replay_ledger_get_by_transaction; -/// Inserts or updates one DEX decode replay ledger row. -pub use db::query_dex_decode_replay_ledger_upsert; /// Reads one normalized DEX row by code. pub use db::query_dexs_get_by_code; /// Lists normalized DEX rows. @@ -921,10 +925,14 @@ pub use dex::MeteoraDlmmCreatePoolDecoded; pub use dex::MeteoraDlmmDecodedEvent; /// Meteora DLMM decoder. pub use dex::MeteoraDlmmDecoder; +/// Decoded Meteora DLMM fee collection event. +pub use dex::MeteoraDlmmFeeDecoded; /// Decoded Meteora DLMM liquidity lifecycle event. pub use dex::MeteoraDlmmLiquidityDecoded; /// Decoded Meteora DLMM pool lifecycle event. pub use dex::MeteoraDlmmPoolLifecycleDecoded; +/// Decoded Meteora DLMM reward or emission event. +pub use dex::MeteoraDlmmRewardDecoded; /// Decoded Meteora DLMM swap event. pub use dex::MeteoraDlmmSwapDecoded; /// Decoded Orca Whirlpools create-pool event. diff --git a/kb_lib/src/local_pipeline_replay.rs b/kb_lib/src/local_pipeline_replay.rs index d4bcf4d..a82d300 100644 --- a/kb_lib/src/local_pipeline_replay.rs +++ b/kb_lib/src/local_pipeline_replay.rs @@ -7,7 +7,8 @@ //! deterministic local pipeline over their signatures. const LOCAL_PIPELINE_DEX_DECODER_SCOPE: &str = "dex_decode.local_pipeline"; -const LOCAL_PIPELINE_DEX_DECODER_VERSION: &str = "dex_decode.v0.7.44.ledger1"; +const LOCAL_PIPELINE_DEX_DECODER_VERSION: &str = + "dex_decode.v0.7.45.dlmm_add_liquidity_strategies1"; fn default_skip_certified_dex_decode() -> bool { return true; @@ -193,9 +194,11 @@ impl LocalPipelineReplayService { signature = %signature, "replaying local pipeline for persisted transaction" ); - let transaction_result = - crate::query_chain_transactions_get_by_signature(self.database.as_ref(), signature.as_str()) - .await; + let transaction_result = crate::query_chain_transactions_get_by_signature( + self.database.as_ref(), + signature.as_str(), + ) + .await; let transaction = match transaction_result { Ok(Some(transaction)) => transaction, Ok(None) => { @@ -260,9 +263,8 @@ impl LocalPipelineReplayService { ); }, None => { - let decode_result = dex_decode - .decode_transaction_by_signature(signature.as_str()) - .await; + let decode_result = + dex_decode.decode_transaction_by_signature(signature.as_str()).await; match decode_result { Ok(decoded_events) => { result.decoded_event_count += decoded_events.len(); @@ -542,11 +544,8 @@ impl LocalPipelineReplayService { signature: &str, decoded_events: &[crate::DexDecodedEventDto], ) -> Result { - let ledger_result = build_success_dex_decode_replay_ledger( - transaction_id, - signature, - decoded_events, - ); + let ledger_result = + build_success_dex_decode_replay_ledger(transaction_id, signature, decoded_events); let ledger = match ledger_result { Ok(ledger) => ledger, Err(error) => return Err(error), @@ -597,10 +596,23 @@ fn build_success_dex_decode_replay_ledger( Err(error) => { return Err(crate::Error::Db(format!( "cannot convert decoded event count '{}' to i64: {}", - decoded_events.len(), error + decoded_events.len(), + error ))); }, }; + let effective_event_count_usize = count_effective_decoded_events(decoded_events); + let effective_event_count_result = i64::try_from(effective_event_count_usize); + let effective_event_count = match effective_event_count_result { + Ok(effective_event_count) => effective_event_count, + Err(error) => { + return Err(crate::Error::Db(format!( + "cannot convert effective decoded event count '{}' to i64: {}", + effective_event_count_usize, error + ))); + }, + }; + let instruction_audit_count = event_count - effective_event_count; let distinct_token_mint_count_usize = count_distinct_decoded_event_token_mints(decoded_events); let distinct_token_mint_count_result = i64::try_from(distinct_token_mint_count_usize); let distinct_token_mint_count = match distinct_token_mint_count_result { @@ -612,7 +624,7 @@ fn build_success_dex_decode_replay_ledger( ))); }, }; - let force_replay_required = event_count > 1 || distinct_token_mint_count > 2; + let force_replay_required = effective_event_count > 1 || distinct_token_mint_count > 2; let decode_status = if event_count == 0 { crate::DexDecodeReplayLedgerDto::STATUS_NO_EVENTS.to_string() } else { @@ -625,6 +637,8 @@ fn build_success_dex_decode_replay_ledger( }; let status_reason = build_dex_decode_replay_ledger_status_reason( event_count, + effective_event_count, + instruction_audit_count, distinct_token_mint_count, force_replay_required, ); @@ -642,9 +656,22 @@ fn build_success_dex_decode_replay_ledger( )); } -fn count_distinct_decoded_event_token_mints( - decoded_events: &[crate::DexDecodedEventDto], -) -> usize { +fn count_effective_decoded_events(decoded_events: &[crate::DexDecodedEventDto]) -> usize { + let mut count = 0_usize; + for event in decoded_events { + if is_instruction_audit_event(event) { + continue; + } + count += 1; + } + return count; +} + +fn is_instruction_audit_event(event: &crate::DexDecodedEventDto) -> bool { + return event.event_kind.ends_with(".instruction_audit"); +} + +fn count_distinct_decoded_event_token_mints(decoded_events: &[crate::DexDecodedEventDto]) -> usize { let mut mints = std::collections::BTreeSet::::new(); for event in decoded_events { insert_optional_mint(&mut mints, &event.lp_mint); @@ -668,6 +695,8 @@ fn insert_optional_mint( fn build_dex_decode_replay_ledger_status_reason( event_count: i64, + effective_event_count: i64, + instruction_audit_count: i64, distinct_token_mint_count: i64, force_replay_required: bool, ) -> std::string::String { @@ -676,11 +705,11 @@ fn build_dex_decode_replay_ledger_status_reason( } if force_replay_required { return format!( - "decode completed but remains unsafe for skip: event_count={event_count}, distinct_token_mint_count={distinct_token_mint_count}" + "decode completed but remains unsafe for skip: event_count={event_count}, effective_event_count={effective_event_count}, instruction_audit_count={instruction_audit_count}, distinct_token_mint_count={distinct_token_mint_count}" ); } return format!( - "decode completed and certified for skip: event_count={event_count}, distinct_token_mint_count={distinct_token_mint_count}" + "decode completed and certified for skip: event_count={event_count}, effective_event_count={effective_event_count}, instruction_audit_count={instruction_audit_count}, distinct_token_mint_count={distinct_token_mint_count}" ); } @@ -692,3 +721,57 @@ pub async fn replay_local_pipeline( let service = crate::LocalPipelineReplayService::new(database); return service.replay_local_pipeline(config).await; } + +#[cfg(test)] +mod tests { + fn make_decoded_event( + event_kind: &str, + token_a_mint: std::option::Option<&str>, + token_b_mint: std::option::Option<&str>, + ) -> crate::DexDecodedEventDto { + return crate::DexDecodedEventDto::new( + 1, + Some(10), + "meteora_dlmm".to_string(), + crate::METEORA_DLMM_PROGRAM_ID.to_string(), + event_kind.to_string(), + Some("pool".to_string()), + None, + token_a_mint.map(|value| return value.to_string()), + token_b_mint.map(|value| return value.to_string()), + None, + "{}".to_string(), + ); + } + + #[test] + fn ledger_certifies_one_effective_event_with_instruction_audits() { + let events = vec![ + make_decoded_event("meteora_dlmm.swap", Some("mint-a"), Some("mint-b")), + make_decoded_event("meteora_dlmm.instruction_audit", None, None), + make_decoded_event("meteora_dlmm.instruction_audit", None, None), + ]; + let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice()) + .expect("ledger must build"); + assert_eq!(ledger.event_count, 3); + assert_eq!(ledger.distinct_token_mint_count, 2); + assert!(!ledger.force_replay_required); + assert_eq!(ledger.certainty, crate::DexDecodeReplayLedgerDto::CERTAINTY_SURE); + assert!(ledger.can_skip_decode()); + } + + #[test] + fn ledger_keeps_multiple_effective_events_unsafe() { + let events = vec![ + make_decoded_event("meteora_dlmm.swap", Some("mint-a"), Some("mint-b")), + make_decoded_event("meteora_dlmm.swap", Some("mint-a"), Some("mint-b")), + make_decoded_event("meteora_dlmm.instruction_audit", None, None), + ]; + let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice()) + .expect("ledger must build"); + assert_eq!(ledger.event_count, 3); + assert!(ledger.force_replay_required); + assert_eq!(ledger.certainty, crate::DexDecodeReplayLedgerDto::CERTAINTY_UNSAFE); + assert!(!ledger.can_skip_decode()); + } +}