This commit is contained in:
2026-05-14 12:49:50 +02:00
parent 348ae7f153
commit edc8da02a3
12 changed files with 823 additions and 120 deletions

View File

@@ -62,7 +62,8 @@
0.7.29 - Ajout dune matrice DEX commune (`dex_support_matrix`) utilisée par le catalogue DEX, la classification transactionnelle et lenregistrement des protocol candidates ; ajout du profil de validation `0.7.29_multi_dex_matrix_baseline` exposant la matrice dans le rapport de validation ; préparation explicite des surfaces planifiées sans inventer de program ids non vérifiés. 0.7.29 - Ajout dune matrice DEX commune (`dex_support_matrix`) utilisée par le catalogue DEX, la classification transactionnelle et lenregistrement des protocol candidates ; ajout du profil de validation `0.7.29_multi_dex_matrix_baseline` exposant la matrice dans le rapport de validation ; préparation explicite des surfaces planifiées sans inventer de program ids non vérifiés.
0.7.30 - Ajout dune taxonomie DEX plus fine pour les événements décodés : `eventLifecycleKind`, `eventActionability`, `nonTradeUseful`, compteurs diagnostics des événements non-trade utiles, trades non actionnables et classifications inconnues ; ajout du profil `0.7.30_non_trade_event_classification` sans modification volontaire de la matérialisation trade/candle. 0.7.30 - Ajout dune taxonomie DEX plus fine pour les événements décodés : `eventLifecycleKind`, `eventActionability`, `nonTradeUseful`, compteurs diagnostics des événements non-trade utiles, trades non actionnables et classifications inconnues ; ajout du profil `0.7.30_non_trade_event_classification` sans modification volontaire de la matérialisation trade/candle.
0.7.31 - Application de la politique Option B : les transactions failed restent traçables dans les événements décodés mais ne peuvent plus alimenter `trade_events`, metrics ou candles ; le replay local réinitialise les tables de matérialisation marché avant reconstruction pour supprimer les anciennes lignes dérivées non actionnables. 0.7.31 - Application de la politique Option B : les transactions failed restent traçables dans les événements décodés mais ne peuvent plus alimenter `trade_events`, metrics ou candles ; le replay local réinitialise les tables de matérialisation marché avant reconstruction pour supprimer les anciennes lignes dérivées non actionnables.
0.7.32 - Clarification des sémantiques de validation locale : distinction entre gaps littéraux, gaps bloquants et paires actionnables, afin déviter de bloquer sur des paires détectées mais non matérialisées par trade. 0.7.32 - Clarification de la sémantique des diagnostics locaux : séparation des gaps littéraux de paires et des gaps bloquants/actionnables, ajout des compteurs de matérialisation par paire, résumé `pairActionabilitySummaries`, profil `0.7.32_validation_report_semantics` et garde-fous sur la matrice DEX sans modification de la matérialisation trade/candle.
0.7.33 - Ajout du profil `0.7.33_pair_trading_readiness`, avec classification des paires directes WSOL, directes stable, inverses stable/WSOL et cross-quotes nécessitant un router. 0.7.33 - Ajout de la classification diagnostique `pairTradingReadiness` pour les paires, avec `quoteAssetClass`, `tradingRouteRequired`, résumé `pairTradingReadinessSummaries`, profil de validation `0.7.33_pair_trading_readiness` et mise à jour de la sélection UI Demo Pipeline 2 sans modifier la matérialisation trade/candle.
0.7.34 - Ajout du profil `0.7.34_non_trade_liquidity_lifecycle`, matérialisation des tables non-trade liquidité/lifecycle, warning non bloquant pour DEX attendus absents du corpus local, première tranche DLMM : `add_liquidity`, `remove_liquidity`, `initialize_position`, `initialize_bin_array`, intégration de la matérialisation non-trade dans les backfills token/pool ciblés, et distinction `PositionOpen`/`PositionClose` dans `LiquidityEventKind`. 0.7.34 - Ajout du profil `0.7.34_non_trade_liquidity_lifecycle`, matérialisation des tables non-trade liquidité/lifecycle, warning non bloquant pour DEX attendus absents du corpus local, première tranche DLMM : `add_liquidity`, `remove_liquidity`, `initialize_position`, `initialize_bin_array`, intégration de la matérialisation non-trade dans les backfills token/pool ciblés, et distinction `PositionOpen`/`PositionClose` dans `LiquidityEventKind`.
0.7.35 - Ajout du profil `0.7.35_non_trade_fee_reward_admin`, événements non-trade p2 : fees, rewards et administration. 0.7.35 - Ajout du profil `0.7.35_non_trade_fee_reward_admin`, matérialisation des événements non-trade fees/rewards/admin, raccordement aux diagnostics locaux et maintien strict de linvariant : aucun fee/reward/admin ne peut produire de trade, metric ou candle.
0.7.36 - Consolidation de la famille Meteora : corpus mixte `meteora_damm_v1`, `meteora_damm_v2`, `meteora_dbc` et `meteora_dlmm`, correction des discriminants DAMM v2 / DBC, validation du profil `0.7.36_meteora_family_consolidation`, et reclassement explicite des swaps DAMM v2 / DBC sans payload montant/prix en `non_actionable_trade` afin déviter tout trade/candle artificiel.

View File

@@ -8,7 +8,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.7.35" version = "0.7.36-D"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"

View File

@@ -4,7 +4,7 @@
`khadhroony-bobobot` est un workspace Rust destiné à la détection, au décodage, à lanalyse et, à terme, au trading semi-automatisé de tokens Solana. `khadhroony-bobobot` est un workspace Rust destiné à la détection, au décodage, à lanalyse et, à terme, au trading semi-automatisé de tokens Solana.
Le README précédent décrivait surtout létat `0.3.1`. Ce fichier reflète létat de reprise autour de `0.7.34` : le socle transport HTTP/WS, la résolution transactionnelle, le modèle SQLite, plusieurs connecteurs DEX, les candles, les signaux analytiques, la validation locale et une matrice DEX commune existent déjà. Le README précédent décrivait surtout létat `0.3.1`. Ce fichier reflète létat de reprise autour de `0.7.36` et ouvre le travail `0.7.37` : le socle transport HTTP/WS, la résolution transactionnelle, le modèle SQLite, plusieurs connecteurs DEX, les candles, les signaux analytiques, la validation locale et une matrice DEX commune existent déjà.
## 1. Objectif ## 1. Objectif
@@ -31,7 +31,7 @@ Le workspace contient deux crates principales.
La logique métier doit rester dans `kb_lib`. `kb_demo_app` doit rester une façade UI/Tauri et ne doit pas récupérer de logique Solana ou DEX profonde. La logique métier doit rester dans `kb_lib`. `kb_demo_app` doit rester une façade UI/Tauri et ne doit pas récupérer de logique Solana ou DEX profonde.
## 3. État actuel autour de `0.7.34` ## 3. État actuel autour de `0.7.36`
### 3.1. Socle stabilisé à ne pas refactorer maintenant ### 3.1. Socle stabilisé à ne pas refactorer maintenant
@@ -61,29 +61,44 @@ Le pipeline `0.7.x` couvre déjà les étapes suivantes :
9. signaux analytiques simples ; 9. signaux analytiques simples ;
10. inspection via lapplication de démonstration. 10. inspection via lapplication de démonstration.
### 3.3. Connecteurs validés manuellement via lapplication de démo ### 3.3. Connecteurs validés ou observés via lapplication de démo
Les connecteurs suivants sont les surfaces prioritaires à verrouiller avant extension massive : Les connecteurs suivants sont les surfaces actuellement les plus importantes pour la validation locale :
- `pump_fun` comme surface de launch / mint initial ; - `pump_fun` comme surface de launch / mint initial ;
- `pump_swap` pour les swaps post-migration Pump ; - `pump_swap` pour les swaps post-migration Pump ;
- `raydium_cpmm` ; - `raydium_cpmm` ;
- `raydium_clmm` ; - `raydium_clmm` ;
- `meteora_dlmm` ; - `meteora_dlmm` ;
- `meteora_damm_v1`, actuellement partiel pour les swaps sans payload montant/prix exploitable. - `meteora_damm_v1`, partiel : les swaps sans payload montant/prix exploitable restent conservés mais non actionnables ;
- `meteora_damm_v2`, observé dans le corpus `0.7.36`, mais les swaps sans payload montant/prix exploitable sont maintenant `non_actionable_trade` ;
- `meteora_dbc`, observé dans le corpus `0.7.36`, mais les swaps sans payload montant/prix exploitable sont maintenant `non_actionable_trade`.
### 3.4. Connecteurs déjà présents mais à consolider par corpus ### 3.4. Connecteurs déjà présents mais à consolider par corpus
Les modules suivants existent ou sont partiellement représentés dans le code, mais doivent être consolidés par corpus local, invariants et documentation : Les modules suivants existent ou sont partiellement représentés dans le code, mais doivent encore être consolidés par corpus local, invariants et documentation :
- `meteora_dbc` ;
- `meteora_damm_v1` ;
- `meteora_damm_v2` ;
- `orca_whirlpools` ; - `orca_whirlpools` ;
- `fluxbeam` ; - `fluxbeam` ;
- `dexlab` ; - `dexlab` ;
- `raydium_amm_v4` legacy ; - `raydium_amm_v4` legacy ;
- launch origins déjà amorcées : `meteora_fun_launch`, `bags`, `moonit`. - launch origins déjà amorcées : `meteora_fun_launch`, `bags`, `moonit` ;
- launch surfaces à venir : `raydium_launchlab`, `raydium_launchpad`, `letsbonk` / `bonk`, `boop_fun`, `moonshot`, `believe`, `heaven`.
### 3.5. Validation acquise en `0.7.36`
La validation `0.7.36_meteora_family_consolidation` est considérée comme réalisée lorsque les invariants suivants restent vrais après replay local :
- `validationPassed = true` ;
- `diagnosticsClean = true` ;
- `blockingIssueCount = 0` ;
- `decodedTradeCandidateWithoutTradeEventCount = 0` ;
- `decodedTradeCandidateWithoutAmountPayloadCount = 0` ;
- `missingTradeEventCount = 0` ;
- `pairWithoutTradeCount = 0` pour les paires actionnables ;
- `pairWithoutCandleCount = 0` pour les paires actionnables.
Point important : `meteora_damm_v2` et `meteora_dbc` peuvent produire beaucoup dévénements `swap` décodés sans produire de `trade_events` lorsque les montants ou prix ne sont pas fiables. Ces événements doivent rester `non_actionable_trade` et ne doivent pas être comptés comme `tradeCandidate` ou `candleCandidate`.
## 4. Matrice DEX et launch surfaces ## 4. Matrice DEX et launch surfaces
@@ -97,7 +112,11 @@ La distinction importante est la suivante :
Depuis `0.7.29`, la matrice de support DEX est portée par `kb_lib/src/dex_support_matrix.rs`. Elle centralise le code interne, la famille, la version, le type de surface, les program ids vérifiés localement, le statut de support, les capacités actuelles et les raisons de skip. Depuis `0.7.29`, la matrice de support DEX est portée par `kb_lib/src/dex_support_matrix.rs`. Elle centralise le code interne, la famille, la version, le type de surface, les program ids vérifiés localement, le statut de support, les capacités actuelles et les raisons de skip.
Depuis `0.7.30`, les événements décodés reçoivent aussi une classification plus fine : `eventLifecycleKind`, `eventActionability` et `nonTradeUseful`. Depuis `0.7.34`, cette classification commence à alimenter les tables non-trade utiles, sans alimenter directement les trades/candles. Le backfill ciblé token/pool appelle aussi cette matérialisation afin que les compteurs `liquidityEventCount` et `poolLifecycleEventCount` soient cohérents sans devoir lancer un replay local séparé. Depuis `0.7.30`, les événements décodés reçoivent aussi une classification plus fine : `eventLifecycleKind`, `eventActionability` et `nonTradeUseful`. Cette classification sert aux diagnostics et prépare la matérialisation future des événements non-trade sans alimenter directement les trades/candles.
Depuis `0.7.32`, les diagnostics distinguent explicitement les gaps littéraux de catalogue (`literalPairWithoutTradeCount`, `literalPairWithoutCandleCount`) des gaps bloquants/actionnables (`blockingPairWithoutTradeCount`, `blockingPairWithoutCandleCount`). Les anciens champs `pairWithoutTradeCount` et `pairWithoutCandleCount` restent exposés comme alias de compatibilité pour les gaps bloquants/actionnables.
Depuis `0.7.33`, les diagnostics ajoutent une classification `pairTradingReadiness` au niveau des paires et des résumés agrégés `pairTradingReadinessSummaries`. Cette classification sépare les paires directement lisibles/tradables contre WSOL ou stable, les paires inversées avec WSOL/stable en base, les paires cross-quote nécessitant un routeur/aggregator, les paires non matérialisées en trade et les cas de quote inconnue. Elle reste purement diagnostique : elle ne modifie ni le replay, ni les `trade_events`, ni les candles.
| Code cible | Type | Statut `0.7.29` | Prochaine action | | Code cible | Type | Statut `0.7.29` | Prochaine action |
|---|---:|---|---| |---|---:|---|---|
@@ -109,9 +128,9 @@ Depuis `0.7.30`, les événements décodés reçoivent aussi une classification
| `raydium_launchpad` | Launch surface | à vérifier | ne pas inventer de program id | | `raydium_launchpad` | Launch surface | à vérifier | ne pas inventer de program id |
| `raydium_amm_v4` | AMM legacy | partiel | traiter après les autres Raydium avec corpus dédié | | `raydium_amm_v4` | AMM legacy | partiel | traiter après les autres Raydium avec corpus dédié |
| `meteora_dlmm` | DLMM | supporté | verrouiller corpus et non-régression | | `meteora_dlmm` | DLMM | supporté | verrouiller corpus et non-régression |
| `meteora_damm_v1` | AMM legacy | partiel | conserver le skip explicite des swaps sans montants exploitables | | `meteora_damm_v1` | AMM legacy | partiel validé | conserver le skip explicite des swaps sans montants exploitables |
| `meteora_damm_v2` | AMM | partiel | corpus et séparation swaps/liquidité/events | | `meteora_damm_v2` | AMM | partiel validé `0.7.36` | conserver les swaps sans amounts en `non_actionable_trade`; ajouter extraction de montants seulement avec preuve |
| `meteora_dbc` | Launch / bonding curve | partiel | lifecycle, migration et swaps exploitables | | `meteora_dbc` | Launch / bonding curve | partiel validé `0.7.36` | conserver les swaps sans amounts en `non_actionable_trade`; étudier migration / launch origin |
| `meteora_dlc` | À vérifier | à vérifier | confirmer surface/program id avant intégration | | `meteora_dlc` | À vérifier | à vérifier | confirmer surface/program id avant intégration |
| `orca_whirlpools` | CLMM | partiel | corpus fiable et validation des instructions utiles | | `orca_whirlpools` | CLMM | partiel | corpus fiable et validation des instructions utiles |
| `fluxbeam` | AMM | partiel | corpus fiable avant validation | | `fluxbeam` | AMM | partiel | corpus fiable avant validation |
@@ -171,24 +190,6 @@ Avant détendre trop agressivement les DEX, ces tables doivent être stabilis
`k_sol_liquidity_events` existe déjà et doit être stabilisée/étendue plutôt que recréée sans nécessité. `k_sol_liquidity_events` existe déjà et doit être stabilisée/étendue plutôt que recréée sans nécessité.
### 5.3. État `0.7.34` des événements non-trade
Le profil `0.7.34_non_trade_liquidity_lifecycle` valide deux choses distinctes :
- les tables et diagnostics non-trade existent pour compter `liquidityEventCount` et `poolLifecycleEventCount` ;
- les décodeurs doivent maintenant émettre les événements utiles, au lieu de les classer comme inconnus ou de les ignorer.
Première tranche couverte : Meteora DLMM. Les discriminants et hints suivants sont pris en charge comme événements non-trade utiles :
| Event kind | Catégorie | Effet attendu |
|---|---|---|
| `meteora_dlmm.add_liquidity` | liquidity | persistance dans `k_sol_liquidity_events` quand le replay matérialise les decoded events |
| `meteora_dlmm.remove_liquidity` | liquidity | persistance dans `k_sol_liquidity_events` |
| `meteora_dlmm.initialize_position` | liquidity / position open | persistance dans `k_sol_liquidity_events` avec `LiquidityEventKind::PositionOpen`, sans génération de trade/candle |
| `meteora_dlmm.initialize_bin_array` | pool lifecycle | persistance dans `k_sol_pool_lifecycle_events` |
Invariant maintenu : ces événements peuvent améliorer le scoring, le contexte de pool et le diagnostic, mais ne doivent jamais créer directement de `trade_events`, pair metrics ou candles.
## 6. Politique de refactor actuelle ## 6. Politique de refactor actuelle
Le code et la documentation sont vivants. Les refactors agressifs sont acceptables lorsque cela rend le pipeline plus propre et plus durable, à condition de respecter ces limites : Le code et la documentation sont vivants. Les refactors agressifs sont acceptables lorsque cela rend le pipeline plus propre et plus durable, à condition de respecter ces limites :
@@ -231,18 +232,20 @@ Les tests peuvent rester plus souples lorsque cela clarifie le test.
## 8. Priorité immédiate ## 8. Priorité immédiate
La reprise doit suivre cet ordre : La prochaine étape est `0.7.37_token_metadata_catalog_enrichment`.
1. conserver la non-régression `0.7.31` : transactions failed traçables mais exclues des `trade_events`, metrics et candles ; Objectif : rendre le catalogue local exploitable visuellement et analytiquement sans toucher aux invariants de décodage/trade validés en `0.7.36`.
2. utiliser la matrice `0.7.29` comme source commune pour le catalogue, la classification et les protocol candidates ;
3. relier progressivement les événements non-trade aux tables existantes : lifecycle, liquidité, fees, rewards, admin ; À faire en priorité :
4. consolider Meteora, surtout `meteora_dlmm` et le cas partiel `meteora_damm_v1` ;
5. ajouter les launch surfaces manquantes comme origines de mint : LaunchLab/Launchpad, LetsBonk/Bonk.fun, Boop.fun, Moonshot/Moonit, Believe, Bags ; 1. ajouter ou compléter un registre local des mints connus : `SOL`, `WSOL`, `USDC`, `USDT`, puis autres mints fréquents si vérifiés ;
6. traiter Heaven ; 2. améliorer le service de backfill metadata pour traiter les tokens déjà présents en base ;
7. consolider Orca/FluxBeam/DexLab ; 3. exposer un résumé de metadata manquantes par asset class, protocole dorigine, DEX et quote asset ;
8. isoler Raydium AMM v4 legacy ; 4. rafraîchir automatiquement ou explicitement les `pair_symbol` après mise à jour des tokens ;
9. effectuer une validation DEX v1 consolidée ; 5. ajouter une commande UI ou clarifier la commande existante pour relancer le metadata backfill ;
10. reprendre ensuite lUI analytique et les vues token/pair/pool. 6. vérifier lidempotence : relancer le backfill metadata ne doit pas recréer tokens/pools/pairs/trades ;
7. conserver les compteurs DEX propres : `blockingIssueCount = 0`, `actionableMissingTradeEventCount = 0`, `missingTradeEventCount = 0` ;
8. préparer ensuite les launch surfaces, qui deviennent létape suivante de la roadmap.
## 9. Fichiers utiles pour reprendre dans une nouvelle session ## 9. Fichiers utiles pour reprendre dans une nouvelle session

View File

@@ -846,24 +846,33 @@ Réalisé :
- exposer `resetMarketMaterializationDeletedCount` dans le résultat de replay UI ; - exposer `resetMarketMaterializationDeletedCount` dans le résultat de replay UI ;
- conserver la validation multi-DEX et la matrice DEX comme garde-fous avant dajouter les surfaces restantes. - conserver la validation multi-DEX et la matrice DEX comme garde-fous avant dajouter les surfaces restantes.
### 6.064. Version `0.7.32` — Transactions inconnues et protocol candidates ### 6.064. Version `0.7.32` — Sémantique des diagnostics et compteurs de validation
Réalisé : Réalisé :
- consolider `k_sol_transaction_classifications`, déjà présente, avec les catégories utiles au suivi DEX, - conserver la politique `0.7.31` : transactions failed traçables mais exclues des `trade_events`, metrics et candles ;
- consolider `k_sol_protocol_candidates`, déjà présente, pour prioriser les programmes inconnus ou partiellement reconnus, - clarifier que `pairWithoutTradeCount` et `pairWithoutCandleCount` sont des compteurs de gaps bloquants/actionnables, pas des compteurs littéraux sur tout le catalogue ;
- classifier les transactions résolues en catégories : known supported, known partial, known non-trade, unknown program, unknown protocol candidate, unknown event kind, failed transaction, non-actionable trade, - ajouter `literalPairWithoutTradeCount` et `literalPairWithoutCandleCount` pour les paires de catalogue sans trade/candle matérialisé ;
- conserver les `program_id`, comptes, signatures, préfixes de `data`, logs et indices dinstructions utiles à lanalyse, - ajouter `blockingPairWithoutTradeCount` et `blockingPairWithoutCandleCount` comme noms explicites des anciens compteurs bloquants ;
- créer des requêtes de diagnostic pour repérer les programmes inconnus fréquents, - ajouter les compteurs de matérialisation par paire : `tradeMaterializedPairCount`, `candleMaterializedPairCount`, `actionablePairCount`, `candleBucketTimeframeCount` et `candlesAreBucketed` ;
- permettre de promouvoir plus tard un protocol candidate vers un vrai DEX/surface sans perdre lhistorique, - ajouter `pairActionabilitySummaries` pour distinguer les paires matérialisées, actionnables sans matérialisation, candidates failed, non-actionables, décodées sans trade candidate et catalog-only ;
- garantir que ces tables nalimentent jamais directement les trades/candles. - ajouter le profil `0.7.32_validation_report_semantics` ;
- ajouter des garde-fous de validation sur la matrice DEX : entrées `supported` entièrement matérialisées, entrées `partial` avec `skipReason`, entrées `planned/to_verify` non activées au catalogue ;
- ne pas modifier la logique de replay, trade aggregation ou candle aggregation validée en `0.7.31`.
### 6.065. Version `0.7.33` — Pair trading readiness et routes de cotation Repoussé après cette clarification : consolider les transactions inconnues et protocol candidates sans polluer les trades/candles.
### 6.065. Version `0.7.33` — Readiness trading des paires
Réalisé : Réalisé :
- classifier les paires `direct_wsol_quote`, `direct_stable_quote`, `inverse_wsol_base`, `inverse_stable_base`, `cross_quote_requires_router` et `non_trade_materialized` ; - ajouter une classification diagnostique `pairTradingReadiness` pour chaque paire inspectée localement ;
- exposer `quoteAssetClass` et `tradingRouteRequired` dans les diagnostics ; - distinguer `direct_wsol_quote`, `direct_stable_quote`, `inverse_wsol_base`, `inverse_stable_base`, `cross_quote_requires_router`, `unknown_quote` et `non_trade_materialized` ;
- éviter de bloquer la validation sur des paires seulement listées ou détectées sans trade matérialisé ; - exposer `quoteAssetClass` et `tradingRouteRequired` dans les diagnostics par paire ;
- préparer la future sélection des paires directement tradables versus paires nécessitant un router. - ajouter `pairTradingReadinessSummaries` dans le résumé local du pipeline ;
- ajouter le profil `0.7.33_pair_trading_readiness` ;
- valider que les résumés de readiness couvrent toutes les paires et restent cohérents avec les compteurs `tradeMaterializedPairCount`, `tradeEventCount` et `pairCandleCount` ;
- ne pas modifier la logique de replay, `trade_events`, metrics ou candles.
Objectif : préparer la future couche dachat/vente en distinguant les paires immédiatement exploitables contre WSOL/stable des paires qui nécessitent inversion de lecture ou routeur/aggregator.
### 6.066. Version `0.7.34` — Événements non-trade v1 : liquidité et cycle de vie pool ### 6.066. Version `0.7.34` — Événements non-trade v1 : liquidité et cycle de vie pool
Réalisé : Réalisé :
@@ -875,13 +884,11 @@ Réalisé :
- conserver le `payload_json` source pour audit, - conserver le `payload_json` source pour audit,
- alimenter les diagnostics locaux avec les compteurs liquidité/lifecycle, - alimenter les diagnostics locaux avec les compteurs liquidité/lifecycle,
- garantir quun événement de liquidité ou de cycle de vie ne produit jamais de candle directement. - garantir quun événement de liquidité ou de cycle de vie ne produit jamais de candle directement.
- première tranche DLMM : reconnaître et persister `meteora_dlmm.add_liquidity`, `meteora_dlmm.remove_liquidity`, `meteora_dlmm.initialize_position` et `meteora_dlmm.initialize_bin_array` comme événements non-trade utiles ;
- intégrer la matérialisation non-trade dans les backfills ciblés token/pool, pas uniquement dans le replay local, afin que les diagnostics reflètent immédiatement les événements DLMM non-trade décodés ;
- distinguer `PositionOpen` et `PositionClose` dans `LiquidityEventKind` au lieu de rabattre les positions CLMM/DLMM sur `Add`/`Remove` ;
- conserver `meteora_damm_v1` manquant comme warning non bloquant lorsque le corpus de backfill local ne contient pas ce DEX.
### 6.067. Version `0.7.35` — Événements non-trade v2 : fees, rewards et administration ### 6.067. Version `0.7.35` — Événements non-trade v2 : fees, rewards et administration
Réalisé : Objectif : conserver les événements utiles au risque, au scoring, à léconomie du pool et à la traçabilité opérationnelle.
À faire :
- ajouter `k_sol_fee_events`, - ajouter `k_sol_fee_events`,
- ajouter `k_sol_reward_events`, - ajouter `k_sol_reward_events`,
@@ -893,20 +900,34 @@ Réalisé :
- documenter clairement que ces événements ne sont ni des trades ni des candles. - documenter clairement que ces événements ne sont ni des trades ni des candles.
### 6.068. Version `0.7.36` — Meteora : DBC / DAMM v1 / DAMM v2 / DLMM ### 6.068. Version `0.7.36` — Meteora : DBC / DAMM v1 / DAMM v2 / DLMM
Objectif : consolider Meteora comme famille multi-programmes au lieu de traiter chaque variante comme un cas isolé incomplet. Réalisé :
- consolidation de Meteora comme famille multi-programmes au lieu de traiter `DBC`, `DAMM v1`, `DAMM v2` et `DLMM` comme des cas isolés ;
- ajout/correction des discriminants et classifications utiles pour `meteora_damm_v2`, `meteora_dbc`, `meteora_damm_v1` et `meteora_dlmm` ;
- correction du cas `meteora_damm_v2` où la classification de data était appelée depuis le mauvais scope ;
- ajout de garde-fous sur les fixtures et les instructions internes afin déviter les faux positifs ;
- validation du profil `0.7.36_meteora_family_consolidation` sur corpus local mixte ;
- conservation de `meteora_damm_v2.swap` et `meteora_dbc.swap` sans payload montant/prix fiable comme `non_actionable_trade` ;
- suppression des faux diagnostics bloquants liés aux swaps Meteora sans amounts : `missingTradeEventCount = 0`, `decodedTradeCandidateWithoutTradeEventCount = 0`, `decodedTradeCandidateWithoutAmountPayloadCount = 0` ;
- maintien de linvariant : aucun événement sans montant/prix exploitable ne peut alimenter `trade_events`, `pair_metrics` ou `pair_candles` ;
- documentation de la limite connue : `meteora_damm_v2` et `meteora_dbc` peuvent être observés et décodés sans être encore matérialisables en trades/candles.
### 6.069. Version `0.7.37` — Token metadata et catalogue local
Objectif : rendre le catalogue local exploitable et lisible avant dajouter davantage de launch surfaces.
À faire : À faire :
- vérifier les programmes et discriminants réellement utilisés pour `Meteora DBC`, `Meteora DAMM v1`, `Meteora DAMM v2` et `Meteora DLMM`, - ajouter ou consolider un registre local des mints connus et stables : `SOL`, `WSOL`, `USDC`, `USDT`, puis autres mints seulement si vérifiés ;
- ajouter `meteora_dlmm` à la couverture cible seulement après corpus fiable, - améliorer le backfill metadata pour traiter les tokens déjà présents dans `k_sol_tokens` sans nécessiter un nouveau backfill transactionnel ;
- constituer un corpus local par variante, - enrichir les tokens depuis les sources disponibles : registre local, payloads DEX, comptes Token-2022, Metaplex, transactions déjà persistées ;
- décoder les créations de pool, swaps, liquidités et événements lifecycle exploitables, - rafraîchir les `pair_symbol` après mise à jour des metadata de tokens ;
- identifier les cas où `DBC` sert de launch origin avant migration vers un AMM, - exposer des diagnostics précis : `tokenMetadataMissingCount` par `dexCode`, `quoteAssetClass`, mint connu/inconnu et origine de découverte ;
- alimenter `k_sol_dex_decoded_events`, les tables pool/pair/listing, les origins et les tables non-trade, - ajouter ou clarifier une commande UI dans `kb_demo_app` pour lancer le backfill metadata et rafraîchir le catalogue ;
- vérifier lidempotence du replay local sur un corpus Meteora mixte, - garantir lidempotence : relancer lenrichissement metadata ne doit pas recréer tokens, pools, paires, trades, candles ou origins ;
- documenter les limites connues des variantes insuffisamment couvertes. - conserver linvariant de validation : le manque de metadata nest pas un diagnostic bloquant tant que les trades/candles actionnables sont sains ;
- ajouter le profil de validation `0.7.37_token_metadata_catalog_enrichment`.
### 6.069. Version `0.7.37` — Launch surfaces : LaunchLab, LetsBonk, Bags, Moonshot/Moonit, Boop.fun, Believe ### 6.070. Version `0.7.38` — Launch surfaces : LaunchLab, LetsBonk, Bags, Moonshot/Moonit, Boop.fun, Believe
Objectif : détecter la première source de mint/lancement des tokens même lorsque le swap final se fait ailleurs. Objectif : détecter la première source de mint/lancement des tokens même lorsque le swap final se fait ailleurs.
À faire : À faire :
@@ -921,7 +942,7 @@ Objectif : détecter la première source de mint/lancement des tokens même lors
- rattacher les launch origins aux pools et paires lorsque les comptes permettent un matching fiable, - rattacher les launch origins aux pools et paires lorsque les comptes permettent un matching fiable,
- exposer les origins dans les diagnostics et lUI dinspection. - exposer les origins dans les diagnostics et lUI dinspection.
### 6.070. Version `0.7.38` — Heaven : corpus, launch et AMM ### 6.071. Version `0.7.39` — Heaven : corpus, launch et AMM
Objectif : ajouter Heaven sans le classer trop tôt comme simple DEX ou simple launchpad. Objectif : ajouter Heaven sans le classer trop tôt comme simple DEX ou simple launchpad.
À faire : À faire :
@@ -933,7 +954,7 @@ Objectif : ajouter Heaven sans le classer trop tôt comme simple DEX ou simple l
- documenter les limites si le corpus ne permet pas encore de matérialiser tous les événements, - documenter les limites si le corpus ne permet pas encore de matérialiser tous les événements,
- vérifier que Heaven ne crée pas de candles invalides en cas dévénement de launch non pricé. - vérifier que Heaven ne crée pas de candles invalides en cas dévénement de launch non pricé.
### 6.071. Version `0.7.39` — Orca / FluxBeam / DexLab : corpus et validation ciblée ### 6.072. Version `0.7.40` — Orca / FluxBeam / DexLab : corpus et validation ciblée
Objectif : consolider les connecteurs déjà présents à partir de corpus locaux vérifiables. Objectif : consolider les connecteurs déjà présents à partir de corpus locaux vérifiables.
À faire : À faire :
@@ -945,7 +966,7 @@ Objectif : consolider les connecteurs déjà présents à partir de corpus locau
- marquer explicitement les variantes partiellement supportées ou heuristiques, - marquer explicitement les variantes partiellement supportées ou heuristiques,
- rejouer les corpus plusieurs fois pour vérifier lidempotence et labsence de trades/candles invalides. - rejouer les corpus plusieurs fois pour vérifier lidempotence et labsence de trades/candles invalides.
### 6.072. Version `0.7.40` — Raydium AMM v4 legacy : corpus et validation ciblée ### 6.073. Version `0.7.41` — Raydium AMM v4 legacy : corpus et validation ciblée
Objectif : traiter le vrai Raydium AMM v4 historique après les autres Raydium, afin de lisoler de `raydium_cpmm`, `raydium_clmm` et des labels Raydium génériques. Objectif : traiter le vrai Raydium AMM v4 historique après les autres Raydium, afin de lisoler de `raydium_cpmm`, `raydium_clmm` et des labels Raydium génériques.
À faire : À faire :
@@ -958,7 +979,7 @@ Objectif : traiter le vrai Raydium AMM v4 historique après les autres Raydium,
- renommer/stabiliser les fonctions internes autour de `raydium_amm_v4` pour éviter lambiguïté avec `raydium_cpmm` et `raydium_clmm`, - renommer/stabiliser les fonctions internes autour de `raydium_amm_v4` pour éviter lambiguïté avec `raydium_cpmm` et `raydium_clmm`,
- documenter les limites connues si le corpus AMM v4 reste faible. - documenter les limites connues si le corpus AMM v4 reste faible.
### 6.073. Version `0.7.41` — Validation DEX v1 consolidée ### 6.074. Version `0.7.42` — Validation DEX v1 consolidée
Objectif : rejouer tous les DEX et launch surfaces supportés et valider les invariants du pipeline complet. Objectif : rejouer tous les DEX et launch surfaces supportés et valider les invariants du pipeline complet.
À faire : À faire :
@@ -971,7 +992,7 @@ Objectif : rejouer tous les DEX et launch surfaces supportés et valider les inv
- conserver une matrice de support par DEX, variante, instruction et type dévénement, - conserver une matrice de support par DEX, variante, instruction et type dévénement,
- verrouiller les invariants avant douvrir lanalyse `0.8.x`. - verrouiller les invariants avant douvrir lanalyse `0.8.x`.
### 6.074. Version `0.7.42` — `kb_demo_app` : overlays analytiques ### 6.075. Version `0.7.43` — `kb_demo_app` : overlays analytiques
Objectif : rendre visibles les signaux analytiques directement sur les graphes et vues de marché. Objectif : rendre visibles les signaux analytiques directement sur les graphes et vues de marché.
À faire : À faire :
@@ -982,7 +1003,7 @@ Objectif : rendre visibles les signaux analytiques directement sur les graphes e
- afficher un panneau latéral listant les signaux liés à une paire et à un timeframe, - afficher un panneau latéral listant les signaux liés à une paire et à un timeframe,
- préparer lextension future vers Ichimoku, Kumo, projections ABCD et égalités temps/prix sans les mélanger au pipeline de décodage DEX. - préparer lextension future vers Ichimoku, Kumo, projections ABCD et égalités temps/prix sans les mélanger au pipeline de décodage DEX.
### 6.075. Version `0.7.43` — `kb_demo_app` : vues consolidées token / pair / pool ### 6.076. Version `0.7.44` — `kb_demo_app` : vues consolidées token / pair / pool
Objectif : fournir une lecture métier plus confortable du modèle `0.7.x`. Objectif : fournir une lecture métier plus confortable du modèle `0.7.x`.
À faire : À faire :
@@ -994,7 +1015,7 @@ Objectif : fournir une lecture métier plus confortable du modèle `0.7.x`.
- préparer une navigation transversale entre objets techniques et objets métier, - préparer une navigation transversale entre objets techniques et objets métier,
- rendre explicites les cas `tradeCount = null`, `lastPriceQuotePerBase = null`, tokens non enrichis et événements conservés uniquement pour analyse. - rendre explicites les cas `tradeCount = null`, `lastPriceQuotePerBase = null`, tokens non enrichis et événements conservés uniquement pour analyse.
### 6.076. Version `0.7.44` — Finition UI `0.7.x` ### 6.077. Version `0.7.45` — Finition UI `0.7.x`
Objectif : stabiliser la couche desktop de validation avant louverture de `0.8.x`. Objectif : stabiliser la couche desktop de validation avant louverture de `0.8.x`.
À faire : À faire :
@@ -1005,7 +1026,7 @@ Objectif : stabiliser la couche desktop de validation avant louverture de `0.
- préparer une base UI suffisamment stable pour la future phase danalyse et filtrage `0.8.x`, - préparer une base UI suffisamment stable pour la future phase danalyse et filtrage `0.8.x`,
- vérifier que les commandes Tauri restent de simples façades vers `kb_lib`. - vérifier que les commandes Tauri restent de simples façades vers `kb_lib`.
### 6.077. Version `0.7.x` — Couverture DEX v1 ### 6.078. Version `0.7.x` — Couverture DEX v1
Objectif : structurer les connecteurs DEX autour dun pipeline complet de résolution, décodage, normalisation métier et classification des événements non-trade. Objectif : structurer les connecteurs DEX autour dun pipeline complet de résolution, décodage, normalisation métier et classification des événements non-trade.
Protocoles et surfaces cibles : Protocoles et surfaces cibles :
@@ -1048,7 +1069,7 @@ Résultat attendu :
- préparation dune détection temps réel hybride et dun backfill ciblé compatible avec les mêmes objets métier, - préparation dune détection temps réel hybride et dun backfill ciblé compatible avec les mêmes objets métier,
- préparation dagrégats DEX plus riches, de candles/OHLCV et dune UI dinspection du pipeline `0.7.x`. - préparation dagrégats DEX plus riches, de candles/OHLCV et dune UI dinspection du pipeline `0.7.x`.
### 6.078. Version `0.8.x` — Analyse et filtrage ### 6.079. Version `0.8.x` — Analyse et filtrage
Objectif : transformer les événements bruts en signaux exploitables. Objectif : transformer les événements bruts en signaux exploitables.
À faire : À faire :
@@ -1063,7 +1084,7 @@ Objectif : transformer les événements bruts en signaux exploitables.
- outils de sélection manuelle de points ABC et projection dun point D selon des règles temps/prix explicites, - outils de sélection manuelle de points ABC et projection dun point D selon des règles temps/prix explicites,
- séparation stricte entre signaux analytiques observés, projections hypothétiques et décisions de trading. - séparation stricte entre signaux analytiques observés, projections hypothétiques et décisions de trading.
### 6.079. Version `1.x.y` — Wallets et swap préparatoire ### 6.080. Version `1.x.y` — Wallets et swap préparatoire
Objectif : préparer la couche daction. Objectif : préparer la couche daction.
À faire : À faire :
@@ -1074,7 +1095,7 @@ Objectif : préparer la couche daction.
- préparation dordres et de swaps, - préparation dordres et de swaps,
- simulation et garde-fous. - simulation et garde-fous.
### 6.080. Version `2.x.y` — Trading semi-automatisé ### 6.081. Version `2.x.y` — Trading semi-automatisé
Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites. Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites.
À faire : À faire :
@@ -1085,7 +1106,7 @@ Objectif : brancher lanalyse à laction tout en gardant des garde-fous exp
- confirmations explicites ou semi-automatiques, - confirmations explicites ou semi-automatiques,
- journaux dexécution. - journaux dexécution.
### 6.081. Version `3.x.y` — Yellowstone gRPC ### 6.082. Version `3.x.y` — Yellowstone gRPC
Objectif : ajouter le connecteur gRPC dédié. Objectif : ajouter le connecteur gRPC dédié.
À faire : À faire :
@@ -1215,20 +1236,17 @@ Le projet doit maintenir au minimum :
## 12. Priorité immédiate ## 12. Priorité immédiate
La priorité immédiate est désormais la suivante : La priorité immédiate est désormais `0.7.37_token_metadata_catalog_enrichment`.
1. conserver la validation acquise `0.7.31` : transactions failed traçables mais exclues des `trade_events`, metrics et candles, aucun trade/candle candidate sans payload montant/prix exploitable, aucun diagnostic bloquant masqué, Ordre de travail recommandé :
2. utiliser la matrice `0.7.29` (`kb_lib/src/dex_support_matrix.rs`) comme source commune pour le catalogue DEX, les mappings program id -> protocole, la classification transactionnelle et les protocol candidates,
3. garder les clients HTTP/WS et managers réseau hors du refactor DEX tant quils ne bloquent pas le pipeline, 1. conserver la validation acquise `0.7.36` : Meteora consolidé, transactions failed traçables mais non actionnables, swaps sans amounts classés `non_actionable_trade`, aucun diagnostic bloquant masqué ;
4. consolider les événements non-trade sans les confondre avec les trades/candles : lifecycle de pool, liquidité, fees, rewards, admin/config, migration et launch/mint, 2. améliorer le catalogue local : metadata de tokens, symboles, noms, asset classes et `pair_symbol` lisibles ;
5. rattacher les launch surfaces aux tokens et aux pools migrés : Raydium LaunchLab/Launchpad, LetsBonk/Bonk.fun, Boop.fun, Moonshot/Moonit, Believe, Bags et Heaven, 3. ajouter un registre local des mints connus et stables : `SOL`, `WSOL`, `USDC`, `USDT`, puis autres mints seulement après vérification ;
6. consolider Meteora avec corpus fiable : `meteora_dlmm`, `meteora_damm_v1`, `meteora_damm_v2`, `meteora_dbc` et `meteora_dlc` si le programme est confirmé, 4. permettre un backfill metadata idempotent sur les tokens déjà présents sans relancer un backfill transactionnel complet ;
7. consolider Orca, FluxBeam et DexLab sur corpus, 5. exposer les compteurs de metadata manquantes par DEX, asset class, quote asset et origine de découverte ;
8. traiter `raydium_amm_v4` legacy seulement après les autres Raydium, avec corpus dédié prouvant le programme `675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8`, 6. ajouter ou clarifier la commande UI permettant de relancer le backfill metadata et le refresh du catalogue ;
9. ajouter une matérialisation dédiée des transactions inconnues ou partiellement décodées pour analyser les DEX manquants sans polluer les trades/candles, 7. vérifier que lenrichissement metadata ne modifie pas les invariants DEX : pas de faux trades, pas de fausses candles, pas de recréation de pools/paires ;
10. effectuer une validation DEX v1 consolidée sur tous les connecteurs supportés avant de considérer la couche DEX `0.7.x` comme stable, 8. déplacer ensuite les launch surfaces vers `0.7.38` : Raydium LaunchLab/Launchpad, LetsBonk/Bonk.fun, Boop.fun, Moonshot/Moonit, Believe et Bags ;
11. ajouter ensuite les overlays des signaux analytiques sur les candles, 9. traiter Heaven en `0.7.39`, puis Orca/FluxBeam/DexLab, puis Raydium AMM v4 legacy ;
12. consolider les vues métier `token / pair / pool` dans `kb_demo_app`, y compris les événements liquidité, lifecycle, fees, rewards et admin, 10. effectuer une validation DEX v1 consolidée avant douvrir réellement `0.8.x` pour lanalyse, les filtres, les patterns et les projections graphiques.
13. stabiliser lergonomie, les filtres, la pagination et la navigation de lUI dinspection,
14. préparer ensuite louverture de `0.8.x` pour lanalyse, les filtres, les patterns et les projections graphiques,
15. préparer enfin Yellowstone gRPC comme extension de capacité, et non comme remplacement du socle HTTP / WS existant.

View File

@@ -166,7 +166,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="demoPipeline2ValidationProfileSelect" class="form-label">Validation profile</label> <label for="demoPipeline2ValidationProfileSelect" class="form-label">Validation profile</label>
<select id="demoPipeline2ValidationProfileSelect" class="form-select"> <select id="demoPipeline2ValidationProfileSelect" class="form-select">
<option value="0.7.35_non_trade_fee_reward_admin" selected>0.7.35non-trade fee/reward admin</option> <option value="0.7.36_meteora_family_consolidation" selected>0.7.36Meteora family consolidation</option>
<option value="0.7.35_non_trade_fee_reward_admin">0.7.35 — non-trade fee/reward admin</option>
<option value="0.7.34_non_trade_liquidity_lifecycle">0.7.34 — non-trade liquidity/lifecycle</option> <option value="0.7.34_non_trade_liquidity_lifecycle">0.7.34 — non-trade liquidity/lifecycle</option>
<option value="0.7.33_pair_trading_readiness">0.7.33 — pair trading readiness</option> <option value="0.7.33_pair_trading_readiness">0.7.33 — pair trading readiness</option>
<option value="0.7.32_validation_report_semantics">0.7.32 — validation report semantics</option> <option value="0.7.32_validation_report_semantics">0.7.32 — validation report semantics</option>

View File

@@ -1,7 +1,7 @@
{ {
"name": "kb-demo-app", "name": "kb-demo-app",
"private": true, "private": true,
"version": "0.7.35", "version": "0.7.36",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1087,7 +1087,7 @@ pub(crate) async fn demo_pipeline2_validate_local_pipeline(
let service = kb_lib::LocalPipelineValidationService::new(database.clone()); let service = kb_lib::LocalPipelineValidationService::new(database.clone());
let profile_code = match request { let profile_code = match request {
Some(request) => request.profile_code, Some(request) => request.profile_code,
None => "0.7.35_non_trade_fee_reward_admin".to_string(), None => "0.7.36_meteora_family_consolidation".to_string(),
}; };
let run_result = match profile_code.as_str() { let run_result = match profile_code.as_str() {
"0.7.27" | "0.7.27_dexes_non_regression" => { "0.7.27" | "0.7.27_dexes_non_regression" => {
@@ -1117,6 +1117,9 @@ pub(crate) async fn demo_pipeline2_validate_local_pipeline(
"0.7.35" | "0.7.35_non_trade_fee_reward_admin" => { "0.7.35" | "0.7.35_non_trade_fee_reward_admin" => {
service.validate_v0_7_35_current_database().await service.validate_v0_7_35_current_database().await
}, },
"0.7.36" | "0.7.36_meteora_family_consolidation" => {
service.validate_v0_7_36_current_database().await
},
other => Err(kb_lib::Error::InvalidState(format!( other => Err(kb_lib::Error::InvalidState(format!(
"unsupported local pipeline validation profile: {other}" "unsupported local pipeline validation profile: {other}"
))), ))),

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "kb-demo-app", "productName": "kb-demo-app",
"version": "0.7.35", "version": "0.7.36",
"identifier": "com.sasedev.kb-demo-app", "identifier": "com.sasedev.kb-demo-app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -2,6 +2,14 @@
//! Meteora DAMM v1 transaction decoder. //! Meteora DAMM v1 transaction decoder.
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] =
[0x49, 0xfe, 0x76, 0xf3, 0xab, 0xc4, 0x4c, 0xd0];
const DAMM_V1_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8];
/// Decoded Meteora DAMM v1 create-pool event. /// Decoded Meteora DAMM v1 create-pool event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDammV1CreatePoolDecoded { pub struct MeteoraDammV1CreatePoolDecoded {
@@ -109,9 +117,6 @@ impl MeteoraDammV1Decoder {
let log_messages = extract_log_messages(&transaction_json); let log_messages = extract_log_messages(&transaction_json);
let mut decoded_events = std::vec::Vec::new(); let mut decoded_events = std::vec::Vec::new();
for instruction in instructions { for instruction in instructions {
if instruction.parent_instruction_id.is_some() {
continue;
}
let program_id_option = &instruction.program_id; let program_id_option = &instruction.program_id;
let program_id = match program_id_option { let program_id = match program_id_option {
Some(program_id) => program_id, Some(program_id) => program_id,
@@ -135,7 +140,20 @@ impl MeteoraDammV1Decoder {
Ok(parsed_json) => parsed_json, Ok(parsed_json) => parsed_json,
Err(error) => return Err(error), Err(error) => return Err(error),
}; };
let instruction_kind = classify_instruction_kind(parsed_json.as_ref(), &log_messages); let instruction_data_result =
decode_instruction_data_json(instruction.data_json.as_ref());
let instruction_data = match instruction_data_result {
Ok(instruction_data) => instruction_data,
Err(error) => return Err(error),
};
if instruction.parent_instruction_id.is_some() && instruction_data.is_none() {
continue;
}
let instruction_kind = classify_instruction_kind(
parsed_json.as_ref(),
instruction_data.as_deref(),
&log_messages,
);
let pool_account = extract_string_by_candidate_keys( let pool_account = extract_string_by_candidate_keys(
parsed_json.as_ref(), parsed_json.as_ref(),
&["pool", "poolAddress", "poolAccount", "amm", "ammPool", "poolState"], &["pool", "poolAddress", "poolAccount", "amm", "ammPool", "poolState"],
@@ -169,6 +187,9 @@ impl MeteoraDammV1Decoder {
let payload_json = serde_json::json!({ let payload_json = serde_json::json!({
"decoder": "meteora_damm_v1", "decoder": "meteora_damm_v1",
"eventKind": "create_pool", "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" }, "classifiedInstructionKind": if used_config { "create_pool_with_config" } else { "create_pool" },
"signature": transaction.signature, "signature": transaction.signature,
"instructionId": instruction_id, "instructionId": instruction_id,
@@ -204,6 +225,9 @@ impl MeteoraDammV1Decoder {
let payload_json = serde_json::json!({ let payload_json = serde_json::json!({
"decoder": "meteora_damm_v1", "decoder": "meteora_damm_v1",
"eventKind": "swap", "eventKind": "swap",
"dataDiscriminatorHex": instruction_data
.as_ref()
.and_then(|data| return first_8_bytes_hex(data.as_slice())),
"classifiedInstructionKind": "swap", "classifiedInstructionKind": "swap",
"signature": transaction.signature, "signature": transaction.signature,
"instructionId": instruction_id, "instructionId": instruction_id,
@@ -237,8 +261,16 @@ impl MeteoraDammV1Decoder {
fn classify_instruction_kind( fn classify_instruction_kind(
parsed_json: std::option::Option<&serde_json::Value>, parsed_json: std::option::Option<&serde_json::Value>,
instruction_data: std::option::Option<&[u8]>,
log_messages: &[std::string::String], log_messages: &[std::string::String],
) -> MeteoraDammV1InstructionKind { ) -> MeteoraDammV1InstructionKind {
let data_kind = classify_instruction_kind_from_data(instruction_data);
if data_kind != MeteoraDammV1InstructionKind::Unknown {
return data_kind;
}
if instruction_data_has_full_discriminator(instruction_data) {
return MeteoraDammV1InstructionKind::Unknown;
}
let parsed_instruction_name = extract_string_by_candidate_keys( let parsed_instruction_name = extract_string_by_candidate_keys(
parsed_json, parsed_json,
&["instruction", "instructionName", "type", "name"], &["instruction", "instructionName", "type", "name"],
@@ -274,6 +306,46 @@ fn classify_instruction_kind(
return MeteoraDammV1InstructionKind::Unknown; return MeteoraDammV1InstructionKind::Unknown;
} }
fn instruction_data_has_full_discriminator(instruction_data: std::option::Option<&[u8]>) -> bool {
let instruction_data = match instruction_data {
Some(instruction_data) => instruction_data,
None => return false,
};
return instruction_data.len() >= 8;
}
fn classify_instruction_kind_from_data(
instruction_data: std::option::Option<&[u8]>,
) -> MeteoraDammV1InstructionKind {
let instruction_data = match instruction_data {
Some(instruction_data) => instruction_data,
None => return MeteoraDammV1InstructionKind::Unknown,
};
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 discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL {
return MeteoraDammV1InstructionKind::CreatePool;
}
if discriminator == DAMM_V1_DISCRIMINATOR_SWAP {
return MeteoraDammV1InstructionKind::Swap;
}
return MeteoraDammV1InstructionKind::Unknown;
}
fn extract_log_messages( fn extract_log_messages(
transaction_json: &serde_json::Value, transaction_json: &serde_json::Value,
) -> std::vec::Vec<std::string::String> { ) -> std::vec::Vec<std::string::String> {
@@ -341,6 +413,10 @@ fn parse_accounts_json(
let text_option = value.as_str(); let text_option = value.as_str();
if let Some(text) = text_option { if let Some(text) = text_option {
accounts.push(text.to_string()); accounts.push(text.to_string());
continue;
}
if let Some(pubkey) = value.get("pubkey").and_then(|nested| return nested.as_str()) {
accounts.push(pubkey.to_string());
} }
} }
return Ok(accounts); return Ok(accounts);
@@ -365,6 +441,48 @@ fn parse_optional_parsed_json(
} }
} }
fn decode_instruction_data_json(
data_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<std::vec::Vec<u8>>, crate::Error> {
let data_json = match data_json {
Some(data_json) => data_json,
None => return Ok(None),
};
let value_result = serde_json::from_str::<serde_json::Value>(data_json.as_str());
let value = match value_result {
Ok(value) => value,
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot parse Meteora DAMM v1 data_json: {}",
error
)));
},
};
if let serde_json::Value::String(base58_text) = value {
let decoded_result = bs58::decode(base58_text.as_str()).into_vec();
match decoded_result {
Ok(decoded) => return Ok(Some(decoded)),
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot decode Meteora DAMM v1 instruction data from base58: {}",
error
)));
},
}
}
return Ok(None);
}
fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
if bytes.len() < 8 {
return None;
}
return Some(format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
));
}
fn extract_string_by_candidate_keys( fn extract_string_by_candidate_keys(
value: std::option::Option<&serde_json::Value>, value: std::option::Option<&serde_json::Value>,
candidate_keys: &[&str], candidate_keys: &[&str],
@@ -640,4 +758,43 @@ mod tests {
}, },
} }
} }
#[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);
}
#[test]
fn meteora_damm_v1_unknown_data_discriminator_does_not_fallback_to_global_swap_logs() {
let data = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x01];
let logs = vec!["Program log: Instruction: Swap".to_string()];
let kind = super::classify_instruction_kind(None, Some(&data), &logs);
assert_eq!(kind, super::MeteoraDammV1InstructionKind::Unknown);
}
#[test]
fn meteora_damm_v1_inner_swap_instruction_with_data_is_not_skipped() {
let decoder = crate::MeteoraDammV1Decoder::new();
let transaction = make_swap_transaction();
let mut instruction = make_swap_instruction();
instruction.parent_instruction_id = Some(500);
instruction.data_json = Some(format!(
"\"{}\"",
bs58::encode(&[0xf8_u8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8, 0x01]).into_string()
));
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::Swap(event) => {
assert_eq!(event.pool_account, Some("DammV1SwapPool111".to_string()));
},
crate::MeteoraDammV1DecodedEvent::CreatePool(_) => panic!("unexpected create event"),
}
}
} }

View File

@@ -2,6 +2,19 @@
//! Meteora DAMM v2 transaction decoder. //! Meteora DAMM v2 transaction decoder.
const DAMM_V2_DISCRIMINATOR_INITIALIZE_POOL: [u8; 8] =
[0x5f, 0xb4, 0x0a, 0xac, 0x54, 0xae, 0xe8, 0x28];
const DAMM_V2_DISCRIMINATOR_INITIALIZE_POOL_WITH_DYNAMIC_CONFIG: [u8; 8] =
[0x95, 0x52, 0x48, 0xc5, 0xfd, 0xfc, 0x44, 0x0f];
const DAMM_V2_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_POOL: [u8; 8] =
[0x14, 0xa1, 0xf1, 0x18, 0xbd, 0xdd, 0xb4, 0x02];
const DAMM_V2_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8];
const DAMM_V2_DISCRIMINATOR_SWAP2: [u8; 8] = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88];
/// Decoded Meteora DAMM v2 create-pool event. /// Decoded Meteora DAMM v2 create-pool event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDammV2CreatePoolDecoded { pub struct MeteoraDammV2CreatePoolDecoded {
@@ -112,9 +125,6 @@ impl MeteoraDammV2Decoder {
let log_messages = extract_log_messages(&transaction_json); let log_messages = extract_log_messages(&transaction_json);
let mut decoded_events = std::vec::Vec::new(); let mut decoded_events = std::vec::Vec::new();
for instruction in instructions { for instruction in instructions {
if instruction.parent_instruction_id.is_some() {
continue;
}
let program_id_option = &instruction.program_id; let program_id_option = &instruction.program_id;
let program_id = match program_id_option { let program_id = match program_id_option {
Some(program_id) => program_id, Some(program_id) => program_id,
@@ -138,7 +148,20 @@ impl MeteoraDammV2Decoder {
Ok(parsed_json) => parsed_json, Ok(parsed_json) => parsed_json,
Err(error) => return Err(error), Err(error) => return Err(error),
}; };
let instruction_kind = classify_instruction_kind(parsed_json.as_ref(), &log_messages); let instruction_data_result =
decode_instruction_data_json(instruction.data_json.as_ref());
let instruction_data = match instruction_data_result {
Ok(instruction_data) => instruction_data,
Err(error) => return Err(error),
};
if instruction.parent_instruction_id.is_some() && instruction_data.is_none() {
continue;
}
let instruction_kind = classify_instruction_kind(
parsed_json.as_ref(),
instruction_data.as_deref(),
&log_messages,
);
let pool_account = extract_string_by_candidate_keys( let pool_account = extract_string_by_candidate_keys(
parsed_json.as_ref(), parsed_json.as_ref(),
&["pool", "poolAddress", "poolAccount", "poolState", "cpAmm"], &["pool", "poolAddress", "poolAccount", "poolState", "cpAmm"],
@@ -179,6 +202,9 @@ impl MeteoraDammV2Decoder {
let payload_json = serde_json::json!({ let payload_json = serde_json::json!({
"decoder": "meteora_damm_v2", "decoder": "meteora_damm_v2",
"eventKind": "create_pool", "eventKind": "create_pool",
"dataDiscriminatorHex": instruction_data
.as_ref()
.and_then(|data| return first_8_bytes_hex(data.as_slice())),
"classifiedInstructionKind": create_kind, "classifiedInstructionKind": create_kind,
"signature": transaction.signature, "signature": transaction.signature,
"instructionId": instruction_id, "instructionId": instruction_id,
@@ -213,10 +239,32 @@ impl MeteoraDammV2Decoder {
let used_swap2 = log_messages_contain_keyword(&log_messages, "swap2") let used_swap2 = log_messages_contain_keyword(&log_messages, "swap2")
|| value_contains_any_key(parsed_json.as_ref(), &["swap2", "isSwap2"]); || value_contains_any_key(parsed_json.as_ref(), &["swap2", "isSwap2"]);
let trade_side = infer_trade_side(&log_messages); let trade_side = infer_trade_side(&log_messages);
let has_trade_amount_payload =
parsed_json_has_trade_amount_or_price_payload(parsed_json.as_ref());
let event_actionability = if has_trade_amount_payload {
"trade_candidate"
} else {
"non_actionable_trade"
};
let materialization_skip_reason = if has_trade_amount_payload {
serde_json::Value::Null
} else {
serde_json::Value::String("swap_without_amount_payload".to_string())
};
let payload_json = serde_json::json!({ let payload_json = serde_json::json!({
"decoder": "meteora_damm_v2", "decoder": "meteora_damm_v2",
"eventKind": "swap", "eventKind": "swap",
"dataDiscriminatorHex": instruction_data
.as_ref()
.and_then(|data| return first_8_bytes_hex(data.as_slice())),
"classifiedInstructionKind": if used_swap2 { "swap2" } else { "swap" }, "classifiedInstructionKind": if used_swap2 { "swap2" } else { "swap" },
"eventCategory": "trade",
"eventLifecycleKind": "trade_swap",
"eventActionability": event_actionability,
"tradeCandidate": has_trade_amount_payload,
"candleCandidate": has_trade_amount_payload,
"nonTradeUseful": false,
"materializationSkipReason": materialization_skip_reason,
"signature": transaction.signature, "signature": transaction.signature,
"instructionId": instruction_id, "instructionId": instruction_id,
"instructionIndex": instruction.instruction_index, "instructionIndex": instruction.instruction_index,
@@ -278,8 +326,16 @@ fn extract_log_messages(
fn classify_instruction_kind( fn classify_instruction_kind(
parsed_json: std::option::Option<&serde_json::Value>, parsed_json: std::option::Option<&serde_json::Value>,
instruction_data: std::option::Option<&[u8]>,
log_messages: &[std::string::String], log_messages: &[std::string::String],
) -> MeteoraDammV2InstructionKind { ) -> MeteoraDammV2InstructionKind {
let data_kind = classify_instruction_kind_from_data(instruction_data);
if data_kind != MeteoraDammV2InstructionKind::Unknown {
return data_kind;
}
if instruction_data_has_full_discriminator(instruction_data) {
return MeteoraDammV2InstructionKind::Unknown;
}
let parsed_instruction_name = extract_string_by_candidate_keys( let parsed_instruction_name = extract_string_by_candidate_keys(
parsed_json, parsed_json,
&["instruction", "instructionName", "type", "name"], &["instruction", "instructionName", "type", "name"],
@@ -331,6 +387,49 @@ fn classify_instruction_kind(
return MeteoraDammV2InstructionKind::Unknown; return MeteoraDammV2InstructionKind::Unknown;
} }
fn instruction_data_has_full_discriminator(instruction_data: std::option::Option<&[u8]>) -> bool {
let instruction_data = match instruction_data {
Some(instruction_data) => instruction_data,
None => return false,
};
return instruction_data.len() >= 8;
}
fn classify_instruction_kind_from_data(
instruction_data: std::option::Option<&[u8]>,
) -> MeteoraDammV2InstructionKind {
let instruction_data = match instruction_data {
Some(instruction_data) => instruction_data,
None => return MeteoraDammV2InstructionKind::Unknown,
};
if instruction_data.len() < 8 {
return MeteoraDammV2InstructionKind::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_V2_DISCRIMINATOR_INITIALIZE_POOL_WITH_DYNAMIC_CONFIG {
return MeteoraDammV2InstructionKind::CreatePoolDynamic;
}
if discriminator == DAMM_V2_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_POOL {
return MeteoraDammV2InstructionKind::CreatePoolCustomizable;
}
if discriminator == DAMM_V2_DISCRIMINATOR_INITIALIZE_POOL {
return MeteoraDammV2InstructionKind::CreatePoolStatic;
}
if discriminator == DAMM_V2_DISCRIMINATOR_SWAP || discriminator == DAMM_V2_DISCRIMINATOR_SWAP2 {
return MeteoraDammV2InstructionKind::Swap;
}
return MeteoraDammV2InstructionKind::Unknown;
}
fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool { fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
let keyword_normalized = normalize_text(keyword); let keyword_normalized = normalize_text(keyword);
for log_message in log_messages { for log_message in log_messages {
@@ -370,6 +469,10 @@ fn parse_accounts_json(
let text_option = value.as_str(); let text_option = value.as_str();
if let Some(text) = text_option { if let Some(text) = text_option {
accounts.push(text.to_string()); accounts.push(text.to_string());
continue;
}
if let Some(pubkey) = value.get("pubkey").and_then(|nested| return nested.as_str()) {
accounts.push(pubkey.to_string());
} }
} }
return Ok(accounts); return Ok(accounts);
@@ -394,6 +497,48 @@ fn parse_optional_parsed_json(
} }
} }
fn decode_instruction_data_json(
data_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<std::vec::Vec<u8>>, crate::Error> {
let data_json = match data_json {
Some(data_json) => data_json,
None => return Ok(None),
};
let value_result = serde_json::from_str::<serde_json::Value>(data_json.as_str());
let value = match value_result {
Ok(value) => value,
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot parse Meteora DAMM v2 data_json: {}",
error
)));
},
};
if let serde_json::Value::String(base58_text) = value {
let decoded_result = bs58::decode(base58_text.as_str()).into_vec();
match decoded_result {
Ok(decoded) => return Ok(Some(decoded)),
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot decode Meteora DAMM v2 instruction data from base58: {}",
error
)));
},
}
}
return Ok(None);
}
fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
if bytes.len() < 8 {
return None;
}
return Some(format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
));
}
fn extract_string_by_candidate_keys( fn extract_string_by_candidate_keys(
value: std::option::Option<&serde_json::Value>, value: std::option::Option<&serde_json::Value>,
candidate_keys: &[&str], candidate_keys: &[&str],
@@ -495,6 +640,62 @@ fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSid
return crate::SwapTradeSide::Unknown; return crate::SwapTradeSide::Unknown;
} }
fn parsed_json_has_trade_amount_or_price_payload(
parsed_json: std::option::Option<&serde_json::Value>,
) -> bool {
let parsed_json = match parsed_json {
Some(parsed_json) => parsed_json,
None => return false,
};
return json_value_contains_any_trade_amount_or_price_key(parsed_json);
}
fn json_value_contains_any_trade_amount_or_price_key(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Object(map) => {
for key in map.keys() {
let normalized = normalize_text(key.as_str());
if is_trade_amount_or_price_key(normalized.as_str()) {
return true;
}
}
for child in map.values() {
if json_value_contains_any_trade_amount_or_price_key(child) {
return true;
}
}
return false;
},
serde_json::Value::Array(values) => {
for child in values {
if json_value_contains_any_trade_amount_or_price_key(child) {
return true;
}
}
return false;
},
_ => return false,
}
}
fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
return normalized_key == "baseamountraw"
|| normalized_key == "quoteamountraw"
|| normalized_key == "baseamount"
|| normalized_key == "quoteamount"
|| normalized_key == "amountin"
|| normalized_key == "amountout"
|| normalized_key == "tokenain"
|| normalized_key == "tokenaout"
|| normalized_key == "tokenbin"
|| normalized_key == "tokenbout"
|| normalized_key == "inputamount"
|| normalized_key == "outputamount"
|| normalized_key == "swapamount"
|| normalized_key == "price"
|| normalized_key == "pricequoteperbase";
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto { fn make_create_transaction() -> crate::ChainTransactionDto {
@@ -670,4 +871,43 @@ mod tests {
}, },
} }
} }
#[test]
fn meteora_damm_v2_swap2_discriminator_is_detected() {
let data = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01];
let kind = super::classify_instruction_kind_from_data(Some(&data));
assert_eq!(kind, super::MeteoraDammV2InstructionKind::Swap);
}
#[test]
fn meteora_damm_v2_unknown_data_discriminator_does_not_fallback_to_global_swap_logs() {
let data = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x01];
let logs = vec!["Program log: Instruction: Swap2".to_string()];
let kind = super::classify_instruction_kind(None, Some(&data), &logs);
assert_eq!(kind, super::MeteoraDammV2InstructionKind::Unknown);
}
#[test]
fn meteora_damm_v2_inner_swap2_instruction_with_data_is_not_skipped() {
let decoder = crate::MeteoraDammV2Decoder::new();
let transaction = make_swap_transaction();
let mut instruction = make_swap_instruction();
instruction.parent_instruction_id = Some(400);
instruction.data_json = Some(format!(
"\"{}\"",
bs58::encode(&[0x41_u8, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01]).into_string()
));
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::MeteoraDammV2DecodedEvent::Swap(event) => {
assert_eq!(event.pool_account, Some("DammV2SwapPool111".to_string()));
},
crate::MeteoraDammV2DecodedEvent::CreatePool(_) => panic!("unexpected create event"),
}
}
} }

View File

@@ -2,6 +2,16 @@
//! Meteora Dynamic Bonding Curve (DBC) transaction decoder. //! Meteora Dynamic Bonding Curve (DBC) transaction decoder.
const DBC_DISCRIMINATOR_CREATE_POOL: [u8; 8] = [0xe9, 0x92, 0xd1, 0x8e, 0xcf, 0x68, 0x40, 0xbc];
const DBC_DISCRIMINATOR_INITIALIZE_POOL: [u8; 8] = [0x5f, 0xb4, 0x0a, 0xac, 0x54, 0xae, 0xe8, 0x28];
const DBC_DISCRIMINATOR_LAUNCH_POOL: [u8; 8] = [0xa6, 0x77, 0xd1, 0xb6, 0xd6, 0x6d, 0x3a, 0xb5];
const DBC_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8];
const DBC_DISCRIMINATOR_SWAP2: [u8; 8] = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88];
/// Decoded Meteora DBC create-pool event. /// Decoded Meteora DBC create-pool event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDbcCreatePoolDecoded { pub struct MeteoraDbcCreatePoolDecoded {
@@ -106,9 +116,6 @@ impl MeteoraDbcDecoder {
let log_messages = extract_log_messages(&transaction_json); let log_messages = extract_log_messages(&transaction_json);
let mut decoded_events = std::vec::Vec::new(); let mut decoded_events = std::vec::Vec::new();
for instruction in instructions { for instruction in instructions {
if instruction.parent_instruction_id.is_some() {
continue;
}
let program_id_option = &instruction.program_id; let program_id_option = &instruction.program_id;
let program_id = match program_id_option { let program_id = match program_id_option {
Some(program_id) => program_id, Some(program_id) => program_id,
@@ -132,7 +139,20 @@ impl MeteoraDbcDecoder {
Ok(parsed_json) => parsed_json, Ok(parsed_json) => parsed_json,
Err(error) => return Err(error), Err(error) => return Err(error),
}; };
let instruction_kind = classify_instruction_kind(parsed_json.as_ref(), &log_messages); let instruction_data_result =
decode_instruction_data_json(instruction.data_json.as_ref());
let instruction_data = match instruction_data_result {
Ok(instruction_data) => instruction_data,
Err(error) => return Err(error),
};
if instruction.parent_instruction_id.is_some() && instruction_data.is_none() {
continue;
}
let instruction_kind = classify_instruction_kind(
parsed_json.as_ref(),
instruction_data.as_deref(),
&log_messages,
);
let pool_account = extract_string_by_candidate_keys( let pool_account = extract_string_by_candidate_keys(
parsed_json.as_ref(), parsed_json.as_ref(),
&["pool", "poolAccount", "poolState", "virtualPool", "poolKey"], &["pool", "poolAccount", "poolState", "virtualPool", "poolKey"],
@@ -162,6 +182,9 @@ impl MeteoraDbcDecoder {
let payload_json = serde_json::json!({ let payload_json = serde_json::json!({
"decoder": "meteora_dbc", "decoder": "meteora_dbc",
"eventKind": "create_pool", "eventKind": "create_pool",
"dataDiscriminatorHex": instruction_data
.as_ref()
.and_then(|data| return first_8_bytes_hex(data.as_slice())),
"classifiedInstructionKind": "create_pool", "classifiedInstructionKind": "create_pool",
"signature": transaction.signature, "signature": transaction.signature,
"instructionId": instruction_id, "instructionId": instruction_id,
@@ -193,10 +216,32 @@ impl MeteoraDbcDecoder {
} }
if instruction_kind == MeteoraDbcInstructionKind::Swap { if instruction_kind == MeteoraDbcInstructionKind::Swap {
let trade_side = infer_trade_side(&log_messages); let trade_side = infer_trade_side(&log_messages);
let has_trade_amount_payload =
parsed_json_has_trade_amount_or_price_payload(parsed_json.as_ref());
let event_actionability = if has_trade_amount_payload {
"trade_candidate"
} else {
"non_actionable_trade"
};
let materialization_skip_reason = if has_trade_amount_payload {
serde_json::Value::Null
} else {
serde_json::Value::String("swap_without_amount_payload".to_string())
};
let payload_json = serde_json::json!({ let payload_json = serde_json::json!({
"decoder": "meteora_dbc", "decoder": "meteora_dbc",
"eventKind": "swap", "eventKind": "swap",
"dataDiscriminatorHex": instruction_data
.as_ref()
.and_then(|data| return first_8_bytes_hex(data.as_slice())),
"classifiedInstructionKind": "swap", "classifiedInstructionKind": "swap",
"eventCategory": "trade",
"eventLifecycleKind": "trade_swap",
"eventActionability": event_actionability,
"tradeCandidate": has_trade_amount_payload,
"candleCandidate": has_trade_amount_payload,
"nonTradeUseful": false,
"materializationSkipReason": materialization_skip_reason,
"signature": transaction.signature, "signature": transaction.signature,
"instructionId": instruction_id, "instructionId": instruction_id,
"instructionIndex": instruction.instruction_index, "instructionIndex": instruction.instruction_index,
@@ -306,6 +351,10 @@ fn parse_accounts_json(
let text_option = value.as_str(); let text_option = value.as_str();
if let Some(text) = text_option { if let Some(text) = text_option {
accounts.push(text.to_string()); accounts.push(text.to_string());
continue;
}
if let Some(pubkey) = value.get("pubkey").and_then(|nested| return nested.as_str()) {
accounts.push(pubkey.to_string());
} }
} }
return Ok(accounts); return Ok(accounts);
@@ -411,6 +460,48 @@ fn value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&st
return false; return false;
} }
fn decode_instruction_data_json(
data_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<std::vec::Vec<u8>>, crate::Error> {
let data_json = match data_json {
Some(data_json) => data_json,
None => return Ok(None),
};
let parsed_result = serde_json::from_str::<serde_json::Value>(data_json.as_str());
let parsed = match parsed_result {
Ok(parsed) => parsed,
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot parse Meteora DBC data_json: {}",
error
)));
},
};
if let serde_json::Value::String(base58_text) = parsed {
let decoded_result = bs58::decode(base58_text.as_str()).into_vec();
match decoded_result {
Ok(decoded) => return Ok(Some(decoded)),
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot decode Meteora DBC instruction data from base58: {}",
error
)));
},
}
}
return Ok(None);
}
fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
if bytes.len() < 8 {
return None;
}
return Some(format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
));
}
fn extract_account( fn extract_account(
accounts: &[std::string::String], accounts: &[std::string::String],
index: usize, index: usize,
@@ -433,8 +524,16 @@ fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSid
fn classify_instruction_kind( fn classify_instruction_kind(
parsed_json: std::option::Option<&serde_json::Value>, parsed_json: std::option::Option<&serde_json::Value>,
instruction_data: std::option::Option<&[u8]>,
log_messages: &[std::string::String], log_messages: &[std::string::String],
) -> MeteoraDbcInstructionKind { ) -> MeteoraDbcInstructionKind {
let data_kind = classify_instruction_kind_from_data(instruction_data);
if data_kind != MeteoraDbcInstructionKind::Unknown {
return data_kind;
}
if instruction_data_has_full_discriminator(instruction_data) {
return MeteoraDbcInstructionKind::Unknown;
}
let parsed_instruction_name = extract_string_by_candidate_keys( let parsed_instruction_name = extract_string_by_candidate_keys(
parsed_json, parsed_json,
&["instruction", "instructionName", "type", "name"], &["instruction", "instructionName", "type", "name"],
@@ -470,6 +569,102 @@ fn classify_instruction_kind(
return MeteoraDbcInstructionKind::Unknown; return MeteoraDbcInstructionKind::Unknown;
} }
fn instruction_data_has_full_discriminator(instruction_data: std::option::Option<&[u8]>) -> bool {
let instruction_data = match instruction_data {
Some(instruction_data) => instruction_data,
None => return false,
};
return instruction_data.len() >= 8;
}
fn classify_instruction_kind_from_data(
instruction_data: std::option::Option<&[u8]>,
) -> MeteoraDbcInstructionKind {
let instruction_data = match instruction_data {
Some(instruction_data) => instruction_data,
None => return MeteoraDbcInstructionKind::Unknown,
};
if instruction_data.len() < 8 {
return MeteoraDbcInstructionKind::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 == DBC_DISCRIMINATOR_CREATE_POOL
|| discriminator == DBC_DISCRIMINATOR_INITIALIZE_POOL
|| discriminator == DBC_DISCRIMINATOR_LAUNCH_POOL
{
return MeteoraDbcInstructionKind::CreatePool;
}
if discriminator == DBC_DISCRIMINATOR_SWAP || discriminator == DBC_DISCRIMINATOR_SWAP2 {
return MeteoraDbcInstructionKind::Swap;
}
return MeteoraDbcInstructionKind::Unknown;
}
fn parsed_json_has_trade_amount_or_price_payload(
parsed_json: std::option::Option<&serde_json::Value>,
) -> bool {
let parsed_json = match parsed_json {
Some(parsed_json) => parsed_json,
None => return false,
};
return json_value_contains_any_trade_amount_or_price_key(parsed_json);
}
fn json_value_contains_any_trade_amount_or_price_key(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Object(map) => {
for key in map.keys() {
let normalized = normalize_log_text(key.as_str());
if is_trade_amount_or_price_key(normalized.as_str()) {
return true;
}
}
for child in map.values() {
if json_value_contains_any_trade_amount_or_price_key(child) {
return true;
}
}
return false;
},
serde_json::Value::Array(values) => {
for child in values {
if json_value_contains_any_trade_amount_or_price_key(child) {
return true;
}
}
return false;
},
_ => return false,
}
}
fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
return normalized_key == "baseamountraw"
|| normalized_key == "quoteamountraw"
|| normalized_key == "baseamount"
|| normalized_key == "quoteamount"
|| normalized_key == "amountin"
|| normalized_key == "amountout"
|| normalized_key == "tokenain"
|| normalized_key == "tokenaout"
|| normalized_key == "tokenbin"
|| normalized_key == "tokenbout"
|| normalized_key == "inputamount"
|| normalized_key == "outputamount"
|| normalized_key == "swapamount"
|| normalized_key == "price"
|| normalized_key == "pricequoteperbase";
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto { fn make_create_transaction() -> crate::ChainTransactionDto {
@@ -688,4 +883,43 @@ mod tests {
}, },
} }
} }
#[test]
fn meteora_dbc_swap2_discriminator_is_detected() {
let data = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01];
let kind = super::classify_instruction_kind_from_data(Some(&data));
assert_eq!(kind, super::MeteoraDbcInstructionKind::Swap);
}
#[test]
fn meteora_dbc_unknown_data_discriminator_does_not_fallback_to_global_swap_logs() {
let data = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x01];
let logs = vec!["Program log: Instruction: Swap2".to_string()];
let kind = super::classify_instruction_kind(None, Some(&data), &logs);
assert_eq!(kind, super::MeteoraDbcInstructionKind::Unknown);
}
#[test]
fn meteora_dbc_inner_swap2_instruction_with_data_is_not_skipped() {
let decoder = crate::MeteoraDbcDecoder::new();
let transaction = make_swap_transaction();
let mut instruction = make_swap_instruction();
instruction.parent_instruction_id = Some(300);
instruction.data_json = Some(format!(
"\"{}\"",
bs58::encode(&[0x41_u8, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01]).into_string()
));
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::MeteoraDbcDecodedEvent::Swap(event) => {
assert_eq!(event.pool_account, Some("DbcPoolSwap111".to_string()));
},
crate::MeteoraDbcDecodedEvent::CreatePool(_) => panic!("unexpected create event"),
}
}
} }

View File

@@ -235,6 +235,25 @@ impl LocalPipelineValidationConfig {
config.profile_code = "0.7.35_non_trade_fee_reward_admin".to_string(); config.profile_code = "0.7.35_non_trade_fee_reward_admin".to_string();
return config; return config;
} }
/// Builds the `0.7.36` Meteora family consolidation validation config.
///
/// This profile keeps the `0.7.35` non-trade counters while making the four
/// Meteora variants explicit in the expected set. Targeted backfills may
/// still miss some variants, so missing variants remain warnings.
pub fn v0_7_36_meteora_family_consolidation() -> Self {
let mut config = Self::v0_7_35_non_trade_fee_reward_admin();
config.profile_code = "0.7.36_meteora_family_consolidation".to_string();
config.expected_dex_codes = vec![
"meteora_dbc".to_string(),
"meteora_damm_v1".to_string(),
"meteora_damm_v2".to_string(),
"meteora_dlmm".to_string(),
];
config.require_all_expected_dexes = false;
config.allow_unexpected_dexes = true;
return config;
}
} }
/// A single local pipeline validation issue. /// A single local pipeline validation issue.
@@ -421,6 +440,14 @@ impl LocalPipelineValidationService {
let config = crate::LocalPipelineValidationConfig::v0_7_35_non_trade_fee_reward_admin(); let config = crate::LocalPipelineValidationConfig::v0_7_35_non_trade_fee_reward_admin();
return self.validate_current_database(&config).await; return self.validate_current_database(&config).await;
} }
/// Diagnoses the current database with the `0.7.36` Meteora family profile.
pub async fn validate_v0_7_36_current_database(
&self,
) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
let config = crate::LocalPipelineValidationConfig::v0_7_36_meteora_family_consolidation();
return self.validate_current_database(&config).await;
}
} }
/// Validates a diagnostics summary without performing database access. /// Validates a diagnostics summary without performing database access.
@@ -540,7 +567,8 @@ pub fn validate_local_pipeline_diagnostics_summary(
} }
let missing_expected_dex_is_warning = config.profile_code let missing_expected_dex_is_warning = config.profile_code
== "0.7.34_non_trade_liquidity_lifecycle" == "0.7.34_non_trade_liquidity_lifecycle"
|| config.profile_code == "0.7.35_non_trade_fee_reward_admin"; || config.profile_code == "0.7.35_non_trade_fee_reward_admin"
|| config.profile_code == "0.7.36_meteora_family_consolidation";
if config.require_all_expected_dexes || missing_expected_dex_is_warning { if config.require_all_expected_dexes || missing_expected_dex_is_warning {
for expected_dex_code in &expected_dex_codes { for expected_dex_code in &expected_dex_codes {
if !observed_dex_codes.contains(expected_dex_code) { if !observed_dex_codes.contains(expected_dex_code) {
@@ -1175,6 +1203,24 @@ mod tests {
assert_eq!(report.pool_admin_event_count, 1); assert_eq!(report.pool_admin_event_count, 1);
} }
#[test]
fn validation_accepts_0_7_36_meteora_family_partial_corpus() {
let mut summary = make_0_7_28_summary_with_meteora();
summary.dex_summaries.retain(|dex_summary| {
return dex_summary.dex_code == "meteora_dlmm";
});
let config = crate::LocalPipelineValidationConfig::v0_7_36_meteora_family_consolidation();
let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
assert!(report.validation_passed);
assert_eq!(report.validation_profile_code, "0.7.36_meteora_family_consolidation");
assert_eq!(report.blocking_issue_count, 0);
assert_eq!(report.warning_count, 3);
assert!(report.expected_dex_codes.contains(&"meteora_dbc".to_string()));
assert!(report.expected_dex_codes.contains(&"meteora_damm_v1".to_string()));
assert!(report.expected_dex_codes.contains(&"meteora_damm_v2".to_string()));
assert!(report.expected_dex_codes.contains(&"meteora_dlmm".to_string()));
}
#[test] #[test]
fn validation_rejects_0_7_33_pair_trading_readiness_mismatch() { fn validation_rejects_0_7_33_pair_trading_readiness_mismatch() {
let mut summary = make_0_7_28_summary_with_meteora(); let mut summary = make_0_7_28_summary_with_meteora();