This commit is contained in:
2026-06-17 16:06:09 +02:00
parent be12f5810b
commit 319be14aa6
37 changed files with 7129 additions and 363 deletions

View File

@@ -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.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.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.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.

View File

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

View File

@@ -4,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` ## É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. 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 : 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 ; 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 ; 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 ; 4. ne créer aucun trade/candle DBC sans montants, sens économique, pool/pair et mints fiables ;

View File

@@ -1,38 +1,39 @@
<!-- file: ROADMAP.md --> <!-- 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. ### `0.7.56 meteora_dbc` — clos
- 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`.
## É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 ; ### Prochaine tranche immédiate
- `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.
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. | Priori| Tranche | Surface | Objectif |
### Phasage immédiat après `0.7.55`
| Priorité | Tranche | Surface | Raison |
|---:|---|---|---| |---:|---|---|---|
| 1 | `0.7.56` | `meteora_dbc` | Prochaine tranche programmée : launch/bonding, swaps exploitables, migration, fees/admin/config depuis corpus neuf. | | 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.57+` | `meteora_*` | Corriger les gaps locaux Meteora reportés volontairement, surface par surface. | | 2 | `0.7.58` | `meteora_damm_v1` | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. |
| 3 | ultérieur | `jupiter_swap` / agrégateurs | `jupiter_swap.route_v2` reste en watchlist résiduelle ; traiter sans double-count des DEX effectifs. | | 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” ## 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.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.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.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.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 | Parité upstream finale : swaps, bins, positions, liquidity, fees/rewards/admin. | | `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.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.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. | | `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 | | 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.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.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.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. | | `0.7.60` | `meteora_vault` | `24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi` | Vault deposit/withdraw/fee/accounting ; pas de candle directe. |

View File

@@ -2,6 +2,21 @@
# Database Event Model Review — `khadhroony-bobobot` `0.7.47-1FE5` # 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 ## 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`. 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`.

View File

@@ -1,15 +1,26 @@
<!-- file: docs/DEX_DECODER_MATRIX.md --> <!-- 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 ## 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éé. 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`). 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é ## 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. | | 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. | | 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. | | 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é. | | 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` | `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. | | 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. | | 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. | | 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. | | 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 | | 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. | | `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. |

View File

@@ -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`. 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. |

View 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.

View File

@@ -1,3 +1,5 @@
<!-- file: docs/prompts/PROMPT_0_7_56_METEORA_DBC.md -->
# Prompt de reprise — khadhroony-bobobot 0.7.56 — meteora_dbc # 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`. 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é : Nom recommandé :
```text ```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. 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.

View File

@@ -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.

View 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.

View 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.

View File

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

View File

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

View File

@@ -26,11 +26,14 @@ pub use dtos::DexDecodedEventDto;
pub use dtos::DexDto; pub use dtos::DexDto;
pub use dtos::DexEventCoverageEntryDto; pub use dtos::DexEventCoverageEntryDto;
pub use dtos::DexEventCoverageSummaryDto; pub use dtos::DexEventCoverageSummaryDto;
pub use dtos::FeeEventAmountDto;
pub use dtos::FeeEventDto; pub use dtos::FeeEventDto;
pub use dtos::InstructionObservationDto; pub use dtos::InstructionObservationDto;
pub use dtos::InstructionObservationSourceRow;
pub use dtos::KnownHttpEndpointDto; pub use dtos::KnownHttpEndpointDto;
pub use dtos::KnownWsEndpointDto; pub use dtos::KnownWsEndpointDto;
pub use dtos::LaunchAttributionDto; pub use dtos::LaunchAttributionDto;
pub use dtos::LaunchEventUpsertInput;
pub use dtos::LaunchSurfaceDto; pub use dtos::LaunchSurfaceDto;
pub use dtos::LaunchSurfaceKeyDto; pub use dtos::LaunchSurfaceKeyDto;
pub use dtos::LiquidityEventDto; pub use dtos::LiquidityEventDto;
@@ -98,6 +101,7 @@ pub use entities::DexDecodedEventEntity;
pub use entities::DexEntity; pub use entities::DexEntity;
pub use entities::DexEventCoverageEntryEntity; pub use entities::DexEventCoverageEntryEntity;
pub use entities::DexEventCoverageSummaryEntity; pub use entities::DexEventCoverageSummaryEntity;
pub use entities::FeeEventAmountEntity;
pub use entities::FeeEventEntity; pub use entities::FeeEventEntity;
pub use entities::InstructionObservationEntity; pub use entities::InstructionObservationEntity;
pub use entities::KnownHttpEndpointEntity; 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_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_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_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_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_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_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_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_by_key;
pub use queries::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint; 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_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_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_delete_by_decoder;
pub use queries::query_dex_event_coverage_entries_list_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; 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_get_by_code;
pub use queries::query_dexs_list; pub use queries::query_dexs_list;
pub use queries::query_dexs_upsert; 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_get_by_decoded_event_id;
pub use queries::query_fee_events_list_recent; pub use queries::query_fee_events_list_recent;
pub use queries::query_fee_events_upsert; 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_recent;
pub use queries::query_instruction_observation_source_rows_list_replay_window; pub use queries::query_instruction_observation_source_rows_list_replay_window;
pub use queries::query_instruction_observations_delete_by_transaction_ids; 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_list_by_filter;
pub use queries::query_instruction_observations_upsert; pub use queries::query_instruction_observations_upsert;
pub use queries::query_known_http_endpoints_get; 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_list_by_pool_id;
pub use queries::query_launch_attributions_upsert; pub use queries::query_launch_attributions_upsert;
pub use queries::query_launch_events_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_get_by_match;
pub use queries::query_launch_surface_keys_list_by_surface_id; pub use queries::query_launch_surface_keys_list_by_surface_id;
pub use queries::query_launch_surface_keys_upsert; pub use queries::query_launch_surface_keys_upsert;

View File

@@ -13,6 +13,7 @@ mod dex_decode_replay_ledger;
mod dex_decoded_event; mod dex_decoded_event;
mod dex_event_coverage_entry; mod dex_event_coverage_entry;
mod fee_event; mod fee_event;
mod fee_event_amount;
mod instruction_observation; mod instruction_observation;
mod known_http_endpoint; mod known_http_endpoint;
mod known_ws_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::DexEventCoverageEntryDto;
pub use dex_event_coverage_entry::DexEventCoverageSummaryDto; pub use dex_event_coverage_entry::DexEventCoverageSummaryDto;
pub use fee_event::FeeEventDto; pub use fee_event::FeeEventDto;
pub use fee_event_amount::FeeEventAmountDto;
pub use instruction_observation::InstructionObservationDto; pub use instruction_observation::InstructionObservationDto;
pub use instruction_observation::InstructionObservationSourceRow; pub use instruction_observation::InstructionObservationSourceRow;
pub use known_http_endpoint::KnownHttpEndpointDto; pub use known_http_endpoint::KnownHttpEndpointDto;

View 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,
});
}
}

View File

@@ -15,6 +15,7 @@ mod dex_decode_replay_ledger;
mod dex_decoded_event; mod dex_decoded_event;
mod dex_event_coverage_entry; mod dex_event_coverage_entry;
mod fee_event; mod fee_event;
mod fee_event_amount;
mod known_http_endpoint; mod known_http_endpoint;
mod known_ws_endpoint; mod known_ws_endpoint;
mod instruction_observation; 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::DexEventCoverageEntryEntity;
pub use dex_event_coverage_entry::DexEventCoverageSummaryEntity; pub use dex_event_coverage_entry::DexEventCoverageSummaryEntity;
pub use fee_event::FeeEventEntity; pub use fee_event::FeeEventEntity;
pub use fee_event_amount::FeeEventAmountEntity;
pub use known_http_endpoint::KnownHttpEndpointEntity; pub use known_http_endpoint::KnownHttpEndpointEntity;
pub use known_ws_endpoint::KnownWsEndpointEntity; pub use known_ws_endpoint::KnownWsEndpointEntity;
pub use instruction_observation::InstructionObservationEntity; pub use instruction_observation::InstructionObservationEntity;

View 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,
}

View File

@@ -13,6 +13,7 @@ mod dex_decode_replay_ledger;
mod dex_decoded_event; mod dex_decoded_event;
mod dex_event_coverage_entry; mod dex_event_coverage_entry;
mod fee_event; mod fee_event;
mod fee_event_amount;
mod instruction_observation; mod instruction_observation;
mod known_http_endpoint; mod known_http_endpoint;
mod known_ws_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_get_by_decoded_event_id;
pub use fee_event::query_fee_events_list_recent; pub use fee_event::query_fee_events_list_recent;
pub use fee_event::query_fee_events_upsert; 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_by_signature;
pub use instruction_observation::query_instruction_observation_source_rows_list_recent; pub use instruction_observation::query_instruction_observation_source_rows_list_recent;
pub use instruction_observation::query_instruction_observation_source_rows_list_replay_window; pub use instruction_observation::query_instruction_observation_source_rows_list_replay_window;

View File

@@ -136,6 +136,10 @@ WHERE decoded_event_id IN (
"k_sol_pool_lifecycle_events", "k_sol_pool_lifecycle_events",
"DELETE FROM k_sol_pool_lifecycle_events WHERE transaction_id = ?", "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_fee_events", "DELETE FROM k_sol_fee_events WHERE transaction_id = ?"),
( (
"k_sol_reward_events", "k_sol_reward_events",

View File

@@ -156,6 +156,10 @@ WHERE id = ?
id, error 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); return Ok(id);
} }
let insert_result = sqlx::query( let insert_result = sqlx::query(
@@ -227,7 +231,13 @@ LIMIT 1
.fetch_one(pool) .fetch_one(pool)
.await; .await;
match id_result { 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) => { Err(error) => {
return Err(crate::Error::Db(format!( return Err(crate::Error::Db(format!(
"cannot fetch inserted k_sol_fee_events id for signature '{}' on sqlite: {}", "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. /// Lists recent fee events ordered from newest to oldest.
pub async fn query_fee_events_list_recent( pub async fn query_fee_events_list_recent(
database: &crate::Database, database: &crate::Database,

View 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());
}
}

View File

@@ -342,6 +342,30 @@ pub(crate) async fn ensure_schema(database: &crate::Database) -> Result<(), crat
if let Err(error) = result { if let Err(error) = result {
return Err(error); 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; let result = create_tbl_reward_events(pool).await;
if let Err(error) = result { if let Err(error) = result {
return Err(error); return Err(error);
@@ -2702,6 +2726,107 @@ WHERE decoded_event_id IS NOT NULL
.await; .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`. /// Creates `k_sol_reward_events`.
async fn create_tbl_reward_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> { async fn create_tbl_reward_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement( return execute_sqlite_schema_statement(

View File

@@ -43,6 +43,7 @@ pub use meteora_damm_v2::MeteoraDammV2SwapDecoded;
pub use meteora_dbc::MeteoraDbcCreatePoolDecoded; pub use meteora_dbc::MeteoraDbcCreatePoolDecoded;
pub use meteora_dbc::MeteoraDbcDecodedEvent; pub use meteora_dbc::MeteoraDbcDecodedEvent;
pub use meteora_dbc::MeteoraDbcDecoder; pub use meteora_dbc::MeteoraDbcDecoder;
pub use meteora_dbc::MeteoraDbcInstructionDecoded;
pub use meteora_dbc::MeteoraDbcSwapDecoded; pub use meteora_dbc::MeteoraDbcSwapDecoded;
pub use meteora_dlmm::MeteoraDlmmCreatePoolDecoded; pub use meteora_dlmm::MeteoraDlmmCreatePoolDecoded;
pub use meteora_dlmm::MeteoraDlmmDecodedEvent; pub use meteora_dlmm::MeteoraDlmmDecodedEvent;

File diff suppressed because it is too large Load Diff

View File

@@ -1045,7 +1045,7 @@ impl DexDecodeService {
event.instruction_id, event.instruction_id,
"meteora_dbc", "meteora_dbc",
event.program_id.clone(), event.program_id.clone(),
"meteora_dbc.create_pool", event.event_kind.as_str(),
event.pool_account.clone(), event.pool_account.clone(),
None, None,
event.token_a_mint.clone(), event.token_a_mint.clone(),
@@ -1063,7 +1063,7 @@ impl DexDecodeService {
event.instruction_id, event.instruction_id,
"meteora_dbc", "meteora_dbc",
event.program_id.clone(), event.program_id.clone(),
"meteora_dbc.swap", event.event_kind.as_str(),
event.pool_account.clone(), event.pool_account.clone(),
None, None,
event.token_a_mint.clone(), event.token_a_mint.clone(),
@@ -1073,6 +1073,24 @@ impl DexDecodeService {
) )
.await; .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;
},
} }
} }

View File

@@ -164,6 +164,9 @@ pub(crate) fn dex_detection_route(
("meteora_dbc", "meteora_dbc.swap") => { ("meteora_dbc", "meteora_dbc.swap") => {
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDbcPool); 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") => { ("meteora_dlmm", "meteora_dlmm.create_pool") => {
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDlmmPool); 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)); 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] #[test]
fn raydium_launchpad_initialize_route_requires_full_pool_context() { fn raydium_launchpad_initialize_route_requires_full_pool_context() {
let event = make_decoded_event( let event = make_decoded_event(

View File

@@ -350,6 +350,9 @@ pub fn is_dex_trade_event_kind(event_kind: &str) -> bool {
if event_kind.ends_with(".swap") { if event_kind.ends_with(".swap") {
return true; return true;
} }
if event_kind.ends_with(".swap2") {
return true;
}
if event_kind.contains(".swap_") { if event_kind.contains(".swap_") {
return true; 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. /// Returns true for liquidity lifecycle changes that must not become candles.
pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool { 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") { if event_kind.contains(".withdraw_pnl") {
return false; return false;
} }
@@ -490,6 +509,14 @@ pub fn is_dex_position_close_event_kind(event_kind: &str) -> bool {
/// Returns true for fee collection events. /// Returns true for fee collection events.
pub fn is_dex_fee_event_kind(event_kind: &str) -> bool { 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.") if event_kind.starts_with("pump_fees.")
&& (event_kind.contains("donation_fee_pda_cranked") && (event_kind.contains("donation_fee_pda_cranked")
|| event_kind.contains("sweep_buyback") || 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" { if event_kind == "raydium_amm_v4.pre_initialize" {
return true; return true;
} }
if event_kind.contains(".create_lock_escrow") { if event_kind.contains(".create_lock_escrow") || event_kind.contains(".create_locker") {
return true; return true;
} }
if event_kind.contains(".initialize_bin_array") { 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") { if event_kind.contains(".migrate_pool_coin_creator") {
return false; return false;
} }
if event_kind.starts_with("meteora_dbc.") && event_kind.contains("metadata") {
return false;
}
if event_kind.contains(".migrate") { if event_kind.contains(".migrate") {
return true; 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. /// Returns true for pool creation or initialization events.
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool { 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" { if event_kind == "raydium_amm_v4.pre_initialize" {
return true; 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. /// Returns true for admin, configuration or permission changes.
pub fn is_dex_admin_event_kind(event_kind: &str) -> bool { 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.") if event_kind.starts_with("pump_fees.")
&& (event_kind.contains("authority") && (event_kind.contains("authority")
|| event_kind.contains("admin") || 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"), "trade");
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap_v2"), "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("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"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.buy_v2"), "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.sell_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.trade_event"), "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("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("raydium_cpmm.swap_base_input"));
assert!(super::is_dex_candle_candidate_event_kind("meteora_dbc.swap2"));
} }
#[test] #[test]

View File

@@ -65,6 +65,25 @@ impl DexEventCoverageService {
Err(error) => return Err(error), 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)); return Ok((search_result.entries.len(), upserted_entry_count));
} }
@@ -99,6 +118,11 @@ impl DexEventCoverageService {
if let Err(error) = duplicate_cleanup_result { if let Err(error) = duplicate_cleanup_result {
return Err(error); 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 { let refreshed_entry_count = match &decoder_code {
Some(decoder_code) => { Some(decoder_code) => {
let refresh_result = let refresh_result =
@@ -159,6 +183,11 @@ impl DexEventCoverageService {
if let Err(error) = duplicate_cleanup_result { if let Err(error) = duplicate_cleanup_result {
return Err(error); 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 { let refreshed_entry_count = match &decoder_code {
Some(decoder_code) => { Some(decoder_code) => {
let refresh_result = 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( fn build_coverage_entry_from_upstream(
@@ -309,6 +382,353 @@ fn build_coverage_entry_from_upstream(
return coverage_entry; 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( fn infer_expected_db_target_for_entry(
decoder_code: &str, decoder_code: &str,
entry_name: &str, entry_name: &str,
@@ -324,6 +744,9 @@ fn infer_expected_db_target_for_entry(
if decoder_code == "pump_fees" { if decoder_code == "pump_fees" {
return infer_pump_fees_expected_db_target(entry_name, entry_kind); 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" if decoder_code == "raydium_cpmm"
&& (entry_name == "swap_event" || entry_name == "anchor_idl_instruction") && (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" { if decoder_code == "raydium_stable_swap" {
return infer_raydium_stable_swap_event_family(entry_name, entry_kind); 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); 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( fn infer_raydium_amm_v4_event_family(
entry_name: &str, entry_name: &str,
entry_kind: &str, entry_kind: &str,
@@ -1479,6 +2129,9 @@ pub(crate) fn known_local_event_kind(
if decoder_code == "pump_swap" { if decoder_code == "pump_swap" {
return pump_swap_local_event_kind(entry_name); 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" { if decoder_code == "raydium_amm_v4" {
return raydium_amm_v4_local_event_kind(entry_name); return raydium_amm_v4_local_event_kind(entry_name);
} }

View File

@@ -200,6 +200,45 @@ fn resolve_instruction_name(
Some(discriminator_hex) => discriminator_hex, Some(discriminator_hex) => discriminator_hex,
None => return None, 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") { if program_id == crate::RAYDIUM_AMM_V4_PROGRAM_ID || decoder_code == Some("raydium_amm_v4") {
let name = match discriminator_hex { let name = match discriminator_hex {
"00" => "raydium_amm_v4.initialize", "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(), 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")
);
}
}

View File

@@ -497,6 +497,10 @@ pub use db::DexEventCoverageEntryEntity;
pub use db::DexEventCoverageSummaryDto; pub use db::DexEventCoverageSummaryDto;
/// Aggregated DEX event coverage summary row. /// Aggregated DEX event coverage summary row.
pub use db::DexEventCoverageSummaryEntity; 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. /// Normalized fee event persisted from useful non-trade DEX events.
pub use db::FeeEventDto; pub use db::FeeEventDto;
/// Persisted fee event row. /// Persisted fee event row.
@@ -655,10 +659,6 @@ pub use db::ProgramInstructionDiscriminatorRowEntity;
/// Aggregated instruction discriminator diagnostic row. /// Aggregated instruction discriminator diagnostic row.
pub use db::ProgramInstructionDiscriminatorSummaryDto; pub use db::ProgramInstructionDiscriminatorSummaryDto;
/// Application-facing protocol candidate DTO. /// 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; pub use db::ProtocolCandidateDto;
/// Persisted protocol candidate row. /// Persisted protocol candidate row.
pub use db::ProtocolCandidateEntity; pub use db::ProtocolCandidateEntity;
@@ -804,13 +804,22 @@ pub use db::query_dexs_get_by_code;
pub use db::query_dexs_list; pub use db::query_dexs_list;
/// Inserts or updates one normalized DEX row by code. /// Inserts or updates one normalized DEX row by code.
pub use db::query_dexs_upsert; 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. /// Returns one fee event by decoded-event id.
pub use db::query_fee_events_get_by_decoded_event_id; pub use db::query_fee_events_get_by_decoded_event_id;
/// Lists recent fee events ordered from newest to oldest. /// Lists recent fee events ordered from newest to oldest.
pub use db::query_fee_events_list_recent; pub use db::query_fee_events_list_recent;
/// Inserts or updates one normalized fee event row. /// Inserts or updates one normalized fee event row.
pub use db::query_fee_events_upsert; 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. /// Lists instruction-observation source rows for one transaction signature.
pub use db::query_instruction_observation_source_rows_list_by_signature; pub use db::query_instruction_observation_source_rows_list_by_signature;
/// Lists recent instruction-observation source rows. /// 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. /// Lists protocol candidate summaries ordered by investigation priority.
pub use db::query_protocol_candidate_summaries_list_by_priority; pub use db::query_protocol_candidate_summaries_list_by_priority;
/// Deletes protocol candidates for one transaction. /// 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; pub use db::query_protocol_candidates_delete_by_transaction_id;
/// Inserts one protocol candidate row. /// Inserts one protocol candidate row.
pub use db::query_protocol_candidates_insert; pub use db::query_protocol_candidates_insert;
@@ -1133,6 +1140,8 @@ pub use dex::MeteoraDbcCreatePoolDecoded;
pub use dex::MeteoraDbcDecodedEvent; pub use dex::MeteoraDbcDecodedEvent;
/// Meteora DBC decoder. /// Meteora DBC decoder.
pub use dex::MeteoraDbcDecoder; pub use dex::MeteoraDbcDecoder;
/// Decoded Meteora DBC instruction or Anchor event.
pub use dex::MeteoraDbcInstructionDecoded;
/// Decoded Meteora DBC swap event. /// Decoded Meteora DBC swap event.
pub use dex::MeteoraDbcSwapDecoded; pub use dex::MeteoraDbcSwapDecoded;
/// Decoded Meteora DLMM create-pool event. /// 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; pub use dex_event_classification::is_dex_candle_candidate_event_kind;
/// Returns true for fee collection DEX events. /// Returns true for fee collection DEX events.
pub use dex_event_classification::is_dex_fee_event_kind; 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; pub use dex_event_classification::is_dex_informational_event_kind;
/// Returns true for launch or bonding-curve creation DEX events. /// Returns true for launch or bonding-curve creation DEX events.
pub use dex_event_classification::is_dex_launch_event_kind; 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. /// Returns all DEX support matrix entries as owned DTOs.
pub use dex_support_matrix::dex_support_matrix_entry_dtos; pub use dex_support_matrix::dex_support_matrix_entry_dtos;
/// Global error type used by the `kb_lib` crate. /// 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; pub use error::Error;
/// Generic asynchronous HTTP client. /// Generic asynchronous HTTP client.
pub use http_client::HttpClient; pub use http_client::HttpClient;
@@ -1394,19 +1401,10 @@ pub use json_rpc_ws::JsonRpcWsRequest;
/// JSON-RPC 2.0 success response. /// JSON-RPC 2.0 success response.
pub use json_rpc_ws::JsonRpcWsSuccessResponse; pub use json_rpc_ws::JsonRpcWsSuccessResponse;
/// Parses a JSON value into a JSON-RPC incoming message. /// 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; pub use json_rpc_ws::is_probable_json_rpc_object_text;
/// Parses a raw text message into a JSON-RPC incoming message. /// 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; pub use json_rpc_ws::parse_json_rpc_ws_incoming_text;
/// Parses a JSON value into a JSON-RPC incoming message. /// 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; pub use json_rpc_ws::parse_json_rpc_ws_incoming_value;
/// Result of one launch surface attribution. /// Result of one launch surface attribution.
pub use launch_origin::LaunchAttributionResult; pub use launch_origin::LaunchAttributionResult;
@@ -1467,14 +1465,8 @@ pub use pair_analytic_signal::PairAnalyticSignalService;
/// One pair-candle aggregation result. /// One pair-candle aggregation result.
pub use pair_candle_aggregation::PairCandleAggregationResult; pub use pair_candle_aggregation::PairCandleAggregationResult;
/// Pair-candle aggregation service. /// 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; pub use pair_candle_aggregation::PairCandleAggregationService;
/// Pair-candle query service. /// 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; pub use pair_candle_query::PairCandleQueryService;
/// Summary produced by a pair-symbol refresh pass. /// Summary produced by a pair-symbol refresh pass.
pub use pair_symbol::PairSymbolRefreshResult; 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. /// Parses a Solana JSON-RPC notification into an official typed payload.
pub use solana_pubsub_ws::parse_solana_ws_typed_notification; pub use solana_pubsub_ws::parse_solana_ws_typed_notification;
/// Parses a typed Solana PubSub notification from a generic websocket event. /// 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; pub use solana_pubsub_ws::parse_solana_ws_typed_notification_from_event;
/// One pool-backfill result summary. /// One pool-backfill result summary.
pub use token_backfill::PoolBackfillResult; pub use token_backfill::PoolBackfillResult;
@@ -1510,22 +1497,14 @@ pub use token_backfill::SignatureBatchBackfillResult;
/// One token-backfill result summary. /// One token-backfill result summary.
pub use token_backfill::TokenBackfillResult; pub use token_backfill::TokenBackfillResult;
/// Historical token backfill service. /// 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; pub use token_backfill::TokenBackfillService;
/// Summary produced by a token metadata backfill pass. /// Summary produced by a token metadata backfill pass.
pub use token_metadata::TokenMetadataBackfillResult; pub use token_metadata::TokenMetadataBackfillResult;
/// Service that enriches persisted token rows with mint and display metadata. /// Service that enriches persisted token rows with mint and display metadata.
pub use token_metadata::TokenMetadataBackfillService; pub use token_metadata::TokenMetadataBackfillService;
/// Guard keeping non-blocking tracing workers alive. /// 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; pub use tracing::TracingGuard;
/// Initializes the global tracing subscriber. /// Initializes the global tracing subscriber.
///
/// This function is expected to be called once at application startup.
pub use tracing::init_tracing; pub use tracing::init_tracing;
/// One trade-aggregation result. /// One trade-aggregation result.
pub use trade_aggregation::TradeAggregationResult; pub use trade_aggregation::TradeAggregationResult;
@@ -1543,8 +1522,7 @@ pub use tx_resolution::TransactionResolutionRequest;
pub use tx_resolution::TransactionResolutionService; pub use tx_resolution::TransactionResolutionService;
/// One forwarded WebSocket notification envelope for transaction resolution. /// One forwarded WebSocket notification envelope for transaction resolution.
pub use tx_resolution::WsTransactionResolutionEnvelope; pub use tx_resolution::WsTransactionResolutionEnvelope;
/// Relay that consumes forwarded WS notifications and resolves matching /// Relay that consumes forwarded WS notifications and resolves matching signatures through HTTP `getTransaction`.
/// signatures through HTTP `getTransaction`.
pub use tx_resolution::WsTransactionResolutionRelay; pub use tx_resolution::WsTransactionResolutionRelay;
/// Runtime statistics for one transaction resolution relay worker. /// Runtime statistics for one transaction resolution relay worker.
pub use tx_resolution::WsTransactionResolutionRelayStats; pub use tx_resolution::WsTransactionResolutionRelayStats;

File diff suppressed because it is too large Load Diff

View File

@@ -114,6 +114,24 @@ impl TradeAggregationService {
continue; 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) if !crate::is_decoded_event_trade_candidate(decoded_event.event_kind.as_str(), &payload)
{ {
tracing::debug!( tracing::debug!(
@@ -150,8 +168,8 @@ impl TradeAggregationService {
quote_token_mint: quote_token_mint.as_deref(), quote_token_mint: quote_token_mint.as_deref(),
base_token_decimals, base_token_decimals,
quote_token_decimals, quote_token_decimals,
base_vault_address: base_vault_address.as_deref(), base_vault_address: effective_base_vault_address,
quote_vault_address: quote_vault_address.as_deref(), quote_vault_address: effective_quote_vault_address,
}; };
let amount_resolution = let amount_resolution =
crate::trade_amount_resolution::resolve_trade_amounts(&amount_input).await; 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( fn should_skip_pump_fun_duplicate_trade_event(
decoded_event: &crate::DexDecodedEventDto, decoded_event: &crate::DexDecodedEventDto,
decoded_events: &[crate::DexDecodedEventDto], decoded_events: &[crate::DexDecodedEventDto],

View File

@@ -253,6 +253,36 @@ pub(crate) async fn resolve_trade_amounts(
return Err(error); 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.") if input.decoded_event.event_kind.starts_with("meteora_dlmm.")
&& (base_amount_raw.is_none() || quote_amount_raw.is_none()) && (base_amount_raw.is_none() || quote_amount_raw.is_none())
{ {
@@ -283,6 +313,21 @@ pub(crate) async fn resolve_trade_amounts(
return Err(error); 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.") { 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( let vault_side = crate::trade_amount_resolution::infer_trade_side_from_vault_balance_deltas(
input.transaction.meta_json.as_deref(), input.transaction.meta_json.as_deref(),
@@ -316,6 +361,17 @@ pub(crate) async fn resolve_trade_amounts(
resolved_trade_side = vault_side; 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() { if price_quote_per_base.is_none() {
price_quote_per_base = price_quote_per_base =
crate::trade_metric_update::compute_price_quote_per_base_from_raw_amounts_with_decimals( 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) = let (input_vault_address, output_vault_address, input_token_account, output_token_account) =
if input.decoded_event.event_kind.ends_with(".buy") if input.decoded_event.event_kind.ends_with(".buy")
|| input || input.decoded_event.event_kind.ends_with(".buy_exact_quote_in")
.decoded_event
.event_kind
.ends_with(".buy_exact_quote_in")
{ {
( (
effective_quote_vault_address, effective_quote_vault_address,
@@ -815,7 +868,8 @@ async fn apply_pump_fun_amount_fallback(
*price_quote_per_base = inferred.2; *price_quote_per_base = inferred.2;
} }
if base_amount_raw.is_none() || quote_amount_raw.is_none() || price_quote_per_base.is_none() { 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, input,
base_amount_raw, base_amount_raw,
quote_amount_raw, quote_amount_raw,
@@ -1111,6 +1165,160 @@ async fn apply_meteora_damm_v1_flattened_cpi_amount_fallback(
.await; .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( async fn apply_flattened_cpi_amount_fallback(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
protocol_label: &str, 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 { fn string_option_equals(left: std::option::Option<&str>, right: &str) -> bool {
let left = match left { let left = match left {
Some(left) => left.trim(), Some(left) => left.trim(),
@@ -1589,7 +1832,6 @@ async fn load_decoded_instruction(
return Ok(instruction_option); return Ok(instruction_option);
} }
fn normalize_pump_swap_anchor_buy_exact_quote_in_amounts( fn normalize_pump_swap_anchor_buy_exact_quote_in_amounts(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
base_amount_raw: &mut std::option::Option<std::string::String>, base_amount_raw: &mut std::option::Option<std::string::String>,

View 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;

View 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;