0.7.36
This commit is contained in:
@@ -62,7 +62,8 @@
|
|||||||
0.7.29 - Ajout d’une matrice DEX commune (`dex_support_matrix`) utilisée par le catalogue DEX, la classification transactionnelle et l’enregistrement 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 d’une matrice DEX commune (`dex_support_matrix`) utilisée par le catalogue DEX, la classification transactionnelle et l’enregistrement 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 d’une 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 d’une 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 l’invariant : 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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
89
README.md
89
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.
|
`khadhroony-bobobot` est un workspace Rust destiné à la détection, au décodage, à l’analyse 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 l’application de démonstration.
|
10. inspection via l’application de démonstration.
|
||||||
|
|
||||||
### 3.3. Connecteurs validés manuellement via l’application de démo
|
### 3.3. Connecteurs validés ou observés via l’application 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 d’origine, 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 l’UI analytique et les vues token/pair/pool.
|
6. vérifier l’idempotence : 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
|
||||||
|
|
||||||
|
|||||||
130
ROADMAP.md
130
ROADMAP.md
@@ -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 d’ajouter les surfaces restantes.
|
- conserver la validation multi-DEX et la matrice DEX comme garde-fous avant d’ajouter 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 d’instructions utiles à l’analyse,
|
- 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 l’historique,
|
- 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 n’alimentent 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 d’achat/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 qu’un événement de liquidité ou de cycle de vie ne produit jamais de candle directement.
|
- garantir qu’un é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 l’invariant : 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 d’ajouter 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 l’idempotence du replay local sur un corpus Meteora mixte,
|
- garantir l’idempotence : relancer l’enrichissement metadata ne doit pas recréer tokens, pools, paires, trades, candles ou origins ;
|
||||||
- documenter les limites connues des variantes insuffisamment couvertes.
|
- conserver l’invariant de validation : le manque de metadata n’est 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 l’UI d’inspection.
|
- exposer les origins dans les diagnostics et l’UI d’inspection.
|
||||||
|
|
||||||
### 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 l’idempotence et l’absence de trades/candles invalides.
|
- rejouer les corpus plusieurs fois pour vérifier l’idempotence et l’absence 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 l’isoler 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 l’isoler 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 l’ambiguïté avec `raydium_cpmm` et `raydium_clmm`,
|
- renommer/stabiliser les fonctions internes autour de `raydium_amm_v4` pour éviter l’ambiguï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 d’ouvrir l’analyse `0.8.x`.
|
- verrouiller les invariants avant d’ouvrir l’analyse `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 l’extension future vers Ichimoku, Kumo, projections ABCD et égalités temps/prix sans les mélanger au pipeline de décodage DEX.
|
- préparer l’extension 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 l’ouverture de `0.8.x`.
|
Objectif : stabiliser la couche desktop de validation avant l’ouverture de `0.8.x`.
|
||||||
|
|
||||||
À faire :
|
À faire :
|
||||||
@@ -1005,7 +1026,7 @@ Objectif : stabiliser la couche desktop de validation avant l’ouverture de `0.
|
|||||||
- préparer une base UI suffisamment stable pour la future phase d’analyse et filtrage `0.8.x`,
|
- préparer une base UI suffisamment stable pour la future phase d’analyse 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 d’un pipeline complet de résolution, décodage, normalisation métier et classification des événements non-trade.
|
Objectif : structurer les connecteurs DEX autour d’un 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 d’une détection temps réel hybride et d’un backfill ciblé compatible avec les mêmes objets métier,
|
- préparation d’une détection temps réel hybride et d’un backfill ciblé compatible avec les mêmes objets métier,
|
||||||
- préparation d’agrégats DEX plus riches, de candles/OHLCV et d’une UI d’inspection du pipeline `0.7.x`.
|
- préparation d’agrégats DEX plus riches, de candles/OHLCV et d’une UI d’inspection 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 d’un point D selon des règles temps/prix explicites,
|
- outils de sélection manuelle de points ABC et projection d’un 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 d’action.
|
Objectif : préparer la couche d’action.
|
||||||
|
|
||||||
À faire :
|
À faire :
|
||||||
@@ -1074,7 +1095,7 @@ Objectif : préparer la couche d’action.
|
|||||||
- préparation d’ordres et de swaps,
|
- préparation d’ordres 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 l’analyse à l’action tout en gardant des garde-fous explicites.
|
Objectif : brancher l’analyse à l’action tout en gardant des garde-fous explicites.
|
||||||
|
|
||||||
À faire :
|
À faire :
|
||||||
@@ -1085,7 +1106,7 @@ Objectif : brancher l’analyse à l’action tout en gardant des garde-fous exp
|
|||||||
- confirmations explicites ou semi-automatiques,
|
- confirmations explicites ou semi-automatiques,
|
||||||
- journaux d’exécution.
|
- journaux d’exé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 qu’ils 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 l’enrichissement 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 d’ouvrir réellement `0.8.x` pour l’analyse, les filtres, les patterns et les projections graphiques.
|
||||||
13. stabiliser l’ergonomie, les filtres, la pagination et la navigation de l’UI d’inspection,
|
|
||||||
14. préparer ensuite l’ouverture de `0.8.x` pour l’analyse, 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.
|
|
||||||
|
|||||||
@@ -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.35 — non-trade fee/reward admin</option>
|
<option value="0.7.36_meteora_family_consolidation" selected>0.7.36 — Meteora 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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}"
|
||||||
))),
|
))),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user