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 @@
- Source address
-
+ Source addresses
+
- 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.
+
+ Before signature
+
+
+
+ Until signature
+
+
+
+ Max pages / address
+
+
+
+ Scan order
+
+ newest_first
+ oldest_first
+
+
+ For pool creation analysis, scan a pool address with enough pages and use oldest_first to process the oldest fetched signatures first.
-
Target event
-
- Any / generic pair-pool discovery
- swap
- add_liquidity
- remove_liquidity
- claim_fee / collect_fee
- claim_reward
- position_open
- position_close
- pool_create / initialize_pool
- pool_admin / config / authority
- unknown_non_swap
- audit_non_swap_like
- unclassified_instruction
-
+
Target events
+
+
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")));
}
}