0.7.56
This commit is contained in:
@@ -88,3 +88,4 @@
|
||||
0.7.53 - Clôture PumpSwap : décodage transaction/log complet, matérialisation `buy/sell/buy_exact_quote_in` depuis sources exactes, events Anchor audit-only, tests synthétiques IDL, validation globale coverage SQL et non-régression Raydium.
|
||||
0.7.54 - Clôture Pump.fun : decoder maximal local depuis IDL Solscan/upstream, décodage des 40 instructions et 23 events Anchor connus, matérialisation validée des trades `buy/sell/buy_exact_sol_in` et `trade_event` v2/exact sans double-count, non-trades launch/fee/reward/admin selon contexte, validation SQL Pump.fun propre et ouverture de `0.7.55 pump_fees`.
|
||||
0.7.55 - Clôture Pump Fees : decoder local maximal `pump_fees` depuis l'IDL locale, `29` instructions et `20` events Anchor couverts, tests synthétiques des Anchor IDL non observés, matérialisation prudente fee/reward/admin/lifecycle, `get_fees` decoded-only, transactions failed audit-only, aucun trade/candle direct et SQL de validation Pump Fees propre.
|
||||
0.7.56 - Clôture Meteora DBC : decoder local maximal depuis l'IDL `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN`, couverture des 28 instructions et 23 events Anchor, matérialisation validée des swaps `swap/swap2`, lifecycle, admin/config, lockers/migrations et fees/surplus/leftover, ajout du modèle `k_sol_fee_event_amounts` pour les legs de montants fee, helper générique parent fee -> legs scalaires, recovery fee `allowlisted_inner_spl_transfer` strictement allowlistée pour anciens DEX, validation croisée Pump/Raydium, 446 tests passés, clippy OK et SQL de fermeture DBC propre.
|
||||
|
||||
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.55"
|
||||
version = "0.7.56"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"
|
||||
|
||||
88
README.md
88
README.md
@@ -4,6 +4,92 @@
|
||||
|
||||
|
||||
|
||||
## État final validé `0.7.56` — `meteora_dbc` + socle `fee_event_amounts`
|
||||
|
||||
La tranche `0.7.56 meteora_dbc` est clôturée côté decoder local maximal, coverage, tests synthétiques, matérialisation prudente et validation SQL sur base dédiée. Le programme traité est :
|
||||
|
||||
```text
|
||||
dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN
|
||||
```
|
||||
|
||||
Source IDL locale prioritaire :
|
||||
|
||||
```text
|
||||
idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json
|
||||
```
|
||||
|
||||
Surface couverte : `28` instructions, `23` events Anchor, `9` accounts et `59` types. Les instructions `swap` et `swap2` sont les seules candidates trade/candle directes, uniquement lorsque les montants et les mints base/quote sont fiables. Les initialisations de virtual pool, migrations, lockers, metadata/config/operator et transferts de créateur alimentent les tables non-trade adaptées ou restent decoded-only avec raison explicite. Les transactions failed restent audit-only.
|
||||
|
||||
Validation locale finale rapportée :
|
||||
|
||||
```text
|
||||
cargo test -p kb_lib -> 446 passed / 0 failed
|
||||
cargo clippy -p kb_lib --all-targets -- -D warnings -> OK
|
||||
480 replayed
|
||||
0 decode skipped
|
||||
480 ledger upserts
|
||||
454 unsafe ledger rows
|
||||
264 trades
|
||||
1 liquidity
|
||||
122 lifecycle
|
||||
0 tokenAccount
|
||||
1056 candle upserts
|
||||
instructionObservations = 7167
|
||||
resetDeleted = 3583
|
||||
catalog = 86 tokens / 60 pools / 60 pairs
|
||||
```
|
||||
|
||||
Matérialisation DBC finale observée :
|
||||
|
||||
```text
|
||||
claim_creator_trading_fee 8 parents / 8 scalar / 8 legs
|
||||
claim_partner_pool_creation_fee 10 parents / 10 scalar / 10 legs
|
||||
claim_protocol_fee 10 parents / 10 scalar / 10 legs
|
||||
claim_protocol_pool_creation_fee 10 parents / 10 scalar / 10 legs
|
||||
claim_trading_fee 11 parents / 6 scalar / 18 legs
|
||||
creator_withdraw_surplus 2 parents / 2 scalar / 2 legs
|
||||
partner_withdraw_surplus 9 parents / 9 scalar / 9 legs
|
||||
withdraw_leftover 10 parents / 10 scalar / 10 legs
|
||||
withdraw_migration_fee 9 parents / 9 scalar / 9 legs
|
||||
zap_protocol_fee 10 parents / 10 scalar / 10 legs
|
||||
Total meteora_dbc 89 fee parents / 96 amount legs
|
||||
```
|
||||
|
||||
La tranche introduit le socle générique `k_sol_fee_event_amounts` :
|
||||
|
||||
- `k_sol_fee_events` reste l'événement fee parent ;
|
||||
- `k_sol_fee_event_amounts` stocke les legs de montants, avec `fee_event_id`, `leg_index`, `token_mint`, `amount_raw`, comptes source/destination et `amount_source` ;
|
||||
- tout parent fee avec `fee_token_mint + fee_amount_raw` crée automatiquement un leg scalaire ;
|
||||
- les fees multi-mint/multi-leg laissent le parent sans montant scalaire et stockent les montants fiables dans les legs ;
|
||||
- une recovery `allowlisted_inner_spl_transfer` existe pour les anciens DEX validés, mais aucun futur décodeur ne l'hérite par défaut ;
|
||||
- tout futur décodeur doit déclarer explicitement sa policy de récupération des montants fee.
|
||||
|
||||
Checks de fermeture `0.7.56` :
|
||||
|
||||
- fallback `upstream_git` `meteora_dbc` pour entrées couvertes localement : vide ;
|
||||
- decoded `meteora_dbc` sans coverage : vide ;
|
||||
- successful non-materialized sans `skip*Reason` ou policy explicite : vide ;
|
||||
- failed transaction avec business materialization : vide ;
|
||||
- multi-target materialization : vide ;
|
||||
- non-swap vers trade/candle : vide ;
|
||||
- parent fee avec `fee_token_mint + fee_amount_raw` mais sans `k_sol_fee_event_amounts` : vide ;
|
||||
- legs fee orphelins : vide ;
|
||||
- recovery générique `allowlisted_inner_spl_transfer` non appliquée à `meteora_dbc`, qui reste couvert par ses chemins spécifiques.
|
||||
|
||||
Contrôles croisés réalisés sur anciennes bases : `raydium_launchpad`, `raydium_cpmm`, `pump_swap`, `pump_fees`, `pump_fun`, `raydium_amm_v4`, `raydium_clmm`, `raydium_stable_swap`. Les parents fees scalaires ont des legs ; les events sans transfert exploitable restent documentés par `fee_instruction_has_no_actual_transfer` ou `fee_instruction_has_only_zero_amount_transfers`.
|
||||
|
||||
Documents de référence :
|
||||
|
||||
```text
|
||||
docs/reports/METEORA_DBC_EVENT_COVERAGE_REPORT.md
|
||||
docs/reports/FEE_EVENT_AMOUNTS_MODEL_NOTE_0_7_56.md
|
||||
docs/VALIDATION_STATUS_0_7_56_FINAL.md
|
||||
validation_sql/SQL_VALIDATION_METEORA_DBC_0_7_56.sql
|
||||
docs/prompts/PROMPT_0_7_57_METEORA_DLMM_FULL_DECODE_MATERIALIZATION.md
|
||||
```
|
||||
|
||||
Prochaine tranche recommandée : `0.7.57 meteora_dlmm` en mode full decode / full materialization, sur le programme `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`, avec vérification complète de l'IDL locale `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json`.
|
||||
|
||||
## État final validé `0.7.55` — `pump_fees`
|
||||
|
||||
La tranche `0.7.55 pump_fees` est clôturée côté decoder local maximal, coverage, tests synthétiques, matérialisation prudente et validation SQL. Le programme `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` est traité comme surface fee/config/accounting : `get_fees` reste decoded-only, les claims social fee alimentent `k_sol_reward_events`, les donation/buyback alimentent `k_sol_fee_events`, les authority/config/tier/update alimentent `k_sol_pool_admin_events`, les créations/init/extend alimentent `k_sol_pool_lifecycle_events`, et aucun trade/candle direct n'est produit.
|
||||
@@ -603,7 +689,7 @@ Si une requête DB est ajoutée ou modifiée, mettre à jour les re-exports dans
|
||||
|
||||
La priorité immédiate après la clôture `0.7.55 pump_fees` est la suivante :
|
||||
|
||||
1. ouvrir `0.7.56 meteora_dbc` sur une base SQLite neuve ;
|
||||
1. reprendre `0.7.57 meteora_dlmm` sur une base SQLite neuve ;
|
||||
2. décoder toutes les instructions/events DBC connus par l'IDL locale et les sources Git ;
|
||||
3. matérialiser uniquement ce qui est prouvable : launch/bonding, swaps fiables, migration, liquidity, fees/admin/config ;
|
||||
4. ne créer aucun trade/candle DBC sans montants, sens économique, pool/pair et mints fiables ;
|
||||
|
||||
57
ROADMAP.md
57
ROADMAP.md
@@ -1,38 +1,39 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
|
||||
# khadhroony-bobobot — Roadmap
|
||||
# Roadmap — khadhroony-bobobot
|
||||
|
||||
### `0.7.55 pump_fees` — clôturé
|
||||
## État courant — clôture `0.7.56 meteora_dbc` et ouverture `0.7.57 meteora_dlmm full decode/materialization`
|
||||
|
||||
- Decoder local `pump_fees` clôturé pour les `29` instructions et `20` events Anchor de l'IDL locale.
|
||||
- Registre enrichi avec les entrées locales absentes du decoder Carbon partiel et conservation de deux discriminators Solscan hors IDL comme surfaces futures.
|
||||
- Classification validée : `get_fees` decoded-only, social fee claim vers reward, donation/buyback vers fee, config/authority/tier/admin vers admin/lifecycle.
|
||||
- Tests synthétiques ajoutés pour les Anchor events IDL non observés dans le corpus.
|
||||
- Invariants propres : aucun fallback `pump_fees`, aucun decoded sans coverage, aucune failed tx matérialisée, aucun multi-target, aucun trade/candle direct.
|
||||
- Dernier replay : `127 replayed`, `150 ledger upserts`, `125 unsafe`, `115 lifecycle`, `2234 instructionObservations`, catalogue `11 tokens / 10 pools / 10 pairs`.
|
||||
### `0.7.56 meteora_dbc` — clos
|
||||
|
||||
## État courant — clôture `0.7.55 pump_fees` et ouverture `0.7.56 meteora_dbc`
|
||||
- Program id : `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN`.
|
||||
- Source prioritaire : `idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json`.
|
||||
- Surface IDL : `28` instructions, `23` events Anchor, `9` accounts, `59` types.
|
||||
- Build final : `cargo test -p kb_lib` -> `446 passed`; clippy `-D warnings` OK.
|
||||
- Replay final DBC : `480 replayed`, `264 trades`, `1 liquidity`, `122 lifecycle`, `1056 candle upserts`, `instructionObservations=7167`, catalogue `86/60/60`.
|
||||
- Fees DBC : `89` parents `k_sol_fee_events`, `96` legs `k_sol_fee_event_amounts`, aucun parent fee scalaire sans leg, aucun leg orphelin.
|
||||
- Socle transversal ajouté : table `k_sol_fee_event_amounts`, helper générique parent fee + legs, backfill/normalisation safe, recovery `allowlisted_inner_spl_transfer` non globale.
|
||||
|
||||
`0.7.55 pump_fees` est clos pour la surface Pump Fees. La tranche couvre les `29` instructions et `20` events Anchor de l'IDL locale, avec tests synthétiques pour les Anchor events non observés. `get_fees` reste decoded-only, les rewards/fees/admin/lifecycle sont matérialisés uniquement sur transactions réussies et données fiables, les failed tx restent audit-only, et aucun trade/candle direct `pump_fees` n'est créé.
|
||||
### Politique fee obligatoire à partir de `0.7.56`
|
||||
|
||||
Décisions de clôture Pump Fees :
|
||||
- Un parent `k_sol_fee_events` avec `fee_token_mint + fee_amount_raw` doit créer automatiquement un leg `k_sol_fee_event_amounts` d'index `0`.
|
||||
- Un fee multi-mint/multi-leg ne doit pas agréger artificiellement le parent ; le parent reste sans montant scalaire, les legs portent les montants fiables.
|
||||
- Les montants issus d'arguments `max`, de bornes ou de `u64::MAX` ne sont pas des fees exécutés.
|
||||
- La recovery par CPI SPL inner transfer n'est pas globale : chaque futur décodeur doit déclarer explicitement sa policy et ses event kinds autorisés.
|
||||
- Les `amount_source` connus à préserver : `parent_fee_event_amount`, `fee_event_amounts`, `inner_spl_transfer`, `lamport_balance_delta`, `allowlisted_inner_spl_transfer`.
|
||||
- Les cas sans transfert exploitable doivent écrire une raison explicite : `fee_instruction_has_no_actual_transfer` ou `fee_instruction_has_only_zero_amount_transfers`.
|
||||
|
||||
- `pump_fees.get_fees` est decoded-only et ne représente pas une fee payée ;
|
||||
- `claim_social_fee_pda*` et `social_fee_pda_claimed` alimentent `k_sol_reward_events` quand la transaction réussit ;
|
||||
- `crank_donation_fee_pda`, `donation_fee_pda_cranked`, `sweep_buyback` et `sweep_buyback_event` alimentent `k_sol_fee_events` quand les montants sont fiables ;
|
||||
- fee sharing/config/authority/tier/update alimentent `k_sol_pool_admin_events` ou `k_sol_pool_lifecycle_events` selon le contexte ;
|
||||
- les discriminators Solscan `revoke_fee_sharing_authority_event` et `transfer_fee_sharing_authority_event` restent conservés en coverage comme surfaces futures non observées ;
|
||||
- les validations fallback, decoded sans coverage, failed materialization, multi-target, successful non-materialized, anti-trade/candle et watchlist `pump_fees` sont propres.
|
||||
### Prochaine tranche immédiate
|
||||
|
||||
Replay final rapporté : `127 replayed`, `0 decode skipped`, `150 ledger upserts`, `125 unsafe`, `115 lifecycle`, `2234 instructionObservations`, catalogue `11 tokens / 10 pools / 10 pairs`. Tests : `431 passed`, clippy OK.
|
||||
|
||||
### Phasage immédiat après `0.7.55`
|
||||
|
||||
| Priorité | Tranche | Surface | Raison |
|
||||
| Priorité | Tranche | Surface | Objectif |
|
||||
|---:|---|---|---|
|
||||
| 1 | `0.7.56` | `meteora_dbc` | Prochaine tranche programmée : launch/bonding, swaps exploitables, migration, fees/admin/config depuis corpus neuf. |
|
||||
| 2 | `0.7.57+` | `meteora_*` | Corriger les gaps locaux Meteora reportés volontairement, surface par surface. |
|
||||
| 3 | ultérieur | `jupiter_swap` / agrégateurs | `jupiter_swap.route_v2` reste en watchlist résiduelle ; traiter sans double-count des DEX effectifs. |
|
||||
| 1 | `0.7.57` | `meteora_dlmm` | Full decode + full materialization : `76` instructions IDL, `30` events Anchor, swaps, exact-out, liquidity/bin/position, fees, rewards, admin/config, limit-order events et side effects sans double-count. |
|
||||
| 2 | `0.7.58` | `meteora_damm_v1` | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. |
|
||||
| 3 | `0.7.59` | `meteora_damm_v2` | Couverture complète : create/custom pools, swaps, liquidity, dynamic config, fees/admin. |
|
||||
| 4 | `0.7.60` | `meteora_vault` | Vault deposit/withdraw/fee/accounting ; pas de candle directe. |
|
||||
| 5 | `0.7.61+` | programmes transversaux | System/SPL/ATA/Compute/Memo/ALT : side effects et contexte, pas de trade direct. |
|
||||
|
||||
La tranche `0.7.57 meteora_dlmm` doit partir d'une base neuve, réutiliser `fee_event_amounts`, ajouter une validation dédiée `validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql`, et clôturer uniquement si toutes les entrées IDL locales sont couvertes, décodées ou explicitement non observées avec tests synthétiques.
|
||||
|
||||
## 0.7.47-1FE5 — Décision de planification : ne plus viser “tous les events en une session”
|
||||
|
||||
@@ -72,8 +73,8 @@ Exceptions : les comptes non-programmes (`platform_config`, token authority, com
|
||||
| `0.7.53` | `pump_swap` | `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA` | Pump / AMM | **Clos** : `buy/sell/buy_exact_quote_in` matérialisés seulement depuis sources exactes ; events Anchor audit-only ; tests synthétiques IDL ; SQL global. |
|
||||
| `0.7.54` | `pump_fun` | `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` | Pump / launch-bonding | **Clos** : decoder maximal IDL/local, trades directs `buy/sell/buy_exact_sol_in`, v2/exact via `trade_event`, non-trades matérialisés selon contexte, validations Pump.fun propres. |
|
||||
| `0.7.55` | `pump_fees` | `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` | Pump / fee | **Clos** : `29` instructions, `20` events Anchor, fee/reward/admin/lifecycle, `get_fees` decoded-only, failed tx audit-only, aucun trade/candle direct. |
|
||||
| `0.7.56` | `meteora_dbc` | `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` | Meteora / DBC | Compléter launch/bonding, swaps exploitables, migration, fees/admin/config. |
|
||||
| `0.7.57` | `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Meteora / DLMM | Parité upstream finale : swaps, bins, positions, liquidity, fees/rewards/admin. |
|
||||
| `0.7.56` | `meteora_dbc` | `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` | Meteora / DBC | Clos : `28` instructions et `23` events Anchor couverts, swaps `swap/swap2`, lifecycle/admin/fees, `k_sol_fee_event_amounts`, validations SQL propres. |
|
||||
| `0.7.57` | `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Meteora / DLMM | Full decode + full materialization : `76` instructions IDL, `30` events Anchor, swaps, bins, positions, liquidity, fees/rewards/admin/limit-order, sans double-count. |
|
||||
| `0.7.58` | `meteora_damm_v1` | `Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB` | Meteora / DAMM v1 | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. |
|
||||
| `0.7.59` | `meteora_damm_v2` | `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` | Meteora / DAMM v2 | Couverture complète : create/custom pools, swaps, liquidity, dynamic config, fees/admin. |
|
||||
| `0.7.60` | `meteora_vault` | `24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi` | Meteora / vault | Vault deposit/withdraw/fee/accounting ; pas de candle directe. |
|
||||
@@ -1439,7 +1440,7 @@ Les comptes non-programmes ne créent pas de tranche decoder autonome. `SOLSCAN_
|
||||
| Version | Decoder / surface | Program id | Objectif |
|
||||
|---:|---|---|---|
|
||||
| `0.7.56` | `meteora_dbc` | `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` | Compléter toutes les instructions/events DBC : launch/bonding, swap exploitable, migration, fees/admin/config. |
|
||||
| `0.7.57` | `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Parité upstream finale : swaps, bins, positions, liquidity, fees/rewards/admin. |
|
||||
| `0.7.57` | `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Full decode + full materialization : `76` instructions IDL, `30` events Anchor, swaps, bins, positions, liquidity, fees/rewards/admin/limit-order, sans double-count. |
|
||||
| `0.7.58` | `meteora_damm_v1` | `Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB` | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. |
|
||||
| `0.7.59` | `meteora_damm_v2` | `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` | Couverture complète : create/custom pools, swaps, liquidity, dynamic config, fees/admin. |
|
||||
| `0.7.60` | `meteora_vault` | `24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi` | Vault deposit/withdraw/fee/accounting ; pas de candle directe. |
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
# Database Event Model Review — `khadhroony-bobobot` `0.7.47-1FE5`
|
||||
|
||||
|
||||
## Note `0.7.56` — modèle fee parent + amount legs
|
||||
|
||||
`0.7.56` ajoute `k_sol_fee_event_amounts` comme table enfant de `k_sol_fee_events`. Cette table est obligatoire dès qu'un event fee porte plusieurs montants, plusieurs mints ou plusieurs destinations. Le parent `k_sol_fee_events` reste l'ancre logique liée au `decoded_event_id`; les legs portent `leg_index`, `token_mint`, `amount_raw`, comptes source/destination et `amount_source`.
|
||||
|
||||
Invariants :
|
||||
|
||||
- tout parent fee avec `fee_token_mint + fee_amount_raw` doit avoir un leg scalaire automatique ;
|
||||
- un fee multi-leg/multi-mint ne doit pas agréger artificiellement le parent ;
|
||||
- les replays doivent supprimer/remplacer les legs avec le parent ;
|
||||
- les transactions failed restent audit-only ;
|
||||
- la recovery `allowlisted_inner_spl_transfer` est strictement allowlistée et ne s'applique pas par défaut aux futurs decoders.
|
||||
|
||||
Le contrôle standard `parent scalar without leg` doit être vide sur toute base de validation. Voir `docs/reports/FEE_EVENT_AMOUNTS_MODEL_NOTE_0_7_56.md`.
|
||||
|
||||
## Conclusion courte
|
||||
|
||||
La base actuelle est **suffisante pour continuer le décodage exhaustif en audit-only**, parce que `k_sol_dex_decoded_events` garde les events décodés avec `payload_json`.
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
<!-- file: docs/DEX_DECODER_MATRIX.md -->
|
||||
|
||||
# DEX Decoder Matrix — `khadhroony-bobobot` `0.7.55 pump_fees closed`
|
||||
# DEX Decoder Matrix — `khadhroony-bobobot` `0.7.56 meteora_dbc closed`
|
||||
|
||||
|
||||
|
||||
## Note `0.7.56 closed` — Meteora DBC clôturé, DLMM ensuite
|
||||
|
||||
La tranche `0.7.56` ferme `meteora_dbc` / `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` depuis l'IDL locale `idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json`.
|
||||
|
||||
Le decoder local couvre les `28` instructions et les `23` events Anchor IDL. Les trades/candles ne sont produits que depuis `swap` / `swap2` avec montants et mints fiables. Les migrations, lockers, lifecycle, admin/config et fees sont matérialisés quand le contexte est fiable ; sinon ils restent decoded-only/audit-only avec raison explicite. Les transactions failed restent audit-only.
|
||||
|
||||
Le modèle fee est maintenant transversal : `k_sol_fee_events` reste le parent, `k_sol_fee_event_amounts` porte les legs. Les parents fees scalaires créent automatiquement un leg ; les fees multi-leg/multi-mint n'agrègent pas artificiellement le parent. La recovery `allowlisted_inner_spl_transfer` est allowlistée et ne s'applique jamais par défaut à un futur decoder.
|
||||
|
||||
La prochaine tranche programmée est `0.7.57 meteora_dlmm` avec objectif full decode + full materialization sur `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`.
|
||||
|
||||
## Note `0.7.55 closed` — Pump Fees clôturé, Meteora DBC ensuite
|
||||
|
||||
La tranche `0.7.55` ferme `pump_fees` comme surface fee/config/accounting. Le decoder local couvre les `29` instructions et `20` events Anchor de l'IDL locale, avec tests synthétiques pour les Anchor events IDL non observés. Les transactions failed restent audit-only, `get_fees` reste decoded-only, et aucun trade/candle direct n'est créé.
|
||||
|
||||
Deux discriminators Solscan non présents dans l'IDL locale restent conservés en coverage comme surfaces futures : `revoke_fee_sharing_authority_event` (`7217653c0ebe993e`) et `transfer_fee_sharing_authority_event` (`7c8fc6f54db808ec`).
|
||||
|
||||
La prochaine tranche programmée est `0.7.56 meteora_dbc`.
|
||||
La tranche suivante après `0.7.55` a été `0.7.56 meteora_dbc`, désormais clôturée ; la prochaine tranche courante est `0.7.57 meteora_dlmm`.
|
||||
|
||||
## Note `0.7.54 closed` — Pump.fun clôturé
|
||||
|
||||
@@ -69,8 +80,8 @@ Cette matrice complète `kb_lib/src/dex_support_matrix.rs`. Elle documente **ce
|
||||
| 7 | `pump_swap` | `supported / 0.7.53 closed` | `buy`, `sell` + `buy_exact_quote_in` matérialisable via `BuyEvent` exact ; instructions non-trade spécialisées : liquidity, fee/creator fee, admin/config, cashback/token incentives, volume accumulator ; events Anchor autonomes audit-only. | Trades/candles uniquement depuis montants exacts ; failed tx decoded-only ; `instruction_bounds_only` reste decoded-only ; tests synthétiques IDL et SQL global ajoutés. |
|
||||
| 8 | `pump_fun` | `supported / 0.7.54 closed` | Surface launch/bonding/migration Pump.fun couverte localement ; trades directs et `trade_event` canonique validés. | Ne rouvrir que pour bug prouvé ou changement externe. |
|
||||
| 9 | `pump_fees` | `supported / 0.7.55 closed` | Surface fee/config/accounting couverte localement : `29` instructions, `20` events Anchor, fee/reward/admin/lifecycle, tests synthétiques Anchor IDL non observés, failed tx audit-only. | Aucun trade/candle direct ; conserver les deux discriminators Solscan hors IDL comme futures surfaces non observées. |
|
||||
| 10 | `meteora_dbc` | `partial / 0.7.56 planned` | Swaps/instruction audits observés ; Demo3 donne du corpus. | Couverture complète DBC : launch/bonding curve, swap, migration, config/admin, fees ; matérialiser seulement ce qui est prouvé. |
|
||||
| 11 | `meteora_dlmm` | `supported / 0.7.57 parity` | Couverture avancée validée en `0.7.45` : swaps, liquidity, positions, lifecycle, fees ; non-trade matérialisé. | Résoudre les audits résiduels non mappés ; comparer Carbon/IDL pour events rewards/admin restants ; revalidation base neuve. |
|
||||
| 10 | `meteora_dbc` | `supported / closed 0.7.56` | Decoder local maximal : `28` instructions, `23` events Anchor, swaps `swap/swap2`, lifecycle/admin/fees, `k_sol_fee_event_amounts`, validation SQL propre. | Ne pas rouvrir sauf bug prouvé ; préserver la policy fee parent+legs et la recovery allowlistée. |
|
||||
| 11 | `meteora_dlmm` | `next / 0.7.57 full decode` | Couverture partielle historique validée en `0.7.45`; IDL locale `76` instructions / `30` events Anchor. | Reprendre depuis base neuve : full decode, full materialization fiable, fees/rewards via `fee_event_amounts`, no double-count avec events Anchor. |
|
||||
| 12 | `meteora_damm_v1` | `supported / 0.7.58 parity` | Couverture `0.7.46` : swap, create_pool, add/remove liquidity, claim_fee, create_lock_escrow, lock_liquidity. | Vérifier les surfaces upstream non observées ; améliorer rattachement pool/pair pour remove_liquidity non matérialisés ; revalidation stricte. |
|
||||
| 13 | `meteora_damm_v2` | `partial / 0.7.59 planned` | `swap`, `instruction_audit`, registry/discriminants et corpus Demo3 existent. | Couvrir tous les events Carbon/source : create pool, liquidity, fees, dynamic config, admin ; déterminer actionability des swaps ; matérialiser si montants fiables. |
|
||||
| 14 | `phoenix_v1` | `audit-only / 0.7.60 planned` | Decoder local audit-only ; `log_audit`, order place/cancel, withdraw ; parsing strict `0x0f`; events `Reduce`, `Place`, `TimeInForce` observés ; `trade_count=0`. | Terminer tous les events Git : `Fill`, `FillSummary`, `Fee`, `Evict`, `ExpiredOrder`, etc. ; ajouter counts/flags audit ; seulement ensuite étudier trade materialization. |
|
||||
@@ -325,3 +336,18 @@ La tranche a été validée sur base SQLite dédiée : tous les discriminants `0
|
||||
| Decoder | Program id | Statut | Source discriminants | Couverture locale | Règles métier |
|
||||
|---|---|---:|---|---|---|
|
||||
| `pump_fees` | `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` | supported / `0.7.55 closed` | `idls/pump_fees.pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ.json` + Carbon partiel + Solscan discriminators | `29` instructions et `20` events Anchor couverts ; tests synthétiques pour les Anchor events IDL non observés ; replay final propre ; watchlist `pump_fees` vide | Aucun trade/candle direct ; `get_fees` decoded-only ; social claim vers reward ; donation/buyback vers fee ; config/authority/tier/admin vers admin/lifecycle ; failed tx audit-only ; deux events Solscan hors IDL conservés non observés. |
|
||||
|
||||
|
||||
## 0.7.56 — Meteora DBC clôturé
|
||||
|
||||
| Decoder | Program id | Statut | Source locale | Couverture | Décision métier |
|
||||
|---|---|---|---|---|---|
|
||||
| `meteora_dbc` | `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` | `supported / closed 0.7.56` | `idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json` | `28` instructions IDL + `23` events Anchor IDL, tests synthétiques et validation SQL dédiée | `swap`/`swap2` seuls vers trade/candle direct ; lifecycle/admin/fee selon contexte ; fees parent+legs ; failed tx audit-only. |
|
||||
|
||||
Validation finale : `446` tests, clippy OK, `480 replayed`, `264 trades`, `1 liquidity`, `122 lifecycle`, `1056 candles`, `89` parents fee DBC, `96` fee amount legs, invariants SQL propres.
|
||||
|
||||
## 0.7.57 — Meteora DLMM prochain
|
||||
|
||||
| Decoder | Program id | Statut | Source locale | Couverture attendue | Décision métier |
|
||||
|---|---|---|---|---|---|
|
||||
| `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | `next / full decode + full materialization` | `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json` | `76` instructions IDL + `30` events Anchor IDL | Swaps/exact-out, liquidity/bin/position, fees/rewards/admin/config, limit order events ; no duplicate trade/candle ; `fee_event_amounts` obligatoire pour fees. |
|
||||
|
||||
@@ -309,3 +309,34 @@ Programme : `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ`. Source locale priorit
|
||||
|
||||
Invariants propres : fallback `pump_fees` vide, decoded sans coverage vide, successful non-materialized sans skip/policy vide, failed tx matérialisée vide, multi-target vide, anti-trade/candle direct vide, watchlist globale sans `pump_fees`.
|
||||
|
||||
|
||||
|
||||
## 0.7.56 — Meteora DBC clôturé
|
||||
|
||||
Programme : `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN`. Source locale prioritaire : `idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json`.
|
||||
|
||||
| Groupe | Entrées | Famille | Cible DB | Local event kind | Décision finale |
|
||||
|---|---|---|---|---|---|
|
||||
| swaps | `swap`, `swap2` | swap | `k_sol_trade_events` + candles | `meteora_dbc.swap`, `meteora_dbc.swap2` | Matérialisés seulement avec montants et mints base/quote fiables ; `swap2` validé via transferts CPI/amount inference. |
|
||||
| virtual pool init | `initialize_virtual_pool_with_spl_token`, `initialize_virtual_pool_with_token2022`, `EvtInitializePool` | pool_create | `k_sol_pool_lifecycle_events` + catalog si contexte complet | `meteora_dbc.initialize_virtual_pool_with_*`, `meteora_dbc.evt_initialize_pool_event` | Lifecycle/catalog prudent ; failed tx audit-only. |
|
||||
| migration / lockers | `migrate_meteora_damm*`, `migration_damm_v2*`, `create_locker`, `EvtCurveComplete` | migration / lifecycle | `k_sol_pool_lifecycle_events` | `meteora_dbc.migrate_*`, `meteora_dbc.migration_*`, `meteora_dbc.create_locker` | Lifecycle, pas liquidity artificielle pour LP claim/lock ; metadata-only decoded-only si contexte insuffisant. |
|
||||
| fees/surplus/leftover | `claim_*fee*`, `withdraw_migration_fee`, `zap_protocol_fee`, `*_withdraw_surplus`, `withdraw_leftover`, events Anchor fee | fee | `k_sol_fee_events` + `k_sol_fee_event_amounts` | `meteora_dbc.*fee*`, `meteora_dbc.*surplus*`, `meteora_dbc.*leftover*` | Montants réels depuis CPI SPL ou lamport delta ; maxima d'instruction refusés ; multi-leg/multi-mint dans `k_sol_fee_event_amounts`. |
|
||||
| admin/config/metadata/operator | `create_config`, `create_operator_account`, `close_*operator*`, metadata, `transfer_pool_creator`, config events | admin_config | `k_sol_pool_admin_events` ou decoded-only | `meteora_dbc.*config*`, `meteora_dbc.*operator*`, `meteora_dbc.*metadata*`, `meteora_dbc.transfer_pool_creator` | Admin si compte/acteur fiables ; payload metadata générique decoded-only avec raison. |
|
||||
| Anchor swap events | `EvtSwap`, `EvtSwap2` | swap/audit | decoded-only sauf corrélation sûre | `meteora_dbc.evt_swap_event`, `meteora_dbc.evt_swap2_event` | Les events portent des montants mais pas toujours le contexte mint/pair ; pas de double-count avec l'instruction swap matérialisée. |
|
||||
|
||||
Validation finale DBC : `89` parents fee, `96` legs fee amount, aucun parent scalaire sans leg, aucun leg orphelin, aucun decoded event local sans coverage, aucun failed tx matérialisé, aucun multi-target, aucun non-swap vers trade/candle.
|
||||
|
||||
## 0.7.57 — Meteora DLMM à reprendre
|
||||
|
||||
Programme : `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`. Source locale : `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json`.
|
||||
|
||||
| Groupe | Entrées IDL | Famille | Cible DB attendue | Décision à valider |
|
||||
|---|---|---|---|---|
|
||||
| swaps | `swap`, `swap2`, `swap_exact_out*`, `swap_with_price_impact*`, `Swap`, `Swap2Evt` | swap | `k_sol_trade_events` + candles | Montants exécutés uniquement ; éviter double-count instruction/event. |
|
||||
| pools / bins | `initialize_*lb_pair*`, `initialize_bin_array*`, `close_bin_array`, `LbPairCreate` | lifecycle | `k_sol_pool_lifecycle_events`, catalog/pair | Catalog si comptes pool/mints/config fiables. |
|
||||
| liquidity / positions | `add_liquidity*`, `remove_liquidity*`, `remove_all_liquidity`, `rebalance_liquidity`, position create/close/update events | liquidity/lifecycle | `k_sol_liquidity_events`, `k_sol_pool_lifecycle_events` | Montants x/y/bin/liquidity fiables ; sinon skip reason. |
|
||||
| fees | `claim_fee*`, `withdraw_protocol_fee`, `zap_protocol_fee`, `ClaimFee*`, `CompositionFee` | fee | `k_sol_fee_events` + `k_sol_fee_event_amounts` | Utiliser le socle fee parent+legs ; policy recovery explicite, non globale. |
|
||||
| rewards | `initialize_reward`, `fund_reward`, `claim_reward*`, `withdraw_ineligible_reward`, reward events | reward | `k_sol_reward_events` | Montant/mint/récompense fiable uniquement. |
|
||||
| admin/config | fee parameters, pair status, activation, operator, token badge, preset parameter | admin_config | `k_sol_pool_admin_events` ou decoded-only | Matérialiser si acteur/compte cible fiable. |
|
||||
| limit/order events | place/cancel/close limit order events | orderbook/audit | `k_sol_orderbook_events` si modèle fiable | Pas de candle directe sans fill exact. |
|
||||
|
||||
|
||||
51
docs/VALIDATION_STATUS_0_7_56_FINAL.md
Normal file
51
docs/VALIDATION_STATUS_0_7_56_FINAL.md
Normal file
@@ -0,0 +1,51 @@
|
||||
<!-- file: docs/VALIDATION_STATUS_0_7_56_FINAL.md -->
|
||||
|
||||
# Validation Status — `0.7.56 meteora_dbc final`
|
||||
|
||||
## Build
|
||||
|
||||
```text
|
||||
cargo test -p kb_lib -> 446 passed / 0 failed
|
||||
cargo clippy -p kb_lib --all-targets -- -D warnings -> OK
|
||||
```
|
||||
|
||||
## Replay final DBC
|
||||
|
||||
```text
|
||||
480 replayed
|
||||
0 decode skipped
|
||||
480 ledger upserts
|
||||
454 unsafe ledger rows
|
||||
264 trades
|
||||
1 liquidity
|
||||
122 lifecycle
|
||||
0 tokenAccount
|
||||
1056 candle upserts
|
||||
instructionObservations = 7167
|
||||
resetDeleted = 3583
|
||||
catalog = 86 tokens / 60 pools / 60 pairs
|
||||
```
|
||||
|
||||
## Fee model final DBC
|
||||
|
||||
```text
|
||||
k_sol_fee_events meteora_dbc = 89 parents
|
||||
k_sol_fee_event_amounts meteora_dbc = 96 legs
|
||||
parent scalar without leg = empty
|
||||
orphan fee amount legs = empty
|
||||
allowlisted recovery on DBC = empty by design
|
||||
```
|
||||
|
||||
## Cross-base fee recovery checks
|
||||
|
||||
| Base | Result |
|
||||
|---|---|
|
||||
| `meteora_dbc` | stable, no regression, 89/96 fee parent/legs. |
|
||||
| `raydium_launchpad` | allowlisted CPI recovery enriched all observed claim/collect fee parents in tested corpus. |
|
||||
| `raydium_cpmm` | creator fee recovered; fund/protocol fee explicit no-transfer in tested corpus. |
|
||||
| `pump_swap` | coin creator fee mostly recovered; zero/no-transfer cases explicit. |
|
||||
| `pump_fees` | donation and sweep buyback recovered when CPI SPL transfers are present. |
|
||||
|
||||
## Closure decision
|
||||
|
||||
`0.7.56 meteora_dbc` is closed. Next tranche: `0.7.57 meteora_dlmm` full decode + full materialization.
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- file: docs/prompts/PROMPT_0_7_56_METEORA_DBC.md -->
|
||||
|
||||
# Prompt de reprise — khadhroony-bobobot 0.7.56 — meteora_dbc
|
||||
|
||||
Tu reprends le workspace Rust/Tauri `khadhroony-bobobot` après clôture technique de `0.7.55 pump_fees`.
|
||||
@@ -210,7 +212,7 @@ Fournir un delta zip contenant uniquement les fichiers modifiés/ajoutés.
|
||||
Nom recommandé :
|
||||
|
||||
```text
|
||||
khadhroony-bobobot-v0.7.56-meteora_dbc-delta-N-files.zip
|
||||
khadhroony-bobobot-v0.7.56-meteora_dbc-delta-pre.xxx.zip
|
||||
```
|
||||
|
||||
Inclure dans chaque livraison : résumé des changements, liste exacte des fichiers modifiés, commandes `cargo fmt`, `cargo test -p kb_lib`, `cargo clippy -p kb_lib --all-targets -- -D warnings`, replay recommandé, SQL à exécuter et résultats attendus.
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
<!-- file: docs/prompts/PROMPT_0_7_57_METEORA_DLMM_FULL_DECODE_MATERIALIZATION.md -->
|
||||
|
||||
# Prompt de reprise — khadhroony-bobobot `0.7.57` — `meteora_dlmm` full decode / full materialization
|
||||
|
||||
Tu reprends le workspace Rust/Tauri `khadhroony-bobobot` après clôture de `0.7.56 meteora_dbc`.
|
||||
|
||||
## 1. Archive et fichiers à fournir
|
||||
|
||||
Utiliser l'archive la plus récente après application des docs `0.7.56 final`.
|
||||
|
||||
Fichiers à lire en priorité :
|
||||
|
||||
- `README.md` ;
|
||||
- `ROADMAP.md` ;
|
||||
- `CHANGELOG.md` ;
|
||||
- `docs/DEX_DECODER_MATRIX.md` ;
|
||||
- `docs/DEX_EVENT_COVERAGE_MATRIX.md` ;
|
||||
- `docs/reports/METEORA_DBC_EVENT_COVERAGE_REPORT.md` ;
|
||||
- `docs/reports/FEE_EVENT_AMOUNTS_MODEL_NOTE_0_7_56.md` ;
|
||||
- `docs/VALIDATION_STATUS_0_7_56_FINAL.md` ;
|
||||
- `validation_sql/SQL_VALIDATION_METEORA_DBC_0_7_56.sql` ;
|
||||
- `validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql` ;
|
||||
- `idls/**`, en particulier `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json` ;
|
||||
- `kb_lib/src/dex/meteora_dlmm.rs` ;
|
||||
- `kb_lib/src/non_trade_event_materialization.rs` ;
|
||||
- `kb_lib/src/db/queries/fee_event_amount.rs`.
|
||||
|
||||
Ne pas supposer que l'ancien support `0.7.45 meteora_dlmm` est suffisant : il était partiel. `0.7.57` doit viser la parité IDL/corpus complète.
|
||||
|
||||
## 2. État validé avant cette version
|
||||
|
||||
`0.7.56 meteora_dbc` est clos.
|
||||
|
||||
Build final :
|
||||
|
||||
```text
|
||||
cargo test -p kb_lib -> 446 passed / 0 failed
|
||||
cargo clippy -p kb_lib --all-targets -- -D warnings -> OK
|
||||
```
|
||||
|
||||
Replay final DBC :
|
||||
|
||||
```text
|
||||
480 replayed
|
||||
0 decode skipped
|
||||
480 ledger upserts
|
||||
454 unsafe ledger rows
|
||||
264 trades
|
||||
1 liquidity
|
||||
122 lifecycle
|
||||
1056 candle upserts
|
||||
instructionObservations = 7167
|
||||
catalog = 86 tokens / 60 pools / 60 pairs
|
||||
```
|
||||
|
||||
Socle fee final :
|
||||
|
||||
```text
|
||||
k_sol_fee_events meteora_dbc = 89 parents
|
||||
k_sol_fee_event_amounts meteora_dbc = 96 legs
|
||||
parent scalar without leg = empty
|
||||
orphan fee amount legs = empty
|
||||
```
|
||||
|
||||
Règles fee à préserver :
|
||||
|
||||
- parent fee scalaire -> leg `k_sol_fee_event_amounts` automatique ;
|
||||
- fee multi-leg/multi-mint -> parent sans agrégation artificielle, legs explicites ;
|
||||
- pas de montant depuis `maxAmount`, `u64::MAX`, bornes ou limites de claim ;
|
||||
- recovery CPI SPL générique uniquement si policy/allowlist explicite ;
|
||||
- aucun futur decoder ne doit hériter de `allowlisted_inner_spl_transfer` par défaut.
|
||||
|
||||
## 3. Objectif de `0.7.57 meteora_dlmm`
|
||||
|
||||
Program id cible :
|
||||
|
||||
```text
|
||||
LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo
|
||||
```
|
||||
|
||||
IDL locale prioritaire :
|
||||
|
||||
```text
|
||||
idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json
|
||||
```
|
||||
|
||||
Nom IDL : `lb_clmm`.
|
||||
|
||||
Surface IDL :
|
||||
|
||||
```text
|
||||
76 instructions
|
||||
30 events Anchor
|
||||
12 accounts
|
||||
```
|
||||
|
||||
Objectif : **full decode + full materialization**.
|
||||
|
||||
Règle forte :
|
||||
|
||||
> Tout ce qui peut être décodé doit être décodé. Tout ce qui peut être matérialisé de façon fiable doit être matérialisé. Ce qui ne peut pas être matérialisé doit rester decoded-only/audit-only avec `skip*Reason` explicite. les instructions/events/anchors/discriminator non observés aprés backfill sur une base de donnée vierge devront avoir des tests syntetiques.
|
||||
|
||||
## 4. Méthode obligatoire
|
||||
|
||||
Créer une nouvelle DB dédiée à `0.7.57 meteora_dlmm`.
|
||||
|
||||
Après chaque backfill ou patch decoder :
|
||||
|
||||
```text
|
||||
skipDexDecode=no
|
||||
forceDexDecode=yes
|
||||
deferInstructionObservations=yes
|
||||
```
|
||||
|
||||
Puis : refresh catalog, replay local, SQL validation, note des compteurs.
|
||||
|
||||
Ne pas rouvrir `meteora_dbc`, Pump ou Raydium sauf bug prouvé par SQL.
|
||||
|
||||
## 5. Sources à comparer
|
||||
|
||||
Comparer au minimum :
|
||||
|
||||
- IDL locale `idls/meteora_dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.json` ;
|
||||
- Carbon Meteora DLMM decoder ;
|
||||
- Pinax/Substreams Solana IDLs ;
|
||||
- Solana Streamer / sol-parser-sdk si DLMM y apparaît ;
|
||||
- code local historique `kb_lib/src/dex/meteora_dlmm.rs` ;
|
||||
- coverage existante `k_sol_dex_event_coverage_entries` ;
|
||||
- corpus SQLite neuf ;
|
||||
- anciens rapports `0.7.45` et matrices pour comprendre ce qui était déjà validé.
|
||||
|
||||
## 6. Checklist d'instructions IDL à inventorier
|
||||
|
||||
| Instruction | Discriminator hex |
|
||||
|---|---|
|
||||
| `add_liquidity` | `b59d59438fb63448` |
|
||||
| `add_liquidity2` | `e4a24e1c46db7473` |
|
||||
| `add_liquidity_by_strategy` | `0703967f94283dc8` |
|
||||
| `add_liquidity_by_strategy2` | `03dd95da6f8d76d5` |
|
||||
| `add_liquidity_by_strategy_one_side` | `2905eeaf64e106cd` |
|
||||
| `add_liquidity_by_weight` | `1c8cee63e7a21595` |
|
||||
| `add_liquidity_by_weight2` | `d13b3f5b6fc899e4` |
|
||||
| `add_liquidity_one_side` | `5e9b6797465fdca5` |
|
||||
| `add_liquidity_one_side_precise` | `a1c26754ab47fa9a` |
|
||||
| `add_liquidity_one_side_precise2` | `2133a3c975627de7` |
|
||||
| `cancel_limit_order` | `849c841f4328e861` |
|
||||
| `claim_fee` | `a9204f8988e84689` |
|
||||
| `claim_fee2` | `70bf65ab1c907fbb` |
|
||||
| `claim_reward` | `955fb5f25e5a9ea2` |
|
||||
| `claim_reward2` | `be037f77b2579db7` |
|
||||
| `close_bin_array` | `44ae5850b5cc13e0` |
|
||||
| `close_claim_fee_operator_account` | `b8d5581fb3658224` |
|
||||
| `close_limit_order_if_empty` | `397c249b7ef95dab` |
|
||||
| `close_operator_account` | `ab09d54a7817031d` |
|
||||
| `close_position` | `7b86510031446262` |
|
||||
| `close_position2` | `ae5a2373ba2893e2` |
|
||||
| `close_position_if_empty` | `3b7cd4765b986e9d` |
|
||||
| `close_preset_parameter` | `04949164861ab53d` |
|
||||
| `close_preset_parameter2` | `27195f6b7411731c` |
|
||||
| `close_token_badge` | `6c92566eb3fe0a68` |
|
||||
| `create_operator_account` | `dd40f695f099e5a3` |
|
||||
| `decrease_position_length` | `c2db882019606925` |
|
||||
| `for_idl_type_generation_do_not_call` | `b46945505f32496c` |
|
||||
| `fund_reward` | `bc32f9a55d97263f` |
|
||||
| `go_to_a_bin` | `9248aee028fd54ae` |
|
||||
| `increase_oracle_length` | `be3d7d57674f9ead` |
|
||||
| `increase_position_length` | `505375d3420d2195` |
|
||||
| `increase_position_length2` | `ffd2cc477389e171` |
|
||||
| `initialize_bin_array` | `235613b94ed44bd3` |
|
||||
| `initialize_bin_array_bitmap_extension` | `2f9de2b40cf02147` |
|
||||
| `initialize_customizable_permissionless_lb_pair` | `2e2729876fb7c840` |
|
||||
| `initialize_customizable_permissionless_lb_pair2` | `f349817e3313f16b` |
|
||||
| `initialize_lb_pair` | `2d9aedd2dd0fa65c` |
|
||||
| `initialize_lb_pair2` | `493b2478ed536cc6` |
|
||||
| `initialize_permission_lb_pair` | `6c66d555fb033515` |
|
||||
| `initialize_position` | `dbc0ea47bebf6650` |
|
||||
| `initialize_position2` | `8f13f291d50f6873` |
|
||||
| `initialize_position_by_operator` | `fbbdbef475fe2394` |
|
||||
| `initialize_position_pda` | `2e527d92558de499` |
|
||||
| `initialize_preset_parameter` | `42bc47d3626d0eba` |
|
||||
| `initialize_reward` | `5f87c0c4f281e644` |
|
||||
| `initialize_token_badge` | `fd4dcd5f1be059df` |
|
||||
| `place_limit_order` | `6cb021ba92e501c5` |
|
||||
| `rebalance_liquidity` | `5c04b0c177b95309` |
|
||||
| `remove_all_liquidity` | `0a333d2370691855` |
|
||||
| `remove_liquidity` | `5055d14818ceb16c` |
|
||||
| `remove_liquidity2` | `e6d7527ff165e392` |
|
||||
| `remove_liquidity_by_range` | `1a526698f04a691a` |
|
||||
| `remove_liquidity_by_range2` | `cc02c391359191cd` |
|
||||
| `set_activation_point` | `5bf90fa51a81fe7d` |
|
||||
| `set_pair_status` | `43f8e7899a95d9ae` |
|
||||
| `set_pair_status_permissionless` | `4e3b98d346b72ed0` |
|
||||
| `set_permissionless_operation_bits` | `543acb8ba351beba` |
|
||||
| `set_pre_activation_duration` | `a53dc9f4829f1664` |
|
||||
| `set_pre_activation_swap_address` | `398b2f7bd850df0a` |
|
||||
| `swap` | `f8c69e91e17587c8` |
|
||||
| `swap2` | `414b3f4ceb5b5b88` |
|
||||
| `swap_exact_out` | `fa49652126cf4bb8` |
|
||||
| `swap_exact_out2` | `2bd7f784893cf351` |
|
||||
| `swap_with_price_impact` | `38ade6d0ade49ccd` |
|
||||
| `swap_with_price_impact2` | `4a62c0d6b1334b33` |
|
||||
| `update_base_fee_parameters` | `4ba8dfa110c3032f` |
|
||||
| `update_dynamic_fee_parameters` | `5ca12ef6ffbd1616` |
|
||||
| `update_fees_and_reward2` | `208eb89a6741b858` |
|
||||
| `update_fees_and_rewards` | `9ae6fa0decd14bdf` |
|
||||
| `update_position_operator` | `cab8678fb4bf74d9` |
|
||||
| `update_reward_duration` | `8aaec4a9d5ebfe6b` |
|
||||
| `update_reward_funder` | `d31c3020d7a02317` |
|
||||
| `withdraw_ineligible_reward` | `94ce2ac3f7316708` |
|
||||
| `withdraw_protocol_fee` | `9ec99ebd215da267` |
|
||||
| `zap_protocol_fee` | `d59bbb2238b65bf0` |
|
||||
|
||||
## 7. Checklist d'events Anchor IDL à inventorier
|
||||
|
||||
| Event | Discriminator hex |
|
||||
|---|---|
|
||||
| `AddLiquidity` | `1f5e7d5ae3343dba` |
|
||||
| `CancelLimitOrderEvt` | `83eac285090ebdd1` |
|
||||
| `ClaimFee` | `4b7a9a308c4a7ba3` |
|
||||
| `ClaimFee2` | `e8abf2613a4d232d` |
|
||||
| `ClaimReward` | `947486cc16ab555f` |
|
||||
| `ClaimReward2` | `1b8ff421502b6e92` |
|
||||
| `CloseLimitOrderEvt` | `8e87084c5c3f7653` |
|
||||
| `CompositionFee` | `80977b6a1166718e` |
|
||||
| `DecreasePositionLength` | `3476eb55aca90f80` |
|
||||
| `DynamicFeeParameterUpdate` | `5858b287c2925bf3` |
|
||||
| `FeeParameterUpdate` | `304cf17590d7f22c` |
|
||||
| `FundReward` | `f6e43a8291aa4fcc` |
|
||||
| `GoToABin` | `3b8a4c448a83b043` |
|
||||
| `IncreaseObservation` | `63f91179a69ccfd7` |
|
||||
| `IncreasePositionLength` | `9def2acc1e38df2e` |
|
||||
| `InitializeReward` | `d399583e953cb146` |
|
||||
| `LbPairCreate` | `b94afc7d1bd7bc6f` |
|
||||
| `PlaceLimitOrderEvt` | `2b4f1ba9f41ce13f` |
|
||||
| `PositionClose` | `ffc4106b1cca3580` |
|
||||
| `PositionCreate` | `908efc549d352579` |
|
||||
| `Rebalancing` | `006d75b33d5bc7c8` |
|
||||
| `RemoveLiquidity` | `74f461e8671f983a` |
|
||||
| `SetPositionPermissionlessOperationBitsEvt` | `c3e593f51d7d30a8` |
|
||||
| `Swap` | `516ce3becdd00ac4` |
|
||||
| `Swap2Evt` | `2e7452d7941b544d` |
|
||||
| `UpdatePositionLockReleasePoint` | `85d642e0400c07bf` |
|
||||
| `UpdatePositionOperator` | `277330ccf62f4239` |
|
||||
| `UpdateRewardDuration` | `dff5e099311da3ac` |
|
||||
| `UpdateRewardFunder` | `e0b2ae4afca555b4` |
|
||||
| `WithdrawIneligibleReward` | `e7bd419566d79af4` |
|
||||
|
||||
## 8. Matérialisation attendue
|
||||
|
||||
### Swaps
|
||||
|
||||
Cibles :
|
||||
|
||||
```text
|
||||
swap
|
||||
swap2
|
||||
swap_exact_out
|
||||
swap_exact_out2
|
||||
swap_with_price_impact
|
||||
swap_with_price_impact2
|
||||
Swap
|
||||
Swap2Evt
|
||||
```
|
||||
|
||||
Décision :
|
||||
|
||||
- `k_sol_trade_events` + candles uniquement si montants exécutés, sens, pool, token X/Y et mints sont fiables ;
|
||||
- les events Anchor swap ne doivent pas double-compter une instruction swap déjà matérialisée ;
|
||||
- exact-out et price-impact ne doivent pas utiliser des bornes comme montants exécutés ;
|
||||
- si le contexte n'est pas fiable : `skipTradeReason` + `skipCandleReason`.
|
||||
|
||||
### Liquidity / bins / positions
|
||||
|
||||
Cibles :
|
||||
|
||||
```text
|
||||
add_liquidity*
|
||||
remove_liquidity*
|
||||
remove_all_liquidity
|
||||
rebalance_liquidity
|
||||
initialize_position*
|
||||
close_position*
|
||||
position create/close/update events
|
||||
initialize_bin_array*
|
||||
close_bin_array
|
||||
```
|
||||
|
||||
Décision :
|
||||
|
||||
- `k_sol_liquidity_events` quand les montants token X/Y, pool, position et acteur sont fiables ;
|
||||
- lifecycle pour création/fermeture position/bin/pair ;
|
||||
- skip reason explicite pour position/bin sans montants exploitables.
|
||||
|
||||
### Pools / catalog
|
||||
|
||||
Cibles :
|
||||
|
||||
```text
|
||||
initialize_lb_pair
|
||||
initialize_lb_pair2
|
||||
initialize_permission_lb_pair
|
||||
initialize_customizable_permissionless_lb_pair
|
||||
initialize_customizable_permissionless_lb_pair2
|
||||
LbPairCreate
|
||||
```
|
||||
|
||||
Décision : lifecycle/catalog/pool/pair si mints X/Y, pool, config/preset et comptes vault sont fiables.
|
||||
|
||||
### Fees
|
||||
|
||||
Cibles :
|
||||
|
||||
```text
|
||||
claim_fee
|
||||
claim_fee2
|
||||
withdraw_protocol_fee
|
||||
zap_protocol_fee
|
||||
ClaimFee
|
||||
ClaimFee2
|
||||
CompositionFee
|
||||
```
|
||||
|
||||
Décision :
|
||||
|
||||
- `k_sol_fee_events` + `k_sol_fee_event_amounts` obligatoires si montant/mint fiable ;
|
||||
- utiliser parent scalaire + leg automatique pour mono-fee ;
|
||||
- utiliser multi-leg sans agrégation parent si plusieurs mints/composants ;
|
||||
- ne pas confondre composition fee inclus dans swap/liquidity avec claim fee matérialisable ;
|
||||
- déclarer explicitement toute policy de recovery ; ne pas ajouter DLMM à l'allowlist générique sans preuve et tests.
|
||||
|
||||
### Rewards
|
||||
|
||||
Cibles :
|
||||
|
||||
```text
|
||||
initialize_reward
|
||||
fund_reward
|
||||
claim_reward
|
||||
claim_reward2
|
||||
withdraw_ineligible_reward
|
||||
ClaimReward
|
||||
ClaimReward2
|
||||
FundReward
|
||||
InitializeReward
|
||||
WithdrawIneligibleReward
|
||||
```
|
||||
|
||||
Décision : `k_sol_reward_events` si montant/mint/reward index fiables ; decoded-only sinon.
|
||||
|
||||
### Admin/config/status/operator/token badge
|
||||
|
||||
Cibles :
|
||||
|
||||
```text
|
||||
update_base_fee_parameters
|
||||
update_dynamic_fee_parameters
|
||||
update_fees_and_rewards
|
||||
update_fees_and_reward2
|
||||
set_pair_status
|
||||
set_pair_status_permissionless
|
||||
set_activation_point
|
||||
set_pre_activation_duration
|
||||
set_pre_activation_swap_address
|
||||
set_permissionless_operation_bits
|
||||
create_operator_account
|
||||
close_operator_account
|
||||
close_claim_fee_operator_account
|
||||
initialize_token_badge
|
||||
close_token_badge
|
||||
initialize_preset_parameter
|
||||
close_preset_parameter
|
||||
close_preset_parameter2
|
||||
```
|
||||
|
||||
Décision : `k_sol_pool_admin_events` si acteur/cible fiables ; decoded-only avec raison sinon.
|
||||
|
||||
### Limit orders / orderbook-like events
|
||||
|
||||
Cibles :
|
||||
|
||||
```text
|
||||
place_limit_order
|
||||
cancel_limit_order
|
||||
close_limit_order_if_empty
|
||||
PlaceLimitOrderEvt
|
||||
CancelLimitOrderEvt
|
||||
CloseLimitOrderEvt
|
||||
```
|
||||
|
||||
Décision : `k_sol_orderbook_events` si sémantique fiable ; pas de trade/candle sans fill exact.
|
||||
|
||||
## 9. SQL de validation attendu
|
||||
|
||||
Créer/mettre à jour :
|
||||
|
||||
```text
|
||||
validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql
|
||||
```
|
||||
|
||||
Le fichier doit vérifier au minimum :
|
||||
|
||||
1. fallback upstream DLMM ;
|
||||
2. instruction observations DLMM ;
|
||||
3. coverage DLMM ;
|
||||
4. decoded DLMM sans coverage ;
|
||||
5. successful non-materialized sans skip reason ;
|
||||
6. failed tx materialization ;
|
||||
7. multi-target materialization ;
|
||||
8. trade/candle sur non-swap ;
|
||||
9. fee parent/legs ;
|
||||
10. orphan fee legs ;
|
||||
11. reward/fee separation ;
|
||||
12. orderbook/limit-order sans double-count ;
|
||||
13. watchlist globale.
|
||||
|
||||
## 10. Invariants de fermeture
|
||||
|
||||
`0.7.57 meteora_dlmm` ne peut être clôturé que si :
|
||||
|
||||
- les `76` instructions et `30` events Anchor IDL sont dans la coverage ou explicitement non observés avec tests synthétiques ;
|
||||
- aucun fallback upstream DLMM ne reste pour les entrées couvertes localement ;
|
||||
- aucun decoded DLMM local sans coverage ;
|
||||
- aucune tx failed n'alimente une table métier ;
|
||||
- aucun event multi-target incohérent ;
|
||||
- aucune ligne successful non-materialized sans `skip*Reason` ou policy explicite ;
|
||||
- aucun non-swap ne produit trade/candle ;
|
||||
- aucun fee parent scalaire sans leg ;
|
||||
- aucun leg fee orphelin ;
|
||||
- aucun reward n'est classé fee par défaut ;
|
||||
- aucun limit/order event ne produit une candle sans fill exact ;
|
||||
- la watchlist globale ne contient plus de backlog dominant `meteora_dlmm`.
|
||||
|
||||
## 11. Contraintes de code à respecter
|
||||
|
||||
- Rust 2024 ;
|
||||
- async-first ;
|
||||
- tracing obligatoire ;
|
||||
- pas de `?`, pas de `unwrap/expect` en production ;
|
||||
- pas de `anyhow` / `thiserror` ;
|
||||
- pas de `mod.rs` ;
|
||||
- pas de `pub mod` : utiliser `mod` + `pub use` ;
|
||||
- imports seulement pour les traits ;
|
||||
- `#![deny(unreachable_pub)]`, `#![warn(missing_docs)]` ;
|
||||
- tests offline ;
|
||||
- pas de macro DB/coverage ;
|
||||
- après modification DB : re-exports `kb_lib/src/db.rs` et `kb_lib/src/lib.rs` ;
|
||||
- après modification decoder : vérifier `kb_lib/src/dex.rs`, `kb_lib/src/lib.rs`, coverage et tests synthétiques.
|
||||
|
||||
## 12. Format de livraison attendu
|
||||
|
||||
Livrer des deltas successifs :
|
||||
|
||||
```text
|
||||
khadhroony-bobobot-v0.7.57-meteora_dlmm-delta-pre.xxx.zip
|
||||
```
|
||||
|
||||
Chaque réponse doit indiquer : fichiers modifiés, raisons, tests à lancer, SQL à exécuter, résultat attendu, et risques éventuels.
|
||||
151
docs/reports/FEE_EVENT_AMOUNTS_MODEL_NOTE_0_7_56.md
Normal file
151
docs/reports/FEE_EVENT_AMOUNTS_MODEL_NOTE_0_7_56.md
Normal file
@@ -0,0 +1,151 @@
|
||||
<!-- file: docs/reports/FEE_EVENT_AMOUNTS_MODEL_NOTE_0_7_56.md -->
|
||||
|
||||
# Note technique — `k_sol_fee_event_amounts` et policy fees — `0.7.56`
|
||||
|
||||
## Objectif
|
||||
|
||||
La table `k_sol_fee_events` était suffisante pour un fee mono-montant, mais insuffisante pour les cas multi-mint, multi-destination ou multi-composant. `0.7.56` ajoute donc `k_sol_fee_event_amounts` comme table de legs rattachée au parent fee.
|
||||
|
||||
## Modèle
|
||||
|
||||
```text
|
||||
k_sol_fee_events
|
||||
id
|
||||
transaction_id
|
||||
decoded_event_id
|
||||
fee_token_mint nullable / vide si multi-leg
|
||||
fee_amount_raw nullable / vide si multi-leg
|
||||
payload_json
|
||||
|
||||
k_sol_fee_event_amounts
|
||||
id
|
||||
fee_event_id
|
||||
transaction_id
|
||||
decoded_event_id
|
||||
leg_index
|
||||
fee_component_kind
|
||||
token_mint
|
||||
amount_raw
|
||||
source_account
|
||||
destination_account
|
||||
amount_source
|
||||
payload_json
|
||||
```
|
||||
|
||||
## Règles invariantes
|
||||
|
||||
1. Un parent fee scalaire avec `fee_token_mint + fee_amount_raw` doit toujours avoir un leg `0`.
|
||||
2. Un parent multi-leg ou multi-mint ne doit pas agréger artificiellement ses montants dans le parent.
|
||||
3. Les legs doivent pointer vers le même `transaction_id` et `decoded_event_id` que le parent.
|
||||
4. Les legs doivent être supprimés/remplacés lors du replay/cleanup parent.
|
||||
5. Les transactions failed ne doivent pas créer de parent ou leg fee métier.
|
||||
6. Les bornes d'instruction (`maxAmount`, `minAmount`, `u64::MAX`) ne sont pas des montants exécutés.
|
||||
|
||||
## Sources de montant acceptées
|
||||
|
||||
| `amount_source` | Usage |
|
||||
|---|---|
|
||||
| `parent_fee_event_amount` | Leg généré automatiquement depuis un parent scalaire déjà fiable. |
|
||||
| `fee_event_amounts` | Source explicite reconstruite par un matérialisateur spécialisé. |
|
||||
| `inner_spl_transfer` | CPI SPL vérifié dans un matérialisateur spécialisé. |
|
||||
| `lamport_balance_delta` | Delta lamports prouvé et contextualisé. |
|
||||
| `allowlisted_inner_spl_transfer` | Recovery générique mais strictement allowlistée pour event kinds déjà validés. |
|
||||
|
||||
## Recovery allowlistée
|
||||
|
||||
La recovery `allowlisted_inner_spl_transfer` n'est pas une règle globale. Elle s'applique seulement aux event kinds explicitement autorisés dans le code. Cette restriction protège les futurs décodeurs : une nouvelle surface ne doit jamais créer des legs fee à partir de transferts internes tant que la sémantique n'a pas été inspectée.
|
||||
|
||||
Résultats de validation croisée en `0.7.56` :
|
||||
|
||||
| Base / surface | Effet observé |
|
||||
|---|---|
|
||||
| `meteora_dbc` | Aucun usage de l'allowlist générique ; chemins spécialisés DBC conservés. |
|
||||
| `raydium_launchpad` | `212` parents enrichis en legs : `claim_creator_fee`, `claim_platform_fee`, `claim_platform_fee_from_vault`, `collect_fee`. |
|
||||
| `raydium_cpmm` | `collect_creator_fee` enrichi ; `collect_fund_fee` / `collect_protocol_fee` explicités sans transfert exploitable. |
|
||||
| `pump_swap` | `collect_coin_creator_fee` et un `transfer_creator_fees_to_pump_v2` enrichis ; autres cas zero/no-transfer explicités. |
|
||||
| `pump_fees` | `crank_donation_fee_pda` et `sweep_buyback` enrichis ; events déjà scalaires conservés. |
|
||||
|
||||
## Contrôles SQL obligatoires
|
||||
|
||||
### Parent scalaire sans leg
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
de.protocol_name,
|
||||
de.event_kind,
|
||||
tx.signature,
|
||||
fee.id AS fee_event_id,
|
||||
fee.fee_token_mint,
|
||||
fee.fee_amount_raw,
|
||||
fee.payload_json
|
||||
FROM k_sol_fee_events fee
|
||||
JOIN k_sol_dex_decoded_events de
|
||||
ON de.id = fee.decoded_event_id
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = fee.transaction_id
|
||||
LEFT JOIN k_sol_fee_event_amounts fea
|
||||
ON fea.fee_event_id = fee.id
|
||||
WHERE COALESCE(TRIM(fee.fee_token_mint), '') <> ''
|
||||
AND COALESCE(TRIM(fee.fee_amount_raw), '') <> ''
|
||||
AND fea.id IS NULL
|
||||
ORDER BY
|
||||
de.protocol_name,
|
||||
de.event_kind,
|
||||
tx.signature
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
Attendu : vide.
|
||||
|
||||
### Legs orphelins
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
fea.id,
|
||||
fea.fee_event_id,
|
||||
fea.transaction_id,
|
||||
fea.decoded_event_id
|
||||
FROM k_sol_fee_event_amounts fea
|
||||
LEFT JOIN k_sol_fee_events fee
|
||||
ON fee.id = fea.fee_event_id
|
||||
WHERE fee.id IS NULL;
|
||||
```
|
||||
|
||||
Attendu : vide.
|
||||
|
||||
### Résumé parent/legs par event
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
de.protocol_name,
|
||||
de.event_kind,
|
||||
COUNT(DISTINCT fee.id) AS fee_parent_count,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN COALESCE(TRIM(fee.fee_token_mint), '') <> ''
|
||||
AND COALESCE(TRIM(fee.fee_amount_raw), '') <> ''
|
||||
THEN fee.id
|
||||
ELSE NULL
|
||||
END) AS parent_with_scalar_amount_count,
|
||||
COUNT(DISTINCT fea.id) AS fee_amount_leg_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_fee_events fee
|
||||
JOIN k_sol_dex_decoded_events de
|
||||
ON de.id = fee.decoded_event_id
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = fee.transaction_id
|
||||
LEFT JOIN k_sol_fee_event_amounts fea
|
||||
ON fea.fee_event_id = fee.id
|
||||
GROUP BY
|
||||
de.protocol_name,
|
||||
de.event_kind
|
||||
ORDER BY
|
||||
de.protocol_name,
|
||||
de.event_kind;
|
||||
```
|
||||
|
||||
## Règles pour `0.7.57 meteora_dlmm`
|
||||
|
||||
- Les instructions `claim_fee`, `claim_fee2`, `withdraw_protocol_fee`, `zap_protocol_fee`, `CompositionFee`, `ClaimFee`, `ClaimFee2` doivent utiliser `k_sol_fee_event_amounts` dès qu'un montant/mint fiable est disponible.
|
||||
- Les rewards (`claim_reward*`, `fund_reward`, `withdraw_ineligible_reward`) vont vers `k_sol_reward_events`, pas vers fees, sauf sémantique contraire prouvée.
|
||||
- Les `composition_fee` de swap/liquidity ne doivent pas être double-comptés si déjà inclus dans le trade/liquidity effectif.
|
||||
- Toute recovery générique doit être déclarée par policy explicite et tests synthétiques ; ne pas ajouter DLMM à l'allowlist sans audit par event kind.
|
||||
161
docs/reports/METEORA_DBC_EVENT_COVERAGE_REPORT.md
Normal file
161
docs/reports/METEORA_DBC_EVENT_COVERAGE_REPORT.md
Normal file
@@ -0,0 +1,161 @@
|
||||
<!-- file: docs/reports/METEORA_DBC_EVENT_COVERAGE_REPORT.md -->
|
||||
|
||||
# Meteora DBC Event Coverage Report — `0.7.56 final`
|
||||
|
||||
## Statut final
|
||||
|
||||
La tranche `0.7.56 meteora_dbc` est clôturée.
|
||||
|
||||
Program id :
|
||||
|
||||
```text
|
||||
dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN
|
||||
```
|
||||
|
||||
Source locale prioritaire :
|
||||
|
||||
```text
|
||||
idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json
|
||||
```
|
||||
|
||||
Surface IDL : `28` instructions, `23` events Anchor, `9` accounts, `59` types.
|
||||
|
||||
## Résultat build/replay final
|
||||
|
||||
```text
|
||||
cargo test -p kb_lib -> 446 passed / 0 failed
|
||||
cargo clippy -p kb_lib --all-targets -- -D warnings -> OK
|
||||
480 replayed
|
||||
0 decode skipped
|
||||
480 ledger upserts
|
||||
454 unsafe ledger rows
|
||||
264 trades
|
||||
1 liquidity
|
||||
122 lifecycle
|
||||
0 tokenAccount
|
||||
1056 candle upserts
|
||||
instructionObservations = 7167
|
||||
resetDeleted = 3583
|
||||
catalog = 86 tokens / 60 pools / 60 pairs
|
||||
```
|
||||
|
||||
Replay final recommandé pour reproduire :
|
||||
|
||||
```text
|
||||
skipDexDecode=no
|
||||
forceDexDecode=yes
|
||||
deferInstructionObservations=yes
|
||||
```
|
||||
|
||||
## Décisions métier verrouillées
|
||||
|
||||
### Swaps
|
||||
|
||||
- `meteora_dbc.swap` et `meteora_dbc.swap2` sont les seules entrées candidates trade/candle directes.
|
||||
- Les trades/candles ne sont produits que si les montants exécutés et les mints base/quote sont fiables.
|
||||
- Les montants de `swap2` doivent être dérivés du layout et/ou des CPI SPL effectifs ; ne pas utiliser naïvement les bornes d'instruction.
|
||||
- Les events Anchor `EvtSwap` / `EvtSwap2` restent decoded-only s'ils ne portent pas un contexte mint/pair suffisant ou s'ils doublonnent l'instruction matérialisée.
|
||||
|
||||
### Lifecycle / migration / lockers
|
||||
|
||||
- `initialize_virtual_pool_with_spl_token` et `initialize_virtual_pool_with_token2022` alimentent lifecycle/catalog lorsque les comptes pool/base/quote/config sont fiables.
|
||||
- `create_locker`, `migrate_meteora_damm_claim_lp_token`, `migrate_meteora_damm_lock_lp_token`, `migration_damm_v2*` et `migrate_meteora_damm*` sont lifecycle, pas liquidity artificielle.
|
||||
- Les metadata-only restent decoded-only avec raison explicite si elles n'apportent pas de cible métier fiable.
|
||||
|
||||
### Admin/config
|
||||
|
||||
- `create_config`, `create_operator_account`, `close_*operator*`, metadata, `transfer_pool_creator` et events config/admin alimentent `k_sol_pool_admin_events` uniquement si l'acteur et la cible sont fiables.
|
||||
- Les payloads génériques ou incomplets restent audit/decoded-only.
|
||||
|
||||
### Fees
|
||||
|
||||
- Les fees DBC utilisent le modèle parent+legs : `k_sol_fee_events` + `k_sol_fee_event_amounts`.
|
||||
- Les maxima d'instruction (`maxAmount*`, `u64::MAX`, bornes de claim) ne sont pas des montants exécutés.
|
||||
- Les montants fiables proviennent des CPI SPL, des legs explicitement reconstruits ou des lamport balance deltas prouvés.
|
||||
- Les events Anchor fee sans mint restent decoded-only avec `skipFeeReason`.
|
||||
- Les cas sans transfert réel portent `fee_instruction_has_no_actual_transfer` ou `fee_instruction_has_only_zero_amount_transfers`.
|
||||
|
||||
## Matérialisation fee finale
|
||||
|
||||
| Event kind | Fee parents | Parents scalaires | Amount legs | Décision |
|
||||
|---|---:|---:|---:|---|
|
||||
| `meteora_dbc.claim_creator_trading_fee` | 8 | 8 | 8 | Mono-leg fiable. |
|
||||
| `meteora_dbc.claim_partner_pool_creation_fee` | 10 | 10 | 10 | Mono-leg fiable. |
|
||||
| `meteora_dbc.claim_protocol_fee` | 10 | 10 | 10 | Mono-leg fiable. |
|
||||
| `meteora_dbc.claim_protocol_pool_creation_fee` | 10 | 10 | 10 | Mono-leg/lamport delta fiable. |
|
||||
| `meteora_dbc.claim_trading_fee` | 11 | 6 | 18 | Mix mono-leg et multi-leg ; parent non agrégé pour multi-leg. |
|
||||
| `meteora_dbc.creator_withdraw_surplus` | 2 | 2 | 2 | Mono-leg fiable ; résiduels sans transfert réel explicités. |
|
||||
| `meteora_dbc.partner_withdraw_surplus` | 9 | 9 | 9 | Mono-leg fiable. |
|
||||
| `meteora_dbc.withdraw_leftover` | 10 | 10 | 10 | Mono-leg fiable. |
|
||||
| `meteora_dbc.withdraw_migration_fee` | 9 | 9 | 9 | Fee, pas migration target ; mono-leg fiable. |
|
||||
| `meteora_dbc.zap_protocol_fee` | 10 | 10 | 10 | Mono-leg fiable. |
|
||||
| **Total `meteora_dbc`** | **89** | n/a | **96** | Parent+legs validé. |
|
||||
|
||||
## Socle `k_sol_fee_event_amounts`
|
||||
|
||||
La version `0.7.56` ajoute un modèle durable pour les fees composés :
|
||||
|
||||
- `k_sol_fee_events` est le parent logique unique lié au `decoded_event_id` ;
|
||||
- `k_sol_fee_event_amounts` contient les legs de montants, avec `leg_index`, `fee_component_kind`, `token_mint`, `amount_raw`, comptes source/destination et `amount_source` ;
|
||||
- un parent avec `fee_token_mint + fee_amount_raw` crée automatiquement un leg scalaire ;
|
||||
- un parent multi-leg/multi-mint laisse les champs scalaires du parent vides et stocke tout dans les legs ;
|
||||
- les deletes/replays nettoient les legs avant ou avec le parent ;
|
||||
- la requête de contrôle `parent scalar without leg` doit rester vide.
|
||||
|
||||
Sources `amount_source` connues en fin de tranche :
|
||||
|
||||
```text
|
||||
parent_fee_event_amount
|
||||
fee_event_amounts
|
||||
inner_spl_transfer
|
||||
lamport_balance_delta
|
||||
allowlisted_inner_spl_transfer
|
||||
```
|
||||
|
||||
## Recovery fee allowlistée
|
||||
|
||||
La recovery `allowlisted_inner_spl_transfer` est volontairement non globale.
|
||||
|
||||
Elle a été testée sur anciennes bases pour enrichir les surfaces déjà connues :
|
||||
|
||||
| Surface testée | Résultat |
|
||||
|---|---|
|
||||
| `raydium_launchpad` | `claim_creator_fee`, `claim_platform_fee`, `claim_platform_fee_from_vault`, `collect_fee` enrichis en legs depuis CPI SPL. |
|
||||
| `raydium_cpmm` | `collect_creator_fee` enrichi ; `collect_fund_fee` et `collect_protocol_fee` restent sans transfert réel exploitable dans le corpus testé. |
|
||||
| `pump_swap` | `collect_coin_creator_fee` et certains `transfer_creator_fees_to_pump_v2` enrichis ; cas zero/no-transfer explicités. |
|
||||
| `pump_fees` | `crank_donation_fee_pda` et `sweep_buyback` enrichis ; events déjà scalaires conservés. |
|
||||
| `meteora_dbc` | Non concerné par l'allowlist générique ; DBC conserve ses chemins spécifiques. |
|
||||
|
||||
Règle pour les prochaines versions : tout nouveau decoder doit déclarer explicitement sa policy de récupération des montants fee. Aucun futur decoder ne doit hériter automatiquement de la recovery CPI SPL.
|
||||
|
||||
## Checks de fermeture
|
||||
|
||||
Les contrôles de fermeture exigés sont propres :
|
||||
|
||||
- fallback `upstream_git` `meteora_dbc` pour entrées couvertes localement : vide ;
|
||||
- decoded `meteora_dbc` sans coverage : vide ;
|
||||
- successful non-materialized sans `skip*Reason` ou policy explicite : vide ;
|
||||
- failed tx avec materialization métier : vide ;
|
||||
- multi-target materialization : vide ;
|
||||
- non-swap DBC vers trade/candle : vide ;
|
||||
- parent fee scalaire sans leg : vide ;
|
||||
- legs fee orphelins : vide ;
|
||||
- watchlist globale sans backlog dominant `meteora_dbc`.
|
||||
|
||||
## Fichiers de référence
|
||||
|
||||
```text
|
||||
kb_lib/src/dex/meteora_dbc.rs
|
||||
kb_lib/src/non_trade_event_materialization.rs
|
||||
kb_lib/src/db/queries/fee_event_amount.rs
|
||||
kb_lib/src/db/entities/fee_event_amount.rs
|
||||
kb_lib/src/db/dtos/fee_event_amount.rs
|
||||
validation_sql/SQL_VALIDATION_METEORA_DBC_0_7_56.sql
|
||||
docs/reports/FEE_EVENT_AMOUNTS_MODEL_NOTE_0_7_56.md
|
||||
docs/VALIDATION_STATUS_0_7_56_FINAL.md
|
||||
docs/prompts/PROMPT_0_7_57_METEORA_DLMM_FULL_DECODE_MATERIALIZATION.md
|
||||
```
|
||||
|
||||
## Décision
|
||||
|
||||
`0.7.56 meteora_dbc` est clôturé. La prochaine tranche est `0.7.57 meteora_dlmm` en full decode + full materialization.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kb-demo-app",
|
||||
"private": true,
|
||||
"version": "0.7.55",
|
||||
"version": "0.7.56",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "kb-demo-app",
|
||||
"version": "0.7.55",
|
||||
"version": "0.7.56",
|
||||
"identifier": "com.sasedev.kb-demo-app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -26,11 +26,14 @@ pub use dtos::DexDecodedEventDto;
|
||||
pub use dtos::DexDto;
|
||||
pub use dtos::DexEventCoverageEntryDto;
|
||||
pub use dtos::DexEventCoverageSummaryDto;
|
||||
pub use dtos::FeeEventAmountDto;
|
||||
pub use dtos::FeeEventDto;
|
||||
pub use dtos::InstructionObservationDto;
|
||||
pub use dtos::InstructionObservationSourceRow;
|
||||
pub use dtos::KnownHttpEndpointDto;
|
||||
pub use dtos::KnownWsEndpointDto;
|
||||
pub use dtos::LaunchAttributionDto;
|
||||
pub use dtos::LaunchEventUpsertInput;
|
||||
pub use dtos::LaunchSurfaceDto;
|
||||
pub use dtos::LaunchSurfaceKeyDto;
|
||||
pub use dtos::LiquidityEventDto;
|
||||
@@ -98,6 +101,7 @@ pub use entities::DexDecodedEventEntity;
|
||||
pub use entities::DexEntity;
|
||||
pub use entities::DexEventCoverageEntryEntity;
|
||||
pub use entities::DexEventCoverageSummaryEntity;
|
||||
pub use entities::FeeEventAmountEntity;
|
||||
pub use entities::FeeEventEntity;
|
||||
pub use entities::InstructionObservationEntity;
|
||||
pub use entities::KnownHttpEndpointEntity;
|
||||
@@ -160,17 +164,17 @@ pub use queries::query_dex_decoded_events_delete_by_key;
|
||||
pub use queries::query_dex_decoded_events_delete_instruction_audit_by_discriminator;
|
||||
pub use queries::query_dex_decoded_events_delete_local_replay_scope_by_transaction_id;
|
||||
pub use queries::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches;
|
||||
pub use queries::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits;
|
||||
pub use queries::query_dex_decoded_events_delete_raydium_clmm_instruction_audit_by_discriminator;
|
||||
pub use queries::query_dex_decoded_events_delete_raydium_launchpad_anchor_self_cpi_audit;
|
||||
pub use queries::query_dex_decoded_events_delete_replaced_raydium_cpmm_instruction_audits;
|
||||
pub use queries::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits;
|
||||
pub use queries::query_dex_decoded_events_delete_related_instruction_audit;
|
||||
pub use queries::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits;
|
||||
pub use queries::query_dex_decoded_events_delete_replaced_raydium_cpmm_instruction_audits;
|
||||
pub use queries::query_dex_decoded_events_get_by_key;
|
||||
pub use queries::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint;
|
||||
pub use queries::query_dex_decoded_events_list_by_transaction_id;
|
||||
pub use queries::query_dex_decoded_events_upsert;
|
||||
pub use queries::query_dex_decoded_events_update_payload_json_by_id;
|
||||
pub use queries::query_dex_decoded_events_upsert;
|
||||
pub use queries::query_dex_event_coverage_entries_delete_by_decoder;
|
||||
pub use queries::query_dex_event_coverage_entries_list_by_decoder;
|
||||
pub use queries::query_dex_event_coverage_entries_list_summary_by_decoder;
|
||||
@@ -180,6 +184,11 @@ pub use queries::query_dex_event_coverage_entries_upsert;
|
||||
pub use queries::query_dexs_get_by_code;
|
||||
pub use queries::query_dexs_list;
|
||||
pub use queries::query_dexs_upsert;
|
||||
pub use queries::query_fee_event_amounts_backfill_single_leg_from_fee_events;
|
||||
pub use queries::query_fee_event_amounts_delete_by_fee_event_id;
|
||||
pub use queries::query_fee_event_amounts_list_by_fee_event_id;
|
||||
pub use queries::query_fee_event_amounts_replace_for_fee_event;
|
||||
pub use queries::query_fee_events_upsert_with_amount_legs;
|
||||
pub use queries::query_fee_events_get_by_decoded_event_id;
|
||||
pub use queries::query_fee_events_list_recent;
|
||||
pub use queries::query_fee_events_upsert;
|
||||
@@ -187,7 +196,6 @@ pub use queries::query_instruction_observation_source_rows_list_by_signature;
|
||||
pub use queries::query_instruction_observation_source_rows_list_recent;
|
||||
pub use queries::query_instruction_observation_source_rows_list_replay_window;
|
||||
pub use queries::query_instruction_observations_delete_by_transaction_ids;
|
||||
pub use dtos::InstructionObservationSourceRow;
|
||||
pub use queries::query_instruction_observations_list_by_filter;
|
||||
pub use queries::query_instruction_observations_upsert;
|
||||
pub use queries::query_known_http_endpoints_get;
|
||||
@@ -200,7 +208,6 @@ pub use queries::query_launch_attributions_get_by_decoded_event_id;
|
||||
pub use queries::query_launch_attributions_list_by_pool_id;
|
||||
pub use queries::query_launch_attributions_upsert;
|
||||
pub use queries::query_launch_events_upsert;
|
||||
pub use dtos::LaunchEventUpsertInput;
|
||||
pub use queries::query_launch_surface_keys_get_by_match;
|
||||
pub use queries::query_launch_surface_keys_list_by_surface_id;
|
||||
pub use queries::query_launch_surface_keys_upsert;
|
||||
|
||||
@@ -13,6 +13,7 @@ mod dex_decode_replay_ledger;
|
||||
mod dex_decoded_event;
|
||||
mod dex_event_coverage_entry;
|
||||
mod fee_event;
|
||||
mod fee_event_amount;
|
||||
mod instruction_observation;
|
||||
mod known_http_endpoint;
|
||||
mod known_ws_endpoint;
|
||||
@@ -88,6 +89,7 @@ pub use dex_decoded_event::DexDecodedEventDto;
|
||||
pub use dex_event_coverage_entry::DexEventCoverageEntryDto;
|
||||
pub use dex_event_coverage_entry::DexEventCoverageSummaryDto;
|
||||
pub use fee_event::FeeEventDto;
|
||||
pub use fee_event_amount::FeeEventAmountDto;
|
||||
pub use instruction_observation::InstructionObservationDto;
|
||||
pub use instruction_observation::InstructionObservationSourceRow;
|
||||
pub use known_http_endpoint::KnownHttpEndpointDto;
|
||||
|
||||
110
kb_lib/src/db/dtos/fee_event_amount.rs
Normal file
110
kb_lib/src/db/dtos/fee_event_amount.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
// file: kb_lib/src/db/dtos/fee_event_amount.rs
|
||||
|
||||
//! Fee event amount DTO.
|
||||
|
||||
/// Application-facing normalized fee event amount leg DTO.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FeeEventAmountDto {
|
||||
/// Optional numeric primary key.
|
||||
pub id: std::option::Option<i64>,
|
||||
/// Related parent fee event id.
|
||||
pub fee_event_id: i64,
|
||||
/// Related transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Related decoded DEX event id, when available.
|
||||
pub decoded_event_id: std::option::Option<i64>,
|
||||
/// Stable leg index within one parent fee event.
|
||||
pub leg_index: u32,
|
||||
/// Fee component kind or semantic role.
|
||||
pub fee_component_kind: std::string::String,
|
||||
/// Token mint used by this fee amount leg.
|
||||
pub token_mint: std::string::String,
|
||||
/// Raw amount for this leg as decimal text.
|
||||
pub amount_raw: std::string::String,
|
||||
/// Source token account or lamport account, when decoded.
|
||||
pub source_account: std::option::Option<std::string::String>,
|
||||
/// Destination token account or lamport account, when decoded.
|
||||
pub destination_account: std::option::Option<std::string::String>,
|
||||
/// Extraction source used to prove this amount.
|
||||
pub amount_source: std::string::String,
|
||||
/// Source/proof payload JSON.
|
||||
pub payload_json: std::string::String,
|
||||
/// Creation timestamp.
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl FeeEventAmountDto {
|
||||
/// Creates a new fee event amount DTO.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
fee_event_id: i64,
|
||||
transaction_id: i64,
|
||||
decoded_event_id: std::option::Option<i64>,
|
||||
leg_index: u32,
|
||||
fee_component_kind: std::string::String,
|
||||
token_mint: std::string::String,
|
||||
amount_raw: std::string::String,
|
||||
source_account: std::option::Option<std::string::String>,
|
||||
destination_account: std::option::Option<std::string::String>,
|
||||
amount_source: std::string::String,
|
||||
payload_json: std::string::String,
|
||||
) -> Self {
|
||||
return Self {
|
||||
id: None,
|
||||
fee_event_id,
|
||||
transaction_id,
|
||||
decoded_event_id,
|
||||
leg_index,
|
||||
fee_component_kind,
|
||||
token_mint,
|
||||
amount_raw,
|
||||
source_account,
|
||||
destination_account,
|
||||
amount_source,
|
||||
payload_json,
|
||||
created_at: chrono::Utc::now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<crate::FeeEventAmountEntity> for FeeEventAmountDto {
|
||||
type Error = crate::Error;
|
||||
|
||||
fn try_from(entity: crate::FeeEventAmountEntity) -> Result<Self, Self::Error> {
|
||||
let leg_index_result = u32::try_from(entity.leg_index);
|
||||
let leg_index = match leg_index_result {
|
||||
Ok(leg_index) => leg_index,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert fee event amount leg_index '{}' to u32: {}",
|
||||
entity.leg_index, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let created_at_result = chrono::DateTime::parse_from_rfc3339(&entity.created_at);
|
||||
let created_at = match created_at_result {
|
||||
Ok(created_at) => created_at.with_timezone(&chrono::Utc),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot parse fee event amount created_at '{}': {}",
|
||||
entity.created_at, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
return Ok(Self {
|
||||
id: Some(entity.id),
|
||||
fee_event_id: entity.fee_event_id,
|
||||
transaction_id: entity.transaction_id,
|
||||
decoded_event_id: entity.decoded_event_id,
|
||||
leg_index,
|
||||
fee_component_kind: entity.fee_component_kind,
|
||||
token_mint: entity.token_mint,
|
||||
amount_raw: entity.amount_raw,
|
||||
source_account: entity.source_account,
|
||||
destination_account: entity.destination_account,
|
||||
amount_source: entity.amount_source,
|
||||
payload_json: entity.payload_json,
|
||||
created_at,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ mod dex_decode_replay_ledger;
|
||||
mod dex_decoded_event;
|
||||
mod dex_event_coverage_entry;
|
||||
mod fee_event;
|
||||
mod fee_event_amount;
|
||||
mod known_http_endpoint;
|
||||
mod known_ws_endpoint;
|
||||
mod instruction_observation;
|
||||
@@ -63,6 +64,7 @@ pub use dex_decoded_event::DexDecodedEventEntity;
|
||||
pub use dex_event_coverage_entry::DexEventCoverageEntryEntity;
|
||||
pub use dex_event_coverage_entry::DexEventCoverageSummaryEntity;
|
||||
pub use fee_event::FeeEventEntity;
|
||||
pub use fee_event_amount::FeeEventAmountEntity;
|
||||
pub use known_http_endpoint::KnownHttpEndpointEntity;
|
||||
pub use known_ws_endpoint::KnownWsEndpointEntity;
|
||||
pub use instruction_observation::InstructionObservationEntity;
|
||||
|
||||
34
kb_lib/src/db/entities/fee_event_amount.rs
Normal file
34
kb_lib/src/db/entities/fee_event_amount.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
// file: kb_lib/src/db/entities/fee_event_amount.rs
|
||||
|
||||
//! Fee event amount entity.
|
||||
|
||||
/// Persisted normalized fee event amount leg row.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
|
||||
pub struct FeeEventAmountEntity {
|
||||
/// Numeric primary key.
|
||||
pub id: i64,
|
||||
/// Related parent fee event id.
|
||||
pub fee_event_id: i64,
|
||||
/// Related transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Related decoded DEX event id, when available.
|
||||
pub decoded_event_id: std::option::Option<i64>,
|
||||
/// Stable leg index within one parent fee event.
|
||||
pub leg_index: i64,
|
||||
/// Fee component kind or semantic role.
|
||||
pub fee_component_kind: std::string::String,
|
||||
/// Token mint used by this fee amount leg.
|
||||
pub token_mint: std::string::String,
|
||||
/// Raw amount for this leg as decimal text.
|
||||
pub amount_raw: std::string::String,
|
||||
/// Source token account or lamport account, when decoded.
|
||||
pub source_account: std::option::Option<std::string::String>,
|
||||
/// Destination token account or lamport account, when decoded.
|
||||
pub destination_account: std::option::Option<std::string::String>,
|
||||
/// Extraction source used to prove this amount.
|
||||
pub amount_source: std::string::String,
|
||||
/// Source/proof payload JSON.
|
||||
pub payload_json: std::string::String,
|
||||
/// Creation timestamp encoded as RFC3339 UTC text.
|
||||
pub created_at: std::string::String,
|
||||
}
|
||||
@@ -13,6 +13,7 @@ mod dex_decode_replay_ledger;
|
||||
mod dex_decoded_event;
|
||||
mod dex_event_coverage_entry;
|
||||
mod fee_event;
|
||||
mod fee_event_amount;
|
||||
mod instruction_observation;
|
||||
mod known_http_endpoint;
|
||||
mod known_ws_endpoint;
|
||||
@@ -99,6 +100,11 @@ pub use dex_event_coverage_entry::query_dex_event_coverage_entries_upsert;
|
||||
pub use fee_event::query_fee_events_get_by_decoded_event_id;
|
||||
pub use fee_event::query_fee_events_list_recent;
|
||||
pub use fee_event::query_fee_events_upsert;
|
||||
pub use fee_event_amount::query_fee_event_amounts_backfill_single_leg_from_fee_events;
|
||||
pub use fee_event_amount::query_fee_event_amounts_delete_by_fee_event_id;
|
||||
pub use fee_event_amount::query_fee_event_amounts_list_by_fee_event_id;
|
||||
pub use fee_event_amount::query_fee_event_amounts_replace_for_fee_event;
|
||||
pub use fee_event_amount::query_fee_events_upsert_with_amount_legs;
|
||||
pub use instruction_observation::query_instruction_observation_source_rows_list_by_signature;
|
||||
pub use instruction_observation::query_instruction_observation_source_rows_list_recent;
|
||||
pub use instruction_observation::query_instruction_observation_source_rows_list_replay_window;
|
||||
|
||||
@@ -136,6 +136,10 @@ WHERE decoded_event_id IN (
|
||||
"k_sol_pool_lifecycle_events",
|
||||
"DELETE FROM k_sol_pool_lifecycle_events WHERE transaction_id = ?",
|
||||
),
|
||||
(
|
||||
"k_sol_fee_event_amounts",
|
||||
"DELETE FROM k_sol_fee_event_amounts WHERE transaction_id = ?",
|
||||
),
|
||||
("k_sol_fee_events", "DELETE FROM k_sol_fee_events WHERE transaction_id = ?"),
|
||||
(
|
||||
"k_sol_reward_events",
|
||||
|
||||
@@ -156,6 +156,10 @@ WHERE id = ?
|
||||
id, error
|
||||
)));
|
||||
}
|
||||
let leg_result = query_fee_events_replace_scalar_amount_leg(database, id, dto).await;
|
||||
if let Err(error) = leg_result {
|
||||
return Err(error);
|
||||
}
|
||||
return Ok(id);
|
||||
}
|
||||
let insert_result = sqlx::query(
|
||||
@@ -227,7 +231,13 @@ LIMIT 1
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
match id_result {
|
||||
Ok(id) => return Ok(id),
|
||||
Ok(id) => {
|
||||
let leg_result = query_fee_events_replace_scalar_amount_leg(database, id, dto).await;
|
||||
if let Err(error) = leg_result {
|
||||
return Err(error);
|
||||
}
|
||||
return Ok(id);
|
||||
},
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot fetch inserted k_sol_fee_events id for signature '{}' on sqlite: {}",
|
||||
@@ -239,6 +249,88 @@ LIMIT 1
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_fee_events_replace_scalar_amount_leg(
|
||||
database: &crate::Database,
|
||||
fee_event_id: i64,
|
||||
dto: &crate::FeeEventDto,
|
||||
) -> Result<(), crate::Error> {
|
||||
let token_mint = match dto.fee_token_mint.as_ref() {
|
||||
Some(token_mint) => {
|
||||
if token_mint.trim().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
token_mint.clone()
|
||||
},
|
||||
None => return Ok(()),
|
||||
};
|
||||
let amount_raw = match dto.fee_amount_raw.as_ref() {
|
||||
Some(amount_raw) => {
|
||||
if amount_raw.trim().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
amount_raw.clone()
|
||||
},
|
||||
None => return Ok(()),
|
||||
};
|
||||
let payload_json = scalar_fee_event_amount_payload_json(dto, token_mint.as_str(), amount_raw.as_str());
|
||||
let amount_leg = crate::FeeEventAmountDto::new(
|
||||
fee_event_id,
|
||||
dto.transaction_id,
|
||||
dto.decoded_event_id,
|
||||
0,
|
||||
dto.event_kind.clone(),
|
||||
token_mint,
|
||||
amount_raw,
|
||||
None,
|
||||
None,
|
||||
"parent_fee_event_amount".to_string(),
|
||||
payload_json,
|
||||
);
|
||||
let replace_result = crate::query_fee_event_amounts_replace_for_fee_event(
|
||||
database,
|
||||
fee_event_id,
|
||||
&[amount_leg],
|
||||
)
|
||||
.await;
|
||||
match replace_result {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn scalar_fee_event_amount_payload_json(
|
||||
dto: &crate::FeeEventDto,
|
||||
token_mint: &str,
|
||||
amount_raw: &str,
|
||||
) -> std::string::String {
|
||||
let mut object = serde_json::Map::new();
|
||||
object.insert(
|
||||
"decodedEventKind".to_string(),
|
||||
serde_json::Value::String(dto.event_kind.clone()),
|
||||
);
|
||||
object.insert(
|
||||
"protocolName".to_string(),
|
||||
serde_json::Value::String(dto.protocol_name.clone()),
|
||||
);
|
||||
object.insert(
|
||||
"legIndex".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(0_u64)),
|
||||
);
|
||||
object.insert(
|
||||
"tokenMint".to_string(),
|
||||
serde_json::Value::String(token_mint.to_string()),
|
||||
);
|
||||
object.insert(
|
||||
"amountRaw".to_string(),
|
||||
serde_json::Value::String(amount_raw.to_string()),
|
||||
);
|
||||
object.insert(
|
||||
"amountSource".to_string(),
|
||||
serde_json::Value::String("parent_fee_event_amount".to_string()),
|
||||
);
|
||||
return serde_json::Value::Object(object).to_string();
|
||||
}
|
||||
|
||||
/// Lists recent fee events ordered from newest to oldest.
|
||||
pub async fn query_fee_events_list_recent(
|
||||
database: &crate::Database,
|
||||
|
||||
677
kb_lib/src/db/queries/fee_event_amount.rs
Normal file
677
kb_lib/src/db/queries/fee_event_amount.rs
Normal file
@@ -0,0 +1,677 @@
|
||||
// file: kb_lib/src/db/queries/fee_event_amount.rs
|
||||
|
||||
//! Queries for `k_sol_fee_event_amounts`.
|
||||
|
||||
/// Deletes fee amount legs for one parent fee event.
|
||||
pub async fn query_fee_event_amounts_delete_by_fee_event_id(
|
||||
database: &crate::Database,
|
||||
fee_event_id: i64,
|
||||
) -> Result<u64, crate::Error> {
|
||||
match database.connection() {
|
||||
crate::DatabaseConnection::Sqlite(pool) => {
|
||||
let delete_result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM k_sol_fee_event_amounts
|
||||
WHERE fee_event_id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(fee_event_id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
match delete_result {
|
||||
Ok(result) => return Ok(result.rows_affected()),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot delete k_sol_fee_event_amounts for fee_event_id '{}' on sqlite: {}",
|
||||
fee_event_id, error
|
||||
)));
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces all fee amount legs for one parent fee event.
|
||||
pub async fn query_fee_event_amounts_replace_for_fee_event(
|
||||
database: &crate::Database,
|
||||
fee_event_id: i64,
|
||||
dtos: &[crate::FeeEventAmountDto],
|
||||
) -> Result<u64, crate::Error> {
|
||||
let delete_result =
|
||||
query_fee_event_amounts_delete_by_fee_event_id(database, fee_event_id).await;
|
||||
if let Err(error) = delete_result {
|
||||
return Err(error);
|
||||
}
|
||||
let mut inserted: u64 = 0;
|
||||
match database.connection() {
|
||||
crate::DatabaseConnection::Sqlite(pool) => {
|
||||
for dto in dtos {
|
||||
let leg_index_i64 = i64::from(dto.leg_index);
|
||||
let insert_result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO k_sol_fee_event_amounts (
|
||||
fee_event_id,
|
||||
transaction_id,
|
||||
decoded_event_id,
|
||||
leg_index,
|
||||
fee_component_kind,
|
||||
token_mint,
|
||||
amount_raw,
|
||||
source_account,
|
||||
destination_account,
|
||||
amount_source,
|
||||
payload_json,
|
||||
created_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(dto.fee_event_id)
|
||||
.bind(dto.transaction_id)
|
||||
.bind(dto.decoded_event_id)
|
||||
.bind(leg_index_i64)
|
||||
.bind(dto.fee_component_kind.clone())
|
||||
.bind(dto.token_mint.clone())
|
||||
.bind(dto.amount_raw.clone())
|
||||
.bind(dto.source_account.clone())
|
||||
.bind(dto.destination_account.clone())
|
||||
.bind(dto.amount_source.clone())
|
||||
.bind(dto.payload_json.clone())
|
||||
.bind(dto.created_at.to_rfc3339())
|
||||
.execute(pool)
|
||||
.await;
|
||||
match insert_result {
|
||||
Ok(_) => inserted += 1,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot insert k_sol_fee_event_amounts leg '{}' for fee_event_id '{}' on sqlite: {}",
|
||||
dto.leg_index, fee_event_id, error
|
||||
)));
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return Ok(inserted);
|
||||
}
|
||||
|
||||
/// Lists fee amount legs for one parent fee event.
|
||||
pub async fn query_fee_event_amounts_list_by_fee_event_id(
|
||||
database: &crate::Database,
|
||||
fee_event_id: i64,
|
||||
) -> Result<std::vec::Vec<crate::FeeEventAmountDto>, crate::Error> {
|
||||
match database.connection() {
|
||||
crate::DatabaseConnection::Sqlite(pool) => {
|
||||
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::FeeEventAmountEntity>(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
fee_event_id,
|
||||
transaction_id,
|
||||
decoded_event_id,
|
||||
leg_index,
|
||||
fee_component_kind,
|
||||
token_mint,
|
||||
amount_raw,
|
||||
source_account,
|
||||
destination_account,
|
||||
amount_source,
|
||||
payload_json,
|
||||
created_at
|
||||
FROM k_sol_fee_event_amounts
|
||||
WHERE fee_event_id = ?
|
||||
ORDER BY leg_index ASC
|
||||
"#,
|
||||
)
|
||||
.bind(fee_event_id)
|
||||
.fetch_all(pool)
|
||||
.await;
|
||||
let entities = match query_result {
|
||||
Ok(entities) => entities,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot list k_sol_fee_event_amounts for fee_event_id '{}' on sqlite: {}",
|
||||
fee_event_id, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let mut dtos = std::vec::Vec::new();
|
||||
for entity in entities {
|
||||
let dto_result = crate::FeeEventAmountDto::try_from(entity);
|
||||
match dto_result {
|
||||
Ok(dto) => dtos.push(dto),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
return Ok(dtos);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts or updates a fee parent and atomically replaces its amount legs.
|
||||
pub async fn query_fee_events_upsert_with_amount_legs(
|
||||
database: &crate::Database,
|
||||
fee_event: &crate::FeeEventDto,
|
||||
amount_legs: &[crate::FeeEventAmountDto],
|
||||
) -> Result<i64, crate::Error> {
|
||||
let fee_event_id_result = crate::query_fee_events_upsert(database, fee_event).await;
|
||||
let fee_event_id = match fee_event_id_result {
|
||||
Ok(fee_event_id) => fee_event_id,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let mut normalized_legs = std::vec::Vec::new();
|
||||
for amount_leg in amount_legs {
|
||||
let mut normalized_leg = amount_leg.clone();
|
||||
normalized_leg.fee_event_id = fee_event_id;
|
||||
normalized_leg.transaction_id = fee_event.transaction_id;
|
||||
normalized_leg.decoded_event_id = fee_event.decoded_event_id;
|
||||
normalized_legs.push(normalized_leg);
|
||||
}
|
||||
let replace_result = query_fee_event_amounts_replace_for_fee_event(
|
||||
database,
|
||||
fee_event_id,
|
||||
normalized_legs.as_slice(),
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = replace_result {
|
||||
return Err(error);
|
||||
}
|
||||
return Ok(fee_event_id);
|
||||
}
|
||||
|
||||
/// Backfills one amount leg for existing fee parents that expose one scalar amount.
|
||||
pub async fn query_fee_event_amounts_backfill_single_leg_from_fee_events(
|
||||
database: &crate::Database,
|
||||
) -> Result<u64, crate::Error> {
|
||||
let entities = match database.connection() {
|
||||
crate::DatabaseConnection::Sqlite(pool) => {
|
||||
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::FeeEventEntity>(
|
||||
r#"
|
||||
SELECT
|
||||
f.id,
|
||||
f.transaction_id,
|
||||
f.decoded_event_id,
|
||||
f.dex_id,
|
||||
f.pool_id,
|
||||
f.pair_id,
|
||||
f.signature,
|
||||
f.slot,
|
||||
f.protocol_name,
|
||||
f.program_id,
|
||||
f.event_kind,
|
||||
f.pool_account,
|
||||
f.actor_wallet,
|
||||
f.fee_token_mint,
|
||||
f.fee_amount_raw,
|
||||
f.payload_json,
|
||||
f.executed_at,
|
||||
f.created_at
|
||||
FROM k_sol_fee_events f
|
||||
WHERE f.fee_token_mint IS NOT NULL
|
||||
AND TRIM(f.fee_token_mint) <> ''
|
||||
AND f.fee_amount_raw IS NOT NULL
|
||||
AND TRIM(f.fee_amount_raw) <> ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM k_sol_fee_event_amounts a
|
||||
WHERE a.fee_event_id = f.id
|
||||
LIMIT 1
|
||||
)
|
||||
ORDER BY f.id ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await;
|
||||
match query_result {
|
||||
Ok(entities) => entities,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot list fee events for single-leg amount backfill on sqlite: {}",
|
||||
error
|
||||
)));
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
let mut backfilled: u64 = 0;
|
||||
for entity in entities {
|
||||
let dto_result = crate::FeeEventDto::try_from(entity);
|
||||
let dto = match dto_result {
|
||||
Ok(dto) => dto,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let fee_event_id = match dto.id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return Err(crate::Error::InvalidState(
|
||||
"fee event backfill row does not contain an id".to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let token_mint = match dto.fee_token_mint.as_ref() {
|
||||
Some(token_mint) => token_mint.clone(),
|
||||
None => continue,
|
||||
};
|
||||
let amount_raw = match dto.fee_amount_raw.as_ref() {
|
||||
Some(amount_raw) => amount_raw.clone(),
|
||||
None => continue,
|
||||
};
|
||||
let payload_json = single_fee_event_amount_backfill_payload_json(
|
||||
&dto,
|
||||
token_mint.as_str(),
|
||||
amount_raw.as_str(),
|
||||
);
|
||||
let amount_leg = crate::FeeEventAmountDto::new(
|
||||
fee_event_id,
|
||||
dto.transaction_id,
|
||||
dto.decoded_event_id,
|
||||
0,
|
||||
dto.event_kind.clone(),
|
||||
token_mint,
|
||||
amount_raw,
|
||||
None,
|
||||
None,
|
||||
"parent_fee_event_backfill".to_string(),
|
||||
payload_json,
|
||||
);
|
||||
let replace_result =
|
||||
query_fee_event_amounts_replace_for_fee_event(database, fee_event_id, &[amount_leg])
|
||||
.await;
|
||||
match replace_result {
|
||||
Ok(inserted) => backfilled += inserted,
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
return Ok(backfilled);
|
||||
}
|
||||
|
||||
fn single_fee_event_amount_backfill_payload_json(
|
||||
dto: &crate::FeeEventDto,
|
||||
token_mint: &str,
|
||||
amount_raw: &str,
|
||||
) -> std::string::String {
|
||||
let mut object = serde_json::Map::new();
|
||||
object.insert(
|
||||
"decodedEventKind".to_string(),
|
||||
serde_json::Value::String(dto.event_kind.clone()),
|
||||
);
|
||||
object.insert("protocolName".to_string(), serde_json::Value::String(dto.protocol_name.clone()));
|
||||
object.insert(
|
||||
"legIndex".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(0_u64)),
|
||||
);
|
||||
object.insert("tokenMint".to_string(), serde_json::Value::String(token_mint.to_string()));
|
||||
object.insert("amountRaw".to_string(), serde_json::Value::String(amount_raw.to_string()));
|
||||
object.insert(
|
||||
"amountSource".to_string(),
|
||||
serde_json::Value::String("parent_fee_event_backfill".to_string()),
|
||||
);
|
||||
return serde_json::Value::Object(object).to_string();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
async fn make_database() -> crate::Database {
|
||||
let tempdir_result = tempfile::tempdir();
|
||||
let tempdir = match tempdir_result {
|
||||
Ok(tempdir) => tempdir,
|
||||
Err(error) => panic!("tempdir must succeed: {}", error),
|
||||
};
|
||||
let database_path = tempdir.path().join("fee_event_amount.sqlite3");
|
||||
let config = crate::DatabaseConfig {
|
||||
enabled: true,
|
||||
backend: crate::DatabaseBackend::Sqlite,
|
||||
sqlite: crate::SqliteDatabaseConfig {
|
||||
path: database_path.to_string_lossy().to_string(),
|
||||
create_if_missing: true,
|
||||
busy_timeout_ms: 5000,
|
||||
max_connections: 1,
|
||||
auto_initialize_schema: true,
|
||||
use_wal: true,
|
||||
},
|
||||
};
|
||||
let database_result = crate::Database::connect_and_initialize(&config).await;
|
||||
match database_result {
|
||||
Ok(database) => return database,
|
||||
Err(error) => panic!("database init must succeed: {}", error),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_leg(
|
||||
fee_event_id: i64,
|
||||
leg_index: u32,
|
||||
token_mint: &str,
|
||||
amount_raw: &str,
|
||||
) -> crate::FeeEventAmountDto {
|
||||
return crate::FeeEventAmountDto::new(
|
||||
fee_event_id,
|
||||
7001,
|
||||
Some(8001),
|
||||
leg_index,
|
||||
"trading_fee".to_string(),
|
||||
token_mint.to_string(),
|
||||
amount_raw.to_string(),
|
||||
Some(format!("Source{}", leg_index)),
|
||||
Some(format!("Destination{}", leg_index)),
|
||||
"inner_spl_transfer".to_string(),
|
||||
serde_json::json!({
|
||||
"test": true,
|
||||
"legIndex": leg_index
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fee_event_amounts_replace_roundtrip_supports_multiple_legs() {
|
||||
let database = make_database().await;
|
||||
let fee_event_id = 501;
|
||||
let legs = vec![
|
||||
make_leg(fee_event_id, 0, crate::WSOL_MINT_ID, "100"),
|
||||
make_leg(fee_event_id, 1, "TokenMint111", "200"),
|
||||
];
|
||||
let replace_result =
|
||||
crate::query_fee_event_amounts_replace_for_fee_event(&database, fee_event_id, &legs)
|
||||
.await;
|
||||
let inserted = match replace_result {
|
||||
Ok(inserted) => inserted,
|
||||
Err(error) => panic!("replace must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(inserted, 2);
|
||||
|
||||
let list_result =
|
||||
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
|
||||
let listed = match list_result {
|
||||
Ok(listed) => listed,
|
||||
Err(error) => panic!("list must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(listed.len(), 2);
|
||||
assert_eq!(listed[0].leg_index, 0);
|
||||
assert_eq!(listed[0].token_mint, crate::WSOL_MINT_ID);
|
||||
assert_eq!(listed[0].amount_raw, "100");
|
||||
assert_eq!(listed[1].leg_index, 1);
|
||||
assert_eq!(listed[1].token_mint, "TokenMint111");
|
||||
assert_eq!(listed[1].amount_raw, "200");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fee_event_amounts_replace_deletes_stale_legs() {
|
||||
let database = make_database().await;
|
||||
let fee_event_id = 502;
|
||||
let initial_legs = vec![
|
||||
make_leg(fee_event_id, 0, crate::WSOL_MINT_ID, "100"),
|
||||
make_leg(fee_event_id, 1, "TokenMint222", "200"),
|
||||
];
|
||||
let initial_result = crate::query_fee_event_amounts_replace_for_fee_event(
|
||||
&database,
|
||||
fee_event_id,
|
||||
&initial_legs,
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = initial_result {
|
||||
panic!("initial replace must succeed: {}", error);
|
||||
}
|
||||
|
||||
let replacement_legs = vec![make_leg(fee_event_id, 0, "TokenMint333", "300")];
|
||||
let replacement_result = crate::query_fee_event_amounts_replace_for_fee_event(
|
||||
&database,
|
||||
fee_event_id,
|
||||
&replacement_legs,
|
||||
)
|
||||
.await;
|
||||
let inserted = match replacement_result {
|
||||
Ok(inserted) => inserted,
|
||||
Err(error) => panic!("replacement must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(inserted, 1);
|
||||
|
||||
let list_result =
|
||||
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
|
||||
let listed = match list_result {
|
||||
Ok(listed) => listed,
|
||||
Err(error) => panic!("list must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(listed.len(), 1);
|
||||
assert_eq!(listed[0].leg_index, 0);
|
||||
assert_eq!(listed[0].token_mint, "TokenMint333");
|
||||
assert_eq!(listed[0].amount_raw, "300");
|
||||
}
|
||||
|
||||
fn make_fee_event(
|
||||
transaction_id: i64,
|
||||
decoded_event_id: i64,
|
||||
signature: &str,
|
||||
fee_token_mint: std::option::Option<&str>,
|
||||
fee_amount_raw: std::option::Option<&str>,
|
||||
) -> crate::FeeEventDto {
|
||||
return crate::FeeEventDto::new(
|
||||
transaction_id,
|
||||
Some(decoded_event_id),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
signature.to_string(),
|
||||
Some(1234),
|
||||
"test_protocol".to_string(),
|
||||
"TestProgram111".to_string(),
|
||||
"test_protocol.claim_fee".to_string(),
|
||||
None,
|
||||
None,
|
||||
fee_token_mint.map(|value| return value.to_string()),
|
||||
fee_amount_raw.map(|value| return value.to_string()),
|
||||
serde_json::json!({"test": true}).to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
async fn insert_legacy_fee_event_parent(
|
||||
database: &crate::Database,
|
||||
fee_event: &crate::FeeEventDto,
|
||||
) -> i64 {
|
||||
let slot_i64 = match fee_event.slot {
|
||||
Some(slot) => {
|
||||
let slot_result = i64::try_from(slot);
|
||||
match slot_result {
|
||||
Ok(slot_i64) => Some(slot_i64),
|
||||
Err(error) => panic!("slot conversion must succeed: {}", error),
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
match database.connection() {
|
||||
crate::DatabaseConnection::Sqlite(pool) => {
|
||||
let insert_result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO k_sol_fee_events (
|
||||
transaction_id,
|
||||
decoded_event_id,
|
||||
dex_id,
|
||||
pool_id,
|
||||
pair_id,
|
||||
signature,
|
||||
slot,
|
||||
protocol_name,
|
||||
program_id,
|
||||
event_kind,
|
||||
pool_account,
|
||||
actor_wallet,
|
||||
fee_token_mint,
|
||||
fee_amount_raw,
|
||||
payload_json,
|
||||
executed_at,
|
||||
created_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(fee_event.transaction_id)
|
||||
.bind(fee_event.decoded_event_id)
|
||||
.bind(fee_event.dex_id)
|
||||
.bind(fee_event.pool_id)
|
||||
.bind(fee_event.pair_id)
|
||||
.bind(fee_event.signature.clone())
|
||||
.bind(slot_i64)
|
||||
.bind(fee_event.protocol_name.clone())
|
||||
.bind(fee_event.program_id.clone())
|
||||
.bind(fee_event.event_kind.clone())
|
||||
.bind(fee_event.pool_account.clone())
|
||||
.bind(fee_event.actor_wallet.clone())
|
||||
.bind(fee_event.fee_token_mint.clone())
|
||||
.bind(fee_event.fee_amount_raw.clone())
|
||||
.bind(fee_event.payload_json.clone())
|
||||
.bind(fee_event.executed_at.to_rfc3339())
|
||||
.bind(fee_event.created_at.to_rfc3339())
|
||||
.execute(pool)
|
||||
.await;
|
||||
if let Err(error) = insert_result {
|
||||
panic!("legacy parent insert must succeed: {}", error);
|
||||
}
|
||||
let id_result = sqlx::query_scalar::<sqlx::Sqlite, i64>(
|
||||
r#"
|
||||
SELECT id
|
||||
FROM k_sol_fee_events
|
||||
WHERE signature = ?
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(fee_event.signature.clone())
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
match id_result {
|
||||
Ok(id) => return id,
|
||||
Err(error) => panic!("legacy parent id fetch must succeed: {}", error),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fee_events_upsert_with_amount_legs_normalizes_parent_id() {
|
||||
let database = make_database().await;
|
||||
let fee_event = make_fee_event(7101, 8101, "signature-helper", None, None);
|
||||
let legs = vec![
|
||||
make_leg(0, 0, crate::WSOL_MINT_ID, "100"),
|
||||
make_leg(0, 1, "TokenMint555", "200"),
|
||||
];
|
||||
let upsert_result =
|
||||
crate::query_fee_events_upsert_with_amount_legs(&database, &fee_event, legs.as_slice())
|
||||
.await;
|
||||
let fee_event_id = match upsert_result {
|
||||
Ok(fee_event_id) => fee_event_id,
|
||||
Err(error) => panic!("upsert with legs must succeed: {}", error),
|
||||
};
|
||||
let list_result =
|
||||
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
|
||||
let listed = match list_result {
|
||||
Ok(listed) => listed,
|
||||
Err(error) => panic!("list must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(listed.len(), 2);
|
||||
assert_eq!(listed[0].fee_event_id, fee_event_id);
|
||||
assert_eq!(listed[0].transaction_id, fee_event.transaction_id);
|
||||
assert_eq!(listed[0].decoded_event_id, fee_event.decoded_event_id);
|
||||
assert_eq!(listed[1].fee_event_id, fee_event_id);
|
||||
assert_eq!(listed[1].transaction_id, fee_event.transaction_id);
|
||||
assert_eq!(listed[1].decoded_event_id, fee_event.decoded_event_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fee_events_upsert_automatically_creates_scalar_amount_leg() {
|
||||
let database = make_database().await;
|
||||
let fee_event = make_fee_event(
|
||||
7151,
|
||||
8151,
|
||||
"signature-auto-leg",
|
||||
Some(crate::WSOL_MINT_ID),
|
||||
Some("777"),
|
||||
);
|
||||
let upsert_result = crate::query_fee_events_upsert(&database, &fee_event).await;
|
||||
let fee_event_id = match upsert_result {
|
||||
Ok(fee_event_id) => fee_event_id,
|
||||
Err(error) => panic!("fee parent upsert must succeed: {}", error),
|
||||
};
|
||||
let list_result =
|
||||
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
|
||||
let listed = match list_result {
|
||||
Ok(listed) => listed,
|
||||
Err(error) => panic!("list must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(listed.len(), 1);
|
||||
assert_eq!(listed[0].fee_event_id, fee_event_id);
|
||||
assert_eq!(listed[0].transaction_id, fee_event.transaction_id);
|
||||
assert_eq!(listed[0].decoded_event_id, fee_event.decoded_event_id);
|
||||
assert_eq!(listed[0].token_mint, crate::WSOL_MINT_ID);
|
||||
assert_eq!(listed[0].amount_raw, "777");
|
||||
assert_eq!(listed[0].amount_source, "parent_fee_event_amount");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fee_event_amounts_backfill_single_leg_from_existing_parent() {
|
||||
let database = make_database().await;
|
||||
let fee_event = make_fee_event(
|
||||
7201,
|
||||
8201,
|
||||
"signature-backfill",
|
||||
Some(crate::WSOL_MINT_ID),
|
||||
Some("999"),
|
||||
);
|
||||
let fee_event_id = insert_legacy_fee_event_parent(&database, &fee_event).await;
|
||||
let backfill_result =
|
||||
crate::query_fee_event_amounts_backfill_single_leg_from_fee_events(&database).await;
|
||||
let backfilled = match backfill_result {
|
||||
Ok(backfilled) => backfilled,
|
||||
Err(error) => panic!("backfill must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(backfilled, 1);
|
||||
let list_result =
|
||||
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
|
||||
let listed = match list_result {
|
||||
Ok(listed) => listed,
|
||||
Err(error) => panic!("list must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(listed.len(), 1);
|
||||
assert_eq!(listed[0].token_mint, crate::WSOL_MINT_ID);
|
||||
assert_eq!(listed[0].amount_raw, "999");
|
||||
assert_eq!(listed[0].amount_source, "parent_fee_event_backfill");
|
||||
|
||||
let second_backfill_result =
|
||||
crate::query_fee_event_amounts_backfill_single_leg_from_fee_events(&database).await;
|
||||
let second_backfilled = match second_backfill_result {
|
||||
Ok(second_backfilled) => second_backfilled,
|
||||
Err(error) => panic!("second backfill must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(second_backfilled, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fee_event_amounts_delete_by_parent_removes_all_legs() {
|
||||
let database = make_database().await;
|
||||
let fee_event_id = 503;
|
||||
let legs = vec![
|
||||
make_leg(fee_event_id, 0, crate::WSOL_MINT_ID, "100"),
|
||||
make_leg(fee_event_id, 1, "TokenMint444", "200"),
|
||||
];
|
||||
let replace_result =
|
||||
crate::query_fee_event_amounts_replace_for_fee_event(&database, fee_event_id, &legs)
|
||||
.await;
|
||||
if let Err(error) = replace_result {
|
||||
panic!("replace must succeed: {}", error);
|
||||
}
|
||||
|
||||
let delete_result =
|
||||
crate::query_fee_event_amounts_delete_by_fee_event_id(&database, fee_event_id).await;
|
||||
let deleted = match delete_result {
|
||||
Ok(deleted) => deleted,
|
||||
Err(error) => panic!("delete must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(deleted, 2);
|
||||
|
||||
let list_result =
|
||||
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
|
||||
let listed = match list_result {
|
||||
Ok(listed) => listed,
|
||||
Err(error) => panic!("list must succeed: {}", error),
|
||||
};
|
||||
assert!(listed.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -342,6 +342,30 @@ pub(crate) async fn ensure_schema(database: &crate::Database) -> Result<(), crat
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
}
|
||||
let result = create_tbl_fee_event_amounts(pool).await;
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
}
|
||||
let result = create_idx_fee_event_amounts_fee_event_id(pool).await;
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
}
|
||||
let result = create_idx_fee_event_amounts_transaction_id(pool).await;
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
}
|
||||
let result = create_idx_fee_event_amounts_decoded_event_id(pool).await;
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
}
|
||||
let result = create_idx_fee_event_amounts_token_mint(pool).await;
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
}
|
||||
let result = create_uix_fee_event_amounts_fee_event_leg(pool).await;
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
}
|
||||
let result = create_tbl_reward_events(pool).await;
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
@@ -2702,6 +2726,107 @@ WHERE decoded_event_id IS NOT NULL
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Creates `k_sol_fee_event_amounts`.
|
||||
async fn create_tbl_fee_event_amounts(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
|
||||
return execute_sqlite_schema_statement(
|
||||
pool,
|
||||
"create_tbl_fee_event_amounts",
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS k_sol_fee_event_amounts (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
fee_event_id INTEGER NOT NULL,
|
||||
transaction_id INTEGER NOT NULL,
|
||||
decoded_event_id INTEGER NULL,
|
||||
leg_index INTEGER NOT NULL,
|
||||
fee_component_kind TEXT NOT NULL,
|
||||
token_mint TEXT NOT NULL,
|
||||
amount_raw TEXT NOT NULL,
|
||||
source_account TEXT NULL,
|
||||
destination_account TEXT NULL,
|
||||
amount_source TEXT NOT NULL,
|
||||
payload_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Creates index on `k_sol_fee_event_amounts(fee_event_id)`.
|
||||
async fn create_idx_fee_event_amounts_fee_event_id(
|
||||
pool: &sqlx::SqlitePool,
|
||||
) -> Result<(), crate::Error> {
|
||||
return execute_sqlite_schema_statement(
|
||||
pool,
|
||||
"create_idx_fee_event_amounts_fee_event_id",
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_fee_event_amounts_fee_event_id
|
||||
ON k_sol_fee_event_amounts (fee_event_id)
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Creates index on `k_sol_fee_event_amounts(transaction_id)`.
|
||||
async fn create_idx_fee_event_amounts_transaction_id(
|
||||
pool: &sqlx::SqlitePool,
|
||||
) -> Result<(), crate::Error> {
|
||||
return execute_sqlite_schema_statement(
|
||||
pool,
|
||||
"create_idx_fee_event_amounts_transaction_id",
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_fee_event_amounts_transaction_id
|
||||
ON k_sol_fee_event_amounts (transaction_id)
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Creates index on `k_sol_fee_event_amounts(decoded_event_id)`.
|
||||
async fn create_idx_fee_event_amounts_decoded_event_id(
|
||||
pool: &sqlx::SqlitePool,
|
||||
) -> Result<(), crate::Error> {
|
||||
return execute_sqlite_schema_statement(
|
||||
pool,
|
||||
"create_idx_fee_event_amounts_decoded_event_id",
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_fee_event_amounts_decoded_event_id
|
||||
ON k_sol_fee_event_amounts (decoded_event_id)
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Creates index on `k_sol_fee_event_amounts(token_mint)`.
|
||||
async fn create_idx_fee_event_amounts_token_mint(
|
||||
pool: &sqlx::SqlitePool,
|
||||
) -> Result<(), crate::Error> {
|
||||
return execute_sqlite_schema_statement(
|
||||
pool,
|
||||
"create_idx_fee_event_amounts_token_mint",
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_fee_event_amounts_token_mint
|
||||
ON k_sol_fee_event_amounts (token_mint)
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Creates unique index on one fee amount leg per parent fee event and leg index.
|
||||
async fn create_uix_fee_event_amounts_fee_event_leg(
|
||||
pool: &sqlx::SqlitePool,
|
||||
) -> Result<(), crate::Error> {
|
||||
return execute_sqlite_schema_statement(
|
||||
pool,
|
||||
"create_uix_fee_event_amounts_fee_event_leg",
|
||||
r#"
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uix_fee_event_amounts_fee_event_leg
|
||||
ON k_sol_fee_event_amounts (fee_event_id, leg_index)
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Creates `k_sol_reward_events`.
|
||||
async fn create_tbl_reward_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
|
||||
return execute_sqlite_schema_statement(
|
||||
|
||||
@@ -43,6 +43,7 @@ pub use meteora_damm_v2::MeteoraDammV2SwapDecoded;
|
||||
pub use meteora_dbc::MeteoraDbcCreatePoolDecoded;
|
||||
pub use meteora_dbc::MeteoraDbcDecodedEvent;
|
||||
pub use meteora_dbc::MeteoraDbcDecoder;
|
||||
pub use meteora_dbc::MeteoraDbcInstructionDecoded;
|
||||
pub use meteora_dbc::MeteoraDbcSwapDecoded;
|
||||
pub use meteora_dlmm::MeteoraDlmmCreatePoolDecoded;
|
||||
pub use meteora_dlmm::MeteoraDlmmDecodedEvent;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1045,7 +1045,7 @@ impl DexDecodeService {
|
||||
event.instruction_id,
|
||||
"meteora_dbc",
|
||||
event.program_id.clone(),
|
||||
"meteora_dbc.create_pool",
|
||||
event.event_kind.as_str(),
|
||||
event.pool_account.clone(),
|
||||
None,
|
||||
event.token_a_mint.clone(),
|
||||
@@ -1063,7 +1063,7 @@ impl DexDecodeService {
|
||||
event.instruction_id,
|
||||
"meteora_dbc",
|
||||
event.program_id.clone(),
|
||||
"meteora_dbc.swap",
|
||||
event.event_kind.as_str(),
|
||||
event.pool_account.clone(),
|
||||
None,
|
||||
event.token_a_mint.clone(),
|
||||
@@ -1073,6 +1073,24 @@ impl DexDecodeService {
|
||||
)
|
||||
.await;
|
||||
},
|
||||
crate::MeteoraDbcDecodedEvent::Instruction(event) => {
|
||||
return self
|
||||
.materialize_named_dex_event(
|
||||
transaction,
|
||||
event.transaction_id,
|
||||
event.instruction_id,
|
||||
"meteora_dbc",
|
||||
event.program_id.clone(),
|
||||
event.event_kind.as_str(),
|
||||
event.pool_account.clone(),
|
||||
None,
|
||||
event.token_a_mint.clone(),
|
||||
event.token_b_mint.clone(),
|
||||
event.related_account.clone(),
|
||||
event.payload_json.clone(),
|
||||
)
|
||||
.await;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,9 @@ pub(crate) fn dex_detection_route(
|
||||
("meteora_dbc", "meteora_dbc.swap") => {
|
||||
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDbcPool);
|
||||
},
|
||||
("meteora_dbc", "meteora_dbc.swap2") => {
|
||||
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDbcPool);
|
||||
},
|
||||
("meteora_dlmm", "meteora_dlmm.create_pool") => {
|
||||
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDlmmPool);
|
||||
},
|
||||
@@ -304,6 +307,26 @@ mod tests {
|
||||
assert!(!crate::dex_detection_route::decoded_event_has_full_pool_context(&event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meteora_dbc_swap2_routes_to_pool_detection() {
|
||||
let event = make_decoded_event(
|
||||
"meteora_dbc",
|
||||
"meteora_dbc.swap2",
|
||||
Some("Pool111"),
|
||||
Some("TokenA111"),
|
||||
Some("TokenB111"),
|
||||
);
|
||||
let route_option = crate::dex_detection_route::dex_detection_route(&event);
|
||||
let route = match route_option {
|
||||
Some(route) => route,
|
||||
None => panic!("route must be selected"),
|
||||
};
|
||||
assert_eq!(route, crate::dex_detection_route::DexDetectionRoute::MeteoraDbcPool);
|
||||
assert!(crate::dex_detection_route::dex_detection_route_requires_full_pool_context(
|
||||
route
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raydium_launchpad_initialize_route_requires_full_pool_context() {
|
||||
let event = make_decoded_event(
|
||||
|
||||
@@ -350,6 +350,9 @@ pub fn is_dex_trade_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.ends_with(".swap") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.ends_with(".swap2") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".swap_") {
|
||||
return true;
|
||||
}
|
||||
@@ -375,6 +378,22 @@ pub fn is_dex_candle_candidate_event_kind(event_kind: &str) -> bool {
|
||||
|
||||
/// Returns true for liquidity lifecycle changes that must not become candles.
|
||||
pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.starts_with("meteora_dbc.")
|
||||
&& (event_kind.contains("fee")
|
||||
|| event_kind.contains("surplus")
|
||||
|| event_kind.contains("leftover")
|
||||
|| event_kind.contains("zap_protocol_fee"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if event_kind == "meteora_dbc.migrate_meteora_damm_claim_lp_token"
|
||||
|| event_kind == "meteora_dbc.migrate_meteora_damm_lock_lp_token"
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if event_kind.starts_with("meteora_dbc.") && event_kind.contains("lp_token") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".withdraw_pnl") {
|
||||
return false;
|
||||
}
|
||||
@@ -490,6 +509,14 @@ pub fn is_dex_position_close_event_kind(event_kind: &str) -> bool {
|
||||
|
||||
/// Returns true for fee collection events.
|
||||
pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.starts_with("meteora_dbc.")
|
||||
&& (event_kind.contains("fee")
|
||||
|| event_kind.contains("surplus")
|
||||
|| event_kind.contains("leftover")
|
||||
|| event_kind.contains("zap_protocol_fee"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if event_kind.starts_with("pump_fees.")
|
||||
&& (event_kind.contains("donation_fee_pda_cranked")
|
||||
|| event_kind.contains("sweep_buyback")
|
||||
@@ -639,7 +666,7 @@ pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind == "raydium_amm_v4.pre_initialize" {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".create_lock_escrow") {
|
||||
if event_kind.contains(".create_lock_escrow") || event_kind.contains(".create_locker") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".initialize_bin_array") {
|
||||
@@ -731,6 +758,9 @@ pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".migrate_pool_coin_creator") {
|
||||
return false;
|
||||
}
|
||||
if event_kind.starts_with("meteora_dbc.") && event_kind.contains("metadata") {
|
||||
return false;
|
||||
}
|
||||
if event_kind.contains(".migrate") {
|
||||
return true;
|
||||
}
|
||||
@@ -742,6 +772,11 @@ pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
|
||||
|
||||
/// Returns true for pool creation or initialization events.
|
||||
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind == "meteora_dbc.evt_initialize_pool_event"
|
||||
|| event_kind.contains(".initialize_virtual_pool")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if event_kind == "raydium_amm_v4.pre_initialize" {
|
||||
return true;
|
||||
}
|
||||
@@ -824,6 +859,15 @@ pub fn is_dex_token_account_close_event_kind(event_kind: &str) -> bool {
|
||||
|
||||
/// Returns true for admin, configuration or permission changes.
|
||||
pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.starts_with("meteora_dbc.")
|
||||
&& (event_kind.contains("config")
|
||||
|| event_kind.contains("operator")
|
||||
|| event_kind.contains("metadata")
|
||||
|| event_kind.contains("pool_creator")
|
||||
|| event_kind == "meteora_dbc.transfer_pool_creator")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if event_kind.starts_with("pump_fees.")
|
||||
&& (event_kind.contains("authority")
|
||||
|| event_kind.contains("admin")
|
||||
@@ -1232,13 +1276,16 @@ mod tests {
|
||||
);
|
||||
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap"), "trade");
|
||||
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap_v2"), "trade");
|
||||
assert_eq!(super::classify_dex_event_category_code("meteora_dbc.swap2"), "trade");
|
||||
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.exact_output"), "trade");
|
||||
assert_eq!(super::classify_dex_event_category_code("pump_fun.buy"), "trade");
|
||||
assert_eq!(super::classify_dex_event_category_code("pump_fun.buy_v2"), "trade");
|
||||
assert_eq!(super::classify_dex_event_category_code("pump_fun.sell_v2"), "trade");
|
||||
assert_eq!(super::classify_dex_event_category_code("pump_fun.trade_event"), "trade");
|
||||
assert!(super::is_dex_trade_event_kind("raydium_cpmm.swap_base_input"));
|
||||
assert!(super::is_dex_trade_event_kind("meteora_dbc.swap2"));
|
||||
assert!(super::is_dex_candle_candidate_event_kind("raydium_cpmm.swap_base_input"));
|
||||
assert!(super::is_dex_candle_candidate_event_kind("meteora_dbc.swap2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -65,6 +65,25 @@ impl DexEventCoverageService {
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
if decoder_code.as_deref().is_none() || decoder_code.as_deref() == Some("meteora_dbc") {
|
||||
let supplemental_entries = local_meteora_dbc_registry_entries();
|
||||
for entry in &supplemental_entries {
|
||||
let coverage_entry = build_coverage_entry_from_upstream(entry);
|
||||
let upsert_result = crate::query_dex_event_coverage_entries_upsert(
|
||||
self.database.as_ref(),
|
||||
&coverage_entry,
|
||||
)
|
||||
.await;
|
||||
match upsert_result {
|
||||
Ok(_) => upserted_entry_count += 1,
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
return Ok((
|
||||
search_result.entries.len() + supplemental_entries.len(),
|
||||
upserted_entry_count,
|
||||
));
|
||||
}
|
||||
return Ok((search_result.entries.len(), upserted_entry_count));
|
||||
}
|
||||
|
||||
@@ -99,6 +118,11 @@ impl DexEventCoverageService {
|
||||
if let Err(error) = duplicate_cleanup_result {
|
||||
return Err(error);
|
||||
}
|
||||
let meteora_dbc_duplicate_cleanup_result =
|
||||
self.cleanup_duplicate_meteora_dbc_logical_coverage_rows(&decoder_code).await;
|
||||
if let Err(error) = meteora_dbc_duplicate_cleanup_result {
|
||||
return Err(error);
|
||||
}
|
||||
let refreshed_entry_count = match &decoder_code {
|
||||
Some(decoder_code) => {
|
||||
let refresh_result =
|
||||
@@ -159,6 +183,11 @@ impl DexEventCoverageService {
|
||||
if let Err(error) = duplicate_cleanup_result {
|
||||
return Err(error);
|
||||
}
|
||||
let meteora_dbc_duplicate_cleanup_result =
|
||||
self.cleanup_duplicate_meteora_dbc_logical_coverage_rows(&decoder_code).await;
|
||||
if let Err(error) = meteora_dbc_duplicate_cleanup_result {
|
||||
return Err(error);
|
||||
}
|
||||
let refreshed_entry_count = match &decoder_code {
|
||||
Some(decoder_code) => {
|
||||
let refresh_result =
|
||||
@@ -278,6 +307,50 @@ WHERE decoder_code = 'pump_swap'
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup_duplicate_meteora_dbc_logical_coverage_rows(
|
||||
&self,
|
||||
decoder_code: &std::option::Option<std::string::String>,
|
||||
) -> Result<u64, crate::Error> {
|
||||
if let Some(decoder_code) = decoder_code {
|
||||
if decoder_code != "meteora_dbc" {
|
||||
return Ok(0);
|
||||
}
|
||||
}
|
||||
match self.database.connection() {
|
||||
crate::DatabaseConnection::Sqlite(pool) => {
|
||||
let query_result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM k_sol_dex_event_coverage_entries
|
||||
WHERE decoder_code = 'meteora_dbc'
|
||||
AND id NOT IN (
|
||||
SELECT MIN(id)
|
||||
FROM k_sol_dex_event_coverage_entries
|
||||
WHERE decoder_code = 'meteora_dbc'
|
||||
GROUP BY
|
||||
decoder_code,
|
||||
COALESCE(program_id, ''),
|
||||
entry_kind,
|
||||
entry_name,
|
||||
COALESCE(discriminator_hex, ''),
|
||||
COALESCE(local_event_kind, '')
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
match query_result {
|
||||
Ok(query_result) => return Ok(query_result.rows_affected()),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot delete duplicate Meteora DBC logical coverage rows on sqlite: {}",
|
||||
error
|
||||
)));
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_coverage_entry_from_upstream(
|
||||
@@ -309,6 +382,353 @@ fn build_coverage_entry_from_upstream(
|
||||
return coverage_entry;
|
||||
}
|
||||
|
||||
fn local_meteora_dbc_registry_entries() -> std::vec::Vec<crate::UpstreamRegistryEntryDto> {
|
||||
let mut entries = std::vec::Vec::new();
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"anchor_self_cpi_log",
|
||||
Some("e445a52e51cb9a1d"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"instruction_audit",
|
||||
None,
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"claim_creator_trading_fee",
|
||||
Some("52dcfabd03556b2d"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"claim_partner_pool_creation_fee",
|
||||
Some("faee1a048b0a65f8"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"claim_protocol_fee",
|
||||
Some("a5e4853063f9ff21"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"claim_protocol_pool_creation_fee",
|
||||
Some("72cd53bcf0991936"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"claim_trading_fee",
|
||||
Some("08ec5931987db151"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"close_claim_protocol_fee_operator",
|
||||
Some("082957235030791a"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"close_operator_account",
|
||||
Some("ab09d54a7817031d"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"create_config",
|
||||
Some("c9cff3724b6f2fbd"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"create_locker",
|
||||
Some("a75a899a4b2f1154"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"create_operator_account",
|
||||
Some("dd40f695f099e5a3"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"create_partner_metadata",
|
||||
Some("c0a8eabfbce2e3ff"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"create_virtual_pool_metadata",
|
||||
Some("2d61bb67fe6d7c86"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"creator_withdraw_surplus",
|
||||
Some("a50389071c864c50"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"initialize_virtual_pool_with_spl_token",
|
||||
Some("8c55d7b06636684f"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"initialize_virtual_pool_with_token2022",
|
||||
Some("a976334e916edc9b"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"migrate_meteora_damm",
|
||||
Some("1b013016b43f76d9"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"migrate_meteora_damm_claim_lp_token",
|
||||
Some("8b85021e5b917f9a"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"migrate_meteora_damm_lock_lp_token",
|
||||
Some("b137ee9dfb58a52a"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"migration_damm_v2",
|
||||
Some("9ca9e66735e45040"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"migration_damm_v2_create_metadata",
|
||||
Some("6dbd1324c3b7de52"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"migration_meteora_damm_create_metadata",
|
||||
Some("2f5e7e73dde2c285"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"partner_withdraw_surplus",
|
||||
Some("a8ad4864c962265c"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"swap",
|
||||
Some("f8c69e91e17587c8"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"swap2",
|
||||
Some("414b3f4ceb5b5b88"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"transfer_pool_creator",
|
||||
Some("1407a9213a93a621"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"withdraw_leftover",
|
||||
Some("14c6caedebf3b742"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"withdraw_migration_fee",
|
||||
Some("ed8e2d178106dea2"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_INSTRUCTION,
|
||||
"zap_protocol_fee",
|
||||
Some("d59bbb2238b65bf0"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_claim_creator_trading_fee_event_local_idl",
|
||||
Some("9ae4d7ca859bd68a"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_claim_pool_creation_fee_event_local_idl",
|
||||
Some("956f952c8840af3e"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_claim_protocol_fee_event_local_idl",
|
||||
Some("baf44bfbbc0d1921"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_claim_protocol_liquidity_migration_fee_event_local_idl",
|
||||
Some("51a8741fa1561b23"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_claim_trading_fee_event_local_idl",
|
||||
Some("1a5375f05cca70fe"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_close_claim_fee_operator_event_local_idl",
|
||||
Some("6f2725376ed8c217"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_create_claim_fee_operator_event_local_idl",
|
||||
Some("1506997844741cb1"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_create_config_event_local_idl",
|
||||
Some("83cfb4aeb449a536"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_create_config_v2_event_local_idl",
|
||||
Some("a34a42bb77c31a90"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_create_meteora_migration_metadata_event_local_idl",
|
||||
Some("63a7853fd68faf8b"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_creator_withdraw_surplus_event_local_idl",
|
||||
Some("9849150f4257359d"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_curve_complete_event_local_idl",
|
||||
Some("e5e756549c864b18"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_initialize_pool_event_local_idl",
|
||||
Some("e432f655cb428625"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_partner_claim_pool_creation_fee_event_local_idl",
|
||||
Some("aedf2c96916259c3"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_partner_metadata_event_local_idl",
|
||||
Some("c87f06370d200896"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_partner_withdraw_migration_fee_event_local_idl",
|
||||
Some("b5697f4308bb7839"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_partner_withdraw_surplus_event_local_idl",
|
||||
Some("c3389809e8482316"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_swap_event_local_idl",
|
||||
Some("1b3c15d58aaabb93"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_swap2_event_local_idl",
|
||||
Some("bd4233a826507599"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_update_pool_creator_event_local_idl",
|
||||
Some("6be1a5ed5b9ed5dc"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_virtual_pool_metadata_event_local_idl",
|
||||
Some("bc12484cc35b264a"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_withdraw_leftover_event_local_idl",
|
||||
Some("bfbd688f6f9c5ee5"),
|
||||
);
|
||||
add_local_meteora_dbc_entry(
|
||||
&mut entries,
|
||||
crate::ENTRY_KIND_EVENT,
|
||||
"evt_withdraw_migration_fee_event_local_idl",
|
||||
Some("1acb5455a11764d6"),
|
||||
);
|
||||
return entries;
|
||||
}
|
||||
|
||||
fn add_local_meteora_dbc_entry(
|
||||
entries: &mut std::vec::Vec<crate::UpstreamRegistryEntryDto>,
|
||||
entry_kind: &str,
|
||||
entry_name: &str,
|
||||
discriminator_hex: std::option::Option<&str>,
|
||||
) {
|
||||
entries.push(crate::UpstreamRegistryEntryDto {
|
||||
source_repo: Some("local_idl".to_string()),
|
||||
source_path: Some(
|
||||
"idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json".to_string(),
|
||||
),
|
||||
decoder_code: "meteora_dbc".to_string(),
|
||||
program_id: Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
|
||||
program_family: "meteora".to_string(),
|
||||
surface_kind: "launch_bonding_curve".to_string(),
|
||||
entry_kind: entry_kind.to_string(),
|
||||
entry_name: entry_name.to_string(),
|
||||
discriminator_hex: discriminator_hex.map(|value| return value.to_string()),
|
||||
discriminator_len: discriminator_hex.map(|_| return 8_u16),
|
||||
proof_status: crate::PROOF_STATUS_UPSTREAM_GIT_MAPPED_UNVERIFIED.to_string(),
|
||||
notes: "supplemental local Meteora DBC IDL coverage row".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
fn infer_expected_db_target_for_entry(
|
||||
decoder_code: &str,
|
||||
entry_name: &str,
|
||||
@@ -324,6 +744,9 @@ fn infer_expected_db_target_for_entry(
|
||||
if decoder_code == "pump_fees" {
|
||||
return infer_pump_fees_expected_db_target(entry_name, entry_kind);
|
||||
}
|
||||
if decoder_code == "meteora_dbc" {
|
||||
return infer_meteora_dbc_expected_db_target(entry_name, entry_kind);
|
||||
}
|
||||
if decoder_code == "raydium_cpmm"
|
||||
&& (entry_name == "swap_event" || entry_name == "anchor_idl_instruction")
|
||||
{
|
||||
@@ -1094,9 +1517,236 @@ fn infer_event_family_for_entry(
|
||||
if decoder_code == "raydium_stable_swap" {
|
||||
return infer_raydium_stable_swap_event_family(entry_name, entry_kind);
|
||||
}
|
||||
if decoder_code == "meteora_dbc" {
|
||||
return infer_meteora_dbc_event_family(entry_name, entry_kind);
|
||||
}
|
||||
return infer_event_family(entry_name, entry_kind);
|
||||
}
|
||||
|
||||
fn infer_meteora_dbc_expected_db_target(
|
||||
entry_name: &str,
|
||||
entry_kind: &str,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
if entry_kind == crate::ENTRY_KIND_PROGRAM || entry_kind == crate::ENTRY_KIND_ACCOUNT {
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
|
||||
}
|
||||
let family = infer_meteora_dbc_event_family(entry_name, entry_kind);
|
||||
let family = match family {
|
||||
Some(family) => family,
|
||||
None => {
|
||||
return Some(
|
||||
crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let normalized_entry_name = entry_name.strip_suffix("_local_idl").unwrap_or(entry_name);
|
||||
if family == "swap" && (normalized_entry_name == "swap" || normalized_entry_name == "swap2") {
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string());
|
||||
}
|
||||
if family == "swap" {
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
|
||||
}
|
||||
if entry_kind == crate::ENTRY_KIND_EVENT
|
||||
&& meteora_dbc_anchor_event_is_decoded_only_source(normalized_entry_name)
|
||||
{
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
|
||||
}
|
||||
if family == "pool_create" || family == "migration" || family == "pool_lifecycle" {
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string());
|
||||
}
|
||||
if family == "liquidity" {
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS.to_string());
|
||||
}
|
||||
if family == "fee" {
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_FEE_EVENTS.to_string());
|
||||
}
|
||||
if family == "admin_config" {
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string());
|
||||
}
|
||||
if family == "audit" {
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
|
||||
}
|
||||
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
|
||||
}
|
||||
|
||||
fn meteora_dbc_anchor_event_is_decoded_only_source(entry_name: &str) -> bool {
|
||||
return matches!(
|
||||
entry_name,
|
||||
"evt_claim_creator_trading_fee_event"
|
||||
| "evt_claim_pool_creation_fee_event"
|
||||
| "evt_claim_protocol_fee_event"
|
||||
| "evt_claim_protocol_liquidity_migration_fee_event"
|
||||
| "evt_claim_trading_fee_event"
|
||||
| "evt_close_claim_fee_operator_event"
|
||||
| "evt_create_claim_fee_operator_event"
|
||||
| "evt_create_config_event"
|
||||
| "evt_create_config_v2_event"
|
||||
| "evt_create_meteora_migration_metadata_event"
|
||||
| "evt_creator_withdraw_surplus_event"
|
||||
| "evt_initialize_pool_event"
|
||||
| "evt_partner_claim_pool_creation_fee_event"
|
||||
| "evt_partner_metadata_event"
|
||||
| "evt_partner_withdraw_migration_fee_event"
|
||||
| "evt_partner_withdraw_surplus_event"
|
||||
| "evt_update_pool_creator_event"
|
||||
| "evt_virtual_pool_metadata_event"
|
||||
| "evt_withdraw_leftover_event"
|
||||
| "evt_withdraw_migration_fee_event"
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_meteora_dbc_event_family(
|
||||
entry_name: &str,
|
||||
entry_kind: &str,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
if entry_kind == crate::ENTRY_KIND_PROGRAM {
|
||||
return None;
|
||||
}
|
||||
if entry_name == "anchor_self_cpi_log" || entry_name == "instruction_audit" {
|
||||
return Some("audit".to_string());
|
||||
}
|
||||
if entry_name == "swap"
|
||||
|| entry_name == "swap2"
|
||||
|| entry_name == "evt_swap_event"
|
||||
|| entry_name == "evt_swap2_event"
|
||||
{
|
||||
return Some("swap".to_string());
|
||||
}
|
||||
if entry_name == "initialize_virtual_pool_with_spl_token"
|
||||
|| entry_name == "initialize_virtual_pool_with_token2022"
|
||||
|| entry_name == "evt_initialize_pool_event"
|
||||
{
|
||||
return Some("pool_create".to_string());
|
||||
}
|
||||
if entry_name == "create_locker" {
|
||||
return Some("pool_lifecycle".to_string());
|
||||
}
|
||||
if entry_name.contains("metadata") {
|
||||
return Some("admin_config".to_string());
|
||||
}
|
||||
if entry_name.contains("fee")
|
||||
|| entry_name.contains("surplus")
|
||||
|| entry_name.contains("leftover")
|
||||
|| entry_name == "zap_protocol_fee"
|
||||
{
|
||||
return Some("fee".to_string());
|
||||
}
|
||||
if entry_name.contains("migrate")
|
||||
|| entry_name.contains("migration")
|
||||
|| entry_name == "evt_curve_complete_event"
|
||||
{
|
||||
return Some("migration".to_string());
|
||||
}
|
||||
if entry_name.contains("lp_token") {
|
||||
return Some("liquidity".to_string());
|
||||
}
|
||||
if entry_name.contains("config")
|
||||
|| entry_name.contains("operator")
|
||||
|| entry_name.contains("metadata")
|
||||
|| entry_name == "transfer_pool_creator"
|
||||
|| entry_name == "evt_update_pool_creator_event"
|
||||
|| entry_name == "evt_partner_metadata_event"
|
||||
|| entry_name == "evt_virtual_pool_metadata_event"
|
||||
{
|
||||
return Some("admin_config".to_string());
|
||||
}
|
||||
return infer_event_family(entry_name, entry_kind);
|
||||
}
|
||||
|
||||
fn meteora_dbc_local_event_kind(entry_name: &str) -> std::option::Option<std::string::String> {
|
||||
if entry_name == "program" || entry_name == "state" {
|
||||
return None;
|
||||
}
|
||||
let normalized_event_name = match entry_name.strip_suffix("_local_idl") {
|
||||
Some(value) => value,
|
||||
None => entry_name,
|
||||
};
|
||||
if normalized_event_name == "anchor_self_cpi_log"
|
||||
|| normalized_event_name == "instruction_audit"
|
||||
{
|
||||
return Some("meteora_dbc.instruction_audit".to_string());
|
||||
}
|
||||
if normalized_event_name == "initialize_virtual_pool_with_spl_token"
|
||||
|| normalized_event_name == "initialize_virtual_pool_with_token2022"
|
||||
{
|
||||
return Some("meteora_dbc.create_pool".to_string());
|
||||
}
|
||||
if entry_name.starts_with("evt_") && !entry_name.ends_with("_local_idl") {
|
||||
return None;
|
||||
}
|
||||
if meteora_dbc_local_instruction_entry_is_known(entry_name)
|
||||
|| meteora_dbc_local_event_entry_is_known(normalized_event_name)
|
||||
{
|
||||
return Some(format!("meteora_dbc.{}", normalized_event_name));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn meteora_dbc_local_instruction_entry_is_known(entry_name: &str) -> bool {
|
||||
return matches!(
|
||||
entry_name,
|
||||
"anchor_self_cpi_log"
|
||||
| "instruction_audit"
|
||||
| "claim_creator_trading_fee"
|
||||
| "claim_partner_pool_creation_fee"
|
||||
| "claim_protocol_fee"
|
||||
| "claim_protocol_pool_creation_fee"
|
||||
| "claim_trading_fee"
|
||||
| "close_claim_protocol_fee_operator"
|
||||
| "close_operator_account"
|
||||
| "create_config"
|
||||
| "create_locker"
|
||||
| "create_operator_account"
|
||||
| "create_partner_metadata"
|
||||
| "create_virtual_pool_metadata"
|
||||
| "creator_withdraw_surplus"
|
||||
| "initialize_virtual_pool_with_spl_token"
|
||||
| "initialize_virtual_pool_with_token2022"
|
||||
| "migrate_meteora_damm"
|
||||
| "migrate_meteora_damm_claim_lp_token"
|
||||
| "migrate_meteora_damm_lock_lp_token"
|
||||
| "migration_damm_v2"
|
||||
| "migration_damm_v2_create_metadata"
|
||||
| "migration_meteora_damm_create_metadata"
|
||||
| "partner_withdraw_surplus"
|
||||
| "swap"
|
||||
| "swap2"
|
||||
| "transfer_pool_creator"
|
||||
| "withdraw_leftover"
|
||||
| "withdraw_migration_fee"
|
||||
| "zap_protocol_fee"
|
||||
);
|
||||
}
|
||||
|
||||
fn meteora_dbc_local_event_entry_is_known(entry_name: &str) -> bool {
|
||||
return matches!(
|
||||
entry_name,
|
||||
"evt_claim_creator_trading_fee_event"
|
||||
| "evt_claim_pool_creation_fee_event"
|
||||
| "evt_claim_protocol_fee_event"
|
||||
| "evt_claim_protocol_liquidity_migration_fee_event"
|
||||
| "evt_claim_trading_fee_event"
|
||||
| "evt_close_claim_fee_operator_event"
|
||||
| "evt_create_claim_fee_operator_event"
|
||||
| "evt_create_config_event"
|
||||
| "evt_create_config_v2_event"
|
||||
| "evt_create_meteora_migration_metadata_event"
|
||||
| "evt_creator_withdraw_surplus_event"
|
||||
| "evt_curve_complete_event"
|
||||
| "evt_initialize_pool_event"
|
||||
| "evt_partner_claim_pool_creation_fee_event"
|
||||
| "evt_partner_metadata_event"
|
||||
| "evt_partner_withdraw_migration_fee_event"
|
||||
| "evt_partner_withdraw_surplus_event"
|
||||
| "evt_swap_event"
|
||||
| "evt_swap2_event"
|
||||
| "evt_update_pool_creator_event"
|
||||
| "evt_virtual_pool_metadata_event"
|
||||
| "evt_withdraw_leftover_event"
|
||||
| "evt_withdraw_migration_fee_event"
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_raydium_amm_v4_event_family(
|
||||
entry_name: &str,
|
||||
entry_kind: &str,
|
||||
@@ -1479,6 +2129,9 @@ pub(crate) fn known_local_event_kind(
|
||||
if decoder_code == "pump_swap" {
|
||||
return pump_swap_local_event_kind(entry_name);
|
||||
}
|
||||
if decoder_code == "meteora_dbc" {
|
||||
return meteora_dbc_local_event_kind(entry_name);
|
||||
}
|
||||
if decoder_code == "raydium_amm_v4" {
|
||||
return raydium_amm_v4_local_event_kind(entry_name);
|
||||
}
|
||||
|
||||
@@ -200,6 +200,45 @@ fn resolve_instruction_name(
|
||||
Some(discriminator_hex) => discriminator_hex,
|
||||
None => return None,
|
||||
};
|
||||
if program_id == crate::METEORA_DBC_PROGRAM_ID || decoder_code == Some("meteora_dbc") {
|
||||
let name = match discriminator_hex {
|
||||
"e445a52e51cb9a1d" => "meteora_dbc.anchor_self_cpi_log",
|
||||
"e992d18ecf6840bc" => "meteora_dbc.create_pool_legacy",
|
||||
"5fb40aac54aee828" => "meteora_dbc.initialize_pool_legacy",
|
||||
"a677d1b6d66d3ab5" => "meteora_dbc.launch_pool_legacy",
|
||||
"52dcfabd03556b2d" => "meteora_dbc.claim_creator_trading_fee",
|
||||
"faee1a048b0a65f8" => "meteora_dbc.claim_partner_pool_creation_fee",
|
||||
"a5e4853063f9ff21" => "meteora_dbc.claim_protocol_fee",
|
||||
"72cd53bcf0991936" => "meteora_dbc.claim_protocol_pool_creation_fee",
|
||||
"08ec5931987db151" => "meteora_dbc.claim_trading_fee",
|
||||
"082957235030791a" => "meteora_dbc.close_claim_protocol_fee_operator",
|
||||
"ab09d54a7817031d" => "meteora_dbc.close_operator_account",
|
||||
"c9cff3724b6f2fbd" => "meteora_dbc.create_config",
|
||||
"a75a899a4b2f1154" => "meteora_dbc.create_locker",
|
||||
"dd40f695f099e5a3" => "meteora_dbc.create_operator_account",
|
||||
"c0a8eabfbce2e3ff" => "meteora_dbc.create_partner_metadata",
|
||||
"2d61bb67fe6d7c86" => "meteora_dbc.create_virtual_pool_metadata",
|
||||
"a50389071c864c50" => "meteora_dbc.creator_withdraw_surplus",
|
||||
"8c55d7b06636684f" => "meteora_dbc.initialize_virtual_pool_with_spl_token",
|
||||
"a976334e916edc9b" => "meteora_dbc.initialize_virtual_pool_with_token2022",
|
||||
"1b013016b43f76d9" => "meteora_dbc.migrate_meteora_damm",
|
||||
"8b85021e5b917f9a" => "meteora_dbc.migrate_meteora_damm_claim_lp_token",
|
||||
"b137ee9dfb58a52a" => "meteora_dbc.migrate_meteora_damm_lock_lp_token",
|
||||
"9ca9e66735e45040" => "meteora_dbc.migration_damm_v2",
|
||||
"6dbd1324c3b7de52" => "meteora_dbc.migration_damm_v2_create_metadata",
|
||||
"2f5e7e73dde2c285" => "meteora_dbc.migration_meteora_damm_create_metadata",
|
||||
"a8ad4864c962265c" => "meteora_dbc.partner_withdraw_surplus",
|
||||
"3688e18aacb6d6a7" => "meteora_dbc.protocol_withdraw_surplus",
|
||||
"f8c69e91e17587c8" => "meteora_dbc.swap",
|
||||
"414b3f4ceb5b5b88" => "meteora_dbc.swap2",
|
||||
"1407a9213a93a621" => "meteora_dbc.transfer_pool_creator",
|
||||
"14c6caedebf3b742" => "meteora_dbc.withdraw_leftover",
|
||||
"ed8e2d178106dea2" => "meteora_dbc.withdraw_migration_fee",
|
||||
"d59bbb2238b65bf0" => "meteora_dbc.zap_protocol_fee",
|
||||
_ => return None,
|
||||
};
|
||||
return Some(name.to_string());
|
||||
}
|
||||
if program_id == crate::RAYDIUM_AMM_V4_PROGRAM_ID || decoder_code == Some("raydium_amm_v4") {
|
||||
let name = match discriminator_hex {
|
||||
"00" => "raydium_amm_v4.initialize",
|
||||
@@ -536,3 +575,31 @@ fn option_i64_key(value: std::option::Option<i64>) -> std::string::String {
|
||||
None => return "-".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn resolves_meteora_dbc_instruction_observation_names() {
|
||||
let anchor_name = super::resolve_instruction_name(
|
||||
crate::METEORA_DBC_PROGRAM_ID,
|
||||
Some("meteora_dbc"),
|
||||
Some("e445a52e51cb9a1d"),
|
||||
);
|
||||
assert_eq!(anchor_name.as_deref(), Some("meteora_dbc.anchor_self_cpi_log"));
|
||||
let swap2_name = super::resolve_instruction_name(
|
||||
crate::METEORA_DBC_PROGRAM_ID,
|
||||
Some("meteora_dbc"),
|
||||
Some("414b3f4ceb5b5b88"),
|
||||
);
|
||||
assert_eq!(swap2_name.as_deref(), Some("meteora_dbc.swap2"));
|
||||
let create_name = super::resolve_instruction_name(
|
||||
crate::METEORA_DBC_PROGRAM_ID,
|
||||
Some("meteora_dbc"),
|
||||
Some("8c55d7b06636684f"),
|
||||
);
|
||||
assert_eq!(
|
||||
create_name.as_deref(),
|
||||
Some("meteora_dbc.initialize_virtual_pool_with_spl_token")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +497,10 @@ pub use db::DexEventCoverageEntryEntity;
|
||||
pub use db::DexEventCoverageSummaryDto;
|
||||
/// Aggregated DEX event coverage summary row.
|
||||
pub use db::DexEventCoverageSummaryEntity;
|
||||
/// Application-facing normalized fee event amount leg DTO.
|
||||
pub use db::FeeEventAmountDto;
|
||||
/// Persisted normalized fee event amount leg row.
|
||||
pub use db::FeeEventAmountEntity;
|
||||
/// Normalized fee event persisted from useful non-trade DEX events.
|
||||
pub use db::FeeEventDto;
|
||||
/// Persisted fee event row.
|
||||
@@ -655,10 +659,6 @@ pub use db::ProgramInstructionDiscriminatorRowEntity;
|
||||
/// Aggregated instruction discriminator diagnostic row.
|
||||
pub use db::ProgramInstructionDiscriminatorSummaryDto;
|
||||
/// Application-facing protocol candidate DTO.
|
||||
///
|
||||
/// A protocol candidate records a program/instruction that should be inspected
|
||||
/// later because it may correspond to an unsupported DEX, launch surface,
|
||||
/// migration path or protocol-specific non-trade event.
|
||||
pub use db::ProtocolCandidateDto;
|
||||
/// Persisted protocol candidate row.
|
||||
pub use db::ProtocolCandidateEntity;
|
||||
@@ -804,13 +804,22 @@ pub use db::query_dexs_get_by_code;
|
||||
pub use db::query_dexs_list;
|
||||
/// Inserts or updates one normalized DEX row by code.
|
||||
pub use db::query_dexs_upsert;
|
||||
/// Deletes amount legs for one normalized fee event.
|
||||
pub use db::query_fee_event_amounts_backfill_single_leg_from_fee_events;
|
||||
/// Deletes fee amount legs for one parent fee event.
|
||||
pub use db::query_fee_event_amounts_delete_by_fee_event_id;
|
||||
/// Lists amount legs for one normalized fee event.
|
||||
pub use db::query_fee_event_amounts_list_by_fee_event_id;
|
||||
/// Replaces amount legs for one normalized fee event.
|
||||
pub use db::query_fee_event_amounts_replace_for_fee_event;
|
||||
/// Returns one fee event by decoded-event id.
|
||||
pub use db::query_fee_events_get_by_decoded_event_id;
|
||||
/// Lists recent fee events ordered from newest to oldest.
|
||||
pub use db::query_fee_events_list_recent;
|
||||
/// Inserts or updates one normalized fee event row.
|
||||
pub use db::query_fee_events_upsert;
|
||||
/// Inserts one on-chain observation row and returns its numeric id.
|
||||
/// Inserts or updates a fee parent and atomically replaces its amount legs.
|
||||
pub use db::query_fee_events_upsert_with_amount_legs;
|
||||
/// Lists instruction-observation source rows for one transaction signature.
|
||||
pub use db::query_instruction_observation_source_rows_list_by_signature;
|
||||
/// Lists recent instruction-observation source rows.
|
||||
@@ -984,8 +993,6 @@ pub use db::query_program_instruction_discriminator_summaries_list_by_program_id
|
||||
/// Lists protocol candidate summaries ordered by investigation priority.
|
||||
pub use db::query_protocol_candidate_summaries_list_by_priority;
|
||||
/// Deletes protocol candidates for one transaction.
|
||||
///
|
||||
/// This is useful before recomputing candidates for a replayed transaction.
|
||||
pub use db::query_protocol_candidates_delete_by_transaction_id;
|
||||
/// Inserts one protocol candidate row.
|
||||
pub use db::query_protocol_candidates_insert;
|
||||
@@ -1133,6 +1140,8 @@ pub use dex::MeteoraDbcCreatePoolDecoded;
|
||||
pub use dex::MeteoraDbcDecodedEvent;
|
||||
/// Meteora DBC decoder.
|
||||
pub use dex::MeteoraDbcDecoder;
|
||||
/// Decoded Meteora DBC instruction or Anchor event.
|
||||
pub use dex::MeteoraDbcInstructionDecoded;
|
||||
/// Decoded Meteora DBC swap event.
|
||||
pub use dex::MeteoraDbcSwapDecoded;
|
||||
/// Decoded Meteora DLMM create-pool event.
|
||||
@@ -1293,6 +1302,7 @@ pub use dex_event_classification::is_dex_admin_event_kind;
|
||||
pub use dex_event_classification::is_dex_candle_candidate_event_kind;
|
||||
/// Returns true for fee collection DEX events.
|
||||
pub use dex_event_classification::is_dex_fee_event_kind;
|
||||
/// Returns true for decoded audit-only events retained for corpus analysis.
|
||||
pub use dex_event_classification::is_dex_informational_event_kind;
|
||||
/// Returns true for launch or bonding-curve creation DEX events.
|
||||
pub use dex_event_classification::is_dex_launch_event_kind;
|
||||
@@ -1347,9 +1357,6 @@ pub use dex_support_matrix::dex_support_matrix_entry_by_program_id;
|
||||
/// Returns all DEX support matrix entries as owned DTOs.
|
||||
pub use dex_support_matrix::dex_support_matrix_entry_dtos;
|
||||
/// Global error type used by the `kb_lib` crate.
|
||||
///
|
||||
/// The project intentionally avoids `anyhow` and `thiserror`, so this
|
||||
/// enum centralizes the main error families with explicit textual messages.
|
||||
pub use error::Error;
|
||||
/// Generic asynchronous HTTP client.
|
||||
pub use http_client::HttpClient;
|
||||
@@ -1394,19 +1401,10 @@ pub use json_rpc_ws::JsonRpcWsRequest;
|
||||
/// JSON-RPC 2.0 success response.
|
||||
pub use json_rpc_ws::JsonRpcWsSuccessResponse;
|
||||
/// Parses a JSON value into a JSON-RPC incoming message.
|
||||
///
|
||||
/// This parser accepts only server-originating incoming message shapes:
|
||||
/// success responses, error responses, and notifications.
|
||||
pub use json_rpc_ws::is_probable_json_rpc_object_text;
|
||||
/// Parses a raw text message into a JSON-RPC incoming message.
|
||||
///
|
||||
/// This parser accepts only server-originating incoming message shapes:
|
||||
/// success responses, error responses, and notifications.
|
||||
pub use json_rpc_ws::parse_json_rpc_ws_incoming_text;
|
||||
/// Parses a JSON value into a JSON-RPC incoming message.
|
||||
///
|
||||
/// This parser accepts only server-originating incoming message shapes:
|
||||
/// success responses, error responses, and notifications.
|
||||
pub use json_rpc_ws::parse_json_rpc_ws_incoming_value;
|
||||
/// Result of one launch surface attribution.
|
||||
pub use launch_origin::LaunchAttributionResult;
|
||||
@@ -1467,14 +1465,8 @@ pub use pair_analytic_signal::PairAnalyticSignalService;
|
||||
/// One pair-candle aggregation result.
|
||||
pub use pair_candle_aggregation::PairCandleAggregationResult;
|
||||
/// Pair-candle aggregation service.
|
||||
///
|
||||
/// This service materializes a small set of standard timeframes in base storage.
|
||||
/// Arbitrary timeframes are rebuilt on demand through `PairCandleQueryService`.
|
||||
pub use pair_candle_aggregation::PairCandleAggregationService;
|
||||
/// Pair-candle query service.
|
||||
///
|
||||
/// Standard materialized timeframes are served from base storage.
|
||||
/// Arbitrary timeframes are rebuilt on demand from `trade_events`.
|
||||
pub use pair_candle_query::PairCandleQueryService;
|
||||
/// Summary produced by a pair-symbol refresh pass.
|
||||
pub use pair_symbol::PairSymbolRefreshResult;
|
||||
@@ -1493,11 +1485,6 @@ pub use solana_pubsub_ws::SolanaWsTypedNotification;
|
||||
/// Parses a Solana JSON-RPC notification into an official typed payload.
|
||||
pub use solana_pubsub_ws::parse_solana_ws_typed_notification;
|
||||
/// Parses a typed Solana PubSub notification from a generic websocket event.
|
||||
///
|
||||
/// This returns:
|
||||
/// - `Ok(Some(...))` for JSON-RPC notification-bearing events
|
||||
/// - `Ok(None)` for events that do not carry a notification
|
||||
/// - `Err(...)` when a notification is present but cannot be decoded
|
||||
pub use solana_pubsub_ws::parse_solana_ws_typed_notification_from_event;
|
||||
/// One pool-backfill result summary.
|
||||
pub use token_backfill::PoolBackfillResult;
|
||||
@@ -1510,22 +1497,14 @@ pub use token_backfill::SignatureBatchBackfillResult;
|
||||
/// One token-backfill result summary.
|
||||
pub use token_backfill::TokenBackfillResult;
|
||||
/// Historical token backfill service.
|
||||
///
|
||||
/// This service reuses the existing transaction projection and downstream
|
||||
/// DEX pipeline instead of introducing a separate historical code path.
|
||||
pub use token_backfill::TokenBackfillService;
|
||||
/// Summary produced by a token metadata backfill pass.
|
||||
pub use token_metadata::TokenMetadataBackfillResult;
|
||||
/// Service that enriches persisted token rows with mint and display metadata.
|
||||
pub use token_metadata::TokenMetadataBackfillService;
|
||||
/// Guard keeping non-blocking tracing workers alive.
|
||||
///
|
||||
/// The guard must remain alive during the whole application lifetime so that
|
||||
/// buffered log records are flushed correctly.
|
||||
pub use tracing::TracingGuard;
|
||||
/// Initializes the global tracing subscriber.
|
||||
///
|
||||
/// This function is expected to be called once at application startup.
|
||||
pub use tracing::init_tracing;
|
||||
/// One trade-aggregation result.
|
||||
pub use trade_aggregation::TradeAggregationResult;
|
||||
@@ -1543,8 +1522,7 @@ pub use tx_resolution::TransactionResolutionRequest;
|
||||
pub use tx_resolution::TransactionResolutionService;
|
||||
/// One forwarded WebSocket notification envelope for transaction resolution.
|
||||
pub use tx_resolution::WsTransactionResolutionEnvelope;
|
||||
/// Relay that consumes forwarded WS notifications and resolves matching
|
||||
/// signatures through HTTP `getTransaction`.
|
||||
/// Relay that consumes forwarded WS notifications and resolves matching signatures through HTTP `getTransaction`.
|
||||
pub use tx_resolution::WsTransactionResolutionRelay;
|
||||
/// Runtime statistics for one transaction resolution relay worker.
|
||||
pub use tx_resolution::WsTransactionResolutionRelayStats;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -114,6 +114,24 @@ impl TradeAggregationService {
|
||||
continue;
|
||||
},
|
||||
};
|
||||
let payload_base_vault_address =
|
||||
crate::trade_aggregation::extract_payload_string_by_candidate_keys(
|
||||
&payload,
|
||||
&["baseVault", "base_vault", "baseVaultAddress", "base_vault_address"],
|
||||
);
|
||||
let payload_quote_vault_address =
|
||||
crate::trade_aggregation::extract_payload_string_by_candidate_keys(
|
||||
&payload,
|
||||
&["quoteVault", "quote_vault", "quoteVaultAddress", "quote_vault_address"],
|
||||
);
|
||||
let effective_base_vault_address = match base_vault_address.as_deref() {
|
||||
Some(base_vault_address) => Some(base_vault_address),
|
||||
None => payload_base_vault_address.as_deref(),
|
||||
};
|
||||
let effective_quote_vault_address = match quote_vault_address.as_deref() {
|
||||
Some(quote_vault_address) => Some(quote_vault_address),
|
||||
None => payload_quote_vault_address.as_deref(),
|
||||
};
|
||||
if !crate::is_decoded_event_trade_candidate(decoded_event.event_kind.as_str(), &payload)
|
||||
{
|
||||
tracing::debug!(
|
||||
@@ -150,8 +168,8 @@ impl TradeAggregationService {
|
||||
quote_token_mint: quote_token_mint.as_deref(),
|
||||
base_token_decimals,
|
||||
quote_token_decimals,
|
||||
base_vault_address: base_vault_address.as_deref(),
|
||||
quote_vault_address: quote_vault_address.as_deref(),
|
||||
base_vault_address: effective_base_vault_address,
|
||||
quote_vault_address: effective_quote_vault_address,
|
||||
};
|
||||
let amount_resolution =
|
||||
crate::trade_amount_resolution::resolve_trade_amounts(&amount_input).await;
|
||||
@@ -212,6 +230,29 @@ impl TradeAggregationService {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_payload_string_by_candidate_keys(
|
||||
payload: &serde_json::Value,
|
||||
keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let object = match payload.as_object() {
|
||||
Some(object) => object,
|
||||
None => return None,
|
||||
};
|
||||
for key in keys {
|
||||
let value = object.get(*key);
|
||||
let value = match value {
|
||||
Some(value) => value,
|
||||
None => continue,
|
||||
};
|
||||
if let Some(text) = value.as_str() {
|
||||
if !text.trim().is_empty() {
|
||||
return Some(text.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn should_skip_pump_fun_duplicate_trade_event(
|
||||
decoded_event: &crate::DexDecodedEventDto,
|
||||
decoded_events: &[crate::DexDecodedEventDto],
|
||||
|
||||
@@ -253,6 +253,36 @@ pub(crate) async fn resolve_trade_amounts(
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
if input.decoded_event.event_kind.starts_with("meteora_dbc.")
|
||||
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
|
||||
{
|
||||
let resolution_result =
|
||||
crate::trade_amount_resolution::apply_meteora_dbc_payload_transfer_amount_fallback(
|
||||
input,
|
||||
&mut base_amount_raw,
|
||||
&mut quote_amount_raw,
|
||||
&mut resolved_trade_side,
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = resolution_result {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
if input.decoded_event.event_kind.starts_with("meteora_dbc.")
|
||||
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
|
||||
{
|
||||
let resolution_result =
|
||||
crate::trade_amount_resolution::apply_meteora_dbc_flattened_cpi_amount_fallback(
|
||||
input,
|
||||
&mut base_amount_raw,
|
||||
&mut quote_amount_raw,
|
||||
&mut resolved_trade_side,
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = resolution_result {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
if input.decoded_event.event_kind.starts_with("meteora_dlmm.")
|
||||
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
|
||||
{
|
||||
@@ -283,6 +313,21 @@ pub(crate) async fn resolve_trade_amounts(
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
if input.decoded_event.event_kind.starts_with("meteora_dbc.")
|
||||
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
|
||||
{
|
||||
let resolution_result = crate::trade_amount_resolution::apply_vault_balance_delta_fallback(
|
||||
input,
|
||||
input.base_vault_address,
|
||||
input.quote_vault_address,
|
||||
&mut base_amount_raw,
|
||||
&mut quote_amount_raw,
|
||||
&mut price_quote_per_base,
|
||||
);
|
||||
if let Err(error) = resolution_result {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
if input.decoded_event.event_kind.starts_with("raydium_amm_v4.") {
|
||||
let vault_side = crate::trade_amount_resolution::infer_trade_side_from_vault_balance_deltas(
|
||||
input.transaction.meta_json.as_deref(),
|
||||
@@ -316,6 +361,17 @@ pub(crate) async fn resolve_trade_amounts(
|
||||
resolved_trade_side = vault_side;
|
||||
}
|
||||
}
|
||||
if input.decoded_event.event_kind.starts_with("meteora_dbc.") {
|
||||
let vault_side = crate::trade_amount_resolution::infer_trade_side_from_vault_balance_deltas(
|
||||
input.transaction.meta_json.as_deref(),
|
||||
input.transaction.transaction_json.as_str(),
|
||||
input.base_vault_address,
|
||||
input.quote_vault_address,
|
||||
);
|
||||
if vault_side.is_some() {
|
||||
resolved_trade_side = vault_side;
|
||||
}
|
||||
}
|
||||
if price_quote_per_base.is_none() {
|
||||
price_quote_per_base =
|
||||
crate::trade_metric_update::compute_price_quote_per_base_from_raw_amounts_with_decimals(
|
||||
@@ -452,10 +508,7 @@ async fn apply_pump_swap_amount_fallbacks(
|
||||
};
|
||||
let (input_vault_address, output_vault_address, input_token_account, output_token_account) =
|
||||
if input.decoded_event.event_kind.ends_with(".buy")
|
||||
|| input
|
||||
.decoded_event
|
||||
.event_kind
|
||||
.ends_with(".buy_exact_quote_in")
|
||||
|| input.decoded_event.event_kind.ends_with(".buy_exact_quote_in")
|
||||
{
|
||||
(
|
||||
effective_quote_vault_address,
|
||||
@@ -815,7 +868,8 @@ async fn apply_pump_fun_amount_fallback(
|
||||
*price_quote_per_base = inferred.2;
|
||||
}
|
||||
if base_amount_raw.is_none() || quote_amount_raw.is_none() || price_quote_per_base.is_none() {
|
||||
let sibling_result = crate::trade_amount_resolution::apply_pump_fun_trade_event_sibling_amount_fallback(
|
||||
let sibling_result =
|
||||
crate::trade_amount_resolution::apply_pump_fun_trade_event_sibling_amount_fallback(
|
||||
input,
|
||||
base_amount_raw,
|
||||
quote_amount_raw,
|
||||
@@ -1111,6 +1165,160 @@ async fn apply_meteora_damm_v1_flattened_cpi_amount_fallback(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn apply_meteora_dbc_flattened_cpi_amount_fallback(
|
||||
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
|
||||
base_amount_raw: &mut std::option::Option<std::string::String>,
|
||||
quote_amount_raw: &mut std::option::Option<std::string::String>,
|
||||
resolved_trade_side: &mut std::option::Option<crate::SwapTradeSide>,
|
||||
) -> Result<(), crate::Error> {
|
||||
return crate::trade_amount_resolution::apply_flattened_cpi_amount_fallback(
|
||||
input,
|
||||
"meteora_dbc",
|
||||
base_amount_raw,
|
||||
quote_amount_raw,
|
||||
resolved_trade_side,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn apply_meteora_dbc_payload_transfer_amount_fallback(
|
||||
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
|
||||
base_amount_raw: &mut std::option::Option<std::string::String>,
|
||||
quote_amount_raw: &mut std::option::Option<std::string::String>,
|
||||
resolved_trade_side: &mut std::option::Option<crate::SwapTradeSide>,
|
||||
) -> Result<(), crate::Error> {
|
||||
let payload_token_a_mint = crate::trade_amount_resolution::extract_string_by_candidate_keys(
|
||||
input.payload,
|
||||
&["tokenAMint", "token_a_mint", "baseMint", "base_mint"],
|
||||
);
|
||||
let payload_token_b_mint = crate::trade_amount_resolution::extract_string_by_candidate_keys(
|
||||
input.payload,
|
||||
&["tokenBMint", "token_b_mint", "quoteMint", "quote_mint"],
|
||||
);
|
||||
let payload_base_vault = crate::trade_amount_resolution::extract_string_by_candidate_keys(
|
||||
input.payload,
|
||||
&["baseVault", "base_vault", "baseVaultAddress", "base_vault_address"],
|
||||
);
|
||||
let payload_quote_vault = crate::trade_amount_resolution::extract_string_by_candidate_keys(
|
||||
input.payload,
|
||||
&["quoteVault", "quote_vault", "quoteVaultAddress", "quote_vault_address"],
|
||||
);
|
||||
let payload_token_a_mint = match payload_token_a_mint {
|
||||
Some(payload_token_a_mint) => payload_token_a_mint,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let payload_token_b_mint = match payload_token_b_mint {
|
||||
Some(payload_token_b_mint) => payload_token_b_mint,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let payload_base_vault = match payload_base_vault {
|
||||
Some(payload_base_vault) => payload_base_vault,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let payload_quote_vault = match payload_quote_vault {
|
||||
Some(payload_quote_vault) => payload_quote_vault,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let decoded_instruction_result = crate::trade_amount_resolution::load_decoded_instruction(
|
||||
input.database,
|
||||
input.decoded_event,
|
||||
)
|
||||
.await;
|
||||
let decoded_instruction = match decoded_instruction_result {
|
||||
Ok(Some(decoded_instruction)) => decoded_instruction,
|
||||
Ok(None) => return Ok(()),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let instructions_result = crate::query_chain_instructions_list_by_transaction_id(
|
||||
input.database,
|
||||
input.decoded_event.transaction_id,
|
||||
)
|
||||
.await;
|
||||
let instructions = match instructions_result {
|
||||
Ok(instructions) => instructions,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let payload_order_result = resolve_amounts_from_flattened_cpi_transfer_window(
|
||||
&decoded_instruction,
|
||||
&instructions,
|
||||
Some(payload_token_a_mint.as_str()),
|
||||
Some(payload_token_b_mint.as_str()),
|
||||
Some(payload_base_vault.as_str()),
|
||||
Some(payload_quote_vault.as_str()),
|
||||
);
|
||||
let payload_order = match payload_order_result {
|
||||
Ok(payload_order) => payload_order,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if payload_order.base_amount_raw.is_none() || payload_order.quote_amount_raw.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
if crate::trade_amount_resolution::optional_mint_pair_matches_payload_order(
|
||||
input.base_token_mint,
|
||||
input.quote_token_mint,
|
||||
payload_token_a_mint.as_str(),
|
||||
payload_token_b_mint.as_str(),
|
||||
) {
|
||||
if base_amount_raw.is_none() {
|
||||
*base_amount_raw = payload_order.base_amount_raw;
|
||||
}
|
||||
if quote_amount_raw.is_none() {
|
||||
*quote_amount_raw = payload_order.quote_amount_raw;
|
||||
}
|
||||
if resolved_trade_side.is_none() {
|
||||
*resolved_trade_side = payload_order.resolved_trade_side;
|
||||
}
|
||||
tracing::debug!(
|
||||
event_kind = %input.decoded_event.event_kind,
|
||||
decoded_event_id = ?input.decoded_event.id,
|
||||
transaction_signature = %input.transaction.signature,
|
||||
pool_account = ?input.decoded_event.pool_account,
|
||||
payload_token_a_mint = %payload_token_a_mint,
|
||||
payload_token_b_mint = %payload_token_b_mint,
|
||||
payload_base_vault = %payload_base_vault,
|
||||
payload_quote_vault = %payload_quote_vault,
|
||||
base_amount_raw = ?base_amount_raw,
|
||||
quote_amount_raw = ?quote_amount_raw,
|
||||
resolved_trade_side = ?resolved_trade_side,
|
||||
"meteora_dbc trade amounts recovered from payload-token CPI transfer window"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if crate::trade_amount_resolution::optional_mint_pair_matches_payload_reverse_order(
|
||||
input.base_token_mint,
|
||||
input.quote_token_mint,
|
||||
payload_token_a_mint.as_str(),
|
||||
payload_token_b_mint.as_str(),
|
||||
) {
|
||||
if base_amount_raw.is_none() {
|
||||
*base_amount_raw = payload_order.quote_amount_raw;
|
||||
}
|
||||
if quote_amount_raw.is_none() {
|
||||
*quote_amount_raw = payload_order.base_amount_raw;
|
||||
}
|
||||
if resolved_trade_side.is_none() {
|
||||
*resolved_trade_side = crate::trade_amount_resolution::reverse_swap_trade_side(
|
||||
payload_order.resolved_trade_side,
|
||||
);
|
||||
}
|
||||
tracing::debug!(
|
||||
event_kind = %input.decoded_event.event_kind,
|
||||
decoded_event_id = ?input.decoded_event.id,
|
||||
transaction_signature = %input.transaction.signature,
|
||||
pool_account = ?input.decoded_event.pool_account,
|
||||
payload_token_a_mint = %payload_token_a_mint,
|
||||
payload_token_b_mint = %payload_token_b_mint,
|
||||
payload_base_vault = %payload_base_vault,
|
||||
payload_quote_vault = %payload_quote_vault,
|
||||
base_amount_raw = ?base_amount_raw,
|
||||
quote_amount_raw = ?quote_amount_raw,
|
||||
resolved_trade_side = ?resolved_trade_side,
|
||||
"meteora_dbc trade amounts recovered from reversed payload-token CPI transfer window"
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
async fn apply_flattened_cpi_amount_fallback(
|
||||
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
|
||||
protocol_label: &str,
|
||||
@@ -1488,6 +1696,41 @@ fn infer_trade_side_from_transfer_directions(
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_mint_pair_matches_payload_order(
|
||||
base_token_mint: std::option::Option<&str>,
|
||||
quote_token_mint: std::option::Option<&str>,
|
||||
payload_token_a_mint: &str,
|
||||
payload_token_b_mint: &str,
|
||||
) -> bool {
|
||||
return crate::trade_amount_resolution::string_option_equals(
|
||||
base_token_mint,
|
||||
payload_token_a_mint,
|
||||
) && crate::trade_amount_resolution::string_option_equals(quote_token_mint, payload_token_b_mint);
|
||||
}
|
||||
|
||||
fn optional_mint_pair_matches_payload_reverse_order(
|
||||
base_token_mint: std::option::Option<&str>,
|
||||
quote_token_mint: std::option::Option<&str>,
|
||||
payload_token_a_mint: &str,
|
||||
payload_token_b_mint: &str,
|
||||
) -> bool {
|
||||
return crate::trade_amount_resolution::string_option_equals(
|
||||
base_token_mint,
|
||||
payload_token_b_mint,
|
||||
) && crate::trade_amount_resolution::string_option_equals(quote_token_mint, payload_token_a_mint);
|
||||
}
|
||||
|
||||
fn reverse_swap_trade_side(
|
||||
trade_side: std::option::Option<crate::SwapTradeSide>,
|
||||
) -> std::option::Option<crate::SwapTradeSide> {
|
||||
match trade_side {
|
||||
Some(crate::SwapTradeSide::BuyBase) => return Some(crate::SwapTradeSide::SellBase),
|
||||
Some(crate::SwapTradeSide::SellBase) => return Some(crate::SwapTradeSide::BuyBase),
|
||||
Some(crate::SwapTradeSide::Unknown) => return Some(crate::SwapTradeSide::Unknown),
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn string_option_equals(left: std::option::Option<&str>, right: &str) -> bool {
|
||||
let left = match left {
|
||||
Some(left) => left.trim(),
|
||||
@@ -1589,7 +1832,6 @@ async fn load_decoded_instruction(
|
||||
return Ok(instruction_option);
|
||||
}
|
||||
|
||||
|
||||
fn normalize_pump_swap_anchor_buy_exact_quote_in_amounts(
|
||||
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
|
||||
base_amount_raw: &mut std::option::Option<std::string::String>,
|
||||
|
||||
440
validation_sql/SQL_VALIDATION_METEORA_DBC_0_7_56.sql
Normal file
440
validation_sql/SQL_VALIDATION_METEORA_DBC_0_7_56.sql
Normal file
@@ -0,0 +1,440 @@
|
||||
-- file: validation_sql/SQL_VALIDATION_METEORA_DBC_0_7_56.sql
|
||||
|
||||
-- 0.7.56 meteora_dbc validation and corpus-seed checklist.
|
||||
-- Run on a dedicated fresh SQLite database for the Meteora DBC tranche.
|
||||
-- Recommended replay settings after each backfill group:
|
||||
-- skipDexDecode=no, forceDexDecode=yes, deferInstructionObservations=yes.
|
||||
-- This file is intentionally read-only: it never mutates the database.
|
||||
|
||||
-- 00. Corpus seed: upstream fallback samples to backfill first.
|
||||
SELECT
|
||||
json_extract(de.payload_json, '$.upstreamEntryName') AS upstream_entry_name,
|
||||
json_extract(de.payload_json, '$.upstreamDiscriminatorHex') AS upstream_discriminator_hex,
|
||||
json_extract(de.payload_json, '$.upstreamSourceRepo') AS source_repo,
|
||||
COUNT(*) AS fallback_count,
|
||||
COUNT(DISTINCT de.transaction_id) AS tx_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
LEFT JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
WHERE de.protocol_name = 'upstream_git'
|
||||
AND de.event_kind = 'upstream_git.instruction_match'
|
||||
AND json_extract(de.payload_json, '$.upstreamDecoderCode') = 'meteora_dbc'
|
||||
GROUP BY upstream_entry_name, upstream_discriminator_hex, source_repo
|
||||
ORDER BY fallback_count DESC, upstream_entry_name, upstream_discriminator_hex;
|
||||
|
||||
-- 01. Corpus seed: local instruction observations.
|
||||
SELECT
|
||||
instruction_name,
|
||||
discriminator_hex,
|
||||
COUNT(*) AS observed_count,
|
||||
COUNT(DISTINCT signature) AS tx_count,
|
||||
MIN(signature) AS sample_signature
|
||||
FROM k_sol_instruction_observations
|
||||
WHERE decoder_code = 'meteora_dbc'
|
||||
GROUP BY instruction_name, discriminator_hex
|
||||
ORDER BY observed_count DESC, instruction_name, discriminator_hex;
|
||||
|
||||
-- 02. Coverage meteora_dbc.
|
||||
SELECT
|
||||
entry_name,
|
||||
entry_kind,
|
||||
event_family,
|
||||
expected_db_target,
|
||||
proof_status,
|
||||
local_event_kind,
|
||||
discriminator_hex,
|
||||
observed_count,
|
||||
materialized_count,
|
||||
trade_count
|
||||
FROM k_sol_dex_event_coverage_entries
|
||||
WHERE decoder_code = 'meteora_dbc'
|
||||
ORDER BY entry_kind, entry_name, discriminator_hex;
|
||||
|
||||
-- 03. Decoded events summary.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(*) AS decoded_count,
|
||||
COUNT(DISTINCT de.transaction_id) AS tx_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
LEFT JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
GROUP BY de.event_kind
|
||||
ORDER BY decoded_count DESC, de.event_kind;
|
||||
|
||||
-- 04. Decoded meteora_dbc events without coverage.
|
||||
-- Target after closure: empty for all locally decoded meteora_dbc rows.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(*) AS decoded_count,
|
||||
COUNT(DISTINCT de.transaction_id) AS tx_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
LEFT JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_dex_event_coverage_entries ce
|
||||
ON ce.decoder_code = 'meteora_dbc'
|
||||
AND ce.local_event_kind = de.event_kind
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
AND ce.id IS NULL
|
||||
GROUP BY de.event_kind
|
||||
ORDER BY decoded_count DESC, de.event_kind;
|
||||
|
||||
-- 05. Residual upstream fallback for entries covered locally.
|
||||
-- Target after local promotion: empty for every entry that has a local_event_kind.
|
||||
SELECT
|
||||
json_extract(ug.payload_json, '$.upstreamEntryName') AS upstream_entry_name,
|
||||
json_extract(ug.payload_json, '$.upstreamDiscriminatorHex') AS upstream_discriminator_hex,
|
||||
json_extract(ug.payload_json, '$.upstreamSourceRepo') AS source_repo,
|
||||
ce.local_event_kind,
|
||||
ce.expected_db_target,
|
||||
ce.proof_status,
|
||||
COUNT(*) AS fallback_count,
|
||||
COUNT(DISTINCT ug.transaction_id) AS tx_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events ug
|
||||
LEFT JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = ug.transaction_id
|
||||
JOIN k_sol_dex_event_coverage_entries ce
|
||||
ON ce.decoder_code = json_extract(ug.payload_json, '$.upstreamDecoderCode')
|
||||
AND ce.entry_name = json_extract(ug.payload_json, '$.upstreamEntryName')
|
||||
AND COALESCE(ce.discriminator_hex, '') = COALESCE(json_extract(ug.payload_json, '$.upstreamDiscriminatorHex'), '')
|
||||
AND ce.local_event_kind IS NOT NULL
|
||||
AND ce.local_event_kind <> ''
|
||||
WHERE ug.protocol_name = 'upstream_git'
|
||||
AND ug.event_kind = 'upstream_git.instruction_match'
|
||||
AND json_extract(ug.payload_json, '$.upstreamDecoderCode') = 'meteora_dbc'
|
||||
GROUP BY upstream_entry_name, upstream_discriminator_hex, source_repo, ce.local_event_kind, ce.expected_db_target, ce.proof_status
|
||||
ORDER BY fallback_count DESC, upstream_entry_name;
|
||||
|
||||
-- 06. Successful non-materialized events without explicit skip reason.
|
||||
-- Target after closure: empty. Decoded-only rows must carry skip*Reason or informational/audit policy.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(*) AS unexplained_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_trade_events te
|
||||
ON te.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_launch_events lae
|
||||
ON lae.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_liquidity_events lie
|
||||
ON lie.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_lifecycle_events ple
|
||||
ON ple.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_fee_events fee
|
||||
ON fee.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_reward_events rew
|
||||
ON rew.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_admin_events adm
|
||||
ON adm.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_orderbook_events obe
|
||||
ON obe.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_token_account_events tae
|
||||
ON tae.decoded_event_id = de.id
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
AND (tx.err_json IS NULL OR tx.err_json = '' OR tx.err_json = 'null')
|
||||
AND COALESCE(json_extract(de.payload_json, '$.eventActionability'), '') NOT IN (
|
||||
'informational',
|
||||
'decoded_only_anchor_event',
|
||||
'decoded_only_missing_mint_context',
|
||||
'decoded_only_with_explicit_skip_reason'
|
||||
)
|
||||
AND te.id IS NULL
|
||||
AND lae.id IS NULL
|
||||
AND lie.id IS NULL
|
||||
AND ple.id IS NULL
|
||||
AND fee.id IS NULL
|
||||
AND rew.id IS NULL
|
||||
AND adm.id IS NULL
|
||||
AND obe.id IS NULL
|
||||
AND tae.id IS NULL
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipTradeReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipCandleReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipLiquidityReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipLifecycleReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipCatalogReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipFeeReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipAdminReason')), '') = ''
|
||||
GROUP BY de.event_kind
|
||||
ORDER BY unexplained_count DESC, de.event_kind;
|
||||
|
||||
-- 07. Failed transaction materialization safety.
|
||||
-- Target after closure: empty. Failed transactions may be decoded for audit, but must not be business-materialized.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(DISTINCT de.id) AS decoded_failed_count,
|
||||
COUNT(DISTINCT te.id) AS trade_count,
|
||||
COUNT(DISTINCT lae.id) AS launch_count,
|
||||
COUNT(DISTINCT lie.id) AS liquidity_count,
|
||||
COUNT(DISTINCT ple.id) AS lifecycle_count,
|
||||
COUNT(DISTINCT fee.id) AS fee_count,
|
||||
COUNT(DISTINCT rew.id) AS reward_count,
|
||||
COUNT(DISTINCT adm.id) AS admin_count,
|
||||
COUNT(DISTINCT obe.id) AS orderbook_count,
|
||||
COUNT(DISTINCT tae.id) AS token_account_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_trade_events te
|
||||
ON te.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_launch_events lae
|
||||
ON lae.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_liquidity_events lie
|
||||
ON lie.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_lifecycle_events ple
|
||||
ON ple.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_fee_events fee
|
||||
ON fee.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_reward_events rew
|
||||
ON rew.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_admin_events adm
|
||||
ON adm.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_orderbook_events obe
|
||||
ON obe.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_token_account_events tae
|
||||
ON tae.decoded_event_id = de.id
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
AND tx.err_json IS NOT NULL
|
||||
AND tx.err_json <> ''
|
||||
AND tx.err_json <> 'null'
|
||||
GROUP BY de.event_kind
|
||||
HAVING trade_count > 0
|
||||
OR launch_count > 0
|
||||
OR liquidity_count > 0
|
||||
OR lifecycle_count > 0
|
||||
OR fee_count > 0
|
||||
OR reward_count > 0
|
||||
OR admin_count > 0
|
||||
OR orderbook_count > 0
|
||||
OR token_account_count > 0
|
||||
ORDER BY decoded_failed_count DESC, de.event_kind;
|
||||
|
||||
-- 08. Multi-target materialization safety.
|
||||
-- Target after closure: empty. One decoded event must not feed several business targets.
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
de.id AS decoded_event_id,
|
||||
de.event_kind,
|
||||
tx.signature,
|
||||
(CASE WHEN te.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN lae.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN lie.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN ple.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN fee.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN rew.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN adm.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN obe.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN tae.id IS NULL THEN 0 ELSE 1 END) AS target_count
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_trade_events te
|
||||
ON te.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_launch_events lae
|
||||
ON lae.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_liquidity_events lie
|
||||
ON lie.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_lifecycle_events ple
|
||||
ON ple.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_fee_events fee
|
||||
ON fee.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_reward_events rew
|
||||
ON rew.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_admin_events adm
|
||||
ON adm.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_orderbook_events obe
|
||||
ON obe.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_token_account_events tae
|
||||
ON tae.decoded_event_id = de.id
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
)
|
||||
WHERE target_count > 1
|
||||
ORDER BY target_count DESC, event_kind, signature;
|
||||
|
||||
-- 09. Materialization summary by table with successful/failed split.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(DISTINCT de.id) AS decoded_count,
|
||||
COUNT(DISTINCT CASE WHEN tx.err_json IS NULL OR tx.err_json = '' OR tx.err_json = 'null' THEN de.id END) AS successful_decoded_count,
|
||||
COUNT(DISTINCT CASE WHEN tx.err_json IS NOT NULL AND tx.err_json <> '' AND tx.err_json <> 'null' THEN de.id END) AS failed_decoded_count,
|
||||
COUNT(DISTINCT te.id) AS trade_count,
|
||||
COUNT(DISTINCT lie.id) AS liquidity_count,
|
||||
COUNT(DISTINCT ple.id) AS lifecycle_count,
|
||||
COUNT(DISTINCT fee.id) AS fee_count,
|
||||
COUNT(DISTINCT adm.id) AS admin_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_trade_events te
|
||||
ON te.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_liquidity_events lie
|
||||
ON lie.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_lifecycle_events ple
|
||||
ON ple.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_fee_events fee
|
||||
ON fee.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_admin_events adm
|
||||
ON adm.decoded_event_id = de.id
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
GROUP BY de.event_kind
|
||||
ORDER BY decoded_count DESC, de.event_kind;
|
||||
|
||||
-- 10. Instruction observation versus coverage.
|
||||
-- Target after closure: every observed discriminator must map to a known coverage row.
|
||||
SELECT
|
||||
io.instruction_name,
|
||||
io.discriminator_hex,
|
||||
COUNT(*) AS observed_count,
|
||||
MIN(io.signature) AS sample_signature,
|
||||
ce.entry_name,
|
||||
ce.local_event_kind,
|
||||
ce.expected_db_target,
|
||||
ce.proof_status
|
||||
FROM k_sol_instruction_observations io
|
||||
LEFT JOIN k_sol_dex_event_coverage_entries ce
|
||||
ON ce.decoder_code = 'meteora_dbc'
|
||||
AND (
|
||||
ce.discriminator_hex = io.discriminator_hex
|
||||
OR ce.entry_name = io.instruction_name
|
||||
)
|
||||
WHERE io.decoder_code = 'meteora_dbc'
|
||||
GROUP BY io.instruction_name, io.discriminator_hex, ce.entry_name, ce.local_event_kind, ce.expected_db_target, ce.proof_status
|
||||
ORDER BY observed_count DESC, io.instruction_name, io.discriminator_hex;
|
||||
|
||||
-- 11. Anti-faux trade/candle for non-swap events.
|
||||
-- Target after closure: empty.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(DISTINCT te.id) AS trade_count,
|
||||
COUNT(DISTINCT pc.id) AS candle_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_trade_events te
|
||||
ON te.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pair_candles pc
|
||||
ON pc.pair_id = te.pair_id
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
AND de.event_kind NOT IN ('meteora_dbc.swap', 'meteora_dbc.swap2')
|
||||
GROUP BY de.event_kind
|
||||
HAVING trade_count > 0 OR candle_count > 0
|
||||
ORDER BY trade_count DESC, candle_count DESC, de.event_kind;
|
||||
|
||||
-- 12. Global watchlist after replay.
|
||||
-- Target after closure: no meteora_dbc as dominant backlog; unrelated residuals may remain.
|
||||
SELECT
|
||||
COALESCE(json_extract(de.payload_json, '$.upstreamDecoderCode'), de.protocol_name) AS backlog_decoder,
|
||||
de.event_kind,
|
||||
json_extract(de.payload_json, '$.upstreamEntryName') AS upstream_entry_name,
|
||||
json_extract(de.payload_json, '$.upstreamDiscriminatorHex') AS upstream_discriminator_hex,
|
||||
COUNT(*) AS decoded_count,
|
||||
COUNT(DISTINCT de.transaction_id) AS tx_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
LEFT JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
WHERE de.protocol_name IN ('upstream_git', 'meteora_dbc')
|
||||
GROUP BY backlog_decoder, de.event_kind, upstream_entry_name, upstream_discriminator_hex
|
||||
ORDER BY decoded_count DESC, backlog_decoder, de.event_kind
|
||||
LIMIT 100;
|
||||
|
||||
-- 13. Fee parent/legs summary after 0.7.56 fee model.
|
||||
SELECT
|
||||
de.protocol_name,
|
||||
de.event_kind,
|
||||
COUNT(DISTINCT fee.id) AS fee_parent_count,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN COALESCE(TRIM(fee.fee_token_mint), '') <> ''
|
||||
AND COALESCE(TRIM(fee.fee_amount_raw), '') <> ''
|
||||
THEN fee.id
|
||||
ELSE NULL
|
||||
END) AS parent_with_scalar_amount_count,
|
||||
COUNT(DISTINCT fea.id) AS fee_amount_leg_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_fee_events fee
|
||||
JOIN k_sol_dex_decoded_events de
|
||||
ON de.id = fee.decoded_event_id
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = fee.transaction_id
|
||||
LEFT JOIN k_sol_fee_event_amounts fea
|
||||
ON fea.fee_event_id = fee.id
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
GROUP BY
|
||||
de.protocol_name,
|
||||
de.event_kind
|
||||
ORDER BY
|
||||
de.protocol_name,
|
||||
de.event_kind;
|
||||
|
||||
-- 14. Fee parent scalar without amount leg.
|
||||
-- Target after closure: empty.
|
||||
SELECT
|
||||
de.protocol_name,
|
||||
de.event_kind,
|
||||
tx.signature,
|
||||
fee.id AS fee_event_id,
|
||||
fee.fee_token_mint,
|
||||
fee.fee_amount_raw,
|
||||
fee.payload_json
|
||||
FROM k_sol_fee_events fee
|
||||
JOIN k_sol_dex_decoded_events de
|
||||
ON de.id = fee.decoded_event_id
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = fee.transaction_id
|
||||
LEFT JOIN k_sol_fee_event_amounts fea
|
||||
ON fea.fee_event_id = fee.id
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
AND COALESCE(TRIM(fee.fee_token_mint), '') <> ''
|
||||
AND COALESCE(TRIM(fee.fee_amount_raw), '') <> ''
|
||||
AND fea.id IS NULL
|
||||
ORDER BY
|
||||
de.protocol_name,
|
||||
de.event_kind,
|
||||
tx.signature
|
||||
LIMIT 100;
|
||||
|
||||
-- 15. Orphan fee amount legs.
|
||||
-- Target after closure: empty.
|
||||
SELECT
|
||||
fea.id,
|
||||
fea.fee_event_id,
|
||||
fea.transaction_id,
|
||||
fea.decoded_event_id
|
||||
FROM k_sol_fee_event_amounts fea
|
||||
LEFT JOIN k_sol_fee_events fee
|
||||
ON fee.id = fea.fee_event_id
|
||||
WHERE fee.id IS NULL;
|
||||
|
||||
-- 16. Generic allowlisted recovery must not be used by DBC.
|
||||
-- Target after closure: empty; DBC uses protocol-specific fee recovery paths.
|
||||
SELECT
|
||||
de.protocol_name,
|
||||
de.event_kind,
|
||||
tx.signature,
|
||||
fee.id AS fee_event_id,
|
||||
fea.leg_index,
|
||||
fea.token_mint,
|
||||
fea.amount_raw,
|
||||
fea.amount_source
|
||||
FROM k_sol_fee_event_amounts fea
|
||||
JOIN k_sol_fee_events fee
|
||||
ON fee.id = fea.fee_event_id
|
||||
JOIN k_sol_dex_decoded_events de
|
||||
ON de.id = fee.decoded_event_id
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = fee.transaction_id
|
||||
WHERE de.protocol_name = 'meteora_dbc'
|
||||
AND fea.amount_source = 'allowlisted_inner_spl_transfer'
|
||||
ORDER BY
|
||||
de.event_kind,
|
||||
tx.signature,
|
||||
fea.leg_index;
|
||||
285
validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql
Normal file
285
validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql
Normal file
@@ -0,0 +1,285 @@
|
||||
-- file: validation_sql/SQL_VALIDATION_METEORA_DLMM_0_7_57.sql
|
||||
|
||||
-- 0.7.57 meteora_dlmm validation checklist.
|
||||
-- Run on a dedicated fresh SQLite database for the Meteora DLMM tranche.
|
||||
-- Recommended replay settings:
|
||||
-- skipDexDecode=no, forceDexDecode=yes, deferInstructionObservations=yes.
|
||||
-- This file is read-only.
|
||||
|
||||
-- 00. Upstream fallback samples to backfill/promote.
|
||||
SELECT
|
||||
json_extract(de.payload_json, '$.upstreamEntryName') AS upstream_entry_name,
|
||||
json_extract(de.payload_json, '$.upstreamDiscriminatorHex') AS upstream_discriminator_hex,
|
||||
json_extract(de.payload_json, '$.upstreamSourceRepo') AS source_repo,
|
||||
COUNT(*) AS fallback_count,
|
||||
COUNT(DISTINCT de.transaction_id) AS tx_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
LEFT JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
WHERE de.protocol_name = 'upstream_git'
|
||||
AND de.event_kind = 'upstream_git.instruction_match'
|
||||
AND json_extract(de.payload_json, '$.upstreamDecoderCode') = 'meteora_dlmm'
|
||||
GROUP BY upstream_entry_name, upstream_discriminator_hex, source_repo
|
||||
ORDER BY fallback_count DESC, upstream_entry_name, upstream_discriminator_hex;
|
||||
|
||||
-- 01. Local instruction observations.
|
||||
SELECT
|
||||
instruction_name,
|
||||
discriminator_hex,
|
||||
COUNT(*) AS observed_count,
|
||||
COUNT(DISTINCT signature) AS tx_count,
|
||||
MIN(signature) AS sample_signature
|
||||
FROM k_sol_instruction_observations
|
||||
WHERE decoder_code = 'meteora_dlmm'
|
||||
GROUP BY instruction_name, discriminator_hex
|
||||
ORDER BY observed_count DESC, instruction_name, discriminator_hex;
|
||||
|
||||
-- 02. Coverage entries.
|
||||
SELECT
|
||||
entry_name,
|
||||
entry_kind,
|
||||
event_family,
|
||||
expected_db_target,
|
||||
proof_status,
|
||||
local_event_kind,
|
||||
discriminator_hex,
|
||||
observed_count,
|
||||
materialized_count,
|
||||
trade_count
|
||||
FROM k_sol_dex_event_coverage_entries
|
||||
WHERE decoder_code = 'meteora_dlmm'
|
||||
ORDER BY entry_kind, entry_name, discriminator_hex;
|
||||
|
||||
-- 03. Decoded DLMM summary.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(*) AS decoded_count,
|
||||
COUNT(DISTINCT de.transaction_id) AS tx_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
LEFT JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
WHERE de.protocol_name = 'meteora_dlmm'
|
||||
GROUP BY de.event_kind
|
||||
ORDER BY decoded_count DESC, de.event_kind;
|
||||
|
||||
-- 04. Decoded DLMM without coverage. Target: empty.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(*) AS decoded_count,
|
||||
COUNT(DISTINCT de.transaction_id) AS tx_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
LEFT JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_dex_event_coverage_entries ce
|
||||
ON ce.decoder_code = 'meteora_dlmm'
|
||||
AND ce.local_event_kind = de.event_kind
|
||||
WHERE de.protocol_name = 'meteora_dlmm'
|
||||
AND ce.id IS NULL
|
||||
GROUP BY de.event_kind
|
||||
ORDER BY decoded_count DESC, de.event_kind;
|
||||
|
||||
-- 05. Successful non-materialized without explicit skip/policy. Target: empty.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(*) AS unexplained_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_trade_events te ON te.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_launch_events lae ON lae.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_liquidity_events lie ON lie.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_lifecycle_events ple ON ple.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_fee_events fee ON fee.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_reward_events rew ON rew.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_admin_events adm ON adm.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_orderbook_events obe ON obe.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_token_account_events tae ON tae.decoded_event_id = de.id
|
||||
WHERE de.protocol_name = 'meteora_dlmm'
|
||||
AND (tx.err_json IS NULL OR tx.err_json = '' OR tx.err_json = 'null')
|
||||
AND COALESCE(json_extract(de.payload_json, '$.eventActionability'), '') NOT IN (
|
||||
'informational',
|
||||
'decoded_only_anchor_event',
|
||||
'decoded_only_missing_mint_context',
|
||||
'decoded_only_with_explicit_skip_reason'
|
||||
)
|
||||
AND te.id IS NULL AND lae.id IS NULL AND lie.id IS NULL AND ple.id IS NULL
|
||||
AND fee.id IS NULL AND rew.id IS NULL AND adm.id IS NULL AND obe.id IS NULL AND tae.id IS NULL
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipTradeReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipCandleReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipLiquidityReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipLifecycleReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipCatalogReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipFeeReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipRewardReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipAdminReason')), '') = ''
|
||||
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipOrderbookReason')), '') = ''
|
||||
GROUP BY de.event_kind
|
||||
ORDER BY unexplained_count DESC, de.event_kind;
|
||||
|
||||
-- 06. Failed transaction materialization. Target: empty.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(DISTINCT de.id) AS decoded_failed_count,
|
||||
COUNT(DISTINCT te.id) AS trade_count,
|
||||
COUNT(DISTINCT lie.id) AS liquidity_count,
|
||||
COUNT(DISTINCT ple.id) AS lifecycle_count,
|
||||
COUNT(DISTINCT fee.id) AS fee_count,
|
||||
COUNT(DISTINCT rew.id) AS reward_count,
|
||||
COUNT(DISTINCT adm.id) AS admin_count,
|
||||
COUNT(DISTINCT obe.id) AS orderbook_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_trade_events te ON te.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_liquidity_events lie ON lie.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_lifecycle_events ple ON ple.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_fee_events fee ON fee.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_reward_events rew ON rew.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_admin_events adm ON adm.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_orderbook_events obe ON obe.decoded_event_id = de.id
|
||||
WHERE de.protocol_name = 'meteora_dlmm'
|
||||
AND tx.err_json IS NOT NULL
|
||||
AND tx.err_json <> ''
|
||||
AND tx.err_json <> 'null'
|
||||
GROUP BY de.event_kind
|
||||
HAVING trade_count > 0 OR liquidity_count > 0 OR lifecycle_count > 0
|
||||
OR fee_count > 0 OR reward_count > 0 OR admin_count > 0 OR orderbook_count > 0
|
||||
ORDER BY decoded_failed_count DESC, de.event_kind;
|
||||
|
||||
-- 07. Multi-target materialization. Target: empty.
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
de.id AS decoded_event_id,
|
||||
de.event_kind,
|
||||
tx.signature,
|
||||
(CASE WHEN te.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN lie.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN ple.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN fee.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN rew.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN adm.id IS NULL THEN 0 ELSE 1 END)
|
||||
+ (CASE WHEN obe.id IS NULL THEN 0 ELSE 1 END) AS target_count
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_trade_events te ON te.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_liquidity_events lie ON lie.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_lifecycle_events ple ON ple.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_fee_events fee ON fee.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_reward_events rew ON rew.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pool_admin_events adm ON adm.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_orderbook_events obe ON obe.decoded_event_id = de.id
|
||||
WHERE de.protocol_name = 'meteora_dlmm'
|
||||
)
|
||||
WHERE target_count > 1
|
||||
ORDER BY target_count DESC, event_kind, signature;
|
||||
|
||||
-- 08. Non-swap trade/candle safety. Target: empty.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(DISTINCT te.id) AS trade_count,
|
||||
COUNT(DISTINCT pc.id) AS candle_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_trade_events te ON te.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_pair_candles pc ON pc.pair_id = te.pair_id
|
||||
WHERE de.protocol_name = 'meteora_dlmm'
|
||||
AND de.event_kind NOT IN (
|
||||
'meteora_dlmm.swap',
|
||||
'meteora_dlmm.swap2',
|
||||
'meteora_dlmm.swap_exact_out',
|
||||
'meteora_dlmm.swap_exact_out2',
|
||||
'meteora_dlmm.swap_with_price_impact',
|
||||
'meteora_dlmm.swap_with_price_impact2'
|
||||
)
|
||||
GROUP BY de.event_kind
|
||||
HAVING trade_count > 0 OR candle_count > 0
|
||||
ORDER BY trade_count DESC, candle_count DESC, de.event_kind;
|
||||
|
||||
-- 09. Fee parent/legs summary.
|
||||
SELECT
|
||||
de.protocol_name,
|
||||
de.event_kind,
|
||||
COUNT(DISTINCT fee.id) AS fee_parent_count,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN COALESCE(TRIM(fee.fee_token_mint), '') <> ''
|
||||
AND COALESCE(TRIM(fee.fee_amount_raw), '') <> ''
|
||||
THEN fee.id
|
||||
ELSE NULL
|
||||
END) AS parent_with_scalar_amount_count,
|
||||
COUNT(DISTINCT fea.id) AS fee_amount_leg_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_fee_events fee
|
||||
JOIN k_sol_dex_decoded_events de ON de.id = fee.decoded_event_id
|
||||
JOIN k_sol_chain_transactions tx ON tx.id = fee.transaction_id
|
||||
LEFT JOIN k_sol_fee_event_amounts fea ON fea.fee_event_id = fee.id
|
||||
WHERE de.protocol_name = 'meteora_dlmm'
|
||||
GROUP BY de.protocol_name, de.event_kind
|
||||
ORDER BY de.protocol_name, de.event_kind;
|
||||
|
||||
-- 10. Fee parent scalar without leg. Target: empty.
|
||||
SELECT
|
||||
de.protocol_name,
|
||||
de.event_kind,
|
||||
tx.signature,
|
||||
fee.id AS fee_event_id,
|
||||
fee.fee_token_mint,
|
||||
fee.fee_amount_raw,
|
||||
fee.payload_json
|
||||
FROM k_sol_fee_events fee
|
||||
JOIN k_sol_dex_decoded_events de ON de.id = fee.decoded_event_id
|
||||
JOIN k_sol_chain_transactions tx ON tx.id = fee.transaction_id
|
||||
LEFT JOIN k_sol_fee_event_amounts fea ON fea.fee_event_id = fee.id
|
||||
WHERE de.protocol_name = 'meteora_dlmm'
|
||||
AND COALESCE(TRIM(fee.fee_token_mint), '') <> ''
|
||||
AND COALESCE(TRIM(fee.fee_amount_raw), '') <> ''
|
||||
AND fea.id IS NULL
|
||||
ORDER BY de.protocol_name, de.event_kind, tx.signature
|
||||
LIMIT 100;
|
||||
|
||||
-- 11. Orphan fee amount legs. Target: empty.
|
||||
SELECT
|
||||
fea.id,
|
||||
fea.fee_event_id,
|
||||
fea.transaction_id,
|
||||
fea.decoded_event_id
|
||||
FROM k_sol_fee_event_amounts fea
|
||||
LEFT JOIN k_sol_fee_events fee ON fee.id = fea.fee_event_id
|
||||
WHERE fee.id IS NULL;
|
||||
|
||||
-- 12. Reward/fee separation overview.
|
||||
SELECT
|
||||
de.event_kind,
|
||||
COUNT(DISTINCT fee.id) AS fee_count,
|
||||
COUNT(DISTINCT rew.id) AS reward_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
JOIN k_sol_chain_transactions tx ON tx.id = de.transaction_id
|
||||
LEFT JOIN k_sol_fee_events fee ON fee.decoded_event_id = de.id
|
||||
LEFT JOIN k_sol_reward_events rew ON rew.decoded_event_id = de.id
|
||||
WHERE de.protocol_name = 'meteora_dlmm'
|
||||
GROUP BY de.event_kind
|
||||
HAVING fee_count > 0 OR reward_count > 0
|
||||
ORDER BY de.event_kind;
|
||||
|
||||
-- 13. Global watchlist after replay.
|
||||
SELECT
|
||||
COALESCE(json_extract(de.payload_json, '$.upstreamDecoderCode'), de.protocol_name) AS backlog_decoder,
|
||||
de.event_kind,
|
||||
json_extract(de.payload_json, '$.upstreamEntryName') AS upstream_entry_name,
|
||||
json_extract(de.payload_json, '$.upstreamDiscriminatorHex') AS upstream_discriminator_hex,
|
||||
COUNT(*) AS decoded_count,
|
||||
COUNT(DISTINCT de.transaction_id) AS tx_count,
|
||||
MIN(tx.signature) AS sample_signature
|
||||
FROM k_sol_dex_decoded_events de
|
||||
LEFT JOIN k_sol_chain_transactions tx ON tx.id = de.transaction_id
|
||||
WHERE de.protocol_name IN ('upstream_git', 'meteora_dlmm')
|
||||
GROUP BY backlog_decoder, de.event_kind, upstream_entry_name, upstream_discriminator_hex
|
||||
ORDER BY decoded_count DESC, backlog_decoder, de.event_kind
|
||||
LIMIT 100;
|
||||
Reference in New Issue
Block a user