From 7bd6593015617115456db164e18632124b7c3310 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 30 May 2026 01:14:30 +0200 Subject: [PATCH] 0.7.46 --- CHANGELOG.md | 6 +- Cargo.toml | 2 +- README.md | 54 +- ROADMAP.md | 212 +- kb_demo_app/frontend/demo3.html | 100 +- .../Demo3OnchainDexDiscoveryRequest.ts | 20 + .../Demo3OnchainDexDiscoveryResult.ts | 17 + .../Demo3OnchainDexPaginationCursor.ts | 22 + kb_demo_app/frontend/ts/demo3.ts | 95 +- kb_demo_app/package.json | 2 +- kb_demo_app/src/demo3.rs | 71 + kb_demo_app/tauri.conf.json | 2 +- kb_lib/src/dex.rs | 4 + kb_lib/src/dex/meteora_damm_v1.rs | 3038 +++++++++++++++-- kb_lib/src/dex/meteora_dlmm.rs | 20 +- kb_lib/src/dex_decode.rs | 89 +- kb_lib/src/dex_event_classification.rs | 43 + kb_lib/src/lib.rs | 10 + kb_lib/src/local_pipeline_replay.rs | 3 +- kb_lib/src/onchain_dex_pair_discovery.rs | 1005 +++++- 20 files changed, 4359 insertions(+), 456 deletions(-) create mode 100644 kb_demo_app/frontend/ts/bindings/Demo3OnchainDexPaginationCursor.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ba616b..0148499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,4 +75,8 @@ 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. +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 upstream Git/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 upstream Git/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. +0.7.46 - Meteora DAMM v1 events : extension conservatoire du decoder `meteora_damm_v1` à partir du mapping upstream Git decoder source `meteora-pools-decoder` et du corpus local fourni pour les discriminants observés `07a68aabceabecf4`, `3095dc823d0b09b2`, `856d2cb338ee7221`, `a9204f8988e84689`, `3657a51345e3dae0` et `1513d02bed3eff57` ; ajout des events create_pool, add/remove liquidity, claim_fee, create_lock_escrow et lock_liquidity ; ajout des exports publics associés ; raccordement de la persistance DEX et de la classification non-trade ; passage du replay local à `dex_decode.v0.7.46.damm_v1_events1`. Le programme Meteora Vault reste seulement référencé comme compte/programme associé quand il apparaît dans les comptes DAMM v1 ; aucun `program_id` vault n’est marqué vérifié sans corpus direct dédié. +0.7.46-demo3 - Correction ciblée de Demo3 pour la découverte/backfill : ajout d’un décodage léger des instructions `meteora_damm_v1` connues par discriminant upstream Git dans `onchain_dex_pair_discovery`, classification instruction-scoped prioritaire pour éviter qu’un `Swap` soit classé `add_liquidity` à cause de logs mixtes de transaction, filtrage `target_event` strict sur les surfaces explicites, conservation des transactions mixtes quand un target explicite est demandé malgré `excludeSwaps`, et ajout des cibles UI `create_lock_escrow` / `lock_liquidity`. +0.7.46-demo3-paged - Amélioration Demo3 discovery : pagination `getSignaturesForAddress` via `before_signature` / `until_signature`, scan de plusieurs adresses source dans une seule requête, déduplication des signatures multi-pool, compteur de pages, curseurs `next_before` par adresse et ordre de traitement `newest_first` / `oldest_first` pour constituer un corpus depuis les premières signatures d’un pool sans promouvoir de nouveau `program_id`. +0.7.46-final - Renommage documentaire et payload des anciens statuts/fonctions liés au dépôt source vers une terminologie générique `upstream_git_*` : `proofStatus` utilise désormais `upstream_git_local_corpus_observed`, `upstream_git_mapped_unverified` et `upstream_git_layout_unverified`; les payloads DAMM v1 utilisent `upstreamInstructionName`; la documentation prépare `0.7.47` comme Upstream Git Registry / DEX discovery preparation au lieu d’une tranche DAMM v2 immédiate. diff --git a/Cargo.toml b/Cargo.toml index 69bed9d..c0d4211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.7.45" +version = "0.7.46" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" diff --git a/README.md b/README.md index 3ec6ce9..703dcd8 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` 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. +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.46` côté workspace. 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 @@ -91,7 +91,7 @@ Les surfaces suivantes existent dans le code, dans la matrice ou dans le corpus | `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 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_v1` | DEX effectif en consolidation `0.7.46` | Swaps présents ; decoder étendu aux create_pool, liquidity, claim_fee et lock events DAMM v1 mappés upstream Git/corpus local. Validation DB à rejouer sur base dédiée. | | `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. | | `orca_whirlpools` | DEX effectif à vérifier | À revalider par corpus dédié avant promotion. | @@ -138,7 +138,17 @@ Validation locale finale sur la base DLMM dédiée : | 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`. +Les deux audits restants sont `e445a52e51cb9a1d + e8abf2613a4d232d`. Ils restent en `meteora_dlmm.instruction_audit`, car aucun mapping upstream Git/IDL suffisamment fiable n’a été confirmé. Ils ne bloquent pas la clôture de `0.7.45`. + +### 3.7. État de travail de `meteora_damm_v1` en `0.7.46` + +La tranche `0.7.46` étend `meteora_damm_v1` à partir du mapping upstream Git decoder source `meteora-pools-decoder` et des discriminants observés dans le corpus local. Les events ajoutés couvrent `create_pool`, `add_liquidity`, `remove_liquidity`, `claim_fee`, `create_lock_escrow` et `lock_liquidity`. + +La version logique du replay local devient `dex_decode.v0.7.46.damm_v1_events1`, ce qui force le redécodage des transactions certifiées sous la version `0.7.45` pour vérifier les nouveaux events DAMM v1. + +Meteora Vault est traité prudemment : le programme associé peut apparaître comme compte dans les instructions DAMM v1, mais aucun decoder `meteora_vault` ni statut `verified_by_corpus` n’est ajouté sans corpus direct séparé. + +Demo3 dispose ensuite d’une correction ciblée pour la découverte `meteora_damm_v1` : les discriminants DAMM v1 connus sont classés directement côté recherche on-chain, le filtrage `target_event` est strict sur les surfaces explicites, et les transactions mixtes ne sont plus éliminées globalement quand une cible précise est demandée. Cela sert à alimenter les backfills par signature ou par pool dans Demo Pipeline 2 sans déplacer de logique métier profonde dans `kb_demo_app`. ## 4. Matrice DEX : priorité révisée @@ -301,11 +311,11 @@ 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` — 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` ; -7. `0.7.49` : revalidation séparée de `orca_whirlpools` ; -8. `0.7.50+` : FluxBeam, DexLab, metaDAO, Printr, puis launch surfaces. +4. `0.7.46` : reprise séparée de `meteora_damm_v1` — clôturée côté corpus DAMM v1 local ; +5. `0.7.47` : Upstream Git Registry / DEX discovery preparation — registre générique des `program_id`, discriminants, instructions et events issus de dépôts Git externes, tous non vérifiés par défaut ; +6. `0.7.48` : reprise séparée de `meteora_damm_v2` ; +7. `0.7.49` : reprise séparée de `meteora_dbc` ; +8. `0.7.50+` : validation progressive des autres DEX/surfaces issus du registre upstream Git : Orca, FluxBeam, DexLab, Lifinity, Phoenix, OpenBook, Stabble, BonkSwap, Boop, Moonshot, Heaven, Wavebreak, Vertigo, Virtuals, Pancake Swap, OKX DEX, Raydium Launchpad/Stable/Locking, puis launch surfaces. Garde-fous constants : @@ -350,3 +360,31 @@ Pour reprendre rapidement le codage dans une nouvelle session, fournir au minimu - `kb_lib/src/db/queries.rs` et `kb_lib/src/db/queries/*`. Ajouter `kb_demo_app/src/demo_pipeline*.rs`, `kb_demo_app/src/demo3.rs`, les fichiers frontend associés et les nouvelles démos seulement si la tâche concerne l’UI, la recherche de corpus, les diagnostics affichés ou le watcher temps réel. + + +### Demo3 multi-target discovery + +Demo3 can search several event surfaces in one on-chain scan by checking multiple target event boxes. Internally this uses the existing `targetEvent` field with comma-separated normalized values, preserving compatibility with older single-target calls. + + +### Demo3 paged / multi-source discovery + +Demo3 can now scan one or several source addresses in a single on-chain discovery run. Source addresses may be pools, vaults, positions, config accounts or mints; the program id remains an instruction filter and no discovered address is promoted as verified automatically. + +The on-chain discovery form supports Solana `getSignaturesForAddress` pagination through `beforeSignature`, `untilSignature`, `maxPages` and `scanOrder`. `newest_first` preserves Solana RPC order. `oldest_first` reverses the fetched window after paging, which is useful when enough pages have been fetched to include the creation-side history of a pool. The JSON result includes `nextBeforeByAddress` cursor hints for subsequent manual windows. + + +### Note 0.7.46 DAMM v1 upstream Git coverage + +La couverture `meteora_damm_v1` inclut désormais les surfaces upstream Git decoder source `meteora-pools-decoder` connues. Les surfaces non rencontrées dans le corpus local restent marquées `upstream_git_mapped_unverified` et doivent être validées par Demo3 + backfill + replay avant d’être considérées comme corpus-confirmed. + +Sur le corpus local élargi, `swap`, `add_balance_liquidity`, `remove_balance_liquidity`, `claim_fee`, `create_lock_escrow`, `lock`, `InitializePermissionlessConstantProductPoolWithConfig` et `InitializePermissionlessConstantProductPoolWithConfig2` sont marqués `upstream_git_local_corpus_observed`. + + +### Note 0.7.47 Upstream Git Registry / DEX discovery preparation + +La version `0.7.47` n’est plus dédiée à un seul DEX. Elle doit introduire un registre upstream Git générique pour les `program_id`, discriminants d’instructions, discriminants d’events, noms d’instructions et familles de programmes issus de dépôts Git externes de decoders Solana. + +Les entrées de ce registre sont des indices de découverte, pas des preuves métier. Elles doivent être marquées `upstream_git_unverified` ou `upstream_git_mapped_unverified` tant qu’elles ne sont pas confirmées par Demo3, backfill, replay local et requêtes SQL. + +Le registre sert à accélérer la constitution de corpus pour les DEX et surfaces suivantes : Meteora DAMM v2/DBC/Vault, Raydium Launchpad/Stable/Locking, Orca Whirlpools, FluxBeam, DexLab, Lifinity AMM v2, Phoenix/OpenBook, Stabble, BonkSwap, Boop, Moonshot, Heaven, Wavebreak, Vertigo, Virtuals, Pancake Swap, OKX DEX, Jupiter/Kamino/Drift et autres programmes utiles à la découverte. diff --git a/ROADMAP.md b/ROADMAP.md index 9046b54..b6069e2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1067,13 +1067,13 @@ 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` ; +- mapping prouvé localement et par IDL/upstream Git 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 ; +- promotion de `remove_liquidity_by_range2`, `add_liquidity_by_strategy2` et `add_liquidity_by_weight` selon les layouts upstream Git 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` ; @@ -1111,31 +1111,95 @@ Events DLMM observés après replay : 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`. +- `e445a52e51cb9a1d + e8abf2613a4d232d` reste en `meteora_dlmm.instruction_audit` avec `proofStatus = observed_local_corpus_anchor_self_cpi_log`, faute de mapping upstream Git/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. +Décision : `0.7.45` est clos pour `meteora_dlmm`. La suite immédiate est `0.7.46 — Demo3 multi-target discovery enabled` 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. -À faire : +Tranche `0.7.46` engagée sur les audits `meteora_damm_v1` observés dans le corpus local et mappés contre upstream Git decoder source `meteora-pools-decoder`. -- valider les swaps exploitables et les cas sans amounts ; -- rechercher create/init pool, liquidity, fee/admin/config et autres events utiles ; -- maintenir la règle : pas de trade/candle si base/quote amount ou prix ne sont pas fiables ; -- produire un corpus SQL minimal pour chaque event promu. +Events DAMM v1 ajoutés côté decoder : -### 6.079. Version `0.7.47` — `meteora_damm_v2` séparé -Objectif : reprendre `meteora_damm_v2` comme DEX effectif séparé, avec traitement spécifique des nombreux `instruction_audit`. +- `meteora_damm_v1.create_pool` pour les créations constant-product avec config upstream Git `InitializePermissionlessConstantProductPoolWithConfig` et `InitializePermissionlessConstantProductPoolWithConfig2`, en plus des chemins legacy déjà présents ; +- `meteora_damm_v1.add_liquidity` pour `AddBalanceLiquidity`, `AddImbalanceLiquidity` et `BootstrapLiquidity` ; +- `meteora_damm_v1.remove_liquidity` pour `RemoveBalanceLiquidity` et `RemoveLiquiditySingleSide` ; +- `meteora_damm_v1.claim_fee` pour `ClaimFee` ; +- `meteora_damm_v1.create_lock_escrow` et `meteora_damm_v1.lock_liquidity` pour les instructions de verrouillage LP. + +Discriminants DAMM v1 traités dans cette tranche : + +| Discriminant | Mapping upstream Git | Event local | Statut | +|---|---|---|---| +| `07a68aabceabecf4` | `InitializePermissionlessConstantProductPoolWithConfig` | `meteora_damm_v1.create_pool` | observé dans corpus local | +| `3095dc823d0b09b2` | `InitializePermissionlessConstantProductPoolWithConfig2` | `meteora_damm_v1.create_pool` | observé dans corpus local | +| `856d2cb338ee7221` | `RemoveBalanceLiquidity` | `meteora_damm_v1.remove_liquidity` | observé dans corpus local | +| `a9204f8988e84689` | `ClaimFee` | `meteora_damm_v1.claim_fee` | observé dans corpus local | +| `3657a51345e3dae0` | `CreateLockEscrow` | `meteora_damm_v1.create_lock_escrow` | observé dans corpus local | +| `1513d02bed3eff57` | `Lock` | `meteora_damm_v1.lock_liquidity` | observé dans corpus local | + +Discriminants DAMM v1 ajoutés au decoder pour complétude upstream Git, même s’ils devront rester soumis au corpus avant mention `verified_by_corpus` : + +- `9118acc2db7d03be` — `InitializeCustomizablePermissionlessConstantProductPool` ; +- `a8e3323ebdab54b0` — `AddBalanceLiquidity` ; +- `4f237a54ad0f5dbf` — `AddImbalanceLiquidity` ; +- `04e4d747e1fd77ce` — `BootstrapLiquidity` ; +- `5454b142feb90afb` — `RemoveLiquiditySingleSide`. + +Le replay passe à la version logique `dex_decode.v0.7.46.damm_v1_events1` afin de redécoder les transactions certifiées sous la version `0.7.45` quand la tranche DAMM v1 est rejouée. + +Validation locale obtenue après replay : + +- `meteora_damm_v1.instruction_audit` vide sur le corpus local DAMM v1 rejoué ; +- `meteora_damm_v1.claim_fee`, `create_pool`, `create_lock_escrow`, `lock_liquidity` et `remove_liquidity` matérialisés dans les tables non-trade attendues ; +- invariant maintenu : aucun event non-trade DAMM v1 ne produit de trade/candle ; +- `cargo test -p kb_lib` et `cargo clippy -p kb_lib --all-targets -- -D warnings` validés localement après correction du warning Clippy. + +Correction Demo3 adossée à `0.7.46` : + +- ajout d’un décodage léger instruction-scoped pour `meteora_damm_v1` dans `onchain_dex_pair_discovery`, sans écriture DB et sans promotion de nouveau `program_id` ; +- les discriminants DAMM v1 connus par upstream Git/corpus sont classés directement en `swap`, `create_pool`, `add_liquidity`, `remove_liquidity`, `claim_fee`, `create_lock_escrow` ou `lock_liquidity` ; +- le filtre `target_event` devient strict pour les surfaces explicites afin qu’un swap ne ressorte pas comme liquidity, et inversement, quand les logs de transaction sont mixtes ; +- `excludeSwaps` ne supprime plus toute une transaction mixte lorsqu’un `target_event` explicite est sélectionné, afin de permettre la découverte d’instructions non-swap dans des routes agrégées ; +- les cibles UI `create_lock_escrow` et `lock_liquidity` sont ajoutées pour faciliter les backfills via Demo Pipeline 2. + +Aucun `program_id` Meteora Vault n’est promu comme vérifié sans corpus direct séparé. + +### 6.079. Version `0.7.47` — Upstream Git Registry / DEX discovery preparation +Objectif : accélérer la découverte multi-DEX en indexant les `program_id`, discriminants d’instructions, discriminants d’events et noms d’instructions issus de dépôts Git externes de decoders Solana, sans les considérer vérifiés par défaut. À faire : +- créer un registre `upstream_registry` dans `kb_lib`, sans dépendre d’un nom de dépôt particulier ; +- stocker pour chaque entrée : `source_repo`, `decoder_code`, `program_id`, famille, type de surface, instruction/event name, discriminator hex, longueur de discriminator, statut de preuve et notes ; +- utiliser les statuts génériques : `upstream_git_unverified`, `upstream_git_mapped_unverified`, `upstream_git_local_corpus_observed`, `upstream_git_local_corpus_materialized` ; +- exposer les entrées à Demo3 pour filtrer par decoder, famille, `program_id`, discriminant, instruction/event name ou statut ; +- permettre à Demo3 de rechercher `any_upstream_unverified` pour trouver des signatures candidates à backfiller ; +- ne produire aucun trade/candle/liquidity/fee/reward/admin automatique depuis le registre ; +- n’utiliser les entrées upstream Git que comme indices de découverte et d’audit tant qu’elles ne sont pas validées par Demo3 + backfill + replay + SQL ; +- garder `kb_demo_app` comme façade UI : toute logique de registry/mapping doit rester dans `kb_lib`. + +Familles prioritaires à indexer en premier : + +- DEX / AMM / CLMM / orderbook : `meteora-damm-v2`, `meteora-dbc`, `meteora-dlmm`, `meteora-vault`, `raydium-amm-v4`, `raydium-clmm`, `raydium-cpmm`, `raydium-launchpad`, `raydium-liquidity-locking`, `raydium-stable-swap`, `orca-whirlpool`, `fluxbeam`, `lifinity-amm-v2`, `phoenix-v1`, `openbook-v2`, `stabble-stable-swap`, `stabble-weighted-swap`, `bonkswap`, `boop`, `moonshot`, `heaven`, `okx-dex`, `pancake-swap`, `vertigo`, `virtuals`, `wavebreak`, `onchain-labs-dex-v1`, `onchain-labs-dex-v2` ; +- agrégateurs / ordres / perps / lending utiles au routage ou à l’analyse : `jupiter-swap`, `jupiter-dca`, `jupiter-limit-order`, `jupiter-limit-order-2`, `jupiter-perpetuals`, `jupiter-lend`, `kamino-lending`, `kamino-vault`, `kamino-farms`, `kamino-limit-order`, `drift-v2`, `marginfi-v2`, `dflow-aggregator-v4`, `zeta` ; +- contexte transactionnel non DEX : `system-program`, `token-program`, `token-2022`, `associated-token-account`, `address-lookup-table`, `memo-program`, `stake-program`, `mpl-token-metadata`, `mpl-core`, `bubblegum`, `name-service`, `marinade-finance`, `solayer-restaking-program`, `swig`, `sharky`, `circle-message-transmitter-v2`, `circle-token-messenger-v2`. + +Aucun de ces programmes ne doit être marqué `verified_by_corpus` uniquement parce qu’il existe dans un dépôt Git externe. + +### 6.080. Version `0.7.48` — `meteora_damm_v2` séparé +Objectif : reprendre `meteora_damm_v2` comme DEX effectif séparé après disponibilité du registre upstream Git. + +À faire : + +- utiliser le registre `0.7.47` comme source d’indices, pas comme preuve ; - vérifier `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` dans le corpus local avant de le marquer `verified_by_corpus` ; - consolider `create_pool`, swaps exploitables, configs dynamiques, fees/admin et events lifecycle ; - conserver les swaps sans payload montant/prix fiables comme `non_actionable_trade` ; - ne promouvoir aucun event depuis `instruction_audit` sans signature de validation. -### 6.080. Version `0.7.48` — `meteora_dbc` séparé +### 6.081. Version `0.7.49` — `meteora_dbc` séparé Objectif : séparer proprement bonding/launch, swap effectif, migration et attribution d’origine dans `meteora_dbc`. À faire : @@ -1145,7 +1209,7 @@ Objectif : séparer proprement bonding/launch, swap effectif, migration et attri - éviter toute candle artificielle sur events de bonding/launch non pricés ; - documenter les signatures/corpus avant toute promotion. -### 6.081. Version `0.7.49` — `orca_whirlpools` séparé +### 6.082. Version `0.7.50` — `orca_whirlpools` séparé Objectif : revalider Orca Whirlpools par corpus dédié avant toute promotion au même niveau que Raydium/Meteora. À faire : @@ -1155,109 +1219,60 @@ Objectif : revalider Orca Whirlpools par corpus dédié avant toute promotion au - matérialiser uniquement les events prouvés ; - ajouter des diagnostics par event kind. -### 6.082. Version `0.7.50` — `fluxbeam` séparé +### 6.083. Version `0.7.51` — `fluxbeam` séparé Objectif : vérifier FluxBeam comme DEX effectif distinct. -À faire : +À faire : constituer un corpus local, vérifier `program_id`, comptes, préfixes `data_json`, swaps, pools, liquidity et events non-trade prouvés. -- constituer un corpus local ; -- vérifier `program_id`, comptes, préfixes `data_json` et familles d’instructions utiles ; -- valider swaps, pools, liquidity et events non-trade prouvés ; -- marquer explicitement les parties heuristiques ou non-actionnables. +### 6.084. Version `0.7.52` — `dexlab` / OpenBook relation +Objectif : vérifier DexLab comme DEX effectif distinct sans le confondre avec OpenBook ou une couche de marché associée. -### 6.083. Version `0.7.51` — `dexlab` séparé -Objectif : vérifier DexLab comme DEX effectif distinct, sans le confondre avec OpenBook ou une autre couche de marché. +À faire : constituer un corpus local, vérifier `program_id`, comptes, préfixes `data_json`, swaps, pools et éventuels liens de market/pool. -À faire : +### 6.085. Version `0.7.53` — Lifinity / Phoenix / OpenBook / Stabble +Objectif : traiter les DEX/orderbooks supplémentaires identifiés par le registre upstream Git. -- constituer un corpus local ; -- vérifier `program_id`, comptes, préfixes `data_json` et familles d’instructions utiles ; -- valider swaps, pools et éventuels liens de market/pool ; -- conserver les cas partiels en audit. +À faire : valider séparément `lifinity_amm_v2`, `phoenix_v1`, `openbook_v2`, `stabble_stable_swap` et `stabble_weighted_swap`, sans matérialiser de trade avant preuve de montants exploitables. -### 6.084. Version `0.7.52` — `metaDAO` candidat DEX -Objectif : rechercher et vérifier metaDAO sans inventer de `program_id`. +### 6.086. Version `0.7.54` — BonkSwap / Boop / Moonshot / Heaven / Wavebreak / Vertigo / Virtuals / Pancake / OKX DEX +Objectif : vérifier les surfaces de swap/launch hybrides ou candidates découvertes via registre et corpus. -À faire : +À faire : séparer DEX effectif, launch surface, routeur/agrégateur et simple candidat ; ne promouvoir aucun `program_id` sans corpus local. -- rechercher les signatures/corpus via Demo3, DEX Screener ou sources externes de découverte ; -- ne considérer une source externe que comme indice ; -- promouvoir uniquement après preuve on-chain locale ; -- documenter chaque programme, event et limite. +### 6.087. Version `0.7.55` — Raydium surfaces complémentaires +Objectif : traiter `raydium_launchpad`, `raydium_liquidity_locking`, `raydium_stable_swap` et éventuelles surfaces Raydium non couvertes par CPMM/CLMM/AMM v4. -### 6.085. Version `0.7.53` — `printr` candidat DEX -Objectif : rechercher et vérifier Printr sans inventer de `program_id`. +À faire : distinguer launch, lock, stable AMM et AMM legacy ; garder les events non prouvés en audit. -À faire : +### 6.088. Version `0.7.56` — Aggregators, limit orders, perps et lending +Objectif : intégrer les programmes utiles au routage, aux ordres, aux perps ou au lending sans les confondre avec les DEX effectifs. -- rechercher les signatures/corpus via Demo3, DEX Screener ou sources externes de découverte ; -- ne considérer une source externe que comme indice ; -- promouvoir uniquement après preuve on-chain locale ; -- documenter chaque programme, event et limite. +À faire : classifier `jupiter_*`, `kamino_*`, `drift_v2`, `marginfi_v2`, `dflow_aggregator_v4`, `zeta` comme contexte/routing/ordres tant qu’ils ne produisent pas directement une surface DEX matérialisable. -### 6.086. Version `0.7.54` — Couverture événementielle DEX consolidée +### 6.089. Version `0.7.57` — Couverture événementielle DEX consolidée Objectif : s’assurer que chaque DEX effectif supporté expose les événements utiles au scoring et au risque sans polluer les trades/candles. -À faire : +À faire : vérifier par DEX `swap`, liquidité, lifecycle, fees, rewards, admin/config, burns/mints utiles, et matérialiser uniquement les événements prouvés. -- vérifier par DEX la couverture `swap` / `tradeCandidate` / `candleCandidate` ; -- vérifier par DEX la couverture liquidité : add/remove/increase/decrease/open/close position ; -- vérifier par DEX les événements lifecycle : create/init/migrate/pause/resume/close ; -- vérifier par DEX les fees, rewards, creator fees, protocol fees et admin/config ; -- vérifier les burns/mints utiles au suivi token/pool sans les transformer en price-action ; -- matérialiser uniquement les événements prouvés dans les tables dédiées ; -- ajouter des compteurs et samples diagnostics par DEX et par type d’événement ; -- conserver l’invariant : aucun fee/reward/admin/liquidity/lifecycle/burn non price-action ne produit de trade, metric ou candle. - -### 6.087. Version `0.7.55` — `kb_demo_app` Demo4 : DEX Screener et sources externes de découverte +### 6.090. Version `0.7.58` — `kb_demo_app` Demo4 : DEX Screener et sources externes de découverte Objectif : utiliser des sources externes comme aides à la découverte de corpus sans les traiter comme vérité métier. -À faire : +À faire : rechercher des paires par token mint, chain, DEX name, pool address ou program id lorsque disponible, comparer avec la base locale, copier les signatures/adresses candidates pour backfill, sans promotion automatique. -- ajouter une `Demo4` pour interroger DEX Screener ou une source équivalente ; -- rechercher des paires par token mint, chain, DEX name, pool address ou program id lorsque disponible ; -- comparer les résultats externes avec les objets locaux : tokens, pools, pairs, listings, decoded events et protocol candidates ; -- afficher les écarts : paire externe absente localement, pool local sans source externe, DEX label ambigu, program id manquant ; -- permettre de copier les signatures/adresses candidates pour backfill ; -- ne jamais promouvoir automatiquement un DEX, un `program_id` ou une paire sur la seule base d’une réponse externe. +### 6.091. Version `0.7.59` — Démos spécialisées launch surfaces après DEX effectifs +Objectif : préparer des vues spécialisées pour les launch surfaces après stabilisation des DEX effectifs. -### 6.088. Version `0.7.56` — Démos spécialisées launch surfaces après DEX effectifs -Objectif : préparer des vues spécialisées pour les launch surfaces, mais seulement après stabilisation des DEX effectifs. +À faire : couvrir `pump_fun`, `raydium_launchpad`, `believe`, `bags`, `moonshot` / `moonit`, `boop_fun`, `letsbonk` / `bonk_fun`, `heaven`, avec séparation stricte entre launch origin, pool origin, DEX effectif et migration. -À faire plus tard : - -- ajouter des démos dédiées à `pump_fun`, `raydium_launchpad` / `raydium_launchlab`, `believe`, `bags`, `moonshot` / `moonit`, `boop_fun`, `letsbonk` / `bonk_fun`, `heaven` ; -- distinguer `launch_origin`, `pool_origin`, `dex_effective` et `migration_target` ; -- rattacher les launch origins aux pools et paires uniquement lorsque les comptes permettent un matching fiable ; -- exposer les origins dans les diagnostics et l’UI d’inspection ; -- maintenir l’interdiction de faux program ids, faux trades et fausses candles. - -### 6.089. Version `0.7.57` — `kb_demo_app` Demo10 : watcher WebSocket live DEX +### 6.092. Version `0.7.60` — `kb_demo_app` Demo10 : watcher WebSocket live DEX Objectif : valider le passage du replay/backfill vers l’observation temps réel contrôlée. -À faire : +À faire : sélectionner endpoints WS/HTTP et DEX/program ids à souscrire, utiliser le pipeline existant, afficher compteurs live, erreurs, subscriptions actives et derniers objets persistés. -- ajouter une démo temps réel type `Demo10` avec bouton `start` / `stop` ; -- permettre de sélectionner les endpoints WS/HTTP et les DEX/program ids à souscrire ; -- lancer le client WebSocket existant sans refactorer inutilement `ws_client.rs` / `ws_manager.rs` ; -- effectuer les `logsSubscribe`, `programSubscribe` ou `accountSubscribe` nécessaires selon le DEX ; -- détecter en temps réel mints, swaps, liquidités et autres événements utiles ; -- écrire en base via le pipeline existant : observations, transactions résolues, decoded events, pools/pairs/listings, trade events, candles et non-trade events ; -- afficher les compteurs live, erreurs, subscriptions actives et derniers objets persistés ; -- prévoir un arrêt propre avec unsubscribe avant close. - -### 6.090. Version `0.7.58` — Validation DEX v1 consolidée +### 6.093. Version `0.7.61` — Validation DEX v1 consolidée Objectif : rejouer tous les DEX effectifs supportés et valider les invariants du pipeline complet avant de revenir aux launch surfaces ou à l’analyse `0.8.x`. -À faire : - -- rejouer des bases neuves couvrant tous les connecteurs DEX supportés ; -- vérifier les compteurs globaux et par DEX : decoded events, trade events, liquidity events, lifecycle events, fee events, reward events, admin events, burns/mints utiles, candles et analytic signals ; -- contrôler que chaque famille d’événements alimente uniquement les tables métier prévues ; -- vérifier les diagnostics bloquants et les samples d’anomalie ; -- documenter les corpus utilisés pour chaque DEX/surface ; -- conserver une matrice de support par DEX, variante, instruction et type d’événement ; -- verrouiller les invariants avant d’ouvrir l’analyse `0.8.x`. +À faire : bases neuves, compteurs globaux et par DEX, diagnostics bloquants, samples d’anomalie, corpus documentés et matrice de support par DEX/variante/instruction/event. ### 6.091. Version `0.8.x` — Analyse et filtrage Objectif : transformer les événements bruts en signaux exploitables. @@ -1469,3 +1484,20 @@ Garde-fous constants : - pas de metadata manquante bloquante ; - pas de refactor réseau inutile tant que les clients HTTP/WS existants suffisent ; - pas de skip replay sur transaction/instruction ambiguë, multi-token ou multi-event sans preuve ledger. + + +### Demo3 discovery note + +Demo3 supports multiple selected target surfaces in one scan. The UI serializes selected checkboxes into the existing `targetEvent` filter as comma-separated values, so the backend remains backward compatible with single-target requests. + + +### Demo3 paged / multi-source note + +Demo3 discovery now supports multiple source addresses, `before` / `until` pagination cursors, per-address max pages and `newest_first` / `oldest_first` processing order. This is intended for targeted corpus construction from known pool/pair addresses, especially when the first signatures can be identified externally with an explorer and then replayed/backfilled through Demo Pipeline 2. External explorers remain discovery aids only; verification still requires local decoder corpus and DB replay. + + +### 0.7.46 — clôture DAMM v1 upstream Git coverage + +La tranche DAMM v1 doit couvrir les instructions/events listés par upstream Git decoder source `meteora-pools-decoder`. Les surfaces non observées localement sont volontairement persistées avec `proofStatus=upstream_git_mapped_unverified`; elles restent à valider par signatures réelles, replay et requêtes SQL. + +Après backfills ciblés, les surfaces `swap` et `add_balance_liquidity` sont confirmées par corpus local et ne doivent plus rester en `upstream_git_mapped_unverified`. Les deux `remove_liquidity` non matérialisés en table liquidity sont expliqués par l’absence de `pool_id/pair_id` local pour leurs pools, pas par un échec de décodage. diff --git a/kb_demo_app/frontend/demo3.html b/kb_demo_app/frontend/demo3.html index 49aa21b..0bae2da 100644 --- a/kb_demo_app/frontend/demo3.html +++ b/kb_demo_app/frontend/demo3.html @@ -64,30 +64,86 @@
- - + +
-
Use address source to discover signatures around a pool, vault, position, config or mint while keeping the program id filter.
+
Use address source to discover signatures around one or several pools, vaults, positions, configs or mints while keeping the program id filter.
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
For pool creation analysis, scan a pool address with enough pages and use oldest_first to process the oldest fetched signatures first.
- - + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
Leave all unchecked for generic discovery. Check several surfaces to scan once and keep candidates matching any selected target.
Use this to find corpus signatures for non-swap decoders without promoting unverified events.
@@ -156,6 +212,8 @@

Résumé

Signatures: 0
+
Unique fetched: 0
+
Pages: 0
Unique candidates: 0
Tx fetched: 0
Missing tx: 0
@@ -176,6 +234,10 @@ Backfill signatures: -
+
+ Next before cursors: + - +
diff --git a/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexDiscoveryRequest.ts b/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexDiscoveryRequest.ts index 2c62b77..0f722e3 100644 --- a/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexDiscoveryRequest.ts +++ b/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexDiscoveryRequest.ts @@ -20,6 +20,26 @@ signatureSource: string | null, * Optional source address used when signature_source is `address`. */ sourceAddress: string | null, +/** + * Optional extra source addresses used for multi-pool discovery. + */ +sourceAddresses: Array, +/** + * Optional `before` cursor passed to Solana getSignaturesForAddress. + */ +beforeSignature: string | null, +/** + * Optional `until` cursor passed to Solana getSignaturesForAddress. + */ +untilSignature: string | null, +/** + * Maximum number of signature pages to fetch per source address. + */ +maxPages: number, +/** + * Signature processing order: newest_first or oldest_first. + */ +scanOrder: string | null, /** * Optional target event family used to find non-swap signatures. */ diff --git a/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexDiscoveryResult.ts b/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexDiscoveryResult.ts index 462dca6..f4d20fd 100644 --- a/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexDiscoveryResult.ts +++ b/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexDiscoveryResult.ts @@ -1,5 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Demo3OnchainDexDiscoveryRequest } from "./Demo3OnchainDexDiscoveryRequest"; +import type { Demo3OnchainDexPaginationCursor } from "./Demo3OnchainDexPaginationCursor"; import type { Demo3OnchainDexPairCandidate } from "./Demo3OnchainDexPairCandidate"; import type { Demo3OnchainDexRejectedCandidateSummary } from "./Demo3OnchainDexRejectedCandidateSummary"; @@ -27,6 +28,22 @@ resolvedSignatureSource: string, * Address scanned with getSignaturesForAddress. */ resolvedSignatureAddress: string, +/** + * All addresses scanned with getSignaturesForAddress. + */ +resolvedSignatureAddresses: Array, +/** + * Cursor hints by scanned address. + */ +nextBeforeByAddress: Array, +/** + * Number of signature pages fetched. + */ +fetchedSignaturePageCount: number, +/** + * Number of unique fetched signatures after de-duplication. + */ +uniqueFetchedSignatureCount: number, /** * Number of unique candidate signatures. */ diff --git a/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexPaginationCursor.ts b/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexPaginationCursor.ts new file mode 100644 index 0000000..8cb6727 --- /dev/null +++ b/kb_demo_app/frontend/ts/bindings/Demo3OnchainDexPaginationCursor.ts @@ -0,0 +1,22 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Pagination cursor hint for one scanned source address. + */ +export type Demo3OnchainDexPaginationCursor = { +/** + * Scanned source address. + */ +address: string, +/** + * Signature usable as beforeSignature for the next page window. + */ +nextBeforeSignature: string | null, +/** + * Raw signature count fetched for this address. + */ +fetchedSignatureCount: number, +/** + * Page count fetched for this address. + */ +fetchedPageCount: number, }; diff --git a/kb_demo_app/frontend/ts/demo3.ts b/kb_demo_app/frontend/ts/demo3.ts index c99d486..dec385e 100644 --- a/kb_demo_app/frontend/ts/demo3.ts +++ b/kb_demo_app/frontend/ts/demo3.ts @@ -117,13 +117,49 @@ function isSolanaAddressLike(value: string): boolean { return /^[1-9A-HJ-NP-Za-km-z]+$/.test(trimmed); } +function splitSourceAddresses(value: string): string[] { + const seen = new Set(); + const addresses: string[] = []; + for (const token of value.split(/[\s,;]+/g)) { + const trimmed = token.trim(); + if (trimmed === "" || seen.has(trimmed)) { + continue; + } + seen.add(trimmed); + addresses.push(trimmed); + } + return addresses; +} + +function isSolanaSignatureLike(value: string): boolean { + const trimmed = value.trim(); + if (trimmed.length < 64 || trimmed.length > 128) { + return false; + } + return /^[1-9A-HJ-NP-Za-km-z]+$/.test(trimmed); +} + +function validateOptionalSignature(value: string | null, label: string): void { + if (value === null || value.trim() === "") { + return; + } + if (!isSolanaSignatureLike(value)) { + throw new Error(`${label} must be a valid Solana transaction signature.`); + } +} + function validateOnchainRequest(request: Demo3OnchainDexDiscoveryRequest): void { - if (request.signatureSource === "address") { - const sourceAddress = request.sourceAddress ?? ""; - if (!isSolanaAddressLike(sourceAddress)) { - throw new Error("Signature source is 'address': Source address must be a real Solana account address, pool, vault, position, config or mint. It cannot be empty or the literal value 'address'."); + const addresses = request.sourceAddresses ?? []; + if (request.signatureSource === "address" && addresses.length === 0) { + throw new Error("Signature source is 'address': provide at least one Solana account, pool, vault, position, config or mint address."); + } + for (const address of addresses) { + if (!isSolanaAddressLike(address)) { + throw new Error(`Invalid source address '${address}'. Provide Solana account addresses separated by commas, spaces or new lines.`); } } + validateOptionalSignature(request.beforeSignature, "Before signature"); + validateOptionalSignature(request.untilSignature, "Until signature"); if (request.programId !== null && !isSolanaAddressLike(request.programId)) { throw new Error("Program id filter must be a valid Solana program id, or empty when using a preset that resolves it."); } @@ -143,6 +179,27 @@ function intValue(id: string, fallback: number): number { return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } +function selectedTargetEvents(): string[] { + return Array.from(document.querySelectorAll('input[name="demo3TargetEventInput"]:checked')) + .map((input) => input.value.trim()) + .filter((value) => value !== ""); +} + +function readTargetEventFilter(): string | null { + const selected = selectedTargetEvents(); + return selected.length === 0 ? null : selected.join(","); +} + +function targetEventLabel(targetEvent: string | null): string { + return targetEvent === null || targetEvent.trim() === "" ? "any" : targetEvent; +} + +function clearTargetEventFilters(): void { + document.querySelectorAll('input[name="demo3TargetEventInput"]').forEach((input) => { + input.checked = false; + }); +} + function escapeHtml(value: string): string { return value .replace(/&/g, "&") @@ -227,12 +284,18 @@ function applyPreset(indexText: string): void { } function readOnchainRequest(): Demo3OnchainDexDiscoveryRequest { + const sourceAddresses = splitSourceAddresses(byId("demo3SourceAddressInput").value); return { dexCode: valueOrNull(byId("demo3DexCodeInput").value), programId: valueOrNull(byId("demo3ProgramIdInput").value), signatureSource: valueOrNull(byId("demo3SignatureSourceSelect").value), - sourceAddress: valueOrNull(byId("demo3SourceAddressInput").value), - targetEvent: valueOrNull(byId("demo3TargetEventSelect").value), + sourceAddress: sourceAddresses.length === 1 ? sourceAddresses[0] : null, + sourceAddresses, + beforeSignature: valueOrNull(byId("demo3BeforeSignatureInput").value), + untilSignature: valueOrNull(byId("demo3UntilSignatureInput").value), + maxPages: intValue("demo3MaxPagesInput", 1), + scanOrder: valueOrNull(byId("demo3ScanOrderSelect").value), + targetEvent: readTargetEventFilter(), excludeSwaps: byId("demo3ExcludeSwapsInput").checked, includeFailed: byId("demo3IncludeFailedInput").checked, httpRole: byId("demo3HttpRoleInput").value.trim() || "history_backfill", @@ -258,12 +321,16 @@ function clearFilters(): void { byId("demo3DexCodeInput").value = ""; byId("demo3ProgramIdInput").value = ""; byId("demo3SignatureSourceSelect").value = "program_id"; - byId("demo3SourceAddressInput").value = ""; + byId("demo3SourceAddressInput").value = ""; + byId("demo3BeforeSignatureInput").value = ""; + byId("demo3UntilSignatureInput").value = ""; + byId("demo3MaxPagesInput").value = "1"; + byId("demo3ScanOrderSelect").value = "newest_first"; byId("demo3PairIdInput").value = ""; byId("demo3PoolAddressInput").value = ""; byId("demo3TokenMintInput").value = ""; byId("demo3SignatureInput").value = ""; - byId("demo3TargetEventSelect").value = ""; + clearTargetEventFilters(); byId("demo3ExcludeSwapsInput").checked = false; byId("demo3IncludeFailedInput").checked = true; byId("demo3PresetSelect").value = ""; @@ -272,6 +339,8 @@ function clearFilters(): void { function renderOnchainResult(result: Demo3OnchainDexDiscoveryResult): void { byId("demo3SummarySignatureCount").textContent = String(result.fetchedSignatureCount); + byId("demo3SummaryUniqueFetchedSignatureCount").textContent = String(result.uniqueFetchedSignatureCount); + byId("demo3SummaryFetchedPageCount").textContent = String(result.fetchedSignaturePageCount); byId("demo3SummaryUniqueSignatureCount").textContent = String(result.uniqueSignatureCount); byId("demo3SummaryFetchedTxCount").textContent = String(result.fetchedTransactionCount); byId("demo3SummaryMissingTxCount").textContent = String(result.missingTransactionCount); @@ -281,9 +350,11 @@ function renderOnchainResult(result: Demo3OnchainDexDiscoveryResult): void { byId("demo3SummaryExtractedCandidateCount").textContent = String(result.extractedCandidateCount); byId("demo3SummaryRejectedCandidateCount").textContent = String(result.targetRejectedCandidateCount); byId("demo3SummaryCandidateCount").textContent = String(result.candidateCount); - const targetEvent = result.request.targetEvent ?? "any"; - byId("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / program=${result.resolvedProgramId} / source=${result.resolvedSignatureSource}:${result.resolvedSignatureAddress} / target=${targetEvent}`; + const targetEvent = targetEventLabel(result.request.targetEvent); + const sourceText = result.resolvedSignatureAddresses.length === 0 ? result.resolvedSignatureAddress : result.resolvedSignatureAddresses.join(","); + byId("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / program=${result.resolvedProgramId} / source=${result.resolvedSignatureSource}:${sourceText} / target=${targetEvent} / order=${result.request.scanOrder ?? "newest_first"}`; byId("demo3UniqueSignatureText").textContent = result.uniqueBackfillSignatures.length === 0 ? "-" : result.uniqueBackfillSignatures.join(", "); + byId("demo3NextBeforeText").textContent = result.nextBeforeByAddress.length === 0 ? "-" : result.nextBeforeByAddress.map((cursor) => `${cursor.address}:${cursor.nextBeforeSignature ?? "-"}`).join(" | "); renderRejectedSummary(result); renderOnchainCandidates(result.candidates); } @@ -374,14 +445,14 @@ async function discoverOnchain(): Promise { return; } setStatus("running", "text-bg-warning"); - appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' source='${request.signatureSource ?? "program_id"}:${request.sourceAddress ?? ""}' target='${request.targetEvent ?? "any"}' excludeSwaps='${request.excludeSwaps}' role='${request.httpRole}'`); + appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' source='${request.signatureSource ?? "program_id"}:${request.sourceAddresses.join(",")}' target='${targetEventLabel(request.targetEvent)}' pages='${request.maxPages}' order='${request.scanOrder ?? "newest_first"}' before='${request.beforeSignature ?? ""}' until='${request.untilSignature ?? ""}' excludeSwaps='${request.excludeSwaps}' role='${request.httpRole}'`); try { const payload = await invoke("demo3_discover_onchain_dex_pairs", { request }); lastResultJson = payload.resultJson; byId("demo3JsonTextarea").value = payload.resultJson; renderOnchainResult(payload.result); setStatus("ok", "text-bg-success"); - appendLogLine(`on-chain discovery completed: candidates='${payload.result.candidateCount}' unique='${payload.result.uniqueSignatureCount}' signatures='${payload.result.fetchedSignatureCount}' extracted='${payload.result.extractedCandidateCount}' rejected='${payload.result.targetRejectedCandidateCount}' skippedSwapTx='${payload.result.skippedSwapLogTransactionCount}'`); + appendLogLine(`on-chain discovery completed: candidates='${payload.result.candidateCount}' unique='${payload.result.uniqueSignatureCount}' signatures='${payload.result.fetchedSignatureCount}' uniqueFetched='${payload.result.uniqueFetchedSignatureCount}' pages='${payload.result.fetchedSignaturePageCount}' extracted='${payload.result.extractedCandidateCount}' rejected='${payload.result.targetRejectedCandidateCount}' skippedSwapTx='${payload.result.skippedSwapLogTransactionCount}'`); } catch (error) { setStatus("error", "text-bg-danger"); appendLogLine(`on-chain discovery failed: ${String(error)}`); diff --git a/kb_demo_app/package.json b/kb_demo_app/package.json index 390a0ca..d46529f 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.45", + "version": "0.7.46", "type": "module", "scripts": { "dev": "vite", diff --git a/kb_demo_app/src/demo3.rs b/kb_demo_app/src/demo3.rs index 500aa72..6db513b 100644 --- a/kb_demo_app/src/demo3.rs +++ b/kb_demo_app/src/demo3.rs @@ -435,6 +435,21 @@ pub(crate) struct Demo3OnchainDexDiscoveryRequest { pub signature_source: std::option::Option, /// Optional source address used when signature_source is `address`. pub source_address: std::option::Option, + /// Optional extra source addresses used for multi-pool discovery. + #[serde(default)] + pub source_addresses: std::vec::Vec, + /// Optional `before` cursor passed to Solana getSignaturesForAddress. + #[serde(default)] + pub before_signature: std::option::Option, + /// Optional `until` cursor passed to Solana getSignaturesForAddress. + #[serde(default)] + pub until_signature: std::option::Option, + /// Maximum number of signature pages to fetch per source address. + #[serde(default)] + pub max_pages: u32, + /// Signature processing order: newest_first or oldest_first. + #[serde(default)] + pub scan_order: std::option::Option, /// Optional target event family used to find non-swap signatures. pub target_event: std::option::Option, /// Whether transactions containing swap-like logs should be skipped. @@ -479,6 +494,16 @@ pub(crate) struct Demo3OnchainDexDiscoveryResult { pub resolved_signature_source: std::string::String, /// Address scanned with getSignaturesForAddress. pub resolved_signature_address: std::string::String, + /// All addresses scanned with getSignaturesForAddress. + pub resolved_signature_addresses: std::vec::Vec, + /// Cursor hints by scanned address. + pub next_before_by_address: std::vec::Vec, + /// Number of signature pages fetched. + #[ts(type = "number")] + pub fetched_signature_page_count: usize, + /// Number of unique fetched signatures after de-duplication. + #[ts(type = "number")] + pub unique_fetched_signature_count: usize, /// Number of unique candidate signatures. #[ts(type = "number")] pub unique_signature_count: usize, @@ -517,6 +542,23 @@ pub(crate) struct Demo3OnchainDexDiscoveryResult { pub candidates: std::vec::Vec, } +/// Pagination cursor hint for one scanned source address. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[ts(export, export_to = "../frontend/ts/bindings/Demo3OnchainDexPaginationCursor.ts")] +#[serde(rename_all = "camelCase")] +pub(crate) struct Demo3OnchainDexPaginationCursor { + /// Scanned source address. + pub address: std::string::String, + /// Signature usable as beforeSignature for the next page window. + pub next_before_signature: std::option::Option, + /// Raw signature count fetched for this address. + #[ts(type = "number")] + pub fetched_signature_count: usize, + /// Page count fetched for this address. + #[ts(type = "number")] + pub fetched_page_count: usize, +} + /// Rejected on-chain discovery candidate summary. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( @@ -685,6 +727,11 @@ fn to_lib_onchain_request( program_id: normalize_optional_text(request.program_id.clone()), signature_source: normalize_optional_text(request.signature_source.clone()), source_address: normalize_optional_text(request.source_address.clone()), + source_addresses: request.source_addresses.clone(), + before_signature: normalize_optional_text(request.before_signature.clone()), + until_signature: normalize_optional_text(request.until_signature.clone()), + max_pages: request.max_pages, + scan_order: normalize_optional_text(request.scan_order.clone()), target_event: normalize_optional_text(request.target_event.clone()), exclude_swaps: request.exclude_swaps, include_failed: request.include_failed, @@ -708,6 +755,11 @@ fn from_lib_onchain_result( program_id: result.request.program_id, signature_source: result.request.signature_source, source_address: result.request.source_address, + source_addresses: result.request.source_addresses, + before_signature: result.request.before_signature, + until_signature: result.request.until_signature, + max_pages: result.request.max_pages, + scan_order: result.request.scan_order, target_event: result.request.target_event, exclude_swaps: result.request.exclude_swaps, include_failed: result.request.include_failed, @@ -720,6 +772,10 @@ fn from_lib_onchain_result( resolved_program_id: result.resolved_program_id, resolved_signature_source: result.resolved_signature_source, resolved_signature_address: result.resolved_signature_address, + resolved_signature_addresses: result.resolved_signature_addresses, + next_before_by_address: from_lib_onchain_pagination_cursors(result.next_before_by_address), + fetched_signature_page_count: result.fetched_signature_page_count, + unique_fetched_signature_count: result.unique_fetched_signature_count, unique_signature_count: result.unique_signature_count, unique_backfill_signatures: result.unique_backfill_signatures, rejected_candidate_summary: from_lib_rejected_candidate_summary( @@ -738,6 +794,21 @@ fn from_lib_onchain_result( }; } +fn from_lib_onchain_pagination_cursors( + values: std::vec::Vec, +) -> std::vec::Vec { + let mut mapped = std::vec::Vec::new(); + for value in values { + mapped.push(Demo3OnchainDexPaginationCursor { + address: value.address, + next_before_signature: value.next_before_signature, + fetched_signature_count: value.fetched_signature_count, + fetched_page_count: value.fetched_page_count, + }); + } + return mapped; +} + fn from_lib_rejected_candidate_summary( values: std::vec::Vec, ) -> std::vec::Vec { diff --git a/kb_demo_app/tauri.conf.json b/kb_demo_app/tauri.conf.json index fcad83a..da1971a 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.45", + "version": "0.7.46", "identifier": "com.sasedev.kb-demo-app", "build": { "beforeDevCommand": "npm run dev", diff --git a/kb_lib/src/dex.rs b/kb_lib/src/dex.rs index cb13279..872d531 100644 --- a/kb_lib/src/dex.rs +++ b/kb_lib/src/dex.rs @@ -26,6 +26,10 @@ pub use fluxbeam::FluxbeamSwapDecoded; pub use meteora_damm_v1::MeteoraDammV1CreatePoolDecoded; pub use meteora_damm_v1::MeteoraDammV1DecodedEvent; pub use meteora_damm_v1::MeteoraDammV1Decoder; +pub use meteora_damm_v1::MeteoraDammV1FeeDecoded; +pub use meteora_damm_v1::MeteoraDammV1LiquidityDecoded; +pub use meteora_damm_v1::MeteoraDammV1PoolAdminDecoded; +pub use meteora_damm_v1::MeteoraDammV1PoolLifecycleDecoded; pub use meteora_damm_v1::MeteoraDammV1SwapDecoded; pub use meteora_damm_v2::MeteoraDammV2CreatePoolDecoded; pub use meteora_damm_v2::MeteoraDammV2DecodedEvent; diff --git a/kb_lib/src/dex/meteora_damm_v1.rs b/kb_lib/src/dex/meteora_damm_v1.rs index d87518e..172322a 100644 --- a/kb_lib/src/dex/meteora_damm_v1.rs +++ b/kb_lib/src/dex/meteora_damm_v1.rs @@ -4,11 +4,112 @@ const DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL: [u8; 8] = [0x5f, 0xb4, 0x0a, 0xac, 0x54, 0xae, 0xe8, 0x28]; - -const DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL_WITH_CONFIG: [u8; 8] = +const DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL_WITH_CONFIG_LEGACY: [u8; 8] = [0x49, 0xfe, 0x76, 0xf3, 0xab, 0xc4, 0x4c, 0xd0]; - +const DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG: [u8; 8] = + [0x07, 0xa6, 0x8a, 0xab, 0xce, 0xab, 0xec, 0xf4]; +const DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG2: [u8; 8] = + [0x30, 0x95, 0xdc, 0x82, 0x3d, 0x0b, 0x09, 0xb2]; +const DAMM_V1_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_CP_POOL: [u8; 8] = + [0x91, 0x18, 0xac, 0xc2, 0xdb, 0x7d, 0x03, 0xbe]; const DAMM_V1_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8]; +const DAMM_V1_DISCRIMINATOR_ADD_BALANCE_LIQUIDITY: [u8; 8] = + [0xa8, 0xe3, 0x32, 0x3e, 0xbd, 0xab, 0x54, 0xb0]; +const DAMM_V1_DISCRIMINATOR_ADD_IMBALANCE_LIQUIDITY: [u8; 8] = + [0x4f, 0x23, 0x7a, 0x54, 0xad, 0x0f, 0x5d, 0xbf]; +const DAMM_V1_DISCRIMINATOR_BOOTSTRAP_LIQUIDITY: [u8; 8] = + [0x04, 0xe4, 0xd7, 0x47, 0xe1, 0xfd, 0x77, 0xce]; +const DAMM_V1_DISCRIMINATOR_REMOVE_BALANCE_LIQUIDITY: [u8; 8] = + [0x85, 0x6d, 0x2c, 0xb3, 0x38, 0xee, 0x72, 0x21]; +const DAMM_V1_DISCRIMINATOR_REMOVE_LIQUIDITY_SINGLE_SIDE: [u8; 8] = + [0x54, 0x54, 0xb1, 0x42, 0xfe, 0xb9, 0x0a, 0xfb]; +const DAMM_V1_DISCRIMINATOR_CLAIM_FEE: [u8; 8] = [0xa9, 0x20, 0x4f, 0x89, 0x88, 0xe8, 0x46, 0x89]; +const DAMM_V1_DISCRIMINATOR_CREATE_LOCK_ESCROW: [u8; 8] = + [0x36, 0x57, 0xa5, 0x13, 0x45, 0xe3, 0xda, 0xe0]; +const DAMM_V1_DISCRIMINATOR_LOCK: [u8; 8] = [0x15, 0x13, 0xd0, 0x2b, 0xed, 0x3e, 0xff, 0x57]; + +const DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONED_POOL: [u8; 8] = + [0x4d, 0x55, 0xb2, 0x9d, 0x32, 0x30, 0xd4, 0x7e]; +const DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_POOL: [u8; 8] = + [0x76, 0xad, 0x29, 0x9d, 0xad, 0x48, 0x61, 0x67]; +const DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_POOL_WITH_FEE_TIER: [u8; 8] = + [0x06, 0x87, 0x44, 0x93, 0xe5, 0x52, 0xa9, 0x71]; +const DAMM_V1_DISCRIMINATOR_ENABLE_OR_DISABLE_POOL: [u8; 8] = + [0x80, 0x06, 0xe4, 0x83, 0x37, 0xa1, 0x34, 0xa9]; +const DAMM_V1_DISCRIMINATOR_SET_POOL_FEES: [u8; 8] = + [0x66, 0x2c, 0x9e, 0x36, 0xcd, 0x25, 0x7e, 0x4e]; +const DAMM_V1_DISCRIMINATOR_OVERRIDE_CURVE_PARAM: [u8; 8] = + [0x62, 0x56, 0xcc, 0x33, 0x5e, 0x47, 0x45, 0xbb]; +const DAMM_V1_DISCRIMINATOR_GET_POOL_INFO: [u8; 8] = + [0x09, 0x30, 0xdc, 0x65, 0x16, 0xf0, 0x4e, 0xc8]; +const DAMM_V1_DISCRIMINATOR_CREATE_MINT_METADATA: [u8; 8] = + [0x0d, 0x46, 0xa8, 0x29, 0xfa, 0x64, 0x94, 0x5a]; +const DAMM_V1_DISCRIMINATOR_CREATE_CONFIG: [u8; 8] = + [0xc9, 0xcf, 0xf3, 0x72, 0x4b, 0x6f, 0x2f, 0xbd]; +const DAMM_V1_DISCRIMINATOR_CLOSE_CONFIG: [u8; 8] = + [0x91, 0x09, 0x48, 0x9d, 0x5f, 0x7d, 0x3d, 0x55]; +const DAMM_V1_DISCRIMINATOR_UPDATE_ACTIVATION_POINT: [u8; 8] = + [0x96, 0x3e, 0x7d, 0xdb, 0xab, 0xdc, 0x1a, 0xed]; +const DAMM_V1_DISCRIMINATOR_WITHDRAW_PROTOCOL_FEES: [u8; 8] = + [0x0b, 0x44, 0xa5, 0x62, 0x12, 0xd0, 0x86, 0x49]; +const DAMM_V1_DISCRIMINATOR_SET_WHITELISTED_VAULT: [u8; 8] = + [0x0c, 0x94, 0x5e, 0x2a, 0x37, 0x39, 0x53, 0xf7]; +const DAMM_V1_DISCRIMINATOR_PARTNER_CLAIM_FEE: [u8; 8] = + [0x39, 0x35, 0xb0, 0x1e, 0x7b, 0x46, 0x34, 0x40]; +const DAMM_V1_EVENT_DISCRIMINATOR_ADD_LIQUIDITY: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x1f, 0x5e, 0x7d, 0x5a, 0xe3, 0x34, 0x3d, 0xba, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_REMOVE_LIQUIDITY: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x74, 0xf4, 0x61, 0xe8, 0x67, 0x1f, 0x98, 0x3a, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_BOOTSTRAP_LIQUIDITY: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x79, 0x7f, 0x26, 0x88, 0x5c, 0x37, 0x0e, 0xf7, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_SWAP: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x51, 0x6c, 0xe3, 0xbe, 0xcd, 0xd0, 0x0a, 0xc4, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_SET_POOL_FEES: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xf5, 0x1a, 0xc6, 0xa4, 0x58, 0x12, 0x4b, 0x09, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_POOL_INFO: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xcf, 0x14, 0x57, 0x61, 0xfb, 0xd4, 0xea, 0x2d, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_TRANSFER_ADMIN: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xe4, 0xa9, 0x83, 0xf4, 0x3d, 0x38, 0x41, 0xfe, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_OVERRIDE_CURVE_PARAM: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xf7, 0x14, 0xa5, 0xf8, 0x4b, 0x05, 0x36, 0xf6, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_POOL_CREATED: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xca, 0x2c, 0x29, 0x58, 0x68, 0xdc, 0x9d, 0x52, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_POOL_ENABLED: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x02, 0x97, 0x12, 0x53, 0xcc, 0x86, 0x5c, 0xbf, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_MIGRATE_FEE_ACCOUNT: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xdf, 0xea, 0xe8, 0x1a, 0xfc, 0x69, 0xb4, 0x7d, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_CREATE_LOCK_ESCROW: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x4a, 0x5e, 0x6a, 0x8d, 0x31, 0x11, 0x62, 0x6d, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_LOCK: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xdc, 0xb7, 0x43, 0xd7, 0x99, 0xcf, 0x38, 0xea, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_CLAIM_FEE: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x4b, 0x7a, 0x9a, 0x30, 0x8c, 0x4a, 0x7b, 0xa3, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_CREATE_CONFIG: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xc7, 0x98, 0x0a, 0x13, 0x27, 0x27, 0x9d, 0x68, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_CLOSE_CONFIG: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xf9, 0xb5, 0x6c, 0x59, 0x04, 0x96, 0x5a, 0xae, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_WITHDRAW_PROTOCOL_FEES: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x1e, 0xf0, 0xcf, 0xc4, 0x8b, 0xef, 0x4f, 0x1c, +]; +const DAMM_V1_EVENT_DISCRIMINATOR_PARTNER_CLAIM_FEES: [u8; 16] = [ + 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x87, 0x83, 0x0a, 0x5e, 0x77, 0xd1, 0xca, 0x30, +]; /// Decoded Meteora DAMM v1 create-pool event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -27,6 +128,8 @@ pub struct MeteoraDammV1CreatePoolDecoded { pub token_a_mint: std::option::Option, /// Optional token B mint. pub token_b_mint: std::option::Option, + /// Optional LP mint. + pub lp_mint: std::option::Option, /// Optional config account. pub config_account: std::option::Option, /// Optional creator / payer. @@ -60,6 +163,112 @@ pub struct MeteoraDammV1SwapDecoded { pub payload_json: serde_json::Value, } +/// Decoded Meteora DAMM v1 liquidity event. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MeteoraDammV1LiquidityDecoded { + /// 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, + /// Persisted event kind. + pub event_kind: std::string::String, + /// Optional pool account. + pub pool_account: std::option::Option, + /// Optional token A mint. + pub token_a_mint: std::option::Option, + /// Optional token B mint. + pub token_b_mint: std::option::Option, + /// Optional LP mint. + pub lp_mint: std::option::Option, + /// Optional actor wallet. + pub actor_wallet: std::option::Option, + /// Optional decoded base/token-A amount. + pub base_amount_raw: std::option::Option, + /// Optional decoded quote/token-B amount. + pub quote_amount_raw: std::option::Option, + /// Optional decoded LP amount. + pub lp_amount_raw: std::option::Option, + /// Decoded payload. + pub payload_json: serde_json::Value, +} + +/// Decoded Meteora DAMM v1 fee event. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MeteoraDammV1FeeDecoded { + /// 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, + /// Persisted event kind. + pub event_kind: std::string::String, + /// Optional pool account. + pub pool_account: std::option::Option, + /// Optional LP mint. + pub lp_mint: std::option::Option, + /// Optional actor or fee owner wallet. + pub actor_wallet: std::option::Option, + /// Optional fee amount when the instruction provides an exact claimed amount. + pub fee_amount_raw: std::option::Option, + /// Decoded payload. + pub payload_json: serde_json::Value, +} + +/// Decoded Meteora DAMM v1 pool lifecycle event. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MeteoraDammV1PoolLifecycleDecoded { + /// 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, + /// Persisted event kind. + pub event_kind: std::string::String, + /// Optional pool account. + pub pool_account: std::option::Option, + /// Optional token A mint. + pub token_a_mint: std::option::Option, + /// Optional token B mint. + pub token_b_mint: std::option::Option, + /// Optional LP mint. + pub lp_mint: std::option::Option, + /// Decoded payload. + pub payload_json: serde_json::Value, +} + +/// Decoded Meteora DAMM v1 pool administration event. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MeteoraDammV1PoolAdminDecoded { + /// 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, + /// Persisted event kind. + pub event_kind: std::string::String, + /// Optional pool account. + pub pool_account: std::option::Option, + /// Optional actor wallet. + pub actor_wallet: std::option::Option, + /// Optional administration action. + pub admin_action: std::option::Option, + /// Decoded payload. + pub payload_json: serde_json::Value, +} + /// Decoded Meteora DAMM v1 event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum MeteoraDammV1DecodedEvent { @@ -67,6 +276,14 @@ pub enum MeteoraDammV1DecodedEvent { CreatePool(MeteoraDammV1CreatePoolDecoded), /// Swap. Swap(MeteoraDammV1SwapDecoded), + /// Liquidity add/remove event. + Liquidity(MeteoraDammV1LiquidityDecoded), + /// Fee claim event. + Fee(MeteoraDammV1FeeDecoded), + /// Pool lifecycle event. + PoolLifecycle(MeteoraDammV1PoolLifecycleDecoded), + /// Pool administration event. + PoolAdmin(MeteoraDammV1PoolAdminDecoded), } /// Meteora DAMM v1 decoder. @@ -75,9 +292,52 @@ pub struct MeteoraDammV1Decoder; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MeteoraDammV1InstructionKind { - CreatePool, - CreatePoolWithConfig, + InitializePermissionedPool, + InitializePermissionlessPool, + InitializePermissionlessPoolWithFeeTier, + CreatePoolLegacy, + CreatePoolWithConfigLegacy, + InitializePermissionlessConstantProductPoolWithConfig, + InitializePermissionlessConstantProductPoolWithConfig2, + InitializeCustomizablePermissionlessConstantProductPool, Swap, + AddBalanceLiquidity, + AddImbalanceLiquidity, + BootstrapLiquidity, + RemoveBalanceLiquidity, + RemoveLiquiditySingleSide, + ClaimFee, + CreateLockEscrow, + Lock, + EnableOrDisablePool, + SetPoolFees, + OverrideCurveParam, + GetPoolInfo, + CreateMintMetadata, + CreateConfig, + CloseConfig, + UpdateActivationPoint, + WithdrawProtocolFees, + SetWhitelistedVault, + PartnerClaimFee, + AddLiquidityEvent, + RemoveLiquidityEvent, + BootstrapLiquidityEvent, + SwapEvent, + SetPoolFeesEvent, + PoolInfoEvent, + TransferAdminEvent, + OverrideCurveParamEvent, + PoolCreatedEvent, + PoolEnabledEvent, + MigrateFeeAccountEvent, + CreateLockEscrowEvent, + LockEvent, + ClaimFeeEvent, + CreateConfigEvent, + CloseConfigEvent, + WithdrawProtocolFeesEvent, + PartnerClaimFeesEvent, Unknown, } @@ -125,11 +385,9 @@ impl MeteoraDammV1Decoder { if program_id.as_str() != crate::METEORA_DAMM_V1_PROGRAM_ID { continue; } - let instruction_id_option = instruction.id; - let instruction_id = match instruction_id_option { - Some(instruction_id) => instruction_id, - None => continue, - }; + if instruction.id.is_none() { + continue; + } let accounts_result = parse_accounts_json(instruction.accounts_json.as_str()); let accounts = match accounts_result { Ok(accounts) => accounts, @@ -154,187 +412,755 @@ impl MeteoraDammV1Decoder { instruction_data.as_deref(), &log_messages, ); - let pool_account = extract_string_by_candidate_keys( - parsed_json.as_ref(), - &["pool", "poolAddress", "poolAccount", "amm", "ammPool", "poolState"], - ) - .or_else(|| return extract_account(&accounts, 0)); - let token_a_mint = extract_string_by_candidate_keys( - parsed_json.as_ref(), - &["tokenAMint", "mintA", "baseMint", "token0Mint", "mint0", "coinMint"], - ) - .or_else(|| return extract_account(&accounts, 1)); - let token_b_mint = extract_string_by_candidate_keys( - parsed_json.as_ref(), - &["tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1", "pcMint"], - ) - .or_else(|| return extract_account(&accounts, 2)); - let config_account = extract_string_by_candidate_keys( - parsed_json.as_ref(), - &["config", "poolConfig", "ammConfig", "tradeFeeConfig"], - ) - .or_else(|| return extract_account(&accounts, 3)); - let creator = extract_string_by_candidate_keys( - parsed_json.as_ref(), - &["creator", "payer", "user", "owner"], - ) - .or_else(|| return extract_account(&accounts, 4)); - if instruction_kind == MeteoraDammV1InstructionKind::CreatePool - || instruction_kind == MeteoraDammV1InstructionKind::CreatePoolWithConfig - { - let used_config = - instruction_kind == MeteoraDammV1InstructionKind::CreatePoolWithConfig; - let payload_json = serde_json::json!({ - "decoder": "meteora_damm_v1", - "eventKind": "create_pool", - "dataDiscriminatorHex": instruction_data - .as_ref() - .and_then(|data| return first_8_bytes_hex(data.as_slice())), - "classifiedInstructionKind": if used_config { "create_pool_with_config" } else { "create_pool" }, - "signature": transaction.signature, - "instructionId": instruction_id, - "instructionIndex": instruction.instruction_index, - "accounts": accounts, - "parsed": parsed_json, - "logMessages": log_messages, - "poolAccount": pool_account, - "tokenAMint": token_a_mint, - "tokenBMint": token_b_mint, - "configAccount": config_account, - "creator": creator - }); - decoded_events.push(crate::MeteoraDammV1DecodedEvent::CreatePool( - crate::MeteoraDammV1CreatePoolDecoded { - transaction_id, - instruction_id, - signature: transaction.signature.clone(), - program_id: program_id.clone(), - pool_account, - token_a_mint, - token_b_mint, - config_account, - creator, - used_config, - payload_json, - }, - )); + if is_create_pool_kind(instruction_kind) { + let event = build_create_pool_event( + transaction_id, + transaction, + instruction, + program_id.as_str(), + &accounts, + parsed_json.as_ref(), + instruction_data.as_deref(), + &log_messages, + instruction_kind, + ); + decoded_events.push(crate::MeteoraDammV1DecodedEvent::CreatePool(event)); continue; } if instruction_kind == MeteoraDammV1InstructionKind::Swap { - let decoded_amounts_result = - crate::meteora_swap_amount_inference::infer_meteora_swap_amounts_from_inner_transfers( - transaction, - instructions, - instruction, - pool_account.as_deref(), - ); - let inner_transfer_amounts = match decoded_amounts_result { - Ok(decoded_amounts) => decoded_amounts, + let event_result = build_swap_event( + transaction_id, + transaction, + instructions, + instruction, + program_id.as_str(), + &accounts, + parsed_json.as_ref(), + instruction_data.as_deref(), + &log_messages, + ); + let event = match event_result { + Ok(event) => event, Err(error) => return Err(error), }; - let mut amount_resolution_source = "none"; - let decoded_amounts = match inner_transfer_amounts { - Some(decoded_amounts) => { - amount_resolution_source = "flattened_cpi_pool_transfer_window"; - Some(decoded_amounts) - }, - None => { - let event_log_amounts_result = - crate::meteora_swap_amount_inference::infer_meteora_damm_v1_swap_amounts_from_event_log( - transaction, - accounts.as_slice(), - log_messages.as_slice(), - pool_account.as_deref(), - ); - match event_log_amounts_result { - Ok(event_log_amounts) => match event_log_amounts { - Some(event_log_amounts) => { - amount_resolution_source = "meteora_damm_v1_swap_event_log"; - Some(event_log_amounts) - }, - None => None, - }, - Err(error) => return Err(error), - } - }, - }; - let fallback_trade_side = infer_trade_side(&log_messages); - let trade_side = match decoded_amounts.as_ref() { - Some(decoded_amounts) => decoded_amounts.trade_side, - None => fallback_trade_side, - }; - let effective_token_a_mint = match decoded_amounts.as_ref() { - Some(decoded_amounts) => Some(decoded_amounts.base_token_mint.clone()), - None => token_a_mint.clone(), - }; - let effective_token_b_mint = match decoded_amounts.as_ref() { - Some(decoded_amounts) => Some(decoded_amounts.quote_token_mint.clone()), - None => token_b_mint.clone(), - }; - let base_vault = match decoded_amounts.as_ref() { - Some(decoded_amounts) => decoded_amounts.base_vault_address.clone(), - None => None, - }; - let quote_vault = match decoded_amounts.as_ref() { - Some(decoded_amounts) => decoded_amounts.quote_vault_address.clone(), - None => None, - }; - let base_amount_raw = match decoded_amounts.as_ref() { - Some(decoded_amounts) => { - serde_json::Value::String(decoded_amounts.base_amount_raw.clone()) - }, - None => serde_json::Value::Null, - }; - let quote_amount_raw = match decoded_amounts.as_ref() { - Some(decoded_amounts) => { - serde_json::Value::String(decoded_amounts.quote_amount_raw.clone()) - }, - None => serde_json::Value::Null, - }; - let payload_json = serde_json::json!({ - "decoder": "meteora_damm_v1", - "eventKind": "swap", - "dataDiscriminatorHex": instruction_data - .as_ref() - .and_then(|data| return first_8_bytes_hex(data.as_slice())), - "classifiedInstructionKind": "swap", - "signature": transaction.signature, - "instructionId": instruction_id, - "instructionIndex": instruction.instruction_index, - "accounts": accounts, - "parsed": parsed_json, - "logMessages": log_messages, - "poolAccount": pool_account, - "tokenAMint": effective_token_a_mint.clone(), - "tokenBMint": effective_token_b_mint.clone(), - "inputTokenAccount": extract_account(&accounts, 1), - "outputTokenAccount": extract_account(&accounts, 2), - "baseVault": base_vault, - "quoteVault": quote_vault, - "baseAmountRaw": base_amount_raw, - "quoteAmountRaw": quote_amount_raw, - "amountResolutionSource": amount_resolution_source, - "tradeSide": format!("{:?}", trade_side) - }); - decoded_events.push(crate::MeteoraDammV1DecodedEvent::Swap( - crate::MeteoraDammV1SwapDecoded { - transaction_id, - instruction_id, - signature: transaction.signature.clone(), - program_id: program_id.clone(), - trade_side, - pool_account, - token_a_mint: effective_token_a_mint, - token_b_mint: effective_token_b_mint, - payload_json, - }, - )); + decoded_events.push(crate::MeteoraDammV1DecodedEvent::Swap(event)); + continue; + } + if is_liquidity_kind(instruction_kind) { + let event = build_liquidity_event( + transaction_id, + transaction, + instruction, + program_id.as_str(), + &accounts, + parsed_json.as_ref(), + instruction_data.as_deref(), + &log_messages, + instruction_kind, + ); + decoded_events.push(crate::MeteoraDammV1DecodedEvent::Liquidity(event)); + continue; + } + if instruction_kind == MeteoraDammV1InstructionKind::ClaimFee { + let event = build_claim_fee_event( + transaction_id, + transaction, + instruction, + program_id.as_str(), + &accounts, + parsed_json.as_ref(), + instruction_data.as_deref(), + &log_messages, + ); + decoded_events.push(crate::MeteoraDammV1DecodedEvent::Fee(event)); + continue; + } + if instruction_kind == MeteoraDammV1InstructionKind::CreateLockEscrow { + let event = build_create_lock_escrow_event( + transaction_id, + transaction, + instruction, + program_id.as_str(), + &accounts, + parsed_json.as_ref(), + instruction_data.as_deref(), + &log_messages, + ); + decoded_events.push(crate::MeteoraDammV1DecodedEvent::PoolLifecycle(event)); + continue; + } + if instruction_kind == MeteoraDammV1InstructionKind::Lock { + let event = build_lock_event( + transaction_id, + transaction, + instruction, + program_id.as_str(), + &accounts, + parsed_json.as_ref(), + instruction_data.as_deref(), + &log_messages, + ); + decoded_events.push(crate::MeteoraDammV1DecodedEvent::PoolAdmin(event)); + continue; + } + if is_upstream_git_fee_kind(instruction_kind) { + let event = build_upstream_git_fee_event( + transaction_id, + transaction, + instruction, + program_id.as_str(), + &accounts, + parsed_json.as_ref(), + instruction_data.as_deref(), + &log_messages, + instruction_kind, + ); + decoded_events.push(crate::MeteoraDammV1DecodedEvent::Fee(event)); + continue; + } + if is_upstream_git_pool_lifecycle_kind(instruction_kind) { + let event = build_upstream_git_pool_lifecycle_event( + transaction_id, + transaction, + instruction, + program_id.as_str(), + &accounts, + parsed_json.as_ref(), + instruction_data.as_deref(), + &log_messages, + instruction_kind, + ); + decoded_events.push(crate::MeteoraDammV1DecodedEvent::PoolLifecycle(event)); + continue; + } + if is_upstream_git_pool_admin_kind(instruction_kind) { + let event = build_upstream_git_pool_admin_event( + transaction_id, + transaction, + instruction, + program_id.as_str(), + &accounts, + parsed_json.as_ref(), + instruction_data.as_deref(), + &log_messages, + instruction_kind, + ); + decoded_events.push(crate::MeteoraDammV1DecodedEvent::PoolAdmin(event)); } } return Ok(decoded_events); } } +#[allow(clippy::too_many_arguments)] +fn build_create_pool_event( + transaction_id: i64, + transaction: &crate::ChainTransactionDto, + instruction: &crate::ChainInstructionDto, + program_id: &str, + accounts: &[std::string::String], + parsed_json: std::option::Option<&serde_json::Value>, + instruction_data: std::option::Option<&[u8]>, + log_messages: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> crate::MeteoraDammV1CreatePoolDecoded { + let pool_account = create_pool_pool_account(parsed_json, accounts, instruction_kind); + let token_a_mint = create_pool_token_a_mint(parsed_json, accounts, instruction_kind); + let token_b_mint = create_pool_token_b_mint(parsed_json, accounts, instruction_kind); + let lp_mint = create_pool_lp_mint(accounts, instruction_kind); + let config_account = create_pool_config_account(parsed_json, accounts, instruction_kind); + let creator = create_pool_creator(parsed_json, accounts, instruction_kind); + let used_config = matches!( + instruction_kind, + MeteoraDammV1InstructionKind::CreatePoolWithConfigLegacy + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 + ); + let decoded_instruction = + decoded_create_pool_instruction_payload(instruction_data, instruction_kind); + let payload_json = serde_json::json!({ + "decoder": "meteora_damm_v1", + "eventKind": "meteora_damm_v1.create_pool", + "dataDiscriminatorHex": instruction_data + .and_then(|data| return first_8_bytes_hex(data)), + "classifiedInstructionKind": instruction_kind_code(instruction_kind), + "upstreamInstructionName": upstream_git_instruction_name(instruction_kind), + "proofStatus": proof_status_for_instruction(instruction_kind), + "signature": transaction.signature, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "innerInstruction": instruction.inner_instruction_index.is_some(), + "accounts": accounts, + "accountRoles": create_pool_account_roles(accounts, instruction_kind), + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "tokenAMint": token_a_mint, + "tokenBMint": token_b_mint, + "lpMint": lp_mint, + "configAccount": config_account, + "creator": creator, + "decodedInstruction": decoded_instruction + }); + return crate::MeteoraDammV1CreatePoolDecoded { + transaction_id, + instruction_id: instruction_id_or_zero(instruction), + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + pool_account, + token_a_mint, + token_b_mint, + lp_mint, + config_account, + creator, + used_config, + payload_json, + }; +} + +#[allow(clippy::too_many_arguments)] +fn build_swap_event( + transaction_id: i64, + transaction: &crate::ChainTransactionDto, + instructions: &[crate::ChainInstructionDto], + instruction: &crate::ChainInstructionDto, + program_id: &str, + accounts: &[std::string::String], + parsed_json: std::option::Option<&serde_json::Value>, + instruction_data: std::option::Option<&[u8]>, + log_messages: &[std::string::String], +) -> Result { + let pool_account = extract_string_by_candidate_keys( + parsed_json, + &["pool", "poolAddress", "poolAccount", "amm", "ammPool", "poolState"], + ) + .or_else(|| return extract_account(accounts, 0)); + let token_a_mint = extract_string_by_candidate_keys( + parsed_json, + &["tokenAMint", "mintA", "baseMint", "token0Mint", "mint0", "coinMint"], + ); + let token_b_mint = extract_string_by_candidate_keys( + parsed_json, + &["tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1", "pcMint"], + ); + let decoded_amounts_result = + crate::meteora_swap_amount_inference::infer_meteora_swap_amounts_from_inner_transfers( + transaction, + instructions, + instruction, + pool_account.as_deref(), + ); + let inner_transfer_amounts = match decoded_amounts_result { + Ok(decoded_amounts) => decoded_amounts, + Err(error) => return Err(error), + }; + let mut amount_resolution_source = "none"; + let decoded_amounts = match inner_transfer_amounts { + Some(decoded_amounts) => { + amount_resolution_source = "flattened_cpi_pool_transfer_window"; + Some(decoded_amounts) + }, + None => { + let event_log_amounts_result = + crate::meteora_swap_amount_inference::infer_meteora_damm_v1_swap_amounts_from_event_log( + transaction, + accounts, + log_messages, + pool_account.as_deref(), + ); + match event_log_amounts_result { + Ok(event_log_amounts) => match event_log_amounts { + Some(event_log_amounts) => { + amount_resolution_source = "meteora_damm_v1_swap_event_log"; + Some(event_log_amounts) + }, + None => None, + }, + Err(error) => return Err(error), + } + }, + }; + let fallback_trade_side = infer_trade_side(log_messages); + let trade_side = match decoded_amounts.as_ref() { + Some(decoded_amounts) => decoded_amounts.trade_side, + None => fallback_trade_side, + }; + let effective_token_a_mint = match decoded_amounts.as_ref() { + Some(decoded_amounts) => Some(decoded_amounts.base_token_mint.clone()), + None => token_a_mint.clone(), + }; + let effective_token_b_mint = match decoded_amounts.as_ref() { + Some(decoded_amounts) => Some(decoded_amounts.quote_token_mint.clone()), + None => token_b_mint.clone(), + }; + let base_vault = match decoded_amounts.as_ref() { + Some(decoded_amounts) => decoded_amounts.base_vault_address.clone(), + None => None, + }; + let quote_vault = match decoded_amounts.as_ref() { + Some(decoded_amounts) => decoded_amounts.quote_vault_address.clone(), + None => None, + }; + let base_amount_raw = match decoded_amounts.as_ref() { + Some(decoded_amounts) => serde_json::Value::String(decoded_amounts.base_amount_raw.clone()), + None => serde_json::Value::Null, + }; + let quote_amount_raw = match decoded_amounts.as_ref() { + Some(decoded_amounts) => { + serde_json::Value::String(decoded_amounts.quote_amount_raw.clone()) + }, + None => serde_json::Value::Null, + }; + let payload_json = serde_json::json!({ + "decoder": "meteora_damm_v1", + "eventKind": "swap", + "dataDiscriminatorHex": instruction_data + .and_then(|data| return first_8_bytes_hex(data)), + "classifiedInstructionKind": "swap", + "upstreamInstructionName": "Swap", + "proofStatus": proof_status_for_instruction(MeteoraDammV1InstructionKind::Swap), + "signature": transaction.signature, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "innerInstruction": instruction.inner_instruction_index.is_some(), + "accounts": accounts, + "accountRoles": swap_account_roles(accounts), + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "tokenAMint": effective_token_a_mint.clone(), + "tokenBMint": effective_token_b_mint.clone(), + "inputTokenAccount": extract_account(accounts, 1), + "outputTokenAccount": extract_account(accounts, 2), + "baseVault": base_vault, + "quoteVault": quote_vault, + "baseAmountRaw": base_amount_raw, + "quoteAmountRaw": quote_amount_raw, + "amountResolutionSource": amount_resolution_source, + "tradeSide": format!("{:?}", trade_side), + "decodedInstruction": decoded_swap_instruction_payload(instruction_data) + }); + return Ok(crate::MeteoraDammV1SwapDecoded { + transaction_id, + instruction_id: instruction_id_or_zero(instruction), + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + trade_side, + pool_account, + token_a_mint: effective_token_a_mint, + token_b_mint: effective_token_b_mint, + payload_json, + }); +} + +#[allow(clippy::too_many_arguments)] +fn build_liquidity_event( + transaction_id: i64, + transaction: &crate::ChainTransactionDto, + instruction: &crate::ChainInstructionDto, + program_id: &str, + accounts: &[std::string::String], + parsed_json: std::option::Option<&serde_json::Value>, + instruction_data: std::option::Option<&[u8]>, + log_messages: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> crate::MeteoraDammV1LiquidityDecoded { + let event_kind = liquidity_event_kind(instruction_kind).to_string(); + let pool_account = extract_account(accounts, 0); + let lp_mint = extract_account(accounts, 1); + let actor_wallet = liquidity_actor_wallet(accounts, instruction_kind); + let decoded_instruction = + decoded_liquidity_instruction_payload(instruction_data, instruction_kind); + let base_amount_raw = + decoded_instruction_amount_string(&decoded_instruction, "tokenAAmountRaw"); + let quote_amount_raw = + decoded_instruction_amount_string(&decoded_instruction, "tokenBAmountRaw"); + let lp_amount_raw = decoded_instruction_amount_string(&decoded_instruction, "lpAmountRaw"); + let payload_json = serde_json::json!({ + "decoder": "meteora_damm_v1", + "eventKind": event_kind, + "dataDiscriminatorHex": instruction_data + .and_then(|data| return first_8_bytes_hex(data)), + "classifiedInstructionKind": instruction_kind_code(instruction_kind), + "upstreamInstructionName": upstream_git_instruction_name(instruction_kind), + "proofStatus": proof_status_for_instruction(instruction_kind), + "signature": transaction.signature, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "innerInstruction": instruction.inner_instruction_index.is_some(), + "accounts": accounts, + "accountRoles": liquidity_account_roles(accounts, instruction_kind), + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "lpMint": lp_mint, + "actorWallet": actor_wallet, + "baseAmountRaw": base_amount_raw, + "quoteAmountRaw": quote_amount_raw, + "lpAmountRaw": lp_amount_raw, + "decodedInstruction": decoded_instruction + }); + return crate::MeteoraDammV1LiquidityDecoded { + transaction_id, + instruction_id: instruction_id_or_zero(instruction), + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + token_a_mint: None, + token_b_mint: None, + lp_mint, + actor_wallet, + base_amount_raw, + quote_amount_raw, + lp_amount_raw, + payload_json, + }; +} + +#[allow(clippy::too_many_arguments)] +fn build_claim_fee_event( + transaction_id: i64, + transaction: &crate::ChainTransactionDto, + instruction: &crate::ChainInstructionDto, + program_id: &str, + accounts: &[std::string::String], + parsed_json: std::option::Option<&serde_json::Value>, + instruction_data: std::option::Option<&[u8]>, + log_messages: &[std::string::String], +) -> crate::MeteoraDammV1FeeDecoded { + let event_kind = "meteora_damm_v1.claim_fee".to_string(); + let pool_account = extract_account(accounts, 0); + let lp_mint = extract_account(accounts, 1); + let actor_wallet = extract_account(accounts, 3); + let decoded_instruction = decoded_claim_fee_instruction_payload(instruction_data); + let max_amount_raw = decoded_instruction_amount_string(&decoded_instruction, "maxAmountRaw"); + let payload_json = serde_json::json!({ + "decoder": "meteora_damm_v1", + "eventKind": event_kind, + "dataDiscriminatorHex": instruction_data + .and_then(|data| return first_8_bytes_hex(data)), + "classifiedInstructionKind": "claim_fee", + "upstreamInstructionName": "ClaimFee", + "proofStatus": proof_status_for_instruction(MeteoraDammV1InstructionKind::ClaimFee), + "signature": transaction.signature, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "innerInstruction": instruction.inner_instruction_index.is_some(), + "accounts": accounts, + "accountRoles": claim_fee_account_roles(accounts), + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "lpMint": lp_mint, + "owner": actor_wallet, + "actorWallet": actor_wallet, + "maxAmountRaw": max_amount_raw, + "feeAmountRaw": serde_json::Value::Null, + "decodedInstruction": decoded_instruction + }); + return crate::MeteoraDammV1FeeDecoded { + transaction_id, + instruction_id: instruction_id_or_zero(instruction), + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + lp_mint, + actor_wallet, + fee_amount_raw: None, + payload_json, + }; +} + +#[allow(clippy::too_many_arguments)] +fn build_create_lock_escrow_event( + transaction_id: i64, + transaction: &crate::ChainTransactionDto, + instruction: &crate::ChainInstructionDto, + program_id: &str, + accounts: &[std::string::String], + parsed_json: std::option::Option<&serde_json::Value>, + instruction_data: std::option::Option<&[u8]>, + log_messages: &[std::string::String], +) -> crate::MeteoraDammV1PoolLifecycleDecoded { + let event_kind = "meteora_damm_v1.create_lock_escrow".to_string(); + let pool_account = extract_account(accounts, 0); + let lp_mint = extract_account(accounts, 3); + let payload_json = serde_json::json!({ + "decoder": "meteora_damm_v1", + "eventKind": event_kind, + "dataDiscriminatorHex": instruction_data + .and_then(|data| return first_8_bytes_hex(data)), + "classifiedInstructionKind": "create_lock_escrow", + "upstreamInstructionName": "CreateLockEscrow", + "proofStatus": proof_status_for_instruction(MeteoraDammV1InstructionKind::CreateLockEscrow), + "signature": transaction.signature, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "innerInstruction": instruction.inner_instruction_index.is_some(), + "accounts": accounts, + "accountRoles": create_lock_escrow_account_roles(accounts), + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "lpMint": lp_mint, + "lockEscrow": extract_account(accounts, 1), + "owner": extract_account(accounts, 2), + "payer": extract_account(accounts, 4), + "decodedInstruction": {} + }); + return crate::MeteoraDammV1PoolLifecycleDecoded { + transaction_id, + instruction_id: instruction_id_or_zero(instruction), + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + token_a_mint: None, + token_b_mint: None, + lp_mint, + payload_json, + }; +} + +#[allow(clippy::too_many_arguments)] +fn build_lock_event( + transaction_id: i64, + transaction: &crate::ChainTransactionDto, + instruction: &crate::ChainInstructionDto, + program_id: &str, + accounts: &[std::string::String], + parsed_json: std::option::Option<&serde_json::Value>, + instruction_data: std::option::Option<&[u8]>, + log_messages: &[std::string::String], +) -> crate::MeteoraDammV1PoolAdminDecoded { + let event_kind = "meteora_damm_v1.lock_liquidity".to_string(); + let pool_account = extract_account(accounts, 0); + let actor_wallet = extract_account(accounts, 3); + let decoded_instruction = decoded_lock_instruction_payload(instruction_data); + let max_amount_raw = decoded_instruction_amount_string(&decoded_instruction, "maxAmountRaw"); + let payload_json = serde_json::json!({ + "decoder": "meteora_damm_v1", + "eventKind": event_kind, + "dataDiscriminatorHex": instruction_data + .and_then(|data| return first_8_bytes_hex(data)), + "classifiedInstructionKind": "lock_liquidity", + "upstreamInstructionName": "Lock", + "proofStatus": proof_status_for_instruction(MeteoraDammV1InstructionKind::Lock), + "signature": transaction.signature, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "innerInstruction": instruction.inner_instruction_index.is_some(), + "accounts": accounts, + "accountRoles": lock_account_roles(accounts), + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "lpMint": extract_account(accounts, 1), + "lockEscrow": extract_account(accounts, 2), + "owner": actor_wallet, + "actorWallet": actor_wallet, + "adminAction": "lock_liquidity", + "maxAmountRaw": max_amount_raw, + "decodedInstruction": decoded_instruction + }); + return crate::MeteoraDammV1PoolAdminDecoded { + transaction_id, + instruction_id: instruction_id_or_zero(instruction), + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + actor_wallet, + admin_action: Some("lock_liquidity".to_string()), + payload_json, + }; +} + +#[allow(clippy::too_many_arguments)] +fn build_upstream_git_fee_event( + transaction_id: i64, + transaction: &crate::ChainTransactionDto, + instruction: &crate::ChainInstructionDto, + program_id: &str, + accounts: &[std::string::String], + parsed_json: std::option::Option<&serde_json::Value>, + instruction_data: std::option::Option<&[u8]>, + log_messages: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> crate::MeteoraDammV1FeeDecoded { + let event_kind = fee_event_kind(instruction_kind).to_string(); + let pool_account = upstream_git_pool_account(accounts, instruction_data, instruction_kind); + let lp_mint = upstream_git_lp_mint(accounts, instruction_kind); + let actor_wallet = upstream_git_actor_wallet(accounts, instruction_data, instruction_kind); + let decoded_instruction = + decoded_upstream_git_instruction_payload(instruction_data, instruction_kind); + let fee_amount_raw = decoded_instruction_amount_string(&decoded_instruction, "feeAmountRaw") + .or_else(|| { + return decoded_instruction_amount_string(&decoded_instruction, "protocolAFeeRaw"); + }) + .or_else(|| return decoded_instruction_amount_string(&decoded_instruction, "feeARaw")) + .or_else(|| return decoded_instruction_amount_string(&decoded_instruction, "amountRaw")); + let payload_json = serde_json::json!({ + "decoder": "meteora_damm_v1", + "eventKind": event_kind, + "dataDiscriminatorHex": instruction_data + .and_then(|data| return discriminator_hex_for_kind(data, instruction_kind)), + "classifiedInstructionKind": instruction_kind_code(instruction_kind), + "upstreamInstructionName": upstream_git_instruction_name(instruction_kind), + "proofStatus": proof_status_for_instruction(instruction_kind), + "signature": transaction.signature, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "innerInstruction": instruction.inner_instruction_index.is_some(), + "accounts": accounts, + "accountRoles": upstream_git_account_roles(accounts, instruction_kind), + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "lpMint": lp_mint, + "actorWallet": actor_wallet, + "feeAmountRaw": fee_amount_raw, + "decodedInstruction": decoded_instruction + }); + return crate::MeteoraDammV1FeeDecoded { + transaction_id, + instruction_id: instruction_id_or_zero(instruction), + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + lp_mint, + actor_wallet, + fee_amount_raw, + payload_json, + }; +} + +#[allow(clippy::too_many_arguments)] +fn build_upstream_git_pool_lifecycle_event( + transaction_id: i64, + transaction: &crate::ChainTransactionDto, + instruction: &crate::ChainInstructionDto, + program_id: &str, + accounts: &[std::string::String], + parsed_json: std::option::Option<&serde_json::Value>, + instruction_data: std::option::Option<&[u8]>, + log_messages: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> crate::MeteoraDammV1PoolLifecycleDecoded { + let event_kind = pool_lifecycle_event_kind(instruction_kind).to_string(); + let decoded_instruction = + decoded_upstream_git_instruction_payload(instruction_data, instruction_kind); + let pool_account = upstream_git_pool_account(accounts, instruction_data, instruction_kind); + let token_a_mint = upstream_git_token_a_mint(accounts, instruction_data, instruction_kind); + let token_b_mint = upstream_git_token_b_mint(accounts, instruction_data, instruction_kind); + let lp_mint = upstream_git_lp_mint(accounts, instruction_kind); + let payload_json = serde_json::json!({ + "decoder": "meteora_damm_v1", + "eventKind": event_kind, + "dataDiscriminatorHex": instruction_data + .and_then(|data| return discriminator_hex_for_kind(data, instruction_kind)), + "classifiedInstructionKind": instruction_kind_code(instruction_kind), + "upstreamInstructionName": upstream_git_instruction_name(instruction_kind), + "proofStatus": proof_status_for_instruction(instruction_kind), + "signature": transaction.signature, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "innerInstruction": instruction.inner_instruction_index.is_some(), + "accounts": accounts, + "accountRoles": upstream_git_account_roles(accounts, instruction_kind), + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "tokenAMint": token_a_mint, + "tokenBMint": token_b_mint, + "lpMint": lp_mint, + "decodedInstruction": decoded_instruction + }); + return crate::MeteoraDammV1PoolLifecycleDecoded { + transaction_id, + instruction_id: instruction_id_or_zero(instruction), + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + token_a_mint, + token_b_mint, + lp_mint, + payload_json, + }; +} + +#[allow(clippy::too_many_arguments)] +fn build_upstream_git_pool_admin_event( + transaction_id: i64, + transaction: &crate::ChainTransactionDto, + instruction: &crate::ChainInstructionDto, + program_id: &str, + accounts: &[std::string::String], + parsed_json: std::option::Option<&serde_json::Value>, + instruction_data: std::option::Option<&[u8]>, + log_messages: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> crate::MeteoraDammV1PoolAdminDecoded { + let event_kind = pool_admin_event_kind(instruction_kind).to_string(); + let pool_account = upstream_git_pool_account(accounts, instruction_data, instruction_kind); + let actor_wallet = upstream_git_actor_wallet(accounts, instruction_data, instruction_kind); + let admin_action = Some(instruction_kind_code(instruction_kind).to_string()); + let decoded_instruction = + decoded_upstream_git_instruction_payload(instruction_data, instruction_kind); + let payload_json = serde_json::json!({ + "decoder": "meteora_damm_v1", + "eventKind": event_kind, + "dataDiscriminatorHex": instruction_data + .and_then(|data| return discriminator_hex_for_kind(data, instruction_kind)), + "classifiedInstructionKind": instruction_kind_code(instruction_kind), + "upstreamInstructionName": upstream_git_instruction_name(instruction_kind), + "proofStatus": proof_status_for_instruction(instruction_kind), + "signature": transaction.signature, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "innerInstructionIndex": instruction.inner_instruction_index, + "innerInstruction": instruction.inner_instruction_index.is_some(), + "accounts": accounts, + "accountRoles": upstream_git_account_roles(accounts, instruction_kind), + "parsed": parsed_json, + "logMessages": log_messages, + "poolAccount": pool_account, + "actorWallet": actor_wallet, + "adminAction": admin_action.clone(), + "decodedInstruction": decoded_instruction + }); + return crate::MeteoraDammV1PoolAdminDecoded { + transaction_id, + instruction_id: instruction_id_or_zero(instruction), + signature: transaction.signature.clone(), + program_id: program_id.to_string(), + event_kind, + pool_account, + actor_wallet, + admin_action, + payload_json, + }; +} + +fn instruction_id_or_zero(instruction: &crate::ChainInstructionDto) -> i64 { + match instruction.id { + Some(instruction_id) => return instruction_id, + None => return 0, + } +} + fn classify_instruction_kind( parsed_json: std::option::Option<&serde_json::Value>, instruction_data: std::option::Option<&[u8]>, @@ -353,28 +1179,37 @@ fn classify_instruction_kind( ); if let Some(parsed_instruction_name) = parsed_instruction_name { let normalized = normalize_text(parsed_instruction_name.as_str()); + if normalized.contains("initializepermissionlessconstantproductpoolwithconfig2") { + return MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2; + } + if normalized.contains("initializepermissionlessconstantproductpoolwithconfig") { + return MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig; + } + if normalized.contains("initializecustomizablepermissionlessconstantproductpool") { + return MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool; + } if normalized.contains("initializepoolwithconfig") { - return MeteoraDammV1InstructionKind::CreatePoolWithConfig; + return MeteoraDammV1InstructionKind::CreatePoolWithConfigLegacy; } if normalized.contains("initializepool") { - return MeteoraDammV1InstructionKind::CreatePool; + return MeteoraDammV1InstructionKind::CreatePoolLegacy; } if normalized == "swap" { return MeteoraDammV1InstructionKind::Swap; } } if value_contains_any_key(parsed_json, &["poolConfig", "ammConfig", "tradeFeeConfig"]) { - return MeteoraDammV1InstructionKind::CreatePoolWithConfig; + return MeteoraDammV1InstructionKind::CreatePoolWithConfigLegacy; } if log_messages_contain_keyword(log_messages, "initialize_pool_with_config") || log_messages_contain_keyword(log_messages, "initializepoolwithconfig") { - return MeteoraDammV1InstructionKind::CreatePoolWithConfig; + return MeteoraDammV1InstructionKind::CreatePoolWithConfigLegacy; } if log_messages_contain_keyword(log_messages, "initialize_pool") || log_messages_contain_keyword(log_messages, "initializepool") { - return MeteoraDammV1InstructionKind::CreatePool; + return MeteoraDammV1InstructionKind::CreatePoolLegacy; } if log_messages_contain_keyword(log_messages, "swap") { return MeteoraDammV1InstructionKind::Swap; @@ -400,28 +1235,1538 @@ fn classify_instruction_kind_from_data( if instruction_data.len() < 8 { return MeteoraDammV1InstructionKind::Unknown; } - let discriminator = [ - instruction_data[0], - instruction_data[1], - instruction_data[2], - instruction_data[3], - instruction_data[4], - instruction_data[5], - instruction_data[6], - instruction_data[7], - ]; - if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL_WITH_CONFIG { - return MeteoraDammV1InstructionKind::CreatePoolWithConfig; + if instruction_data.len() >= 16 { + let event_discriminator = first_16_bytes(instruction_data); + let event_kind = classify_event_kind_from_data(event_discriminator); + if event_kind != MeteoraDammV1InstructionKind::Unknown { + return event_kind; + } + } + let discriminator = first_8_bytes(instruction_data); + if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL_WITH_CONFIG_LEGACY { + return MeteoraDammV1InstructionKind::CreatePoolWithConfigLegacy; } if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL { - return MeteoraDammV1InstructionKind::CreatePool; + return MeteoraDammV1InstructionKind::CreatePoolLegacy; + } + if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG { + return MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig; + } + if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG2 { + return MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2; + } + if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_CP_POOL { + return MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool; } if discriminator == DAMM_V1_DISCRIMINATOR_SWAP { return MeteoraDammV1InstructionKind::Swap; } + if discriminator == DAMM_V1_DISCRIMINATOR_ADD_BALANCE_LIQUIDITY { + return MeteoraDammV1InstructionKind::AddBalanceLiquidity; + } + if discriminator == DAMM_V1_DISCRIMINATOR_ADD_IMBALANCE_LIQUIDITY { + return MeteoraDammV1InstructionKind::AddImbalanceLiquidity; + } + if discriminator == DAMM_V1_DISCRIMINATOR_BOOTSTRAP_LIQUIDITY { + return MeteoraDammV1InstructionKind::BootstrapLiquidity; + } + if discriminator == DAMM_V1_DISCRIMINATOR_REMOVE_BALANCE_LIQUIDITY { + return MeteoraDammV1InstructionKind::RemoveBalanceLiquidity; + } + if discriminator == DAMM_V1_DISCRIMINATOR_REMOVE_LIQUIDITY_SINGLE_SIDE { + return MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide; + } + if discriminator == DAMM_V1_DISCRIMINATOR_CLAIM_FEE { + return MeteoraDammV1InstructionKind::ClaimFee; + } + if discriminator == DAMM_V1_DISCRIMINATOR_CREATE_LOCK_ESCROW { + return MeteoraDammV1InstructionKind::CreateLockEscrow; + } + if discriminator == DAMM_V1_DISCRIMINATOR_LOCK { + return MeteoraDammV1InstructionKind::Lock; + } + if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONED_POOL { + return MeteoraDammV1InstructionKind::InitializePermissionedPool; + } + if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_POOL { + return MeteoraDammV1InstructionKind::InitializePermissionlessPool; + } + if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_POOL_WITH_FEE_TIER { + return MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier; + } + if discriminator == DAMM_V1_DISCRIMINATOR_ENABLE_OR_DISABLE_POOL { + return MeteoraDammV1InstructionKind::EnableOrDisablePool; + } + if discriminator == DAMM_V1_DISCRIMINATOR_SET_POOL_FEES { + return MeteoraDammV1InstructionKind::SetPoolFees; + } + if discriminator == DAMM_V1_DISCRIMINATOR_OVERRIDE_CURVE_PARAM { + return MeteoraDammV1InstructionKind::OverrideCurveParam; + } + if discriminator == DAMM_V1_DISCRIMINATOR_GET_POOL_INFO { + return MeteoraDammV1InstructionKind::GetPoolInfo; + } + if discriminator == DAMM_V1_DISCRIMINATOR_CREATE_MINT_METADATA { + return MeteoraDammV1InstructionKind::CreateMintMetadata; + } + if discriminator == DAMM_V1_DISCRIMINATOR_CREATE_CONFIG { + return MeteoraDammV1InstructionKind::CreateConfig; + } + if discriminator == DAMM_V1_DISCRIMINATOR_CLOSE_CONFIG { + return MeteoraDammV1InstructionKind::CloseConfig; + } + if discriminator == DAMM_V1_DISCRIMINATOR_UPDATE_ACTIVATION_POINT { + return MeteoraDammV1InstructionKind::UpdateActivationPoint; + } + if discriminator == DAMM_V1_DISCRIMINATOR_WITHDRAW_PROTOCOL_FEES { + return MeteoraDammV1InstructionKind::WithdrawProtocolFees; + } + if discriminator == DAMM_V1_DISCRIMINATOR_SET_WHITELISTED_VAULT { + return MeteoraDammV1InstructionKind::SetWhitelistedVault; + } + if discriminator == DAMM_V1_DISCRIMINATOR_PARTNER_CLAIM_FEE { + return MeteoraDammV1InstructionKind::PartnerClaimFee; + } return MeteoraDammV1InstructionKind::Unknown; } +fn classify_event_kind_from_data(discriminator: [u8; 16]) -> MeteoraDammV1InstructionKind { + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_ADD_LIQUIDITY { + return MeteoraDammV1InstructionKind::AddLiquidityEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_REMOVE_LIQUIDITY { + return MeteoraDammV1InstructionKind::RemoveLiquidityEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_BOOTSTRAP_LIQUIDITY { + return MeteoraDammV1InstructionKind::BootstrapLiquidityEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_SWAP { + return MeteoraDammV1InstructionKind::SwapEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_SET_POOL_FEES { + return MeteoraDammV1InstructionKind::SetPoolFeesEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_POOL_INFO { + return MeteoraDammV1InstructionKind::PoolInfoEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_TRANSFER_ADMIN { + return MeteoraDammV1InstructionKind::TransferAdminEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_OVERRIDE_CURVE_PARAM { + return MeteoraDammV1InstructionKind::OverrideCurveParamEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_POOL_CREATED { + return MeteoraDammV1InstructionKind::PoolCreatedEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_POOL_ENABLED { + return MeteoraDammV1InstructionKind::PoolEnabledEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_MIGRATE_FEE_ACCOUNT { + return MeteoraDammV1InstructionKind::MigrateFeeAccountEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_CREATE_LOCK_ESCROW { + return MeteoraDammV1InstructionKind::CreateLockEscrowEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_LOCK { + return MeteoraDammV1InstructionKind::LockEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_CLAIM_FEE { + return MeteoraDammV1InstructionKind::ClaimFeeEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_CREATE_CONFIG { + return MeteoraDammV1InstructionKind::CreateConfigEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_CLOSE_CONFIG { + return MeteoraDammV1InstructionKind::CloseConfigEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_WITHDRAW_PROTOCOL_FEES { + return MeteoraDammV1InstructionKind::WithdrawProtocolFeesEvent; + } + if discriminator == DAMM_V1_EVENT_DISCRIMINATOR_PARTNER_CLAIM_FEES { + return MeteoraDammV1InstructionKind::PartnerClaimFeesEvent; + } + return MeteoraDammV1InstructionKind::Unknown; +} + +fn is_create_pool_kind(instruction_kind: MeteoraDammV1InstructionKind) -> bool { + return matches!( + instruction_kind, + MeteoraDammV1InstructionKind::InitializePermissionedPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier + | MeteoraDammV1InstructionKind::CreatePoolLegacy + | MeteoraDammV1InstructionKind::CreatePoolWithConfigLegacy + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 + | MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool + | MeteoraDammV1InstructionKind::PoolCreatedEvent + ); +} + +fn is_liquidity_kind(instruction_kind: MeteoraDammV1InstructionKind) -> bool { + return matches!( + instruction_kind, + MeteoraDammV1InstructionKind::AddBalanceLiquidity + | MeteoraDammV1InstructionKind::AddImbalanceLiquidity + | MeteoraDammV1InstructionKind::BootstrapLiquidity + | MeteoraDammV1InstructionKind::RemoveBalanceLiquidity + | MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide + | MeteoraDammV1InstructionKind::AddLiquidityEvent + | MeteoraDammV1InstructionKind::RemoveLiquidityEvent + | MeteoraDammV1InstructionKind::BootstrapLiquidityEvent + ); +} + +fn liquidity_event_kind(instruction_kind: MeteoraDammV1InstructionKind) -> &'static str { + match instruction_kind { + MeteoraDammV1InstructionKind::AddBalanceLiquidity + | MeteoraDammV1InstructionKind::AddImbalanceLiquidity + | MeteoraDammV1InstructionKind::BootstrapLiquidity + | MeteoraDammV1InstructionKind::AddLiquidityEvent + | MeteoraDammV1InstructionKind::BootstrapLiquidityEvent => { + return "meteora_damm_v1.add_liquidity"; + }, + MeteoraDammV1InstructionKind::RemoveBalanceLiquidity + | MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide + | MeteoraDammV1InstructionKind::RemoveLiquidityEvent => { + return "meteora_damm_v1.remove_liquidity"; + }, + _ => return "meteora_damm_v1.liquidity_unknown", + } +} + +fn is_upstream_git_fee_kind(instruction_kind: MeteoraDammV1InstructionKind) -> bool { + return matches!( + instruction_kind, + MeteoraDammV1InstructionKind::WithdrawProtocolFees + | MeteoraDammV1InstructionKind::PartnerClaimFee + | MeteoraDammV1InstructionKind::ClaimFeeEvent + | MeteoraDammV1InstructionKind::WithdrawProtocolFeesEvent + | MeteoraDammV1InstructionKind::PartnerClaimFeesEvent + ); +} + +fn is_upstream_git_pool_lifecycle_kind(instruction_kind: MeteoraDammV1InstructionKind) -> bool { + return matches!( + instruction_kind, + MeteoraDammV1InstructionKind::CreateMintMetadata + | MeteoraDammV1InstructionKind::CreateLockEscrowEvent + | MeteoraDammV1InstructionKind::PoolCreatedEvent + ); +} + +fn is_upstream_git_pool_admin_kind(instruction_kind: MeteoraDammV1InstructionKind) -> bool { + return matches!( + instruction_kind, + MeteoraDammV1InstructionKind::EnableOrDisablePool + | MeteoraDammV1InstructionKind::SetPoolFees + | MeteoraDammV1InstructionKind::OverrideCurveParam + | MeteoraDammV1InstructionKind::GetPoolInfo + | MeteoraDammV1InstructionKind::CreateConfig + | MeteoraDammV1InstructionKind::CloseConfig + | MeteoraDammV1InstructionKind::UpdateActivationPoint + | MeteoraDammV1InstructionKind::SetWhitelistedVault + | MeteoraDammV1InstructionKind::SwapEvent + | MeteoraDammV1InstructionKind::SetPoolFeesEvent + | MeteoraDammV1InstructionKind::PoolInfoEvent + | MeteoraDammV1InstructionKind::TransferAdminEvent + | MeteoraDammV1InstructionKind::OverrideCurveParamEvent + | MeteoraDammV1InstructionKind::PoolEnabledEvent + | MeteoraDammV1InstructionKind::MigrateFeeAccountEvent + | MeteoraDammV1InstructionKind::LockEvent + | MeteoraDammV1InstructionKind::CreateConfigEvent + | MeteoraDammV1InstructionKind::CloseConfigEvent + ); +} + +fn fee_event_kind(instruction_kind: MeteoraDammV1InstructionKind) -> &'static str { + match instruction_kind { + MeteoraDammV1InstructionKind::WithdrawProtocolFees + | MeteoraDammV1InstructionKind::WithdrawProtocolFeesEvent => { + return "meteora_damm_v1.withdraw_protocol_fees"; + }, + MeteoraDammV1InstructionKind::PartnerClaimFee + | MeteoraDammV1InstructionKind::PartnerClaimFeesEvent => { + return "meteora_damm_v1.partner_claim_fee"; + }, + MeteoraDammV1InstructionKind::ClaimFeeEvent => return "meteora_damm_v1.claim_fee_event", + _ => return "meteora_damm_v1.fee_event", + } +} + +fn pool_lifecycle_event_kind(instruction_kind: MeteoraDammV1InstructionKind) -> &'static str { + match instruction_kind { + MeteoraDammV1InstructionKind::CreateMintMetadata => { + return "meteora_damm_v1.create_mint_metadata"; + }, + MeteoraDammV1InstructionKind::CreateLockEscrowEvent => { + return "meteora_damm_v1.create_lock_escrow_event"; + }, + MeteoraDammV1InstructionKind::PoolCreatedEvent => { + return "meteora_damm_v1.create_pool_event"; + }, + _ => return "meteora_damm_v1.pool_lifecycle_event", + } +} + +fn pool_admin_event_kind(instruction_kind: MeteoraDammV1InstructionKind) -> &'static str { + match instruction_kind { + MeteoraDammV1InstructionKind::EnableOrDisablePool + | MeteoraDammV1InstructionKind::PoolEnabledEvent => { + return "meteora_damm_v1.set_pool_enabled"; + }, + MeteoraDammV1InstructionKind::SetPoolFees + | MeteoraDammV1InstructionKind::SetPoolFeesEvent => { + return "meteora_damm_v1.set_pool_fees"; + }, + MeteoraDammV1InstructionKind::OverrideCurveParam + | MeteoraDammV1InstructionKind::OverrideCurveParamEvent => { + return "meteora_damm_v1.override_curve_param"; + }, + MeteoraDammV1InstructionKind::GetPoolInfo | MeteoraDammV1InstructionKind::PoolInfoEvent => { + return "meteora_damm_v1.pool_info"; + }, + MeteoraDammV1InstructionKind::CreateConfig + | MeteoraDammV1InstructionKind::CreateConfigEvent => return "meteora_damm_v1.create_config", + MeteoraDammV1InstructionKind::CloseConfig + | MeteoraDammV1InstructionKind::CloseConfigEvent => return "meteora_damm_v1.close_config", + MeteoraDammV1InstructionKind::UpdateActivationPoint => { + return "meteora_damm_v1.update_activation_point"; + }, + MeteoraDammV1InstructionKind::SetWhitelistedVault => { + return "meteora_damm_v1.set_whitelisted_vault"; + }, + MeteoraDammV1InstructionKind::TransferAdminEvent => return "meteora_damm_v1.transfer_admin", + MeteoraDammV1InstructionKind::MigrateFeeAccountEvent => { + return "meteora_damm_v1.migrate_fee_account"; + }, + MeteoraDammV1InstructionKind::LockEvent => return "meteora_damm_v1.lock_liquidity_event", + MeteoraDammV1InstructionKind::SwapEvent => return "meteora_damm_v1.upstream_git_event_swap", + _ => return "meteora_damm_v1.pool_admin", + } +} + +fn instruction_kind_code(instruction_kind: MeteoraDammV1InstructionKind) -> &'static str { + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionedPool => { + return "initialize_permissioned_pool"; + }, + MeteoraDammV1InstructionKind::InitializePermissionlessPool => { + return "initialize_permissionless_pool"; + }, + MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier => { + return "initialize_permissionless_pool_with_fee_tier"; + }, + MeteoraDammV1InstructionKind::CreatePoolLegacy => return "initialize_pool_legacy", + MeteoraDammV1InstructionKind::CreatePoolWithConfigLegacy => { + return "initialize_pool_with_config_legacy"; + }, + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig => { + return "initialize_permissionless_constant_product_pool_with_config"; + }, + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return "initialize_permissionless_constant_product_pool_with_config2"; + }, + MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return "initialize_customizable_permissionless_constant_product_pool"; + }, + MeteoraDammV1InstructionKind::Swap => return "swap", + MeteoraDammV1InstructionKind::AddBalanceLiquidity => return "add_balance_liquidity", + MeteoraDammV1InstructionKind::AddImbalanceLiquidity => return "add_imbalance_liquidity", + MeteoraDammV1InstructionKind::BootstrapLiquidity => return "bootstrap_liquidity", + MeteoraDammV1InstructionKind::RemoveBalanceLiquidity => return "remove_balance_liquidity", + MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide => { + return "remove_liquidity_single_side"; + }, + MeteoraDammV1InstructionKind::ClaimFee => return "claim_fee", + MeteoraDammV1InstructionKind::CreateLockEscrow => return "create_lock_escrow", + MeteoraDammV1InstructionKind::Lock => return "lock", + MeteoraDammV1InstructionKind::EnableOrDisablePool => return "enable_or_disable_pool", + MeteoraDammV1InstructionKind::SetPoolFees => return "set_pool_fees", + MeteoraDammV1InstructionKind::OverrideCurveParam => return "override_curve_param", + MeteoraDammV1InstructionKind::GetPoolInfo => return "get_pool_info", + MeteoraDammV1InstructionKind::CreateMintMetadata => return "create_mint_metadata", + MeteoraDammV1InstructionKind::CreateConfig => return "create_config", + MeteoraDammV1InstructionKind::CloseConfig => return "close_config", + MeteoraDammV1InstructionKind::UpdateActivationPoint => return "update_activation_point", + MeteoraDammV1InstructionKind::WithdrawProtocolFees => return "withdraw_protocol_fees", + MeteoraDammV1InstructionKind::SetWhitelistedVault => return "set_whitelisted_vault", + MeteoraDammV1InstructionKind::PartnerClaimFee => return "partner_claim_fee", + MeteoraDammV1InstructionKind::AddLiquidityEvent => return "add_liquidity_event", + MeteoraDammV1InstructionKind::RemoveLiquidityEvent => return "remove_liquidity_event", + MeteoraDammV1InstructionKind::BootstrapLiquidityEvent => return "bootstrap_liquidity_event", + MeteoraDammV1InstructionKind::SwapEvent => return "swap_event", + MeteoraDammV1InstructionKind::SetPoolFeesEvent => return "set_pool_fees_event", + MeteoraDammV1InstructionKind::PoolInfoEvent => return "pool_info_event", + MeteoraDammV1InstructionKind::TransferAdminEvent => return "transfer_admin_event", + MeteoraDammV1InstructionKind::OverrideCurveParamEvent => { + return "override_curve_param_event"; + }, + MeteoraDammV1InstructionKind::PoolCreatedEvent => return "pool_created_event", + MeteoraDammV1InstructionKind::PoolEnabledEvent => return "pool_enabled_event", + MeteoraDammV1InstructionKind::MigrateFeeAccountEvent => return "migrate_fee_account_event", + MeteoraDammV1InstructionKind::CreateLockEscrowEvent => return "create_lock_escrow_event", + MeteoraDammV1InstructionKind::LockEvent => return "lock_event", + MeteoraDammV1InstructionKind::ClaimFeeEvent => return "claim_fee_event", + MeteoraDammV1InstructionKind::CreateConfigEvent => return "create_config_event", + MeteoraDammV1InstructionKind::CloseConfigEvent => return "close_config_event", + MeteoraDammV1InstructionKind::WithdrawProtocolFeesEvent => { + return "withdraw_protocol_fees_event"; + }, + MeteoraDammV1InstructionKind::PartnerClaimFeesEvent => return "partner_claim_fees_event", + MeteoraDammV1InstructionKind::Unknown => return "unknown", + } +} + +fn upstream_git_instruction_name(instruction_kind: MeteoraDammV1InstructionKind) -> &'static str { + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionedPool => { + return "InitializePermissionedPool"; + }, + MeteoraDammV1InstructionKind::InitializePermissionlessPool => { + return "InitializePermissionlessPool"; + }, + MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier => { + return "InitializePermissionlessPoolWithFeeTier"; + }, + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig => { + return "InitializePermissionlessConstantProductPoolWithConfig"; + }, + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return "InitializePermissionlessConstantProductPoolWithConfig2"; + }, + MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return "InitializeCustomizablePermissionlessConstantProductPool"; + }, + MeteoraDammV1InstructionKind::AddBalanceLiquidity => return "AddBalanceLiquidity", + MeteoraDammV1InstructionKind::AddImbalanceLiquidity => return "AddImbalanceLiquidity", + MeteoraDammV1InstructionKind::BootstrapLiquidity => return "BootstrapLiquidity", + MeteoraDammV1InstructionKind::RemoveBalanceLiquidity => return "RemoveBalanceLiquidity", + MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide => { + return "RemoveLiquiditySingleSide"; + }, + MeteoraDammV1InstructionKind::ClaimFee => return "ClaimFee", + MeteoraDammV1InstructionKind::CreateLockEscrow => return "CreateLockEscrow", + MeteoraDammV1InstructionKind::Lock => return "Lock", + MeteoraDammV1InstructionKind::Swap => return "Swap", + MeteoraDammV1InstructionKind::CreatePoolLegacy => return "InitializePool", + MeteoraDammV1InstructionKind::CreatePoolWithConfigLegacy => { + return "InitializePoolWithConfig"; + }, + MeteoraDammV1InstructionKind::EnableOrDisablePool => return "EnableOrDisablePool", + MeteoraDammV1InstructionKind::SetPoolFees => return "SetPoolFees", + MeteoraDammV1InstructionKind::OverrideCurveParam => return "OverrideCurveParam", + MeteoraDammV1InstructionKind::GetPoolInfo => return "GetPoolInfo", + MeteoraDammV1InstructionKind::CreateMintMetadata => return "CreateMintMetadata", + MeteoraDammV1InstructionKind::CreateConfig => return "CreateConfig", + MeteoraDammV1InstructionKind::CloseConfig => return "CloseConfig", + MeteoraDammV1InstructionKind::UpdateActivationPoint => return "UpdateActivationPoint", + MeteoraDammV1InstructionKind::WithdrawProtocolFees => return "WithdrawProtocolFees", + MeteoraDammV1InstructionKind::SetWhitelistedVault => return "SetWhitelistedVault", + MeteoraDammV1InstructionKind::PartnerClaimFee => return "PartnerClaimFee", + MeteoraDammV1InstructionKind::AddLiquidityEvent => return "AddLiquidityEvent", + MeteoraDammV1InstructionKind::RemoveLiquidityEvent => return "RemoveLiquidityEvent", + MeteoraDammV1InstructionKind::BootstrapLiquidityEvent => return "BootstrapLiquidityEvent", + MeteoraDammV1InstructionKind::SwapEvent => return "SwapEvent", + MeteoraDammV1InstructionKind::SetPoolFeesEvent => return "SetPoolFeesEvent", + MeteoraDammV1InstructionKind::PoolInfoEvent => return "PoolInfoEvent", + MeteoraDammV1InstructionKind::TransferAdminEvent => return "TransferAdminEvent", + MeteoraDammV1InstructionKind::OverrideCurveParamEvent => return "OverrideCurveParamEvent", + MeteoraDammV1InstructionKind::PoolCreatedEvent => return "PoolCreatedEvent", + MeteoraDammV1InstructionKind::PoolEnabledEvent => return "PoolEnabledEvent", + MeteoraDammV1InstructionKind::MigrateFeeAccountEvent => return "MigrateFeeAccountEvent", + MeteoraDammV1InstructionKind::CreateLockEscrowEvent => return "CreateLockEscrowEvent", + MeteoraDammV1InstructionKind::LockEvent => return "LockEvent", + MeteoraDammV1InstructionKind::ClaimFeeEvent => return "ClaimFeeEvent", + MeteoraDammV1InstructionKind::CreateConfigEvent => return "CreateConfigEvent", + MeteoraDammV1InstructionKind::CloseConfigEvent => return "CloseConfigEvent", + MeteoraDammV1InstructionKind::WithdrawProtocolFeesEvent => { + return "WithdrawProtocolFeesEvent"; + }, + MeteoraDammV1InstructionKind::PartnerClaimFeesEvent => return "PartnerClaimFeesEvent", + MeteoraDammV1InstructionKind::Unknown => return "Unknown", + } +} + +fn proof_status_for_instruction(instruction_kind: MeteoraDammV1InstructionKind) -> &'static str { + match instruction_kind { + MeteoraDammV1InstructionKind::CreatePoolLegacy + | MeteoraDammV1InstructionKind::CreatePoolWithConfigLegacy => { + return "legacy_local_decoder_instruction"; + }, + MeteoraDammV1InstructionKind::Swap + | MeteoraDammV1InstructionKind::AddBalanceLiquidity + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 + | MeteoraDammV1InstructionKind::RemoveBalanceLiquidity + | MeteoraDammV1InstructionKind::ClaimFee + | MeteoraDammV1InstructionKind::CreateLockEscrow + | MeteoraDammV1InstructionKind::Lock => { + return "upstream_git_local_corpus_observed"; + }, + MeteoraDammV1InstructionKind::Unknown => return "unclassified_local_corpus_instruction", + _ => return "upstream_git_mapped_unverified", + } +} + +fn create_pool_pool_account( + parsed_json: std::option::Option<&serde_json::Value>, + accounts: &[std::string::String], + _instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + return extract_string_by_candidate_keys( + parsed_json, + &["pool", "poolAddress", "poolAccount", "amm", "ammPool", "poolState"], + ) + .or_else(|| return extract_account(accounts, 0)); +} + +fn create_pool_token_a_mint( + parsed_json: std::option::Option<&serde_json::Value>, + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + let parsed = extract_string_by_candidate_keys( + parsed_json, + &["tokenAMint", "mintA", "baseMint", "token0Mint", "mint0", "coinMint"], + ); + if parsed.is_some() { + return parsed; + } + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return extract_account(accounts, 3); + }, + MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return extract_account(accounts, 2); + }, + MeteoraDammV1InstructionKind::InitializePermissionedPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier => { + return extract_account(accounts, 2); + }, + _ => return extract_account(accounts, 1), + } +} + +fn create_pool_token_b_mint( + parsed_json: std::option::Option<&serde_json::Value>, + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + let parsed = extract_string_by_candidate_keys( + parsed_json, + &["tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1", "pcMint"], + ); + if parsed.is_some() { + return parsed; + } + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return extract_account(accounts, 4); + }, + MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return extract_account(accounts, 3); + }, + MeteoraDammV1InstructionKind::InitializePermissionedPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier => { + return extract_account(accounts, 3); + }, + _ => return extract_account(accounts, 2), + } +} + +fn create_pool_lp_mint( + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return extract_account(accounts, 2); + }, + MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool + | MeteoraDammV1InstructionKind::InitializePermissionedPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier => { + return extract_account(accounts, 1); + }, + _ => return None, + } +} + +fn create_pool_config_account( + parsed_json: std::option::Option<&serde_json::Value>, + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + let parsed = extract_string_by_candidate_keys( + parsed_json, + &["config", "poolConfig", "ammConfig", "tradeFeeConfig"], + ); + if parsed.is_some() { + return parsed; + } + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return extract_account(accounts, 1); + }, + _ => return extract_account(accounts, 3), + } +} + +fn create_pool_creator( + parsed_json: std::option::Option<&serde_json::Value>, + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + let parsed = + extract_string_by_candidate_keys(parsed_json, &["creator", "payer", "user", "owner"]); + if parsed.is_some() { + return parsed; + } + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return extract_account(accounts, 18); + }, + MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return extract_account(accounts, 17); + }, + MeteoraDammV1InstructionKind::InitializePermissionedPool => { + return extract_account(accounts, 15); + }, + MeteoraDammV1InstructionKind::InitializePermissionlessPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier => { + return extract_account(accounts, 17); + }, + _ => return extract_account(accounts, 4), + } +} + +fn liquidity_actor_wallet( + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + match instruction_kind { + MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide => { + return extract_account(accounts, 12); + }, + _ => return extract_account(accounts, 13), + } +} + +fn decoded_create_pool_instruction_payload( + instruction_data: std::option::Option<&[u8]>, + instruction_kind: MeteoraDammV1InstructionKind, +) -> serde_json::Value { + let data = match instruction_data { + Some(data) => data, + None => return serde_json::Value::Null, + }; + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return serde_json::json!({ + "tokenAAmountRaw": u64_le_string_at(data, 8), + "tokenBAmountRaw": u64_le_string_at(data, 16) + }); + }, + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return serde_json::json!({ + "tokenAAmountRaw": u64_le_string_at(data, 8), + "tokenBAmountRaw": u64_le_string_at(data, 16), + "activationPointRaw": option_u64_le_string_at(data, 24) + }); + }, + _ => return serde_json::Value::Null, + } +} + +fn decoded_swap_instruction_payload( + instruction_data: std::option::Option<&[u8]>, +) -> serde_json::Value { + let data = match instruction_data { + Some(data) => data, + None => return serde_json::Value::Null, + }; + return serde_json::json!({ + "inAmountRaw": u64_le_string_at(data, 8), + "minimumOutAmountRaw": u64_le_string_at(data, 16) + }); +} + +fn decoded_liquidity_instruction_payload( + instruction_data: std::option::Option<&[u8]>, + instruction_kind: MeteoraDammV1InstructionKind, +) -> serde_json::Value { + let data = match instruction_data { + Some(data) => data, + None => return serde_json::Value::Null, + }; + match instruction_kind { + MeteoraDammV1InstructionKind::AddBalanceLiquidity => { + return serde_json::json!({ + "lpAmountRaw": u64_le_string_at(data, 8), + "maximumTokenAAmountRaw": u64_le_string_at(data, 16), + "maximumTokenBAmountRaw": u64_le_string_at(data, 24) + }); + }, + MeteoraDammV1InstructionKind::AddImbalanceLiquidity => { + return serde_json::json!({ + "minimumLpAmountRaw": u64_le_string_at(data, 8), + "tokenAAmountRaw": u64_le_string_at(data, 16), + "tokenBAmountRaw": u64_le_string_at(data, 24), + "lpAmountRaw": serde_json::Value::Null + }); + }, + MeteoraDammV1InstructionKind::BootstrapLiquidity => { + return serde_json::json!({ + "tokenAAmountRaw": u64_le_string_at(data, 8), + "tokenBAmountRaw": u64_le_string_at(data, 16), + "lpAmountRaw": serde_json::Value::Null + }); + }, + MeteoraDammV1InstructionKind::RemoveBalanceLiquidity => { + return serde_json::json!({ + "lpAmountRaw": u64_le_string_at(data, 8), + "minimumTokenAOutAmountRaw": u64_le_string_at(data, 16), + "minimumTokenBOutAmountRaw": u64_le_string_at(data, 24), + "tokenAAmountRaw": serde_json::Value::Null, + "tokenBAmountRaw": serde_json::Value::Null + }); + }, + MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide => { + return serde_json::json!({ + "lpAmountRaw": u64_le_string_at(data, 8), + "minimumOutAmountRaw": u64_le_string_at(data, 16), + "tokenAAmountRaw": serde_json::Value::Null, + "tokenBAmountRaw": serde_json::Value::Null + }); + }, + _ => return serde_json::Value::Null, + } +} + +fn decoded_claim_fee_instruction_payload( + instruction_data: std::option::Option<&[u8]>, +) -> serde_json::Value { + let data = match instruction_data { + Some(data) => data, + None => return serde_json::Value::Null, + }; + return serde_json::json!({ + "maxAmountRaw": u64_le_string_at(data, 8) + }); +} + +fn decoded_lock_instruction_payload( + instruction_data: std::option::Option<&[u8]>, +) -> serde_json::Value { + let data = match instruction_data { + Some(data) => data, + None => return serde_json::Value::Null, + }; + return serde_json::json!({ + "maxAmountRaw": u64_le_string_at(data, 8) + }); +} + +fn decoded_instruction_amount_string( + value: &serde_json::Value, + key: &str, +) -> std::option::Option { + let object = match value.as_object() { + Some(object) => object, + None => return None, + }; + let raw = match object.get(key) { + Some(raw) => raw, + None => return None, + }; + match raw { + serde_json::Value::String(text) => return Some(text.clone()), + serde_json::Value::Number(number) => return Some(number.to_string()), + _ => return None, + } +} + +fn u64_le_string_at(data: &[u8], offset: usize) -> serde_json::Value { + let number = u64_le_at(data, offset); + match number { + Some(number) => return serde_json::Value::String(number.to_string()), + None => return serde_json::Value::Null, + } +} + +fn option_u64_le_string_at(data: &[u8], offset: usize) -> serde_json::Value { + if data.len() <= offset { + return serde_json::Value::Null; + } + let tag = data[offset]; + if tag == 0 { + return serde_json::Value::Null; + } + if tag == 1 { + return u64_le_string_at(data, offset + 1); + } + return serde_json::Value::Null; +} + +fn u64_le_at(data: &[u8], offset: usize) -> std::option::Option { + let end = match offset.checked_add(8) { + Some(end) => end, + None => return None, + }; + if data.len() < end { + return None; + } + let bytes = [ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + data[offset + 5], + data[offset + 6], + data[offset + 7], + ]; + return Some(u64::from_le_bytes(bytes)); +} + +fn first_8_bytes(bytes: &[u8]) -> [u8; 8] { + return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]]; +} + +fn first_16_bytes(bytes: &[u8]) -> [u8; 16] { + return [ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], + bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], + ]; +} + +fn create_pool_account_roles( + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> serde_json::Value { + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("config", 1), + ("lpMint", 2), + ("tokenAMint", 3), + ("tokenBMint", 4), + ("aVault", 5), + ("bVault", 6), + ("aTokenVault", 7), + ("bTokenVault", 8), + ("aVaultLpMint", 9), + ("bVaultLpMint", 10), + ("aVaultLp", 11), + ("bVaultLp", 12), + ("payerTokenA", 13), + ("payerTokenB", 14), + ("payerPoolLp", 15), + ("protocolTokenAFee", 16), + ("protocolTokenBFee", 17), + ("payer", 18), + ("rent", 19), + ("mintMetadata", 20), + ("metadataProgram", 21), + ("vaultProgram", 22), + ("tokenProgram", 23), + ("associatedTokenProgram", 24), + ("systemProgram", 25), + ], + ); + }, + MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("lpMint", 1), + ("tokenAMint", 2), + ("tokenBMint", 3), + ("aVault", 4), + ("bVault", 5), + ("aTokenVault", 6), + ("bTokenVault", 7), + ("aVaultLpMint", 8), + ("bVaultLpMint", 9), + ("aVaultLp", 10), + ("bVaultLp", 11), + ("payerTokenA", 12), + ("payerTokenB", 13), + ("payerPoolLp", 14), + ("protocolTokenAFee", 15), + ("protocolTokenBFee", 16), + ("payer", 17), + ("rent", 18), + ("mintMetadata", 19), + ("metadataProgram", 20), + ("vaultProgram", 21), + ("tokenProgram", 22), + ("associatedTokenProgram", 23), + ("systemProgram", 24), + ], + ); + }, + _ => { + return named_accounts_json( + accounts, + &[("pool", 0), ("tokenAMint", 1), ("tokenBMint", 2)], + ); + }, + } +} + +fn swap_account_roles(accounts: &[std::string::String]) -> serde_json::Value { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("userSourceToken", 1), + ("userDestinationToken", 2), + ("aVault", 3), + ("bVault", 4), + ("aTokenVault", 5), + ("bTokenVault", 6), + ("aVaultLpMint", 7), + ("bVaultLpMint", 8), + ("aVaultLp", 9), + ("bVaultLp", 10), + ("protocolTokenFee", 11), + ("user", 12), + ("vaultProgram", 13), + ("tokenProgram", 14), + ], + ); +} + +fn liquidity_account_roles( + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> serde_json::Value { + if instruction_kind == MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("lpMint", 1), + ("userPoolLp", 2), + ("aVaultLp", 3), + ("bVaultLp", 4), + ("aVault", 5), + ("bVault", 6), + ("aVaultLpMint", 7), + ("bVaultLpMint", 8), + ("aTokenVault", 9), + ("bTokenVault", 10), + ("userDestinationToken", 11), + ("user", 12), + ("vaultProgram", 13), + ("tokenProgram", 14), + ], + ); + } + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("lpMint", 1), + ("userPoolLp", 2), + ("aVaultLp", 3), + ("bVaultLp", 4), + ("aVault", 5), + ("bVault", 6), + ("aVaultLpMint", 7), + ("bVaultLpMint", 8), + ("aTokenVault", 9), + ("bTokenVault", 10), + ("userAToken", 11), + ("userBToken", 12), + ("user", 13), + ("vaultProgram", 14), + ("tokenProgram", 15), + ], + ); +} + +fn claim_fee_account_roles(accounts: &[std::string::String]) -> serde_json::Value { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("lpMint", 1), + ("lockEscrow", 2), + ("owner", 3), + ("sourceTokens", 4), + ("escrowVault", 5), + ("tokenProgram", 6), + ("aTokenVault", 7), + ("bTokenVault", 8), + ("aVault", 9), + ("bVault", 10), + ("aVaultLp", 11), + ("bVaultLp", 12), + ("aVaultLpMint", 13), + ("bVaultLpMint", 14), + ("userAToken", 15), + ("userBToken", 16), + ("vaultProgram", 17), + ], + ); +} + +fn create_lock_escrow_account_roles(accounts: &[std::string::String]) -> serde_json::Value { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("lockEscrow", 1), + ("owner", 2), + ("lpMint", 3), + ("payer", 4), + ("systemProgram", 5), + ], + ); +} + +fn lock_account_roles(accounts: &[std::string::String]) -> serde_json::Value { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("lpMint", 1), + ("lockEscrow", 2), + ("owner", 3), + ("sourceTokens", 4), + ("escrowVault", 5), + ("tokenProgram", 6), + ("aVault", 7), + ("bVault", 8), + ("aVaultLp", 9), + ("bVaultLp", 10), + ("aVaultLpMint", 11), + ("bVaultLpMint", 12), + ], + ); +} + +fn named_accounts_json( + accounts: &[std::string::String], + roles: &[(&str, usize)], +) -> serde_json::Value { + let mut object = serde_json::Map::new(); + for (role, index) in roles { + let account = extract_account(accounts, *index); + match account { + Some(account) => { + object.insert((*role).to_string(), serde_json::Value::String(account)); + }, + None => { + object.insert((*role).to_string(), serde_json::Value::Null); + }, + } + } + return serde_json::Value::Object(object); +} + +fn discriminator_hex_for_kind( + bytes: &[u8], + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + if is_upstream_git_event_kind(instruction_kind) && bytes.len() >= 16 { + return Some(format!( + "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + bytes[0], + bytes[1], + bytes[2], + bytes[3], + bytes[4], + bytes[5], + bytes[6], + bytes[7], + bytes[8], + bytes[9], + bytes[10], + bytes[11], + bytes[12], + bytes[13], + bytes[14], + bytes[15], + )); + } + return first_8_bytes_hex(bytes); +} + +fn is_upstream_git_event_kind(instruction_kind: MeteoraDammV1InstructionKind) -> bool { + return matches!( + instruction_kind, + MeteoraDammV1InstructionKind::AddLiquidityEvent + | MeteoraDammV1InstructionKind::RemoveLiquidityEvent + | MeteoraDammV1InstructionKind::BootstrapLiquidityEvent + | MeteoraDammV1InstructionKind::SwapEvent + | MeteoraDammV1InstructionKind::SetPoolFeesEvent + | MeteoraDammV1InstructionKind::PoolInfoEvent + | MeteoraDammV1InstructionKind::TransferAdminEvent + | MeteoraDammV1InstructionKind::OverrideCurveParamEvent + | MeteoraDammV1InstructionKind::PoolCreatedEvent + | MeteoraDammV1InstructionKind::PoolEnabledEvent + | MeteoraDammV1InstructionKind::MigrateFeeAccountEvent + | MeteoraDammV1InstructionKind::CreateLockEscrowEvent + | MeteoraDammV1InstructionKind::LockEvent + | MeteoraDammV1InstructionKind::ClaimFeeEvent + | MeteoraDammV1InstructionKind::CreateConfigEvent + | MeteoraDammV1InstructionKind::CloseConfigEvent + | MeteoraDammV1InstructionKind::WithdrawProtocolFeesEvent + | MeteoraDammV1InstructionKind::PartnerClaimFeesEvent + ); +} + +fn upstream_git_pool_account( + accounts: &[std::string::String], + instruction_data: std::option::Option<&[u8]>, + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + match instruction_kind { + MeteoraDammV1InstructionKind::PoolCreatedEvent => { + return pubkey_base58_at(instruction_data, 16 + 32 + 32 + 32 + 1); + }, + MeteoraDammV1InstructionKind::BootstrapLiquidityEvent => { + return pubkey_base58_at(instruction_data, 16 + 8 + 8 + 8); + }, + MeteoraDammV1InstructionKind::SetPoolFeesEvent => { + return pubkey_base58_at(instruction_data, 16 + 8 + 8 + 8 + 8); + }, + MeteoraDammV1InstructionKind::TransferAdminEvent => { + return pubkey_base58_at(instruction_data, 16 + 32 + 32); + }, + MeteoraDammV1InstructionKind::OverrideCurveParamEvent => { + return pubkey_base58_at(instruction_data, 16 + 8 + 8); + }, + MeteoraDammV1InstructionKind::PoolEnabledEvent => { + return pubkey_base58_at(instruction_data, 16); + }, + MeteoraDammV1InstructionKind::MigrateFeeAccountEvent => { + return pubkey_base58_at(instruction_data, 16); + }, + MeteoraDammV1InstructionKind::CreateLockEscrowEvent => { + return pubkey_base58_at(instruction_data, 16); + }, + MeteoraDammV1InstructionKind::LockEvent => return pubkey_base58_at(instruction_data, 16), + MeteoraDammV1InstructionKind::ClaimFeeEvent => { + return pubkey_base58_at(instruction_data, 16); + }, + MeteoraDammV1InstructionKind::CreateConfig + | MeteoraDammV1InstructionKind::CreateConfigEvent + | MeteoraDammV1InstructionKind::CloseConfig + | MeteoraDammV1InstructionKind::CloseConfigEvent => return None, + _ => return extract_account(accounts, 0), + } +} + +fn upstream_git_token_a_mint( + accounts: &[std::string::String], + instruction_data: std::option::Option<&[u8]>, + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + match instruction_kind { + MeteoraDammV1InstructionKind::PoolCreatedEvent => { + return pubkey_base58_at(instruction_data, 16 + 32); + }, + MeteoraDammV1InstructionKind::InitializePermissionedPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier => { + return extract_account(accounts, 2); + }, + _ => return None, + } +} + +fn upstream_git_token_b_mint( + accounts: &[std::string::String], + instruction_data: std::option::Option<&[u8]>, + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + match instruction_kind { + MeteoraDammV1InstructionKind::PoolCreatedEvent => { + return pubkey_base58_at(instruction_data, 16 + 32 + 32); + }, + MeteoraDammV1InstructionKind::InitializePermissionedPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier => { + return extract_account(accounts, 3); + }, + _ => return None, + } +} + +fn upstream_git_lp_mint( + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionedPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier + | MeteoraDammV1InstructionKind::CreateMintMetadata => return extract_account(accounts, 1), + MeteoraDammV1InstructionKind::ClaimFeeEvent + | MeteoraDammV1InstructionKind::WithdrawProtocolFees + | MeteoraDammV1InstructionKind::WithdrawProtocolFeesEvent + | MeteoraDammV1InstructionKind::PartnerClaimFee + | MeteoraDammV1InstructionKind::PartnerClaimFeesEvent => return None, + _ => return extract_account(accounts, 1), + } +} + +fn upstream_git_actor_wallet( + accounts: &[std::string::String], + instruction_data: std::option::Option<&[u8]>, + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + match instruction_kind { + MeteoraDammV1InstructionKind::CreateConfig | MeteoraDammV1InstructionKind::CloseConfig => { + return extract_account(accounts, 1); + }, + MeteoraDammV1InstructionKind::EnableOrDisablePool + | MeteoraDammV1InstructionKind::OverrideCurveParam + | MeteoraDammV1InstructionKind::UpdateActivationPoint + | MeteoraDammV1InstructionKind::SetWhitelistedVault => return extract_account(accounts, 1), + MeteoraDammV1InstructionKind::SetPoolFees => return extract_account(accounts, 1), + MeteoraDammV1InstructionKind::PartnerClaimFee => return extract_account(accounts, 7), + MeteoraDammV1InstructionKind::ClaimFeeEvent => { + return pubkey_base58_at(instruction_data, 16 + 32); + }, + MeteoraDammV1InstructionKind::PartnerClaimFeesEvent => { + return pubkey_base58_at(instruction_data, 16 + 8 + 8); + }, + MeteoraDammV1InstructionKind::TransferAdminEvent => { + return pubkey_base58_at(instruction_data, 16); + }, + MeteoraDammV1InstructionKind::CreateLockEscrowEvent => { + return pubkey_base58_at(instruction_data, 16 + 32); + }, + MeteoraDammV1InstructionKind::LockEvent => { + return pubkey_base58_at(instruction_data, 16 + 32); + }, + _ => return extract_account(accounts, 1), + } +} + +fn decoded_upstream_git_instruction_payload( + instruction_data: std::option::Option<&[u8]>, + instruction_kind: MeteoraDammV1InstructionKind, +) -> serde_json::Value { + let data = match instruction_data { + Some(data) => data, + None => return serde_json::Value::Null, + }; + match instruction_kind { + MeteoraDammV1InstructionKind::EnableOrDisablePool => { + return serde_json::json!({ "enabled": bool_at(data, 8) }); + }, + MeteoraDammV1InstructionKind::UpdateActivationPoint => { + return serde_json::json!({ "newActivationPointRaw": u64_le_string_at(data, 8) }); + }, + MeteoraDammV1InstructionKind::SetWhitelistedVault => { + return serde_json::json!({ "whitelistedVault": pubkey_base58_at(Some(data), 8) }); + }, + MeteoraDammV1InstructionKind::PartnerClaimFee => { + return serde_json::json!({ + "maxAmountARaw": u64_le_string_at(data, 8), + "maxAmountBRaw": u64_le_string_at(data, 16), + }); + }, + MeteoraDammV1InstructionKind::WithdrawProtocolFees => { + return serde_json::json!({}); + }, + MeteoraDammV1InstructionKind::AddLiquidityEvent + | MeteoraDammV1InstructionKind::BootstrapLiquidityEvent => { + return serde_json::json!({ + "lpAmountRaw": u64_le_string_at(data, 16), + "tokenAAmountRaw": u64_le_string_at(data, 24), + "tokenBAmountRaw": u64_le_string_at(data, 32), + "pool": bootstrap_event_pool_for_payload(data, instruction_kind), + }); + }, + MeteoraDammV1InstructionKind::RemoveLiquidityEvent => { + return serde_json::json!({ + "lpAmountRaw": u64_le_string_at(data, 16), + "tokenAAmountRaw": u64_le_string_at(data, 24), + "tokenBAmountRaw": u64_le_string_at(data, 32), + }); + }, + MeteoraDammV1InstructionKind::SwapEvent => { + return serde_json::json!({ + "inAmountRaw": u64_le_string_at(data, 16), + "outAmountRaw": u64_le_string_at(data, 24), + "tradeFeeRaw": u64_le_string_at(data, 32), + "protocolFeeRaw": u64_le_string_at(data, 40), + "hostFeeRaw": u64_le_string_at(data, 48), + }); + }, + MeteoraDammV1InstructionKind::SetPoolFeesEvent => { + return serde_json::json!({ + "tradeFeeNumeratorRaw": u64_le_string_at(data, 16), + "tradeFeeDenominatorRaw": u64_le_string_at(data, 24), + "protocolTradeFeeNumeratorRaw": u64_le_string_at(data, 32), + "protocolTradeFeeDenominatorRaw": u64_le_string_at(data, 40), + "pool": pubkey_base58_at(Some(data), 48), + }); + }, + MeteoraDammV1InstructionKind::PoolInfoEvent => { + return serde_json::json!({ + "tokenAAmountRaw": u64_le_string_at(data, 16), + "tokenBAmountRaw": u64_le_string_at(data, 24), + "virtualPriceRawBytes": bytes_hex_at(data, 32, 8), + "currentTimestampRaw": u64_le_string_at(data, 40), + }); + }, + MeteoraDammV1InstructionKind::PoolEnabledEvent => { + return serde_json::json!({ + "pool": pubkey_base58_at(Some(data), 16), + "enabled": bool_at(data, 48), + }); + }, + MeteoraDammV1InstructionKind::LockEvent => { + return serde_json::json!({ + "pool": pubkey_base58_at(Some(data), 16), + "owner": pubkey_base58_at(Some(data), 48), + "amountRaw": u64_le_string_at(data, 80), + }); + }, + MeteoraDammV1InstructionKind::ClaimFeeEvent => { + return serde_json::json!({ + "pool": pubkey_base58_at(Some(data), 16), + "owner": pubkey_base58_at(Some(data), 48), + "amountRaw": u64_le_string_at(data, 80), + "feeAmountRaw": u64_le_string_at(data, 80), + "aFeeRaw": u64_le_string_at(data, 88), + "bFeeRaw": u64_le_string_at(data, 96), + }); + }, + MeteoraDammV1InstructionKind::WithdrawProtocolFeesEvent => { + return serde_json::json!({ + "pool": pubkey_base58_at(Some(data), 16), + "protocolAFeeRaw": u64_le_string_at(data, 48), + "protocolBFeeRaw": u64_le_string_at(data, 56), + "protocolAFeeOwner": pubkey_base58_at(Some(data), 64), + "protocolBFeeOwner": pubkey_base58_at(Some(data), 96), + }); + }, + MeteoraDammV1InstructionKind::PartnerClaimFeesEvent => { + return serde_json::json!({ + "pool": pubkey_base58_at(Some(data), 16), + "feeARaw": u64_le_string_at(data, 48), + "feeBRaw": u64_le_string_at(data, 56), + "partner": pubkey_base58_at(Some(data), 64), + }); + }, + MeteoraDammV1InstructionKind::PoolCreatedEvent => { + return serde_json::json!({ + "lpMint": pubkey_base58_at(Some(data), 16), + "tokenAMint": pubkey_base58_at(Some(data), 48), + "tokenBMint": pubkey_base58_at(Some(data), 80), + "poolTypeRaw": u8_at(data, 112), + "pool": pubkey_base58_at(Some(data), 113), + }); + }, + MeteoraDammV1InstructionKind::CreateConfigEvent => { + return serde_json::json!({ + "tradeFeeNumeratorRaw": u64_le_string_at(data, 16), + "protocolTradeFeeNumeratorRaw": u64_le_string_at(data, 24), + "config": pubkey_base58_at(Some(data), 32), + }); + }, + MeteoraDammV1InstructionKind::CloseConfigEvent => { + return serde_json::json!({ "config": pubkey_base58_at(Some(data), 16) }); + }, + MeteoraDammV1InstructionKind::CreateLockEscrowEvent => { + return serde_json::json!({ + "pool": pubkey_base58_at(Some(data), 16), + "owner": pubkey_base58_at(Some(data), 48), + }); + }, + MeteoraDammV1InstructionKind::TransferAdminEvent => { + return serde_json::json!({ + "admin": pubkey_base58_at(Some(data), 16), + "newAdmin": pubkey_base58_at(Some(data), 48), + "pool": pubkey_base58_at(Some(data), 80), + }); + }, + MeteoraDammV1InstructionKind::OverrideCurveParamEvent => { + return serde_json::json!({ + "newAmpRaw": u64_le_string_at(data, 16), + "updatedTimestampRaw": u64_le_string_at(data, 24), + "pool": pubkey_base58_at(Some(data), 32), + }); + }, + MeteoraDammV1InstructionKind::MigrateFeeAccountEvent => { + return serde_json::json!({ + "pool": pubkey_base58_at(Some(data), 16), + "newAdminTokenAFee": pubkey_base58_at(Some(data), 48), + "newAdminTokenBFee": pubkey_base58_at(Some(data), 80), + "tokenAAmountRaw": u64_le_string_at(data, 112), + "tokenBAmountRaw": u64_le_string_at(data, 120), + }); + }, + _ => return serde_json::json!({}), + } +} + +fn upstream_git_account_roles( + accounts: &[std::string::String], + instruction_kind: MeteoraDammV1InstructionKind, +) -> serde_json::Value { + match instruction_kind { + MeteoraDammV1InstructionKind::InitializePermissionedPool => { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("lpMint", 1), + ("tokenAMint", 2), + ("tokenBMint", 3), + ("admin", 15), + ("feeOwner", 16), + ], + ); + }, + MeteoraDammV1InstructionKind::InitializePermissionlessPool + | MeteoraDammV1InstructionKind::InitializePermissionlessPoolWithFeeTier => { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("lpMint", 1), + ("tokenAMint", 2), + ("tokenBMint", 3), + ("payer", 17), + ("feeOwner", 18), + ], + ); + }, + MeteoraDammV1InstructionKind::EnableOrDisablePool + | MeteoraDammV1InstructionKind::OverrideCurveParam + | MeteoraDammV1InstructionKind::UpdateActivationPoint + | MeteoraDammV1InstructionKind::SetWhitelistedVault => { + return named_accounts_json(accounts, &[("pool", 0), ("admin", 1)]); + }, + MeteoraDammV1InstructionKind::SetPoolFees => { + return named_accounts_json(accounts, &[("pool", 0), ("feeOperator", 1)]); + }, + MeteoraDammV1InstructionKind::CreateConfig => { + return named_accounts_json( + accounts, + &[("config", 0), ("admin", 1), ("systemProgram", 2)], + ); + }, + MeteoraDammV1InstructionKind::CloseConfig => { + return named_accounts_json( + accounts, + &[("config", 0), ("admin", 1), ("rentReceiver", 2)], + ); + }, + MeteoraDammV1InstructionKind::WithdrawProtocolFees => { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("aVaultLp", 1), + ("protocolTokenAFee", 2), + ("protocolTokenBFee", 3), + ("treasuryTokenA", 4), + ("treasuryTokenB", 5), + ("tokenProgram", 6), + ], + ); + }, + MeteoraDammV1InstructionKind::PartnerClaimFee => { + return named_accounts_json( + accounts, + &[ + ("pool", 0), + ("aVaultLp", 1), + ("protocolTokenAFee", 2), + ("protocolTokenBFee", 3), + ("partnerTokenA", 4), + ("partnerTokenB", 5), + ("tokenProgram", 6), + ("partnerAuthority", 7), + ], + ); + }, + _ => return named_accounts_json(accounts, &[]), + } +} + +fn bootstrap_event_pool_for_payload( + data: &[u8], + instruction_kind: MeteoraDammV1InstructionKind, +) -> std::option::Option { + if instruction_kind == MeteoraDammV1InstructionKind::BootstrapLiquidityEvent { + return pubkey_base58_at(Some(data), 40); + } + return None; +} + +fn pubkey_base58_at( + data: std::option::Option<&[u8]>, + offset: usize, +) -> std::option::Option { + let data = match data { + Some(data) => data, + None => return None, + }; + let end = match offset.checked_add(32) { + Some(end) => end, + None => return None, + }; + if data.len() < end { + return None; + } + return Some(bs58::encode(&data[offset..end]).into_string()); +} + +fn bool_at(data: &[u8], offset: usize) -> serde_json::Value { + if data.len() <= offset { + return serde_json::Value::Null; + } + return serde_json::Value::Bool(data[offset] != 0); +} + +fn u8_at(data: &[u8], offset: usize) -> serde_json::Value { + if data.len() <= offset { + return serde_json::Value::Null; + } + return serde_json::Value::Number(serde_json::Number::from(data[offset])); +} + +fn bytes_hex_at(data: &[u8], offset: usize, len: usize) -> serde_json::Value { + let end = match offset.checked_add(len) { + Some(end) => end, + None => return serde_json::Value::Null, + }; + if data.len() < end { + return serde_json::Value::Null; + } + let mut text = std::string::String::new(); + for byte in &data[offset..end] { + text.push_str(format!("{:02x}", byte).as_str()); + } + return serde_json::Value::String(text); +} + fn extract_log_messages( transaction_json: &serde_json::Value, ) -> std::vec::Vec { @@ -784,6 +3129,37 @@ mod tests { return dto; } + fn instruction_with_data( + id: i64, + transaction_id: i64, + accounts: serde_json::Value, + data: &[u8], + ) -> crate::ChainInstructionDto { + let mut dto = crate::ChainInstructionDto::new( + transaction_id, + None, + 0, + None, + Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()), + Some("meteora-damm-v1".to_string()), + Some(1), + accounts.to_string(), + Some(format!("\"{}\"", bs58::encode(data).into_string())), + None, + None, + ); + dto.id = Some(id); + return dto; + } + + fn data_with_u64(discriminator: [u8; 8], values: &[u64]) -> std::vec::Vec { + let mut data = discriminator.to_vec(); + for value in values { + data.extend_from_slice(&value.to_le_bytes()); + } + return data; + } + #[test] fn meteora_damm_v1_create_pool_is_detected() { let decoder = crate::MeteoraDammV1Decoder::new(); @@ -804,8 +3180,8 @@ mod tests { assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string())); assert!(event.used_config); }, - crate::MeteoraDammV1DecodedEvent::Swap(_) => { - panic!("unexpected swap event") + _ => { + panic!("unexpected event") }, } } @@ -829,17 +3205,265 @@ mod tests { assert_eq!(event.token_a_mint, Some("DammV1SwapTokenA111".to_string())); assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string())); }, - crate::MeteoraDammV1DecodedEvent::CreatePool(_) => { - panic!("unexpected create event") + _ => { + panic!("unexpected event") }, } } #[test] - fn meteora_damm_v1_swap_discriminator_is_detected() { - let data = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8, 0x01]; - let kind = super::classify_instruction_kind_from_data(Some(&data)); - assert_eq!(kind, super::MeteoraDammV1InstructionKind::Swap); + fn meteora_damm_v1_upstream_git_create_pool_with_config_is_detected() { + let decoder = crate::MeteoraDammV1Decoder::new(); + let mut transaction = make_create_transaction(); + transaction.id = Some(601); + let data = data_with_u64( + super::DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG, + &[10, 20], + ); + let instruction = instruction_with_data( + 602, + 601, + serde_json::json!([ + "Pool111", + "Config111", + "LpMint111", + "MintA111", + "MintB111", + "AVault111", + "BVault111", + "ATokenVault111", + "BTokenVault111", + "AVaultLpMint111", + "BVaultLpMint111", + "AVaultLp111", + "BVaultLp111", + "PayerA111", + "PayerB111", + "PayerLp111", + "ProtocolA111", + "ProtocolB111", + "Payer111", + "Rent111", + "Metadata111", + "MetadataProgram111", + "VaultProgram111", + "TokenProgram111", + "AtaProgram111", + "System111" + ]), + data.as_slice(), + ); + 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::MeteoraDammV1DecodedEvent::CreatePool(event) => { + assert_eq!(event.pool_account, Some("Pool111".to_string())); + assert_eq!(event.config_account, Some("Config111".to_string())); + assert_eq!(event.lp_mint, Some("LpMint111".to_string())); + assert_eq!(event.token_a_mint, Some("MintA111".to_string())); + assert_eq!(event.token_b_mint, Some("MintB111".to_string())); + assert_eq!(event.creator, Some("Payer111".to_string())); + }, + _ => panic!("unexpected event"), + } + } + + #[test] + fn meteora_damm_v1_remove_balance_liquidity_is_detected() { + let decoder = crate::MeteoraDammV1Decoder::new(); + let transaction = make_swap_transaction(); + let data = + data_with_u64(super::DAMM_V1_DISCRIMINATOR_REMOVE_BALANCE_LIQUIDITY, &[100, 7, 9]); + let instruction = instruction_with_data( + 604, + 503, + serde_json::json!([ + "Pool111", + "LpMint111", + "UserLp111", + "AVaultLp111", + "BVaultLp111", + "AVault111", + "BVault111", + "AVaultLpMint111", + "BVaultLpMint111", + "ATokenVault111", + "BTokenVault111", + "UserA111", + "UserB111", + "User111", + "VaultProgram111", + "TokenProgram111" + ]), + data.as_slice(), + ); + 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::MeteoraDammV1DecodedEvent::Liquidity(event) => { + assert_eq!(event.event_kind, "meteora_damm_v1.remove_liquidity"); + assert_eq!(event.pool_account, Some("Pool111".to_string())); + assert_eq!(event.lp_mint, Some("LpMint111".to_string())); + assert_eq!(event.lp_amount_raw, Some("100".to_string())); + assert_eq!(event.actor_wallet, Some("User111".to_string())); + }, + _ => panic!("unexpected event"), + } + } + + #[test] + fn meteora_damm_v1_claim_fee_is_detected() { + let decoder = crate::MeteoraDammV1Decoder::new(); + let transaction = make_swap_transaction(); + let data = data_with_u64(super::DAMM_V1_DISCRIMINATOR_CLAIM_FEE, &[500]); + let instruction = instruction_with_data( + 605, + 503, + serde_json::json!([ + "Pool111", + "LpMint111", + "LockEscrow111", + "Owner111", + "SourceTokens111", + "EscrowVault111", + "TokenProgram111", + "ATokenVault111", + "BTokenVault111", + "AVault111", + "BVault111", + "AVaultLp111", + "BVaultLp111", + "AVaultLpMint111", + "BVaultLpMint111", + "UserA111", + "UserB111", + "VaultProgram111" + ]), + data.as_slice(), + ); + 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::MeteoraDammV1DecodedEvent::Fee(event) => { + assert_eq!(event.event_kind, "meteora_damm_v1.claim_fee"); + assert_eq!(event.pool_account, Some("Pool111".to_string())); + assert_eq!(event.actor_wallet, Some("Owner111".to_string())); + }, + _ => panic!("unexpected event"), + } + } + + #[test] + fn meteora_damm_v1_lock_instructions_are_detected() { + let decoder = crate::MeteoraDammV1Decoder::new(); + let transaction = make_swap_transaction(); + let create_lock_data = super::DAMM_V1_DISCRIMINATOR_CREATE_LOCK_ESCROW.to_vec(); + let lock_data = data_with_u64(super::DAMM_V1_DISCRIMINATOR_LOCK, &[300]); + let create_lock_instruction = instruction_with_data( + 606, + 503, + serde_json::json!([ + "Pool111", + "LockEscrow111", + "Owner111", + "LpMint111", + "Payer111", + "System111" + ]), + create_lock_data.as_slice(), + ); + let lock_instruction = instruction_with_data( + 607, + 503, + serde_json::json!([ + "Pool111", + "LpMint111", + "LockEscrow111", + "Owner111", + "SourceTokens111", + "EscrowVault111", + "TokenProgram111", + "AVault111", + "BVault111", + "AVaultLp111", + "BVaultLp111", + "AVaultLpMint111", + "BVaultLpMint111" + ]), + lock_data.as_slice(), + ); + let decoded_result = + decoder.decode_transaction(&transaction, &[create_lock_instruction, lock_instruction]); + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => panic!("decode must succeed: {}", error), + }; + assert_eq!(decoded.len(), 2); + match &decoded[0] { + crate::MeteoraDammV1DecodedEvent::PoolLifecycle(event) => { + assert_eq!(event.event_kind, "meteora_damm_v1.create_lock_escrow"); + }, + _ => panic!("unexpected event"), + } + match &decoded[1] { + crate::MeteoraDammV1DecodedEvent::PoolAdmin(event) => { + assert_eq!(event.event_kind, "meteora_damm_v1.lock_liquidity"); + assert_eq!(event.actor_wallet, Some("Owner111".to_string())); + }, + _ => panic!("unexpected event"), + } + } + + #[test] + fn meteora_damm_v1_discriminators_are_detected() { + let cases = vec![ + ( + super::DAMM_V1_DISCRIMINATOR_SWAP, + super::MeteoraDammV1InstructionKind::Swap, + ), + ( + super::DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG, + super::MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig, + ), + ( + super::DAMM_V1_DISCRIMINATOR_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG2, + super::MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2, + ), + ( + super::DAMM_V1_DISCRIMINATOR_REMOVE_BALANCE_LIQUIDITY, + super::MeteoraDammV1InstructionKind::RemoveBalanceLiquidity, + ), + ( + super::DAMM_V1_DISCRIMINATOR_CLAIM_FEE, + super::MeteoraDammV1InstructionKind::ClaimFee, + ), + ( + super::DAMM_V1_DISCRIMINATOR_LOCK, + super::MeteoraDammV1InstructionKind::Lock, + ), + ( + super::DAMM_V1_DISCRIMINATOR_CREATE_LOCK_ESCROW, + super::MeteoraDammV1InstructionKind::CreateLockEscrow, + ), + ]; + for (discriminator, expected) in cases { + let mut data = discriminator.to_vec(); + data.push(1); + let kind = super::classify_instruction_kind_from_data(Some(data.as_slice())); + assert_eq!(kind, expected); + } } #[test] @@ -870,7 +3494,7 @@ mod tests { crate::MeteoraDammV1DecodedEvent::Swap(event) => { assert_eq!(event.pool_account, Some("DammV1SwapPool111".to_string())); }, - crate::MeteoraDammV1DecodedEvent::CreatePool(_) => panic!("unexpected create event"), + _ => panic!("unexpected event"), } } } diff --git a/kb_lib/src/dex/meteora_dlmm.rs b/kb_lib/src/dex/meteora_dlmm.rs index 942af69..82e4fbd 100644 --- a/kb_lib/src/dex/meteora_dlmm.rs +++ b/kb_lib/src/dex/meteora_dlmm.rs @@ -468,7 +468,7 @@ impl MeteoraDlmmDecoder { "accounts": accounts, "parsed": parsed_json, "logMessages": log_messages, - "proofStatus": "observed_local_corpus_and_known_carbon_layout", + "proofStatus": "upstream_git_local_corpus_observed", "position": position_account, "actorWallet": actor_wallet, "rentReceiver": rent_receiver, @@ -992,7 +992,7 @@ fn decode_anchor_lb_pair_create_event( "anchorEventName": "lb_pair_create_event", "anchorEventDiscriminatorHex": "b94afc7d1bd7bc6f", "anchorEventPayloadSize": data.len().saturating_sub(8), - "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "proofStatus": "upstream_git_layout_unverified", "lbPair": lb_pair, "poolAccount": lb_pair, "binStep": bin_step, @@ -1052,7 +1052,7 @@ fn decode_anchor_liquidity_event( "anchorEventName": anchor_event_name, "anchorEventDiscriminatorHex": event_discriminator_hex, "anchorEventPayloadSize": data.len().saturating_sub(8), - "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "proofStatus": "upstream_git_layout_unverified", "lbPair": lb_pair, "poolAccount": lb_pair, "from": from, @@ -1112,7 +1112,7 @@ fn decode_anchor_claim_fee_event( "anchorEventName": "claim_fee_event", "anchorEventDiscriminatorHex": "4b7a9a308c4a7ba3", "anchorEventPayloadSize": data.len().saturating_sub(8), - "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "proofStatus": "upstream_git_layout_unverified", "lbPair": lb_pair, "poolAccount": lb_pair, "position": position, @@ -1167,7 +1167,7 @@ fn decode_anchor_claim_reward_event( "anchorEventName": "claim_reward_event", "anchorEventDiscriminatorHex": "947486cc16ab555f", "anchorEventPayloadSize": data.len().saturating_sub(8), - "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "proofStatus": "upstream_git_layout_unverified", "lbPair": lb_pair, "poolAccount": lb_pair, "position": position, @@ -1222,7 +1222,7 @@ fn decode_anchor_fund_reward_event( "anchorEventName": "fund_reward_event", "anchorEventDiscriminatorHex": "f6e43a8291aa4fcc", "anchorEventPayloadSize": data.len().saturating_sub(8), - "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "proofStatus": "upstream_git_layout_unverified", "lbPair": lb_pair, "poolAccount": lb_pair, "funder": funder, @@ -1275,7 +1275,7 @@ fn decode_anchor_position_create_event( "anchorEventName": "position_create_event", "anchorEventDiscriminatorHex": "908efc549d352579", "anchorEventPayloadSize": data.len().saturating_sub(8), - "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "proofStatus": "upstream_git_layout_unverified", "lbPair": lb_pair, "poolAccount": lb_pair, "position": position, @@ -1328,7 +1328,7 @@ fn decode_anchor_position_close_event( "anchorEventName": "position_close_event", "anchorEventDiscriminatorHex": "ffc4106b1cca3580", "anchorEventPayloadSize": data.len().saturating_sub(8), - "proofStatus": "known_carbon_layout_pending_local_corpus_validation", + "proofStatus": "upstream_git_layout_unverified", "position": position, "owner": owner, "actorWallet": owner @@ -2248,7 +2248,7 @@ fn resolve_dlmm_instruction_proof_status( MeteoraDlmmInstructionName::AddLiquidityByStrategy2 | MeteoraDlmmInstructionName::AddLiquidityByWeight | MeteoraDlmmInstructionName::RemoveLiquidityByRange2 => { - return "observed_local_corpus_and_known_carbon_layout"; + return "upstream_git_local_corpus_observed"; }, _ => return "decoded_from_instruction_discriminator_or_local_hint", } @@ -2858,7 +2858,7 @@ mod tests { } #[test] - fn meteora_dlmm_swap_accounts_are_mapped_from_carbon_layout() { + fn meteora_dlmm_swap_accounts_are_mapped_from_upstream_git_layout() { let accounts = vec![ "LbPair111".to_string(), "Bitmap111".to_string(), diff --git a/kb_lib/src/dex_decode.rs b/kb_lib/src/dex_decode.rs index b1fffc1..f62ba0b 100644 --- a/kb_lib/src/dex_decode.rs +++ b/kb_lib/src/dex_decode.rs @@ -564,7 +564,7 @@ impl DexDecodeService { None, event.token_a_mint.clone(), event.token_b_mint.clone(), - event.config_account.clone(), + event.lp_mint.clone(), event.payload_json.clone(), ) .await; @@ -589,6 +589,78 @@ impl DexDecodeService { ) .await; }, + crate::MeteoraDammV1DecodedEvent::Liquidity(event) => { + return self + .materialize_named_dex_event( + transaction, + event.transaction_id, + event.instruction_id, + "meteora_damm_v1", + event.program_id.clone(), + event.event_kind.as_str(), + event.pool_account.clone(), + None, + event.token_a_mint.clone(), + event.token_b_mint.clone(), + event.lp_mint.clone(), + event.payload_json.clone(), + ) + .await; + }, + crate::MeteoraDammV1DecodedEvent::Fee(event) => { + return self + .materialize_named_dex_event( + transaction, + event.transaction_id, + event.instruction_id, + "meteora_damm_v1", + event.program_id.clone(), + event.event_kind.as_str(), + event.pool_account.clone(), + None, + None, + None, + event.lp_mint.clone(), + event.payload_json.clone(), + ) + .await; + }, + crate::MeteoraDammV1DecodedEvent::PoolLifecycle(event) => { + return self + .materialize_named_dex_event( + transaction, + event.transaction_id, + event.instruction_id, + "meteora_damm_v1", + event.program_id.clone(), + event.event_kind.as_str(), + event.pool_account.clone(), + None, + event.token_a_mint.clone(), + event.token_b_mint.clone(), + event.lp_mint.clone(), + event.payload_json.clone(), + ) + .await; + }, + crate::MeteoraDammV1DecodedEvent::PoolAdmin(event) => { + return self + .materialize_named_dex_event( + transaction, + event.transaction_id, + event.instruction_id, + "meteora_damm_v1", + event.program_id.clone(), + event.event_kind.as_str(), + event.pool_account.clone(), + None, + None, + None, + None, + event.payload_json.clone(), + ) + .await; + }, } } @@ -1927,6 +1999,9 @@ fn instruction_audit_event_kind_by_protocol( "raydium_clmm" => return Some("raydium_clmm.instruction_audit"), "raydium_cpmm" => return Some("raydium_cpmm.instruction_audit"), "meteora_dlmm" => return Some("meteora_dlmm.instruction_audit"), + "meteora_damm_v1" => return Some("meteora_damm_v1.instruction_audit"), + "meteora_damm_v2" => return Some("meteora_damm_v2.instruction_audit"), + "meteora_dbc" => return Some("meteora_dbc.instruction_audit"), _ => return None, } } @@ -3134,6 +3209,18 @@ mod tests { super::instruction_audit_event_kind_by_protocol("meteora_dlmm"), Some("meteora_dlmm.instruction_audit") ); + assert_eq!( + super::instruction_audit_event_kind_by_protocol("meteora_damm_v1"), + Some("meteora_damm_v1.instruction_audit") + ); + assert_eq!( + super::instruction_audit_event_kind_by_protocol("meteora_damm_v2"), + Some("meteora_damm_v2.instruction_audit") + ); + assert_eq!( + super::instruction_audit_event_kind_by_protocol("meteora_dbc"), + Some("meteora_dbc.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 a7109eb..f0469c1 100644 --- a/kb_lib/src/dex_event_classification.rs +++ b/kb_lib/src/dex_event_classification.rs @@ -173,6 +173,9 @@ pub fn classify_dex_event_lifecycle_kind(event_kind: &str) -> DexEventLifecycleK if is_dex_informational_event_kind(event_kind) { return DexEventLifecycleKind::InstructionAudit; } + if event_kind.contains(".create_lock_escrow") { + return DexEventLifecycleKind::PoolCreation; + } if is_dex_token_burn_event_kind(event_kind) { return DexEventLifecycleKind::Burn; } @@ -408,6 +411,12 @@ pub fn is_dex_fee_event_kind(event_kind: &str) -> bool { if event_kind.contains("claim_fee") { return true; } + if event_kind.contains("withdraw_protocol_fees") { + return true; + } + if event_kind.contains("partner_claim_fee") { + return true; + } return false; } @@ -424,6 +433,9 @@ pub fn is_dex_reward_event_kind(event_kind: &str) -> bool { /// Returns true for pool, pair, launch, mint, burn or migration lifecycle events. pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool { + if event_kind.contains(".create_lock_escrow") { + return true; + } if event_kind.contains(".initialize_bin_array") { return true; } @@ -534,6 +546,9 @@ pub fn is_dex_pair_creation_event_kind(event_kind: &str) -> bool { /// Returns true for admin, configuration or permission changes. pub fn is_dex_admin_event_kind(event_kind: &str) -> bool { + if event_kind.contains(".lock_liquidity") { + return true; + } if event_kind.contains("admin") { return true; } @@ -1102,4 +1117,32 @@ mod tests { "non_trade_useful" ); } + + #[test] + fn classifies_damm_v1_lock_events_as_non_trade_useful() { + assert_eq!( + super::classify_dex_event_category_code("meteora_damm_v1.create_lock_escrow"), + "pool_lifecycle" + ); + assert_eq!( + super::classify_dex_event_lifecycle_kind_code("meteora_damm_v1.create_lock_escrow"), + "pool_creation" + ); + assert_eq!( + super::classify_dex_event_category_code("meteora_damm_v1.lock_liquidity"), + "admin" + ); + assert_eq!( + super::classify_dex_event_lifecycle_kind_code("meteora_damm_v1.lock_liquidity"), + "admin_config" + ); + assert_eq!( + super::classify_dex_event_actionability_code( + "meteora_damm_v1.lock_liquidity", + false, + false, + ), + "non_trade_useful" + ); + } } diff --git a/kb_lib/src/lib.rs b/kb_lib/src/lib.rs index 6767019..05e1a7d 100644 --- a/kb_lib/src/lib.rs +++ b/kb_lib/src/lib.rs @@ -901,6 +901,14 @@ pub use dex::MeteoraDammV1CreatePoolDecoded; pub use dex::MeteoraDammV1DecodedEvent; /// Meteora DAMM v1 decoder. pub use dex::MeteoraDammV1Decoder; +/// Decoded Meteora DAMM v1 fee event. +pub use dex::MeteoraDammV1FeeDecoded; +/// Decoded Meteora DAMM v1 liquidity event. +pub use dex::MeteoraDammV1LiquidityDecoded; +/// Decoded Meteora DAMM v1 pool administration event. +pub use dex::MeteoraDammV1PoolAdminDecoded; +/// Decoded Meteora DAMM v1 pool lifecycle event. +pub use dex::MeteoraDammV1PoolLifecycleDecoded; /// Decoded Meteora DAMM v1 swap event. pub use dex::MeteoraDammV1SwapDecoded; /// Decoded Meteora DAMM v2 create-pool event. @@ -1158,6 +1166,8 @@ pub use non_trade_event_materialization::NonTradeEventMaterializationResult; pub use non_trade_event_materialization::NonTradeEventMaterializationService; /// Candidate account inferred from generic transaction evidence. pub use onchain_dex_pair_discovery::OnchainDexCandidateAccountDto; +/// Cursor hint for one on-chain DEX discovery source address. +pub use onchain_dex_pair_discovery::OnchainDexPaginationCursorDto; /// Candidate transaction/instruction observed on-chain for one DEX program id. pub use onchain_dex_pair_discovery::OnchainDexPairCandidateDto; /// Request for on-chain DEX pair/pool discovery. diff --git a/kb_lib/src/local_pipeline_replay.rs b/kb_lib/src/local_pipeline_replay.rs index a82d300..0c64c16 100644 --- a/kb_lib/src/local_pipeline_replay.rs +++ b/kb_lib/src/local_pipeline_replay.rs @@ -7,8 +7,7 @@ //! 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.45.dlmm_add_liquidity_strategies1"; +const LOCAL_PIPELINE_DEX_DECODER_VERSION: &str = "dex_decode.v0.7.46.damm_v1_events1"; fn default_skip_certified_dex_decode() -> bool { return true; diff --git a/kb_lib/src/onchain_dex_pair_discovery.rs b/kb_lib/src/onchain_dex_pair_discovery.rs index 2e9fc02..54853a5 100644 --- a/kb_lib/src/onchain_dex_pair_discovery.rs +++ b/kb_lib/src/onchain_dex_pair_discovery.rs @@ -20,6 +20,21 @@ pub struct OnchainDexPairDiscoveryRequestDto { pub signature_source: std::option::Option, /// Optional address used with `signature_source = address` to fetch signatures for a pool/vault/position/config/mint account. pub source_address: std::option::Option, + /// Optional extra source addresses. Demo3 scans every valid address in this list and de-duplicates signatures. + #[serde(default)] + pub source_addresses: std::vec::Vec, + /// Optional signature cursor passed to Solana `getSignaturesForAddress.before`. + #[serde(default)] + pub before_signature: std::option::Option, + /// Optional signature cursor passed to Solana `getSignaturesForAddress.until`. + #[serde(default)] + pub until_signature: std::option::Option, + /// Maximum number of signature pages to fetch per source address. + #[serde(default)] + pub max_pages: u32, + /// Signature processing order: `newest_first` or `oldest_first` after all requested pages are fetched. + #[serde(default)] + pub scan_order: std::option::Option, /// Optional target event family used to score and filter candidate signatures. pub target_event: std::option::Option, /// Whether transactions containing swap-like logs should be skipped. @@ -50,6 +65,14 @@ pub struct OnchainDexPairDiscoveryResultDto { pub resolved_signature_source: std::string::String, /// Address scanned through `getSignaturesForAddress`. pub resolved_signature_address: std::string::String, + /// All addresses scanned through `getSignaturesForAddress` after normalization. + pub resolved_signature_addresses: std::vec::Vec, + /// Last page cursor candidates by source address, useful for a manual next paged run. + pub next_before_by_address: std::vec::Vec, + /// Number of signature pages fetched across all source addresses. + pub fetched_signature_page_count: usize, + /// Number of unique signatures fetched after de-duplicating multi-source scans. + pub unique_fetched_signature_count: usize, /// Number of unique signatures returned as candidates. pub unique_signature_count: usize, /// Unique signatures returned as backfill-ready hints. @@ -78,6 +101,20 @@ pub struct OnchainDexPairDiscoveryResultDto { pub candidates: std::vec::Vec, } +/// Cursor hint for one source address scanned by Demo3. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OnchainDexPaginationCursorDto { + /// Source address scanned by Solana `getSignaturesForAddress`. + pub address: std::string::String, + /// Signature usable as the next `before_signature` value for this same source address. + pub next_before_signature: std::option::Option, + /// Number of raw signatures fetched for this address. + pub fetched_signature_count: usize, + /// Number of RPC pages fetched for this address. + pub fetched_page_count: usize, +} + /// Rejected candidate summary for one discovery run. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] @@ -212,31 +249,32 @@ impl OnchainDexPairDiscoveryService { Ok(resolved) => resolved, Err(error) => return Err(error), }; - let signature_source = match resolve_signature_source(&normalized_request, &resolved) { - Ok(signature_source) => signature_source, + let signature_sources = match resolve_signature_sources(&normalized_request, &resolved) { + Ok(signature_sources) => signature_sources, Err(error) => return Err(error), }; - let signatures_result = self - .fetch_signatures( - normalized_request.http_role.as_str(), - signature_source.address.clone(), - normalized_request.signature_limit as usize, - ) - .await; - let signatures = match signatures_result { - Ok(signatures) => signatures, + let signature_fetch_result = + self.fetch_signature_pages(&normalized_request, &signature_sources).await; + let signature_fetch = match signature_fetch_result { + Ok(signature_fetch) => signature_fetch, Err(error) => return Err(error), }; let mut result = crate::OnchainDexPairDiscoveryResultDto { request: normalized_request.clone(), resolved_dex_code: resolved.dex_code.clone(), resolved_program_id: resolved.program_id.clone(), - resolved_signature_source: signature_source.source.clone(), - resolved_signature_address: signature_source.address.clone(), + resolved_signature_source: summarize_signature_source(signature_sources.as_slice()), + resolved_signature_address: summarize_signature_address(signature_sources.as_slice()), + resolved_signature_addresses: signature_sources_to_addresses( + signature_sources.as_slice(), + ), + next_before_by_address: signature_fetch.next_before_by_address, + fetched_signature_page_count: signature_fetch.fetched_signature_page_count, + unique_fetched_signature_count: signature_fetch.unique_signature_count, unique_signature_count: 0, unique_backfill_signatures: std::vec::Vec::new(), rejected_candidate_summary: std::vec::Vec::new(), - fetched_signature_count: signatures.len(), + fetched_signature_count: signature_fetch.raw_signature_count, fetched_transaction_count: 0, missing_transaction_count: 0, failed_transaction_count: 0, @@ -251,7 +289,7 @@ impl OnchainDexPairDiscoveryService { let candidate_limit = normalized_request.candidate_limit as usize; let mut returned_candidate_keys: std::vec::Vec = std::vec::Vec::new(); let mut scanned = 0usize; - for signature_status in signatures { + for signature_status in signature_fetch.signatures { if scanned >= transaction_limit { break; } @@ -326,27 +364,111 @@ impl OnchainDexPairDiscoveryService { return Ok(result); } - async fn fetch_signatures( + async fn fetch_signature_pages( &self, - http_role: &str, - address: std::string::String, - limit: usize, - ) -> Result< - std::vec::Vec, - crate::Error, - > { - let effective_limit = clamp_usize(limit, 1, 1000); - let config = solana_rpc_client_api::config::RpcSignaturesForAddressConfig { - before: None, - until: None, - limit: Some(effective_limit), - commitment: None, - min_context_slot: None, - }; - return self - .http_pool - .get_signatures_for_address_for_role(http_role, address, Some(config)) - .await; + request: &crate::OnchainDexPairDiscoveryRequestDto, + sources: &[ResolvedSignatureSource], + ) -> Result { + let effective_limit = clamp_usize(request.signature_limit as usize, 1, 1000); + let effective_max_pages = clamp_usize(request.max_pages as usize, 1, 25); + let mut unique_signatures: std::vec::Vec< + solana_rpc_client_api::response::RpcConfirmedTransactionStatusWithSignature, + > = std::vec::Vec::new(); + let mut seen_signatures: std::vec::Vec = std::vec::Vec::new(); + let mut next_before_by_address = std::vec::Vec::new(); + let mut raw_signature_count = 0usize; + let mut fetched_signature_page_count = 0usize; + let initial_before_signature = + match parse_signature_cursor(request.before_signature.as_deref(), "before_signature") { + Ok(value) => value, + Err(error) => return Err(error), + }; + let until_signature = + match parse_signature_cursor(request.until_signature.as_deref(), "until_signature") { + Ok(value) => value, + Err(error) => return Err(error), + }; + for source in sources { + let mut fetched_for_source = 0usize; + let mut pages_for_source = 0usize; + let mut next_before_signature = initial_before_signature.clone(); + let mut last_seen_signature: std::option::Option = None; + let mut page_index = 0usize; + while page_index < effective_max_pages { + let config = solana_rpc_client_api::config::RpcSignaturesForAddressConfig { + before: next_before_signature.clone(), + until: until_signature.clone(), + limit: Some(effective_limit), + commitment: None, + min_context_slot: None, + }; + let page_result = self + .http_pool + .get_signatures_for_address_for_role( + request.http_role.as_str(), + source.address.clone(), + Some(config), + ) + .await; + let page = match page_result { + Ok(page) => page, + Err(error) => return Err(error), + }; + pages_for_source += 1; + fetched_signature_page_count += 1; + let page_len = page.len(); + raw_signature_count += page_len; + if page.is_empty() { + break; + } + let mut next_cursor_option: std::option::Option = None; + for signature_status in page { + fetched_for_source += 1; + next_cursor_option = Some(signature_status.signature.clone()); + last_seen_signature = Some(signature_status.signature.clone()); + if seen_signatures + .iter() + .any(|existing| return existing == &signature_status.signature) + { + continue; + } + seen_signatures.push(signature_status.signature.clone()); + unique_signatures.push(signature_status); + } + let next_cursor = match next_cursor_option { + Some(next_cursor) => next_cursor, + None => break, + }; + let parsed_next_cursor = match parse_signature_cursor( + Some(next_cursor.as_str()), + "next_before_signature", + ) { + Ok(parsed_next_cursor) => parsed_next_cursor, + Err(error) => return Err(error), + }; + next_before_signature = parsed_next_cursor; + if page_len < effective_limit { + break; + } + page_index += 1; + } + next_before_by_address.push(crate::OnchainDexPaginationCursorDto { + address: source.address.clone(), + next_before_signature: last_seen_signature, + fetched_signature_count: fetched_for_source, + fetched_page_count: pages_for_source, + }); + } + if normalize_scan_order(request.scan_order.as_deref()) == "oldest_first" { + unique_signatures.reverse(); + } + return Ok(SignaturePageFetch { + signatures: unique_signatures, + raw_signature_count, + fetched_signature_page_count, + unique_signature_count: seen_signatures.len(), + next_before_by_address, + }); } async fn fetch_transaction( @@ -374,6 +496,16 @@ struct ResolvedSignatureSource { address: std::string::String, } +#[derive(Debug, Clone)] +struct SignaturePageFetch { + signatures: + std::vec::Vec, + raw_signature_count: usize, + fetched_signature_page_count: usize, + unique_signature_count: usize, + next_before_by_address: std::vec::Vec, +} + #[derive(Debug, Clone)] struct OnchainInstructionCandidate { instruction_index: std::option::Option, @@ -410,6 +542,43 @@ struct TokenBalanceAccumulator { post_amount_raw: std::option::Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Demo3MeteoraDammV1InstructionKind { + InitializePermissionlessConstantProductPoolWithConfig, + InitializePermissionlessConstantProductPoolWithConfig2, + InitializeCustomizablePermissionlessConstantProductPool, + Swap, + AddBalanceLiquidity, + AddImbalanceLiquidity, + BootstrapLiquidity, + RemoveBalanceLiquidity, + RemoveLiquiditySingleSide, + ClaimFee, + CreateLockEscrow, + Lock, + Unknown, +} + +const DEMO3_DAMM_V1_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG: [u8; 8] = + [0x07, 0xa6, 0x8a, 0xab, 0xce, 0xab, 0xec, 0xf4]; +const DEMO3_DAMM_V1_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG2: [u8; 8] = + [0x30, 0x95, 0xdc, 0x82, 0x3d, 0x0b, 0x09, 0xb2]; +const DEMO3_DAMM_V1_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_CP_POOL: [u8; 8] = + [0x91, 0x18, 0xac, 0xc2, 0xdb, 0x7d, 0x03, 0xbe]; +const DEMO3_DAMM_V1_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8]; +const DEMO3_DAMM_V1_ADD_BALANCE_LIQUIDITY: [u8; 8] = + [0xa8, 0xe3, 0x32, 0x3e, 0xbd, 0xab, 0x54, 0xb0]; +const DEMO3_DAMM_V1_ADD_IMBALANCE_LIQUIDITY: [u8; 8] = + [0x4f, 0x23, 0x7a, 0x54, 0xad, 0x0f, 0x5d, 0xbf]; +const DEMO3_DAMM_V1_BOOTSTRAP_LIQUIDITY: [u8; 8] = [0x04, 0xe4, 0xd7, 0x47, 0xe1, 0xfd, 0x77, 0xce]; +const DEMO3_DAMM_V1_REMOVE_BALANCE_LIQUIDITY: [u8; 8] = + [0x85, 0x6d, 0x2c, 0xb3, 0x38, 0xee, 0x72, 0x21]; +const DEMO3_DAMM_V1_REMOVE_LIQUIDITY_SINGLE_SIDE: [u8; 8] = + [0x54, 0x54, 0xb1, 0x42, 0xfe, 0xb9, 0x0a, 0xfb]; +const DEMO3_DAMM_V1_CLAIM_FEE: [u8; 8] = [0xa9, 0x20, 0x4f, 0x89, 0x88, 0xe8, 0x46, 0x89]; +const DEMO3_DAMM_V1_CREATE_LOCK_ESCROW: [u8; 8] = [0x36, 0x57, 0xa5, 0x13, 0x45, 0xe3, 0xda, 0xe0]; +const DEMO3_DAMM_V1_LOCK: [u8; 8] = [0x15, 0x13, 0xd0, 0x2b, 0xed, 0x3e, 0xff, 0x57]; + #[derive(Debug, Clone)] struct OnchainCandidateExtraction { candidates: std::vec::Vec, @@ -427,6 +596,11 @@ fn normalize_request( program_id: normalize_optional_string(request.program_id), signature_source: normalize_signature_source(request.signature_source), source_address: normalize_optional_string(request.source_address), + source_addresses: normalize_source_addresses(request.source_addresses), + before_signature: normalize_optional_signature(request.before_signature), + until_signature: normalize_optional_signature(request.until_signature), + max_pages: clamp_u32(default_if_zero(request.max_pages, 1), 1, 25), + scan_order: Some(normalize_scan_order(request.scan_order.as_deref()).to_string()), target_event: normalize_target_event(request.target_event), exclude_swaps: request.exclude_swaps, include_failed: request.include_failed, @@ -458,17 +632,64 @@ fn normalize_optional_string( return Some(value); } +fn normalize_optional_signature( + value: std::option::Option, +) -> std::option::Option { + return normalize_optional_string(value); +} + +fn normalize_source_addresses( + values: std::vec::Vec, +) -> std::vec::Vec { + let mut output = std::vec::Vec::new(); + for value in values { + append_source_address_tokens(value.as_str(), &mut output); + } + return output; +} + +fn append_source_address_tokens(value: &str, output: &mut std::vec::Vec) { + for token in value.split(|character: char| { + return character == ',' || character == ';' || character.is_whitespace(); + }) { + let trimmed = token.trim(); + if trimmed.is_empty() { + continue; + } + push_unique_string(output, trimmed.to_string()); + } +} + +fn default_if_zero(value: u32, fallback: u32) -> u32 { + if value == 0 { + return fallback; + } + return value; +} + +fn normalize_scan_order(value: std::option::Option<&str>) -> &str { + let value = match value { + Some(value) => value.trim().to_ascii_lowercase().replace(['-', ' '], "_"), + None => return "newest_first", + }; + if value == "oldest_first" || value == "oldest" { + return "oldest_first"; + } + return "newest_first"; +} + fn normalize_target_event( value: std::option::Option, ) -> std::option::Option { let value = match normalize_optional_string(value) { - Some(value) => value.to_ascii_lowercase(), + Some(value) => value, None => return None, }; - if value == "any" || value == "all" { + let targets = split_target_event_filter(Some(value.as_str())); + if targets.is_empty() { return None; } - return Some(value.replace(['-', ' '], "_")); + return Some(targets.join(",")); } fn normalize_signature_source( @@ -524,37 +745,36 @@ fn resolve_program_id( return Ok(ResolvedDiscoveryTarget { dex_code: Some(dex_code), program_id }); } -fn resolve_signature_source( +fn resolve_signature_sources( request: &crate::OnchainDexPairDiscoveryRequestDto, resolved: &ResolvedDiscoveryTarget, -) -> Result { +) -> Result, crate::Error> { let explicit_source = request.signature_source.as_deref(); - let source_address = request.source_address.clone(); - if explicit_source == Some("address") { - let address = match source_address { - Some(address) => address, - None => { - return Err(crate::Error::Config( - "signature_source='address' requires a non-empty source_address".to_string(), - )); - }, - }; - if !looks_like_solana_address(address.as_str()) { - return Err(crate::Error::Config(format!( - "invalid source_address '{}' for on-chain DEX discovery; provide a Solana account address, pool, vault, position, config or mint, not the literal source type", - address - ))); - } - return Ok(ResolvedSignatureSource { source: "address".to_string(), address }); + let mut addresses = std::vec::Vec::new(); + if let Some(source_address) = &request.source_address { + append_source_address_tokens(source_address.as_str(), &mut addresses); } - if let Some(address) = source_address { - if !looks_like_solana_address(address.as_str()) { - return Err(crate::Error::Config(format!( - "invalid source_address '{}' for on-chain DEX discovery; clear it or provide a valid Solana account address", - address - ))); + for source_address in &request.source_addresses { + append_source_address_tokens(source_address.as_str(), &mut addresses); + } + if explicit_source == Some("address") || !addresses.is_empty() { + if addresses.is_empty() { + return Err(crate::Error::Config( + "signature_source='address' requires at least one non-empty source address" + .to_string(), + )); } - return Ok(ResolvedSignatureSource { source: "address".to_string(), address }); + let mut sources = std::vec::Vec::new(); + for address in addresses { + if !looks_like_solana_address(address.as_str()) { + return Err(crate::Error::Config(format!( + "invalid source_address '{}' for on-chain DEX discovery; provide Solana account addresses separated by commas, spaces or new lines", + address + ))); + } + sources.push(ResolvedSignatureSource { source: "address".to_string(), address }); + } + return Ok(sources); } if !looks_like_solana_address(resolved.program_id.as_str()) { return Err(crate::Error::Config(format!( @@ -562,10 +782,51 @@ fn resolve_signature_source( resolved.program_id ))); } - return Ok(ResolvedSignatureSource { + return Ok(vec![ResolvedSignatureSource { source: "program_id".to_string(), address: resolved.program_id.clone(), - }); + }]); +} + +fn summarize_signature_source(sources: &[ResolvedSignatureSource]) -> std::string::String { + if sources.len() > 1 { + return "addresses".to_string(); + } + let source = match sources.first() { + Some(source) => source.source.clone(), + None => return "program_id".to_string(), + }; + return source; +} + +fn summarize_signature_address(sources: &[ResolvedSignatureSource]) -> std::string::String { + if sources.len() > 1 { + let mut text = std::string::String::new(); + let mut index = 0usize; + for source in sources { + if index > 0 { + text.push(','); + } + text.push_str(source.address.as_str()); + index += 1; + } + return text; + } + let address = match sources.first() { + Some(source) => source.address.clone(), + None => return std::string::String::new(), + }; + return address; +} + +fn signature_sources_to_addresses( + sources: &[ResolvedSignatureSource], +) -> std::vec::Vec { + let mut addresses = std::vec::Vec::new(); + for source in sources { + push_unique_string(&mut addresses, source.address.clone()); + } + return addresses; } fn looks_like_solana_address(value: &str) -> bool { @@ -581,6 +842,39 @@ fn looks_like_solana_address(value: &str) -> bool { return true; } +fn looks_like_solana_signature(value: &str) -> bool { + let length = value.len(); + if !(64..=128).contains(&length) { + return false; + } + for character in value.chars() { + if !is_base58_character(character) { + return false; + } + } + return true; +} + +fn parse_signature_cursor( + value: std::option::Option<&str>, + label: &str, +) -> Result, crate::Error> { + let value = match value { + Some(value) => value.trim(), + None => return Ok(None), + }; + if value.is_empty() { + return Ok(None); + } + if !looks_like_solana_signature(value) { + return Err(crate::Error::Config(format!( + "{} must be a Solana transaction signature, got '{}'", + label, value + ))); + } + return Ok(Some(value.to_string())); +} + fn is_base58_character(character: char) -> bool { return matches!( character, @@ -704,6 +998,18 @@ fn decode_known_candidate( logs, ); } + if program_id == crate::METEORA_DAMM_V1_PROGRAM_ID { + return decode_meteora_damm_v1_candidate( + signature, + slot, + block_time, + failed, + program_id, + dex_code, + instruction, + logs, + ); + } return None; } @@ -907,6 +1213,243 @@ fn build_raydium_cpmm_candidate( }; } +fn decode_meteora_damm_v1_candidate( + signature: &str, + slot: std::option::Option, + block_time: std::option::Option, + failed: bool, + program_id: &str, + dex_code: std::option::Option, + instruction: &OnchainInstructionCandidate, + logs: &[std::string::String], +) -> std::option::Option { + let data = match decode_onchain_instruction_data(instruction.data.as_deref()) { + Some(data) => data, + None => return None, + }; + let instruction_kind = classify_demo3_meteora_damm_v1_instruction(data.as_slice()); + if instruction_kind == Demo3MeteoraDammV1InstructionKind::Unknown { + return None; + } + let candidate_kind = demo3_meteora_damm_v1_candidate_kind(instruction_kind).to_string(); + let instruction_name = demo3_meteora_damm_v1_instruction_name(instruction_kind).to_string(); + let pool_address = demo3_account_at(instruction.accounts.as_slice(), 0); + let token_a_mint = + demo3_meteora_damm_v1_token_a_mint(instruction.accounts.as_slice(), instruction_kind); + let token_b_mint = + demo3_meteora_damm_v1_token_b_mint(instruction.accounts.as_slice(), instruction_kind); + let verified_pool_address = pool_address.clone(); + let backfill_hint = + demo3_meteora_damm_v1_backfill_hint(instruction_kind, pool_address.as_deref(), signature); + return Some(crate::OnchainDexPairCandidateDto { + signature: signature.to_string(), + slot, + block_time, + failed, + program_id: program_id.to_string(), + dex_code, + candidate_kind, + confidence: "high".to_string(), + instruction_index: instruction.instruction_index, + inner_instruction_index: instruction.inner_instruction_index, + instruction_name: Some(instruction_name), + instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()), + pool_address, + token_a_mint, + token_b_mint, + verified_pool_address, + observed_token_mints: std::vec::Vec::new(), + token_balance_deltas: std::vec::Vec::new(), + candidate_pool_accounts: std::vec::Vec::new(), + candidate_token_vault_accounts: std::vec::Vec::new(), + candidate_program_accounts: std::vec::Vec::new(), + account_samples: sample_strings(instruction.accounts.as_slice(), 12), + log_samples: sample_logs(logs, 8), + backfill_hint, + }); +} + +fn decode_onchain_instruction_data( + data: std::option::Option<&str>, +) -> std::option::Option> { + let data = match data { + Some(data) => data.trim(), + None => return None, + }; + if data.is_empty() { + return None; + } + let decoded_result = bs58::decode(data).into_vec(); + match decoded_result { + Ok(decoded) => return Some(decoded), + Err(_) => return None, + } +} + +fn classify_demo3_meteora_damm_v1_instruction(data: &[u8]) -> Demo3MeteoraDammV1InstructionKind { + if data.len() < 8 { + return Demo3MeteoraDammV1InstructionKind::Unknown; + } + let discriminator = &data[0..8]; + if discriminator == DEMO3_DAMM_V1_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG.as_slice() { + return Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig; + } + if discriminator == DEMO3_DAMM_V1_INITIALIZE_PERMISSIONLESS_CP_POOL_WITH_CONFIG2.as_slice() { + return Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2; + } + if discriminator == DEMO3_DAMM_V1_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_CP_POOL.as_slice() { + return Demo3MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool; + } + if discriminator == DEMO3_DAMM_V1_SWAP.as_slice() { + return Demo3MeteoraDammV1InstructionKind::Swap; + } + if discriminator == DEMO3_DAMM_V1_ADD_BALANCE_LIQUIDITY.as_slice() { + return Demo3MeteoraDammV1InstructionKind::AddBalanceLiquidity; + } + if discriminator == DEMO3_DAMM_V1_ADD_IMBALANCE_LIQUIDITY.as_slice() { + return Demo3MeteoraDammV1InstructionKind::AddImbalanceLiquidity; + } + if discriminator == DEMO3_DAMM_V1_BOOTSTRAP_LIQUIDITY.as_slice() { + return Demo3MeteoraDammV1InstructionKind::BootstrapLiquidity; + } + if discriminator == DEMO3_DAMM_V1_REMOVE_BALANCE_LIQUIDITY.as_slice() { + return Demo3MeteoraDammV1InstructionKind::RemoveBalanceLiquidity; + } + if discriminator == DEMO3_DAMM_V1_REMOVE_LIQUIDITY_SINGLE_SIDE.as_slice() { + return Demo3MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide; + } + if discriminator == DEMO3_DAMM_V1_CLAIM_FEE.as_slice() { + return Demo3MeteoraDammV1InstructionKind::ClaimFee; + } + if discriminator == DEMO3_DAMM_V1_CREATE_LOCK_ESCROW.as_slice() { + return Demo3MeteoraDammV1InstructionKind::CreateLockEscrow; + } + if discriminator == DEMO3_DAMM_V1_LOCK.as_slice() { + return Demo3MeteoraDammV1InstructionKind::Lock; + } + return Demo3MeteoraDammV1InstructionKind::Unknown; +} + +fn demo3_meteora_damm_v1_candidate_kind( + instruction_kind: Demo3MeteoraDammV1InstructionKind, +) -> &'static str { + match instruction_kind { + Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 + | Demo3MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return "create_pool"; + }, + Demo3MeteoraDammV1InstructionKind::Swap => return "swap", + Demo3MeteoraDammV1InstructionKind::AddBalanceLiquidity + | Demo3MeteoraDammV1InstructionKind::AddImbalanceLiquidity + | Demo3MeteoraDammV1InstructionKind::BootstrapLiquidity => return "add_liquidity", + Demo3MeteoraDammV1InstructionKind::RemoveBalanceLiquidity + | Demo3MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide => return "remove_liquidity", + Demo3MeteoraDammV1InstructionKind::ClaimFee => return "claim_fee", + Demo3MeteoraDammV1InstructionKind::CreateLockEscrow => return "create_lock_escrow", + Demo3MeteoraDammV1InstructionKind::Lock => return "lock_liquidity", + Demo3MeteoraDammV1InstructionKind::Unknown => return "unclassified_instruction", + } +} + +fn demo3_meteora_damm_v1_instruction_name( + instruction_kind: Demo3MeteoraDammV1InstructionKind, +) -> &'static str { + match instruction_kind { + Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig => { + return "meteora_damm_v1.InitializePermissionlessConstantProductPoolWithConfig"; + }, + Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return "meteora_damm_v1.InitializePermissionlessConstantProductPoolWithConfig2"; + }, + Demo3MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return "meteora_damm_v1.InitializeCustomizablePermissionlessConstantProductPool"; + }, + Demo3MeteoraDammV1InstructionKind::Swap => return "meteora_damm_v1.Swap", + Demo3MeteoraDammV1InstructionKind::AddBalanceLiquidity => { + return "meteora_damm_v1.AddBalanceLiquidity"; + }, + Demo3MeteoraDammV1InstructionKind::AddImbalanceLiquidity => { + return "meteora_damm_v1.AddImbalanceLiquidity"; + }, + Demo3MeteoraDammV1InstructionKind::BootstrapLiquidity => { + return "meteora_damm_v1.BootstrapLiquidity"; + }, + Demo3MeteoraDammV1InstructionKind::RemoveBalanceLiquidity => { + return "meteora_damm_v1.RemoveBalanceLiquidity"; + }, + Demo3MeteoraDammV1InstructionKind::RemoveLiquiditySingleSide => { + return "meteora_damm_v1.RemoveLiquiditySingleSide"; + }, + Demo3MeteoraDammV1InstructionKind::ClaimFee => return "meteora_damm_v1.ClaimFee", + Demo3MeteoraDammV1InstructionKind::CreateLockEscrow => { + return "meteora_damm_v1.CreateLockEscrow"; + }, + Demo3MeteoraDammV1InstructionKind::Lock => return "meteora_damm_v1.Lock", + Demo3MeteoraDammV1InstructionKind::Unknown => return "meteora_damm_v1.Unknown", + } +} + +fn demo3_meteora_damm_v1_token_a_mint( + accounts: &[std::string::String], + instruction_kind: Demo3MeteoraDammV1InstructionKind, +) -> std::option::Option { + match instruction_kind { + Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return demo3_account_at(accounts, 3); + }, + Demo3MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return demo3_account_at(accounts, 2); + }, + _ => return None, + } +} + +fn demo3_meteora_damm_v1_token_b_mint( + accounts: &[std::string::String], + instruction_kind: Demo3MeteoraDammV1InstructionKind, +) -> std::option::Option { + match instruction_kind { + Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig + | Demo3MeteoraDammV1InstructionKind::InitializePermissionlessConstantProductPoolWithConfig2 => { + return demo3_account_at(accounts, 4); + }, + Demo3MeteoraDammV1InstructionKind::InitializeCustomizablePermissionlessConstantProductPool => { + return demo3_account_at(accounts, 3); + }, + _ => return None, + } +} + +fn demo3_meteora_damm_v1_backfill_hint( + instruction_kind: Demo3MeteoraDammV1InstructionKind, + pool_address: std::option::Option<&str>, + signature: &str, +) -> std::string::String { + let label = demo3_meteora_damm_v1_candidate_kind(instruction_kind); + if let Some(pool_address) = pool_address { + return format!( + "Upstream Git DAMM v1 {} candidate; backfill pool in Demo Pipeline 2: {} ; signature: {}", + label, pool_address, signature + ); + } + return format!( + "Upstream Git DAMM v1 {} candidate; inspect/backfill transaction signature: {}", + label, signature + ); +} + +fn demo3_account_at( + accounts: &[std::string::String], + index: usize, +) -> std::option::Option { + if index >= accounts.len() { + return None; + } + return Some(accounts[index].clone()); +} + fn build_heuristic_candidate( signature: &str, slot: std::option::Option, @@ -1714,16 +2257,16 @@ fn infer_candidate_kind( text.push_str(log_text.as_str()); let lower = text.to_ascii_lowercase(); let instruction_lower = instruction_text.to_ascii_lowercase(); + if instruction_lower.contains("swap") + || instruction_lower.contains("buy") + || instruction_lower.contains("sell") + { + return "swap".to_string(); + } if prefer_instruction_local_classification { if text_matches_non_swap_target(lower.as_str()) { return infer_non_swap_candidate_kind(lower.as_str()); } - if instruction_lower.contains("swap") - || instruction_lower.contains("buy") - || instruction_lower.contains("sell") - { - return "swap".to_string(); - } if has_raw_instruction_data { return "unclassified_instruction".to_string(); } @@ -1768,17 +2311,20 @@ fn infer_candidate_kind( fn target_event_prefers_instruction_local_classification( target_event: std::option::Option<&str>, ) -> bool { - match target_event { - Some("unknown_non_swap") - | Some("audit_non_swap_like") - | Some("unclassified_instruction") - | Some("non_swap") => return true, - _ => return false, + for target in split_target_event_filter(target_event) { + if target == "unknown_non_swap" + || target == "audit_non_swap_like" + || target == "unclassified_instruction" + || target == "non_swap" + { + return true; + } } + return false; } fn target_event_keeps_mixed_swap_transactions(target_event: std::option::Option<&str>) -> bool { - return target_event_prefers_instruction_local_classification(target_event); + return !split_target_event_filter(target_event).is_empty(); } fn instruction_data_prefix( @@ -1843,33 +2389,82 @@ fn infer_non_swap_candidate_kind(lower: &str) -> std::string::String { return "non_swap_activity".to_string(); } +fn split_target_event_filter( + target_event: std::option::Option<&str>, +) -> std::vec::Vec { + let mut targets = std::vec::Vec::new(); + let value = match target_event { + Some(value) => value, + None => return targets, + }; + for token in value.split(|character: char| { + return character == ',' || character == ';' || character.is_whitespace(); + }) { + let normalized = token.trim().to_ascii_lowercase().replace(['-', ' '], "_"); + if normalized.is_empty() || normalized == "any" || normalized == "all" { + continue; + } + push_unique_string(&mut targets, normalized); + } + return targets; +} + fn refine_candidate_for_target( candidate: &mut crate::OnchainDexPairCandidateDto, target_event: std::option::Option<&str>, ) { - let target_event = match target_event { - Some(target_event) => target_event, + if split_target_event_filter(target_event).is_empty() { + return; + } + let matched_target_event = match first_matching_target_event(candidate, target_event) { + Some(matched_target_event) => matched_target_event, None => return, }; - if candidate_matches_target_event(candidate, Some(target_event)) { - if candidate.confidence == "low" { - candidate.confidence = "medium".to_string(); - } - candidate.backfill_hint = format!( - "Target '{}' candidate; inspect/backfill transaction signature: {}", - target_event, candidate.signature - ); + if candidate.confidence == "low" { + candidate.confidence = "medium".to_string(); } + let pool_address = + candidate.verified_pool_address.as_deref().or(candidate.pool_address.as_deref()); + if let Some(pool_address) = pool_address { + candidate.backfill_hint = format!( + "Target '{}' candidate; backfill pool in Demo Pipeline 2: {} ; signature: {}", + matched_target_event, pool_address, candidate.signature + ); + return; + } + candidate.backfill_hint = format!( + "Target '{}' candidate; inspect/backfill transaction signature: {}", + matched_target_event, candidate.signature + ); +} + +fn first_matching_target_event( + candidate: &crate::OnchainDexPairCandidateDto, + target_event: std::option::Option<&str>, +) -> std::option::Option { + let targets = split_target_event_filter(target_event); + if targets.is_empty() { + return Some("any".to_string()); + } + for target in targets { + if candidate_matches_single_target_event(candidate, target.as_str()) { + return Some(target); + } + } + return None; } fn candidate_matches_target_event( candidate: &crate::OnchainDexPairCandidateDto, target_event: std::option::Option<&str>, ) -> bool { - let target_event = match target_event { - Some(target_event) => target_event, - None => return true, - }; + return first_matching_target_event(candidate, target_event).is_some(); +} + +fn candidate_matches_single_target_event( + candidate: &crate::OnchainDexPairCandidateDto, + target_event: &str, +) -> bool { if target_event == "unknown_non_swap" || target_event == "audit_non_swap_like" || target_event == "non_swap" @@ -1880,6 +2475,12 @@ fn candidate_matches_target_event( return candidate.candidate_kind == "unclassified_instruction" && !candidate_is_known_trade_like_surface(candidate); } + if exact_candidate_kind_matches_target(candidate.candidate_kind.as_str(), target_event) { + return true; + } + if candidate_kind_is_explicit_surface(candidate.candidate_kind.as_str()) { + return false; + } let mut text = candidate.candidate_kind.clone(); text.push(' '); if let Some(instruction_name) = &candidate.instruction_name { @@ -1911,17 +2512,58 @@ fn candidate_matches_target_event( "pool_admin" | "admin" | "config" | "authority" => { return text_matches_pool_admin(lower.as_str()); }, - "audit_non_swap_like" | "non_swap" => { - return candidate_is_non_swap_audit_candidate(candidate); + "create_lock_escrow" => { + return lower.contains("createlockescrow") + || lower.contains("create_lock_escrow") + || lower.contains("create lock escrow"); }, - "unclassified_instruction" => { - return candidate.candidate_kind == "unclassified_instruction" - && !candidate_is_known_trade_like_surface(candidate); + "lock_liquidity" | "lock" => { + return lower.contains("lockliquidity") + || lower.contains("lock_liquidity") + || lower.contains("lock liquidity") + || lower.contains("meteora_damm_v1.lock"); }, _ => return true, } } +fn exact_candidate_kind_matches_target(candidate_kind: &str, target_event: &str) -> bool { + match target_event { + "swap" => return candidate_kind == "swap" || candidate_kind == "trade_like_unclassified", + "add_liquidity" => return candidate_kind == "add_liquidity", + "remove_liquidity" => return candidate_kind == "remove_liquidity", + "claim_fee" => return candidate_kind == "claim_fee", + "claim_reward" | "reward" => return candidate_kind == "claim_reward", + "position_open" | "open_position" => return candidate_kind == "position_open", + "position_close" | "close_position" => return candidate_kind == "position_close", + "pool_create" | "create_pool" => { + return candidate_kind == "create_pool" || candidate_kind == "initialize_pool"; + }, + "pool_admin" | "admin" | "config" | "authority" => { + return candidate_kind == "pool_admin" || candidate_kind == "lock_liquidity"; + }, + "create_lock_escrow" => return candidate_kind == "create_lock_escrow", + "lock_liquidity" | "lock" => return candidate_kind == "lock_liquidity", + _ => return false, + } +} + +fn candidate_kind_is_explicit_surface(candidate_kind: &str) -> bool { + return candidate_kind == "swap" + || candidate_kind == "trade_like_unclassified" + || candidate_kind == "add_liquidity" + || candidate_kind == "remove_liquidity" + || candidate_kind == "claim_fee" + || candidate_kind == "claim_reward" + || candidate_kind == "position_open" + || candidate_kind == "position_close" + || candidate_kind == "pool_admin" + || candidate_kind == "create_pool" + || candidate_kind == "initialize_pool" + || candidate_kind == "create_lock_escrow" + || candidate_kind == "lock_liquidity"; +} + fn candidate_is_non_swap_audit_candidate(candidate: &crate::OnchainDexPairCandidateDto) -> bool { if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) { return false; @@ -1939,6 +2581,8 @@ fn candidate_is_non_swap_audit_candidate(candidate: &crate::OnchainDexPairCandid || candidate.candidate_kind == "pool_admin" || candidate.candidate_kind == "create_pool" || candidate.candidate_kind == "initialize_pool" + || candidate.candidate_kind == "create_lock_escrow" + || candidate.candidate_kind == "lock_liquidity" { return true; } @@ -2213,6 +2857,11 @@ mod tests { program_id: None, signature_source: None, source_address: None, + source_addresses: std::vec::Vec::new(), + before_signature: None, + until_signature: None, + max_pages: 1, + scan_order: None, target_event: None, exclude_swaps: false, include_failed: true, @@ -2238,6 +2887,11 @@ mod tests { program_id: None, signature_source: None, source_address: None, + source_addresses: std::vec::Vec::new(), + before_signature: None, + until_signature: None, + max_pages: 1, + scan_order: None, target_event: None, exclude_swaps: false, include_failed: true, @@ -2288,19 +2942,165 @@ mod tests { #[test] fn non_trade_liquidity_candidate_remains_available_for_non_swap_audit() { - let candidate = crate::OnchainDexPairCandidateDto { + let candidate = make_candidate("add_liquidity", "AddLiquidity"); + assert!(super::candidate_matches_target_event(&candidate, Some("audit_non_swap_like"))); + } + + #[test] + fn heuristic_swap_instruction_name_wins_over_mixed_liquidity_logs() { + let logs = vec![ + "Program log: Instruction: AddLiquidity".to_string(), + "Program log: deposit".to_string(), + ]; + let instruction_name = "Swap".to_string(); + let kind = + super::infer_candidate_kind(Some(&instruction_name), logs.as_slice(), true, false); + assert_eq!(kind, "swap".to_string()); + } + + #[test] + fn target_swap_rejects_explicit_liquidity_candidate_from_mixed_logs() { + let mut candidate = make_candidate("add_liquidity", "AddLiquidity"); + candidate.log_samples.push("Program log: Instruction: Swap".to_string()); + assert!(!super::candidate_matches_target_event(&candidate, Some("swap"))); + } + + #[test] + fn target_add_liquidity_rejects_explicit_swap_candidate_from_mixed_logs() { + let mut candidate = make_candidate("swap", "Swap"); + candidate.log_samples.push("Program log: Instruction: AddLiquidity".to_string()); + assert!(!super::candidate_matches_target_event(&candidate, Some("add_liquidity"))); + } + + #[test] + fn meteora_damm_v1_claim_fee_candidate_is_decoded_for_demo3() { + let mut data = super::DEMO3_DAMM_V1_CLAIM_FEE.to_vec(); + data.extend_from_slice(&500_u64.to_le_bytes()); + let instruction = super::OnchainInstructionCandidate { + instruction_index: Some(2), + inner_instruction_index: None, + program_id: Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()), + accounts: vec![ + "Pool111".to_string(), + "LpMint111".to_string(), + "LockEscrow111".to_string(), + "Owner111".to_string(), + "SourceTokens111".to_string(), + "EscrowVault111".to_string(), + "TokenProgram111".to_string(), + "ATokenVault111".to_string(), + "BTokenVault111".to_string(), + "AVault111".to_string(), + "BVault111".to_string(), + "AVaultLp111".to_string(), + "BVaultLp111".to_string(), + "AVaultLpMint111".to_string(), + "BVaultLpMint111".to_string(), + "UserA111".to_string(), + "UserB111".to_string(), + "VaultProgram111".to_string(), + ], + data: Some(bs58::encode(data.as_slice()).into_string()), + parsed: None, + }; + let decoded = super::decode_meteora_damm_v1_candidate( + "sig111", + Some(42), + Some(1234), + false, + crate::METEORA_DAMM_V1_PROGRAM_ID, + Some("meteora_damm_v1".to_string()), + &instruction, + &[], + ); + let candidate = match decoded { + Some(candidate) => candidate, + None => panic!("candidate must decode"), + }; + assert_eq!(candidate.candidate_kind, "claim_fee".to_string()); + assert_eq!(candidate.pool_address, Some("Pool111".to_string())); + assert_eq!(candidate.verified_pool_address, Some("Pool111".to_string())); + assert!(super::candidate_matches_target_event(&candidate, Some("claim_fee"))); + } + + #[test] + fn split_target_event_filter_keeps_multiple_targets() { + let targets = + super::split_target_event_filter(Some("claim_fee, remove-liquidity;pool_create all")); + assert_eq!( + targets, + vec![ + "claim_fee".to_string(), + "remove_liquidity".to_string(), + "pool_create".to_string(), + ] + ); + } + + #[test] + fn resolve_multiple_source_addresses_from_single_field() { + let request = crate::OnchainDexPairDiscoveryRequestDto { + dex_code: Some("meteora_damm_v1".to_string()), + program_id: Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()), + signature_source: Some("address".to_string()), + source_address: Some( + "BCXjm4FfSoquZQJV5Wcje1g1pSHW2hFMU9wDE98Nyatb, EYAVndnE1Fm88iat97rWKYcEARNrgHA47v6vnjTzvw7w".to_string(), + ), + source_addresses: std::vec::Vec::new(), + before_signature: None, + until_signature: None, + max_pages: 2, + scan_order: Some("oldest_first".to_string()), + target_event: Some("claim_fee,remove_liquidity".to_string()), + exclude_swaps: true, + include_failed: true, + http_role: "history_backfill".to_string(), + signature_limit: 10, + transaction_limit: 5, + candidate_limit: 3, + }; + let normalized = super::normalize_request(request); + assert_eq!(normalized.max_pages, 2); + assert_eq!(normalized.scan_order, Some("oldest_first".to_string())); + assert_eq!(normalized.target_event, Some("claim_fee,remove_liquidity".to_string())); + let resolved = super::ResolvedDiscoveryTarget { + dex_code: Some("meteora_damm_v1".to_string()), + program_id: crate::METEORA_DAMM_V1_PROGRAM_ID.to_string(), + }; + let sources = super::resolve_signature_sources(&normalized, &resolved); + match sources { + Ok(sources) => { + assert_eq!(sources.len(), 2); + assert_eq!( + sources[0].address, + "BCXjm4FfSoquZQJV5Wcje1g1pSHW2hFMU9wDE98Nyatb".to_string() + ); + assert_eq!( + sources[1].address, + "EYAVndnE1Fm88iat97rWKYcEARNrgHA47v6vnjTzvw7w".to_string() + ); + }, + Err(error) => panic!("multi source resolution must succeed: {error}"), + } + } + + fn make_candidate( + candidate_kind: &str, + instruction_name: &str, + ) -> crate::OnchainDexPairCandidateDto { + return crate::OnchainDexPairCandidateDto { signature: "sig".to_string(), slot: None, block_time: None, failed: false, program_id: "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG".to_string(), dex_code: Some("meteora_damm_v2".to_string()), - candidate_kind: "add_liquidity".to_string(), + candidate_kind: candidate_kind.to_string(), confidence: "medium".to_string(), instruction_index: Some(1), inner_instruction_index: None, - instruction_name: Some("AddLiquidity".to_string()), - instruction_data_prefix: Some("nonTradePrefix".to_string()), + instruction_name: Some(instruction_name.to_string()), + instruction_data_prefix: Some("prefix".to_string()), pool_address: None, token_a_mint: None, token_b_mint: None, @@ -2314,6 +3114,5 @@ mod tests { log_samples: std::vec::Vec::new(), backfill_hint: "hint".to_string(), }; - assert!(super::candidate_matches_target_event(&candidate, Some("audit_non_swap_like"))); } }