This commit is contained in:
2026-06-15 20:16:27 +02:00
parent 3b908b318e
commit 045af4931c
44 changed files with 5328 additions and 113 deletions

View File

@@ -86,3 +86,4 @@
0.7.51 - Raydium AMM v4 event coverage clôturé : decoder maximal local pour tous les discriminants officiels AMM v4 `00..11`, spécialisation des swaps `swap_base_in/out` et `swap_base_in/out_v2`, suppression durable du `raydium_amm_v4.swap` legacy, index AMM v4 en discriminant 1 octet, matérialisation validée des swaps, liquidity, lifecycle, fees, admin/config et side effects orderbook, `pre_initialize` conservé comme lifecycle audit deprecated/partial, `simulate_info` decoded-only, reset replay renforcé par `protocol_name`, validation des invariants failed/non-swap/single-target/unexplained gaps et maintien de `raydium_pool_v4` en audit conditionnel sans decoder autonome.
0.7.52 - Raydium Stable Swap event coverage clôturé : decoder legacy 1 octet pour la surface locale `00..0d`, matérialisation lifecycle/liquidity/admin/fee/orderbook selon contexte, swaps `swap_base_in/out` matérialisés uniquement depuis deltas de vaults exacts (`stable_swap_vault_balance_delta`), conservation des bornes dinstruction comme audit-only, failed transactions decoded-only avec skip reasons, validation locale 407 tests et clippy `-D warnings` OK.
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`.

View File

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

View File

@@ -2,6 +2,74 @@
# khadhroony-bobobot
## État final validé `0.7.54` — `pump_fun`
La tranche `0.7.54 pump_fun` est clôturée côté coverage, décodage local maximal, matérialisation métier prudente et validation SQL. Elle ferme la surface Pump.fun principale avant l'ouverture de `0.7.55 pump_fees`.
Program id canonique :
```text
6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
```
Source IDL locale prioritaire :
```text
idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json
```
Points verrouillés :
- les `40` instructions et `23` events Anchor connus par l'IDL locale sont inventoriés et couverts localement ;
- les instructions IDL-only absentes du registre upstream initial sont intégrées côté coverage, notamment `buy_v2`, `sell_v2`, `buy_exact_quote_in_v2`, `migrate_v2`, `claim_cashback_v2`, `collect_creator_fee_v2`, `distribute_creator_fees_v2` et `update_buyback_config` ;
- `pump_fun.buy` et `pump_fun.sell` restent matérialisés directement comme trades quand les montants sont fiables ;
- `pump_fun.buy_exact_sol_in` est matérialisé directement, y compris pour les logs `Program data` Anchor tronqués quand les montants exacts sont extractibles ;
- `pump_fun.buy_v2`, `pump_fun.sell_v2` et `pump_fun.buy_exact_quote_in_v2` restent des instructions audit/coverage/routing : elles ne sont pas matérialisées directement ;
- la matérialisation canonique des trades v2/exact passe par `pump_fun.trade_event` quand l'event Anchor porte les montants exécutés et se corrèle sans ambiguïté à l'instruction ;
- les `trade_event` couverts par un trade direct reçoivent un skip explicite afin d'éviter le double-count ;
- les familles non-trade alimentent uniquement les tables prévues (`launch`, `fee`, `reward`, `admin`, `lifecycle`) ou restent decoded-only/audit-only avec raison explicite ;
- les transactions failed restent décodables pour audit mais ne produisent aucun business event.
Validation locale finale rapportée après replay forcé :
```text
1679 replayed
0 decode skipped
1679 ledger upserts
145 unsafe ledger rows
89 trades
0 liquidity
10 lifecycle
0 tokenAccount
348 candle upserts
instructionObservations = 13905
resetDeleted = 1112
catalog = 52 tokens / 50 pools / 50 pairs
```
Matérialisation Pump.fun finale observée :
```text
pump_fun.buy 17 trades
pump_fun.sell 25 trades
pump_fun.buy_exact_sol_in 15 trades
pump_fun.trade_event 25 trades
```
Checks de fermeture :
- fallback `upstream_git` Pump.fun : vide ;
- decoded Pump.fun sans coverage : vide ;
- fallback upstream résiduel pour entrées couvertes : vide ;
- successful non-materialized sans skip reason : vide ;
- failed transaction avec business materialization : vide ;
- multi-target materialization : vide ;
- trade candidates Pump.fun sans matérialisation ni skip : vide ;
- watchlist globale : plus aucun `pump_fun`, backlog restant principalement `pump_fees`, puis `jupiter_swap` et `dflow_aggregator_v4`.
La suite immédiate est `0.7.55 pump_fees` sur nouvelle base SQLite, avec politique identique : tout ce qui peut être décodé doit l'être, et tout ce qui peut être matérialisé de manière fiable doit l'être.
## État final validé `0.7.53` — `pump_swap`
La tranche `0.7.53 pump_swap` est clôturée côté décodage transaction/log et matérialisation métier. Elle ferme le program id unique `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA` sans rouvrir Raydium.
@@ -58,7 +126,7 @@ Livrables `0.7.53` :
- `validation_sql/SQL_VALIDATION_DEX_COVERAGE_GLOBAL_0_7_53.sql` ;
- `idls/` comme corpus local d'IDL Solscan à comparer aux sources Git.
La suite immédiate est `pump_fees` / `pump_fun` selon priorité de backlog observé. Les petits gaps Meteora sont volontairement reportés aux tranches Meteora futures.
La suite immédiate après `0.7.54 pump_fun` est `pump_fees` (`0.7.55`). Les petits gaps Meteora restent volontairement reportés aux tranches Meteora futures.
## État final validé `0.7.51` — `raydium_amm_v4`
@@ -487,14 +555,14 @@ Si une requête DB est ajoutée ou modifiée, mettre à jour les re-exports dans
## 8. Priorité immédiate
La priorité immédiate après la clôture `0.7.53` est la suivante :
La priorité immédiate après la clôture `0.7.54 pump_fun` est la suivante :
1. `0.7.53` est clos pour `pump_swap` : ne rouvrir que pour correction de bug, pas pour ajout fonctionnel IDL déjà couvert ;
2. maintenir les checks globaux de surveillance dans `validation_sql/SQL_VALIDATION_DEX_COVERAGE_GLOBAL_0_7_53.sql` après chaque gros backfill ;
3. traiter ensuite le backlog observé, en priorité `pump_fees`, puis `pump_fun`, puis `jupiter_swap` si lobjectif devient lanalyse des routes/agrégateurs ;
4. reporter volontairement les corrections Meteora restantes (`meteora_dlmm.swap`, `meteora_damm_v2.swap`, `meteora_damm_v2.instruction_audit`) aux tranches Meteora dédiées ;
5. ne pas rouvrir `raydium_amm_v4`, `raydium_clmm` ou `raydium_cpmm` tant que les requêtes Raydium normalisées restent vides ;
6. garder `raydium_launchpad` et `raydium_stable_swap` en surveillance : les entrées non observées restent `upstream_git_mapped_unverified`, pas des régressions.
1. ouvrir `0.7.55 pump_fees` sur une base SQLite neuve ;
2. décoder toutes les instructions et tous les events connus de `idls/pump_fees.pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ.json` ;
3. matérialiser tout ce qui est prouvable : fee accounting, fee sharing, social/donation fee PDA, buyback, config/admin, rewards éventuelles ;
4. ne créer aucun trade/candle direct pour `pump_fees` sauf preuve transactionnelle forte d'un swap économique autonome ;
5. maintenir les checks Pump.fun/PumpSwap/Raydium en non-régression ;
6. reporter Meteora/Jupiter/dFlow aux tranches prévues sauf si une dépendance stricte apparaît pendant l'analyse `pump_fees`.
Garde-fous constants :
@@ -605,8 +673,8 @@ La suite fonctionnelle reprend par Raydium avant Meteora :
4. `0.7.51``raydium_amm_v4` ;
5. `0.7.52``raydium_stable_swap` — clôturé ;
6. `0.7.53``pump_swap` — clôturé ;
7. `0.7.54``pump_fees` ;
8. `0.7.55``pump_fun` ;
7. `0.7.54``pump_fun` ;
8. `0.7.55``pump_fees` ;
9. `0.7.56+` — Meteora, routers/agrégateurs, Phoenix/OpenBook, Orca puis les autres DEX/surfaces.
`raydium_pool_v4.json` reste repoussé en audit conditionnel tardif, pas une tranche bloquante.

View File

@@ -2,28 +2,29 @@
# khadhroony-bobobot — Roadmap
## État courant — clôture `0.7.53 pump_swap`
## État courant — clôture `0.7.54 pump_fun` et ouverture `0.7.55 pump_fees`
`0.7.53` est clos pour `pump_swap`. La version ferme le décodage transaction/log de PumpSwap, la matérialisation `buy`, `sell` et `buy_exact_quote_in` depuis sources exactes, les events Anchor audit-only, les tests synthétiques IDL, et la surveillance SQL globale. Les futures interventions PumpSwap doivent être des corrections de bugs ou des adaptations à un changement externe prouvé, pas lajout dentrées IDL déjà connues.
`0.7.54 pump_fun` est clos pour la surface Pump.fun principale. La tranche a transformé l'ouverture documentaire en decoder local maximal : inventaire des `40` instructions et `23` events Anchor de l'IDL locale, coverage local, décodage Anchor/self-CPI/log, tests synthétiques, routes catalogue et matérialisation prudente.
Décisions de clôture :
Décisions de clôture Pump.fun :
- `pump_swap.buy_exact_quote_in` est matérialisé uniquement avec `amountSource=pump_swap_anchor_buy_event`; les rows `instruction_bounds_only` restent decoded-only ;
- les events Anchor `buy_event`, `sell_event`, `deposit_event`, `withdraw_event`, `create_pool_event`, etc. restent audit-only pour éviter le double-count avec les instructions locales ;
- `claim_token_incentives_event` est testé et prêt à matérialiser `reward` si un corpus réussi apparaît ; les signatures observées côté instruction étaient failed et ne doivent pas produire de reward ;
- `sync_user_volume_accumulator_event` reste `implemented_idl_unobserved` : plus de 60/70 signatures supplémentaires ont confirmé linstruction sans faire apparaître levent ;
- Raydium AMM v4 / CLMM / CPMM ne présentent plus de gap ciblé après normalisation des observations ;
- les gaps Meteora sont explicitement différés.
- `pump_fun.buy` et `pump_fun.sell` restent matérialisés directement quand les montants sont fiables ;
- `pump_fun.buy_exact_sol_in` est matérialisé directement, y compris quand un `Program data` tronqué permet d'extraire les montants exacts ;
- `pump_fun.buy_v2`, `pump_fun.sell_v2` et `pump_fun.buy_exact_quote_in_v2` sont decoded/audit/routing, mais ne sont pas matérialisés directement ;
- `pump_fun.trade_event` devient la source canonique des montants exécutés pour les v2/exact quand il est corrélé à l'instruction sans ambiguïté ;
- les `trade_event` déjà couverts par un trade direct reçoivent un skip explicite pour empêcher le double-count ;
- les non-trades Pump.fun sont matérialisés seulement vers `launch`, `fee`, `reward`, `admin` ou `lifecycle` quand le contexte est fiable ; sinon ils restent decoded-only/audit-only avec skip reason ;
- les validations `Q00`, `Q04`, `Q05`, `Q06`, `Q07`, `Q08`, `Q11` sont propres ; la watchlist globale ne contient plus de `pump_fun`.
### Phasage immédiat après `0.7.53`
Replay final rapporté : `1679 replayed`, `89 trades`, `10 lifecycle`, `348 candle upserts`, `13905 instructionObservations`, catalogue `52 tokens / 50 pools / 50 pairs`.
### Phasage immédiat après `0.7.54`
| Priorité | Tranche | Surface | Raison |
|---:|---|---|---|
| 1 | `0.7.54` | `pump_fees` | Backlog observé dominant (`get_fees` très fréquent) ; aucun trade/candle direct attendu. |
| 2 | `0.7.55` | `pump_fun` | Launch/bonding/migration et creator fees observés en fallback upstream. |
| 3 | `0.7.56+` | `meteora_*` | Corriger les gaps locaux Meteora reportés volontairement. |
| 4 | ultérieur | `jupiter_swap` / agrégateurs | Routes et comptes auxiliaires à traiter sans double-count des DEX effectifs. |
| 1 | `0.7.55` | `pump_fees` | Backlog global restant : `get_fees`, `create_fee_sharing_config`, `update_fee_shares`; programme fee associé à Pump. Tout décoder et tout matérialiser si fiable. |
| 2 | `0.7.56+` | `meteora_*` | Corriger les gaps locaux Meteora reportés volontairement. |
| 3 | ultérieur | `jupiter_swap` / agrégateurs | Routes et comptes auxiliaires à traiter sans double-count des DEX effectifs. |
## 0.7.47-1FE5 — Décision de planification : ne plus viser “tous les events en une session”
@@ -62,8 +63,8 @@ Exceptions : les comptes non-programmes (`platform_config`, token authority, com
| Version cible | Decoder / surface | Program id | Famille | Objectif de clôture |
|---|---|---|---|---|
| `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_fees` | `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` | Pump / fee | Couvrir fee accounting/claim/config observés ; aucun trade/candle direct. |
| `0.7.55` | `pump_fun` | `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` | Pump / launch-bonding | Couvrir create, buy/sell bonding, migration/graduate, config/update ; séparer bonding curve et DEX effectif. |
| `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 | Couvrir fee accounting/claim/config observés ; aucun trade/candle direct. |
| `0.7.56` | `meteora_dbc` | `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` | Meteora / DBC | Compléter launch/bonding, swaps exploitables, migration, fees/admin/config. |
| `0.7.57` | `meteora_dlmm` | `LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo` | Meteora / DLMM | Parité upstream finale : swaps, bins, positions, liquidity, fees/rewards/admin. |
| `0.7.58` | `meteora_damm_v1` | `Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB` | Meteora / DAMM v1 | Parité upstream finale : pools, swaps, liquidity, lock, fees/admin. |
@@ -1423,8 +1424,8 @@ Les comptes non-programmes ne créent pas de tranche decoder autonome. `SOLSCAN_
| Version | Decoder / surface | Program id | Objectif |
|---:|---|---|---|
| `0.7.53` | `pump_swap` | `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA` | Clos : `buy/sell/buy_exact_quote_in` depuis sources exactes, non-trades spécialisés, events Anchor audit-only. |
| `0.7.54` | `pump_fees` | `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` | Couvrir fee accounting/claim/config ; aucun trade/candle direct. |
| `0.7.55` | `pump_fun` | `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` | Couvrir launch/bonding/migration : create, buy/sell bonding, update/config, graduate/migrate. |
| `0.7.54` | `pump_fun` | `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` | **Clos** : decoder maximal local, trades directs et `trade_event` canonique, non-trades selon contexte, validations propres. |
| `0.7.55` | `pump_fees` | `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` | Couvrir fee accounting/claim/config ; aucun trade/candle direct. |
#### Bloc Meteora
@@ -1738,8 +1739,8 @@ Ordre de travail recommandé pour la suite :
9. `0.7.51` : `raydium_amm_v4` — clos ;
10. `0.7.52` : `raydium_stable_swap` — clos ;
11. `0.7.53` : `pump_swap` / `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA` — clos ;
12. `0.7.54` : `pump_fees` / `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` ;
13. `0.7.55` : `pump_fun` / `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` ;
12. `0.7.54` : `pump_fun` / `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` ;
13. `0.7.55` : `pump_fees` / `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` ;
14. `0.7.56+` : appliquer le phasage strict “une version = un `program_id`” défini en section `6.085`.
Garde-fous constants :

View File

@@ -1,3 +1,5 @@
<!-- file: docs/DB_EVENT_MODEL_REVIEW.md -->
# Database Event Model Review — `khadhroony-bobobot` `0.7.47-1FE5`
## Conclusion courte

View File

@@ -1,4 +1,22 @@
# DEX Decoder Matrix — `khadhroony-bobobot` `0.7.53 final`
<!-- file: docs/DEX_DECODER_MATRIX.md -->
# DEX Decoder Matrix — `khadhroony-bobobot` `0.7.54 pump_fun closed`
## Note `0.7.54 closed` — Pump.fun clos, Pump Fees ensuite
La tranche `0.7.54` ferme `pump_fun` avant `pump_fees`. La surface Pump.fun principale est couverte depuis le code local, l'IDL Solscan locale `idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json`, le registre upstream et le corpus SQLite.
Décisions structurantes :
- `pump_fun` est `supported / closed` côté decoder et validation locale ;
- toutes les instructions/events connus de l'IDL locale sont inventoriés ;
- `buy`, `sell`, `buy_exact_sol_in` peuvent être matérialisés directement avec montants fiables ;
- `buy_v2`, `sell_v2`, `buy_exact_quote_in_v2` restent decoded/audit/routing et s'appuient sur `pump_fun.trade_event` pour la matérialisation canonique ;
- `pump_fun.trade_event` matérialise les v2/exact quand les montants exécutés et la corrélation instruction sont prouvés ;
- les non-trades Pump.fun alimentent uniquement les tables business adaptées ou restent audit-only avec skip reason.
La prochaine tranche est `0.7.55 pump_fees`.
## Note `0.7.53 final` — PumpSwap clôturé et sources IDL locales
@@ -41,7 +59,7 @@ Cette matrice complète `kb_lib/src/dex_support_matrix.rs`. Elle documente **ce
| 5 | `raydium_stable_swap` | `supported / 0.7.52 closed` | Decoder legacy 1 octet, surface `00..0d`, swaps matérialisés depuis deltas vault exacts. | Surveiller seulement de nouveaux discriminants ou `swap_event` observé. |
| 6 | `raydium_pool_v4` | `to_verify / late-phase conditional audit` | IDL annexe mentionnée par fnzero, non présente dans l'archive locale, pas de program id/rôle confirmé ici. | Ne pas promouvoir tant que program id distinct, rôle exact et corpus exploitable ne sont pas confirmé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` | `partial / 0.7.55 launch_surface` | Création/token launch partiellement décodée ; intégrée au pipeline de listings. | Traiter tous les events Pump.fun disponibles : buy/sell/migrate/create/update ; séparer bonding/launch de DEX effectif ; valider migration vers PumpSwap. |
| 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 ; `pump_fees` suit en `0.7.55`. |
| 9 | `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_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_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. |
@@ -97,7 +115,7 @@ Un event peut devenir `materialized` uniquement si :
| Code | Rôle | Surface | Program id status | Observed | Decoded | Materialized | Status | Skip reason |
|---|---|---|---|---:|---:|---:|---|---|
| `pump_fun` | `launch_surface` | `launch` | `known` | non | oui | oui | `partial` | launch_surface_requires_migration_linking_before_live_trading |
| `pump_fun` | `launch_surface` | `launch/bonding` | `known` | oui | oui | oui | `0.7.54_closed` | Decoder maximal IDL/local ; v2/exact matérialisés via `trade_event` canonique ; non-trades selon contexte. |
| `pump_swap` | `dex_effective` | `AMM` | `known` | oui | oui | oui | `supported` | |
| `raydium_cpmm` | `dex_effective` | `AMM` | `known` | oui | oui | oui | `supported` | |
| `raydium_clmm` | `dex_effective` | `CLMM` | `known` | oui | oui | oui | `supported` | |
@@ -279,6 +297,13 @@ La tranche a été validée sur base SQLite dédiée : tous les discriminants `0
|---|---|---|---|---|
| `raydium_stable_swap` | `5quBtoiQqxF9Jv6KYKctB59NT3gtJD2Y65kdnB1Uev3h` | supported / closed | legacy 1 octet | Surface locale `00..0d` couverte ; swaps `swap_base_in/out` matérialisés uniquement depuis deltas vault exacts ; instruction bounds et failed tx restent decoded-only. |
## 0.7.54 — Pump.fun
| Decoder | Program id | Statut | Source discriminants | Couverture locale initiale | Règles métier |
|---|---|---:|---|---|---|
| `pump_fun` | `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` | supported / 0.7.54 closed | upstream registry + `idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json` + corpus SQLite validé | `40` instructions et `23` events Anchor connus couverts ; `buy/sell/buy_exact_sol_in` matérialisés ; `buy_v2/sell_v2/buy_exact_quote_in_v2` audit/routing ; `trade_event` matérialise les montants exécutés v2/exact | `k_sol_trade_events` uniquement avec montants exacts ; `create/migrate` vers `k_sol_launch_events` ; creator fees vers `k_sol_fee_events` ; cashback/incentives vers `k_sol_reward_events` ; admin/config vers `k_sol_pool_admin_events` ; decoded-only/audit-only avec skip reason sinon |
## 0.7.53 — PumpSwap
| Decoder | Program id | Statut | Source discriminants | Couverture locale | Règles métier |

View File

@@ -1,4 +1,6 @@
# DEX Event Coverage Matrix — `khadhroony-bobobot` `0.7.53 final`
<!-- file: docs/DEX_EVENT_COVERAGE_MATRIX.md -->
# DEX Event Coverage Matrix — `khadhroony-bobobot` `0.7.54 pump_fun closed`
Cette matrice complète `docs/DEX_DECODER_MATRIX.md` avec une lecture par familles d'événements. Elle ne remplace pas la preuve locale : une entrée Git/IDL reste un indice tant qu'elle n'est pas observée dans le corpus local puis validée par replay et SQL.
@@ -214,6 +216,37 @@ Status: **closed on local corpus**.
Stable Swap swaps are not materialized from instruction min/max bounds. `swap_base_in/out` require `amountSource=stable_swap_vault_balance_delta`; `stable_swap_instruction_bounds_only` remains decoded-only and, in the final corpus, appears only on failed transactions.
## 0.7.54 — `pump_fun` closed
Program id unique : `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`.
Source locale prioritaire : `idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json`.
Replay final rapporté : `1679 replayed`, `89 trades`, `10 lifecycle`, `348 candle upserts`, catalogue `52 tokens / 50 pools / 50 pairs`.
| Entry / groupe | Discriminator | Family | Expected DB target | Local event kind cible | Status final |
|---|---:|---|---|---|---|
| `create` / `create_v2` / `create_event` | `181ec828051c0777` / `d6904cec5f8b31b4` / `1b72a94ddeeb6376` | `launch` | `k_sol_launch_events` ou decoded-only | `pump_fun.create*` | couvert ; launch matérialisé quand mint/bonding/creator sont fiables |
| `migrate` / `migrate_v2` / migration events | `9beae792ec9ea21e` / `bbcb121fceedfe29` / voir IDL | `migration` | `k_sol_launch_events` ou decoded-only | `pump_fun.migrate*` | couvert ; migration matérialisée quand contexte fiable |
| `buy` / `sell` | `66063d1201daebea` / `33e685a4017f83ad` | `swap` | `k_sol_trade_events` | `pump_fun.buy` / `pump_fun.sell` | matérialisés directement avec montants fiables : `17` buy, `25` sell |
| `buy_exact_sol_in` | `38fc74089edfcd5f` | `swap` | `k_sol_trade_events` | `pump_fun.buy_exact_sol_in` | matérialisé directement ; `15` trades, y compris logs `Program data` tronqués exploitables |
| `buy_v2` / `sell_v2` / `buy_exact_quote_in_v2` | `b817ee6167c5d33d` / `5df6823ce7e940b2` / `c2ab1c46684d5b2f` | `swap` | audit/routing + `trade_event` canonique | `pump_fun.*_v2` | decoded/covered, non matérialisés directement ; les montants exécutés sont matérialisés via `pump_fun.trade_event` |
| `trade_event` | `bddb7fd34ee661ee` | `swap` | `k_sol_trade_events` | `pump_fun.trade_event` | `72` decoded / `25` trades ; source canonique des v2/exact quand corrélée ; skip explicite si couvert par trade direct |
| `collect_creator_fee*` / `distribute_creator_fees*` | voir IDL | `fee` | `k_sol_fee_events` ou decoded-only | `pump_fun.collect_creator_fee*`, `pump_fun.distribute_creator_fees*` | couvert ; matérialisation fee seulement avec montant/acteur fiables |
| `claim_cashback*` / `claim_token_incentives` / volume accumulators | voir IDL | `reward` | `k_sol_reward_events` ou decoded-only | `pump_fun.claim_*`, volume accumulator events | couvert ; rewards matérialisées seulement si preuve suffisante |
| creator/admin/config group | voir IDL | `admin_config` | `k_sol_pool_admin_events` ou decoded-only | `pump_fun.admin_*`, `pump_fun.set_*`, `pump_fun.toggle_*`, quote/buyback/reserve entries | couvert ; promotion seulement si action/comptes exploitables |
### Invariants de fermeture `0.7.54`
- Aucun `pump_fun` local decoded event sans coverage.
- Aucun fallback `upstream_git` résiduel pour les entrées Pump.fun couvertes localement.
- Aucun business event matérialisé depuis transaction failed.
- Aucun non-swap Pump.fun matérialisé en trade/candle.
- Aucun double-count entre instruction trade directe et `trade_event` Anchor.
- Aucun trade candidate Pump.fun réussi sans matérialisation ni skip reason.
- Les entrées IDL-only sont couvertes localement ; les non observées restent `mapped_unverified` ou audit-only, pas des gaps bloquants.
## 0.7.53 — `pump_swap`
Program id unique : `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA`.
@@ -247,5 +280,5 @@ Program id unique : `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA`.
- `pump_swap` ne présente plus de decoded event local sans coverage dans le corpus de clôture.
- `buy_exact_quote_in` est matérialisé seulement quand le `BuyEvent` Anchor donne les montants exacts ; les bornes dinstruction seules restent non actionnables.
- Les events Anchor `*_event` sont décodés en audit-only pour éviter les doublons, sauf exception matérialisable explicitement testée.
- Les gaps globaux restants sont classés comme backlog upstream (`pump_fees`, `pump_fun`, `jupiter_swap`, agrégateurs), gaps Meteora reportés, ou observations non attribuées.
- Les gaps globaux restants sont classés comme backlog upstream (`pump_fun`, puis `pump_fees`, `jupiter_swap`, agrégateurs), gaps Meteora reportés, ou observations non attribuées.
- Les checks Raydium AMM v4 / CLMM / CPMM normalisés sont vides ; aucune correction Raydium nest incluse dans cette clôture.

View File

@@ -1,3 +1,5 @@
<!-- file: docs/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md -->
# Raydium Launchpad event coverage report — `0.7.50`
## Scope

View File

@@ -1,3 +1,5 @@
<!-- file: docs/SOLSCAN_ACCOUNT_SOURCE_MATRIX.md -->
# Solscan account source matrix
This file records the manual Solscan account inventory added during the `0.7.50` Raydium Launchpad closure. It is a source catalogue, not a support guarantee. Entries with `solscan_program_idl` can be used as IDL candidates; entries with `no_idl` require source/corpus work before decoder promotion.

View File

@@ -1,4 +1,4 @@
<!-- file: VALIDATION_STATUS_0_7_51.md -->
<!-- file: docs/VALIDATION_STATUS_0_7_51.md -->
# Validation status — `0.7.51 raydium_amm_v4`

View File

@@ -1,4 +1,4 @@
# file: VALIDATION_STATUS_0_7_51_MAX_DECODER.md
<!-- file: docs/VALIDATION_STATUS_0_7_51_MAX_DECODER.md -->
# Validation status — `0.7.51 raydium_amm_v4 max-decoder`

View File

@@ -1,3 +1,5 @@
<!-- file: docs/VALIDATION_STATUS_0_7_52_FINAL.md -->
# Validation status — 0.7.52 Raydium Stable Swap final
## Scope

View File

@@ -1,3 +1,5 @@
<!-- file: docs/prompts/NEXT_SESSION_PROMPT_0.7.47_1FE5_CONTINUATION_V2.md -->
# Prompt de reprise — khadhroony-bobobot `0.7.47-1FE5`
Reprise du projet `khadhroony-bobobot`.

View File

@@ -1,3 +1,5 @@
<!-- file: docs/prompts/NEXT_SESSION_PROMPT_0.7.47_EVENT_COVERAGE_V3.md -->
# Prompt de reprise — khadhroony-bobobot `0.7.47-1FE5` / Event coverage
Reprise du projet `khadhroony-bobobot`.

View File

@@ -1,3 +1,5 @@
<!-- file: docs/prompts/NEXT_SESSION_PROMPT_0.7.47_UPSTREAM_REGISTRY.md -->
# Prompt de reprise — khadhroony-bobobot `0.7.47`
Reprise du projet `khadhroony-bobobot`.

View File

@@ -1,3 +1,5 @@
<!-- file: docs/prompts/NEXT_SESSION_PROMPT_0.7.49_RAYDIUM_CLMM.md -->
# Prompt de reprise — khadhroony-bobobot `0.7.49` / Raydium CLMM event coverage
Reprise du projet `khadhroony-bobobot` après clôture fonctionnelle de `0.7.48 raydium_cpmm`.

View File

@@ -269,4 +269,4 @@ Commencer par analyser larchive fournie :
- fichiers à modifier ;
- hypothèse de classification par entry ;
- SQL initial de backfill/validation ;
4. proposer puis produire le premier delta minimal.
4. proposer puis produire le premier delta archive minimal.

View File

@@ -0,0 +1,347 @@
<!-- file: docs/prompts/PROMPT_0_7_55_PUMP_FEES.md -->
# Prompt de reprise — khadhroony-bobobot 0.7.55 — pump_fees
Tu reprends le workspace Rust/Tauri `khadhroony-bobobot` après clôture technique de `0.7.54 pump_fun`.
## 1. Archive et fichiers à fournir
Utiliser l'archive la plus récente après clôture `0.7.54 pump_fun`.
À considérer comme sources locales de savoir :
- code Rust du workspace ;
- `README.md`, `ROADMAP.md`, `CHANGELOG.md` ;
- `docs/DEX_DECODER_MATRIX.md` ;
- `docs/DEX_EVENT_COVERAGE_MATRIX.md` ;
- `docs/reports/PUMP_FUN_EVENT_COVERAGE_REPORT.md` ;
- `validation_sql/SQL_VALIDATION_PUMP_FUN_0_7_54.sql` ;
- `validation_sql/SQL_VALIDATION_PUMP_FUN_MATERIALIZATION_0_7_54.sql` ;
- `idls/**`, en particulier `idls/pump_fees.pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ.json` ;
- les logs/requêtes SQL collés pendant la session.
Ne pas supposer que la documentation est parfaite : vérifier contre le code, l'IDL locale et le corpus SQLite.
## 2. État validé avant cette version
`0.7.54 pump_fun` est clos.
Replay final rapporté :
```text
1679 replayed
0 decode skipped
1679 ledger upserts
145 unsafe ledger rows
89 trades
0 liquidity
10 lifecycle
0 tokenAccount
348 candle upserts
instructionObservations = 13905
resetDeleted = 1112
catalog = 52 tokens / 50 pools / 50 pairs
```
Checks de fermeture Pump.fun :
- upstream fallback Pump.fun : vide ;
- decoded Pump.fun sans coverage : vide ;
- successful non-materialized sans skip reason : vide ;
- failed transaction materialization safety : vide ;
- multi-target materialization safety : vide ;
- trade candidates Pump.fun sans matérialisation ni skip : vide ;
- watchlist globale : plus aucun `pump_fun`.
Décisions Pump.fun à préserver :
- `buy`, `sell`, `buy_exact_sol_in` sont matérialisés directement quand les montants sont fiables ;
- `buy_v2`, `sell_v2`, `buy_exact_quote_in_v2` ne sont pas matérialisés directement ;
- `trade_event` est la source canonique des montants exécutés v2/exact ;
- aucun double-count entre instruction trade et event Anchor ;
- transactions failed audit-only.
Ne pas rouvrir `pump_fun`, `pump_swap` ou Raydium sauf bug prouvé par SQL/code.
## 3. Objectif de `0.7.55 pump_fees`
Ouvrir et clôturer la surface `pump_fees`.
Program id cible :
```text
pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ
```
IDL locale :
```text
idls/pump_fees.pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ.json
```
exemple de code:
https://github.com/sevenlabs-hq/carbon/tree/main/decoders/pump-fees-decoder
L'IDL locale contient au minimum :
- `29` instructions ;
- `20` events ;
- `9` accounts ;
- `34` types.
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.
Le programme `pump_fees` est a priori un programme de fee/config/accounting. Aucun trade/candle direct n'est attendu sauf preuve transactionnelle très forte d'un swap économique autonome.
## 4. Backlog initial observé
La watchlist globale après `0.7.54 pump_fun` montre notamment :
```text
pump_fees get_fees e7257e55cf5b3f34 173 decoded / 171 tx
pump_fees create_fee_sharing_config c34e564c6f34fbd5 21 decoded / 21 tx
pump_fees update_fee_shares bd0d8863bba4ed23 14 decoded / 14 tx
```
Ces trois entrées doivent être les premières sources de corpus/backfill.
## 5. Périmètre fonctionnel
### Inclus
- décodage de toutes les instructions `pump_fees` connues par l'IDL locale ;
- décodage de tous les events Anchor `pump_fees` connus par l'IDL locale ;
- décodage Borsh des arguments et payloads quand les layouts sont définis ;
- classification coverage par famille : fee, reward, admin/config, buyback, social fee, donation fee, fee sharing, account lifecycle ;
- matérialisation vers les tables métier existantes quand les données sont fiables :
- `k_sol_fee_events` ;
- `k_sol_reward_events` si cashback/social/donation/claim représente une récompense exploitable ;
- `k_sol_pool_admin_events` pour config/admin/authority/tier/update ;
- `k_sol_pool_lifecycle_events` si création/initialisation de compte/config est pertinente ;
- `k_sol_dex_decoded_events_only` pour les vues/calculs/audit-only ;
- SQL de validation dédié ;
- documentation finale et rapport.
### Hors périmètre sauf preuve stricte
- nouveau trade/candle direct ;
- réouverture Pump.fun/PumpSwap ;
- Raydium/Meteora/Jupiter/dFlow ;
- refactor réseau ou UI non nécessaire.
## 6. Méthode obligatoire : nouvelle base SQLite
Créer une nouvelle DB dédiée à `0.7.55 pump_fees`.
Ne pas réutiliser l'ancienne DB de validation Pump.fun sauf pour lire des signatures de départ.
Après chaque backfill ou patch decoder :
```text
skipDexDecode=no
forceDexDecode=yes
deferInstructionObservations=yes
```
Puis :
- refresh catalog ;
- replay local ;
- relancer SQL de validation ;
- noter les compteurs replay.
## 7. Corpus et backfills
Construire le corpus local à partir de :
1. signatures `sample_signature` de la watchlist globale ;
2. filtres Solscan.io par program id + instruction/discriminator quand disponibles ;
3. Demo3 discovery multi-source/multi-target ;
4. batch backfill par groupes de signatures ;
5. program/signature backfill ciblé si nécessaire ;
6. signatures issues des requêtes SQL `instruction_observations`, fallback upstream et decoded-only résiduels.
Démarrer par :
- `get_fees` / `e7257e55cf5b3f34` ;
- `create_fee_sharing_config` / `c34e564c6f34fbd5` ;
- `update_fee_shares` / `bd0d8863bba4ed23`.
Ensuite couvrir les autres instructions/events de l'IDL locale, même non observés, par tests synthétiques lorsque le layout est connu.
## 8. Instructions IDL locales à inventorier
Inventorier et classifier au minimum :
- `claim_social_fee_pda` ;
- `claim_social_fee_pda_v2` ;
- `crank_donation_fee_pda` ;
- `create_donation_fee_pda` ;
- `create_fee_sharing_config` ;
- `create_social_fee_pda` ;
- `extend_fee_config` ;
- `get_fees` ;
- `initialize_buyback` ;
- `initialize_fee_config` ;
- `initialize_fee_program_global` ;
- `reset_fee_sharing_config` ;
- `reset_fee_sharing_config_v2` ;
- `revoke_fee_sharing_authority` ;
- `set_authority` ;
- `set_claim_rate_limit` ;
- `set_disable_flags` ;
- `set_social_claim_authority` ;
- `sweep_buyback` ;
- `transfer_fee_sharing_authority` ;
- `update_admin` ;
- `update_buyback_authority` ;
- `update_buyback_claim_rate_limit` ;
- `update_fee_config` ;
- `update_fee_shares` ;
- `update_fee_shares_v2` ;
- `update_stable_fee_config` ;
- `upsert_fee_tiers` ;
- `upsert_stable_fee_tiers`.
Events Anchor à inventorier :
- `CreateFeeSharingConfigEvent` ;
- `DonationFeePdaCranked` ;
- `DonationFeePdaCreated` ;
- `ExtendFeeConfigEvent` ;
- `InitializeFeeConfigEvent` ;
- `InitializeFeeProgramGlobalEvent` ;
- `ResetFeeSharingConfigEvent` ;
- `SetAuthorityEvent` ;
- `SetClaimRateLimitEvent` ;
- `SetDisableFlagsEvent` ;
- `SetSocialClaimAuthorityEvent` ;
- `SocialFeePdaClaimed` ;
- `SocialFeePdaCreated` ;
- `SweepBuybackEvent` ;
- `UpdateAdminEvent` ;
- `UpdateFeeConfigEvent` ;
- `UpdateFeeSharesEvent` ;
- `UpdateStableFeeConfigEvent` ;
- `UpsertFeeTiersEvent` ;
- `UpsertStableFeeTiersEvent`.
## 9. Contraintes de code Rust
Respecter strictement les conventions du projet :
- Rust 2024 ;
- pas de `?` ;
- pas de `unwrap()` / `expect()` en code applicatif ;
- pas de `anyhow` / `thiserror` ;
- `match` / `if let Err` explicites ;
- async-first ;
- `tracing` obligatoire ;
- pas de `mod.rs` ;
- pas de `pub mod` ; utiliser `mod` + `pub use` ;
- imports limités, types appelés de façon qualifiée quand c'est la convention locale ;
- tests offline ;
- ne pas casser `#![deny(unreachable_pub)]` et `#![warn(missing_docs)]`.
Si des requêtes DB sont ajoutées ou déplacées, penser aux re-exports :
- `kb_lib/src/db.rs` ;
- `kb_lib/src/lib.rs`.
## 10. Matérialisation attendue
Ne pas se contenter de decoded-only si une matérialisation fiable est possible.
Classification cible :
- `get_fees` : probablement decoded-only ou fee calculation audit ; ne pas matérialiser comme fee payé sans transfert/montant réalisé ;
- fee sharing config : `k_sol_pool_admin_events` ou lifecycle/config si comptes exploitables ;
- social/donation fee PDA create/claim/crank : `k_sol_fee_events`, `k_sol_reward_events` ou admin/lifecycle selon le sens exact des flux ;
- buyback init/sweep/update : fee/admin/buyback selon comptes et montants ;
- authority/config/tier updates : `k_sol_pool_admin_events` ;
- Anchor events : matérialiser s'ils portent le montant/acteur/compte fiable ; sinon audit-only avec skip reason ;
- transactions failed : decoded-only/audit-only, jamais business matérialisé.
Aucun `pump_fees` ne doit créer de `k_sol_trade_events` ni de candle sauf preuve irréfutable d'un trade économique autonome et non doublonné.
## 11. SQL de validation attendu
Créer :
```text
validation_sql/SQL_VALIDATION_PUMP_FEES_0_7_55.sql
```
Requêtes minimales :
1. upstream fallback samples `pump_fees` ;
2. local instruction observations `pump_fees` ;
3. coverage `pump_fees` ;
4. decoded events `pump_fees` sans coverage ;
5. residual upstream fallback pour entrées couvertes ;
6. successful non-materialized sans skip reason ;
7. failed transaction materialization safety ;
8. multi-target materialization safety ;
9. materialization summary par table ;
10. instruction observation versus coverage ;
11. contrôle anti-trade/candle direct `pump_fees` ;
12. global watchlist après replay.
## 12. Invariants de fermeture
La tranche `0.7.55` ne doit être considérée close que si :
- aucun fallback `upstream_git` `pump_fees` ne reste pour les entrées couvertes localement ;
- aucun decoded event `pump_fees` local sans coverage ;
- aucune transaction failed n'alimente une table métier ;
- aucun event multi-target incohérent ;
- aucune ligne successful non-materialized sans `skip*Reason` ;
- aucun trade/candle `pump_fees` artificiel ;
- toutes les instructions/events de l'IDL locale sont soit décodés/matérialisés, soit audit-only, soit non observés mais couverts par tests synthétiques ;
- la watchlist globale ne contient plus de `pump_fees` comme backlog dominant.
## 13. Documentation à mettre à jour en fin de tranche
Mettre à jour :
- `CHANGELOG.md` ;
- `README.md` ;
- `ROADMAP.md` ;
- `docs/DEX_DECODER_MATRIX.md` ;
- `docs/DEX_EVENT_COVERAGE_MATRIX.md` ;
- créer `docs/reports/PUMP_FEES_EVENT_COVERAGE_REPORT.md` ;
- créer ou mettre à jour le SQL de validation dédié.
## 14. Format de livraison attendu
Fournir un delta zip contenant uniquement les fichiers modifiés/ajoutés.
Nom recommandé :
```text
khadhroony-bobobot-v0.7.55-pump_fees-delta-N-files.zip
```
Chaque livraison doit inclure :
- résumé des changements ;
- liste exacte des fichiers modifiés ;
- commandes à lancer :
- `cargo fmt` ;
- `cargo test -p kb_lib` ;
- `cargo clippy -p kb_lib --all-targets -- -D warnings` ;
- replay recommandé ;
- SQL à exécuter ;
- résultats attendus.
## 15. Première tâche demandée
1. Inspecter le code et l'IDL `pump_fees` locale.
2. Comparer `upstream_registry_generated.rs`, `idls/pump_fees...json` et le corpus SQL.
3. Créer une base SQLite neuve `0.7.55`.
4. Backfiller les signatures `get_fees`, `create_fee_sharing_config`, `update_fee_shares`.
5. Ajouter le decoder local maximal `pump_fees` : instructions + events + tests synthétiques.
6. Ajouter coverage/materialization/validation SQL.
7. Rejouer et fermer seulement si tous les invariants sont propres.

View File

@@ -1,3 +1,5 @@
<!-- file: docs/prompts/PROMPT_REPRISE_khadhroony-bobobot_0.7.48-raydium-cpmm.md -->
# Prompt de reprise — khadhroony-bobobot `0.7.48` / Raydium CPMM event coverage
Reprise du projet `khadhroony-bobobot` après clôture de `0.7.48-pre`.

View File

@@ -1,3 +1,5 @@
<!-- file: docs/reports/DEX_COVERAGE_GLOBAL_WATCHLIST_0_7_53.md -->
# DEX coverage global watchlist — `0.7.53`
## Objet

View File

@@ -0,0 +1,127 @@
<!-- file: docs/reports/PUMP_FUN_EVENT_COVERAGE_REPORT.md -->
# Pump.fun event coverage report — clôture `0.7.54`
## Statut du rapport
Ce rapport clôture la tranche `0.7.54 pump_fun` côté coverage, décodage local, matérialisation métier prudente et validation SQL.
Program id canonique :
```text
6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
```
Source IDL locale prioritaire :
```text
idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json
```
## Sources utilisées
- `kb_lib/src/dex/pump_fun.rs` ;
- `kb_lib/src/dex_decode.rs` ;
- `kb_lib/src/trade_aggregation.rs` ;
- `kb_lib/src/trade_amount_resolution.rs` ;
- `kb_lib/src/dex_detection_route.rs` ;
- `kb_lib/src/dex_event_coverage.rs` ;
- `kb_lib/src/upstream_registry_generated.rs` ;
- `idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json` ;
- `validation_sql/SQL_VALIDATION_PUMP_FUN_0_7_54.sql` ;
- `validation_sql/SQL_VALIDATION_PUMP_FUN_MATERIALIZATION_0_7_54.sql` ;
- corpus SQLite bâti par backfills Demo3/signatures/pools et replay forcé.
## Couverture finale
L'IDL locale Pump.fun contient `40` instructions et `23` events Anchor. La tranche a ajouté la couverture locale des instructions/events connues, y compris les instructions IDL-only absentes du registre upstream initial :
- `add_quote_mint` ;
- `buy_exact_quote_in_v2` ;
- `buy_v2` ;
- `claim_cashback_v2` ;
- `collect_creator_fee_v2` ;
- `distribute_creator_fees_v2` ;
- `migrate_v2` ;
- `remove_quote_mint` ;
- `sell_v2` ;
- `set_virtual_quote_reserves` ;
- `update_buyback_config`.
Les events Anchor sont reconnus depuis `Program data:` et depuis le transport Anchor self-CPI/log `e445a52e51cb9a1d` quand présent.
## Règles de matérialisation finales
### Trades
| Source locale | Matérialisation | Règle |
|---|---|---|
| `pump_fun.buy` | `k_sol_trade_events` | directe si montants fiables |
| `pump_fun.sell` | `k_sol_trade_events` | directe si montants fiables |
| `pump_fun.buy_exact_sol_in` | `k_sol_trade_events` | directe ; les logs `Program data` tronqués sont exploités quand les montants exacts sont extractibles |
| `pump_fun.buy_v2` | non directe | instruction audit/coverage/routing uniquement |
| `pump_fun.sell_v2` | non directe | instruction audit/coverage/routing uniquement |
| `pump_fun.buy_exact_quote_in_v2` | non directe | instruction audit/coverage/routing uniquement |
| `pump_fun.trade_event` | `k_sol_trade_events` | source canonique des montants exécutés v2/exact quand corrélée sans ambiguïté |
Les `trade_event` déjà couverts par une instruction directe reçoivent un skip explicite afin d'éviter tout double-count.
### Non-trades
Les événements non-trade sont matérialisés uniquement vers leur table métier ciblée quand les comptes, acteurs et montants sont fiables :
- `k_sol_launch_events` pour create/migrate/graduate ;
- `k_sol_fee_events` pour creator fees, fee distribution et minimum fee ;
- `k_sol_reward_events` pour cashback, incentives et volume accumulators exploitables ;
- `k_sol_pool_admin_events` pour admin/config/creator/global authority ;
- `k_sol_pool_lifecycle_events` pour initialization/lifecycle.
Sinon, ils restent decoded-only/audit-only avec `skip*Reason` explicite. Les transactions failed ne produisent aucune matérialisation métier.
## Replay final rapporté
```text
1679 replayed
0 decode skipped
1679 ledger upserts
145 unsafe ledger rows
89 trades
0 liquidity
10 lifecycle
0 tokenAccount
348 candle upserts
instructionObservations = 13905
resetDeleted = 1112
catalog = 52 tokens / 50 pools / 50 pairs
```
## Matérialisation finale Pump.fun observée
```text
pump_fun.buy 17 trades
pump_fun.sell 25 trades
pump_fun.buy_exact_sol_in 15 trades
pump_fun.trade_event 25 trades
```
Les variantes v2/exact restent à `0` dans `k_sol_trade_events` par `decoded_event_id` d'instruction, ce qui est attendu : leur matérialisation canonique se fait via `pump_fun.trade_event`.
## Checks de fermeture SQL
Résultats finaux rapportés :
- `Q00` upstream fallback Pump.fun : vide ;
- `Q04` decoded Pump.fun sans coverage : vide ;
- `Q05` fallback upstream couvert localement : vide ;
- `Q06` successful non-materialized sans skip reason : vide ;
- `Q07` failed transaction materialization safety : vide ;
- `Q08` multi-target materialization safety : vide ;
- `Q11` trade candidates sans trade ni skip : vide ;
- `Q12` watchlist globale : plus de `pump_fun` ; restent `pump_fees`, `jupiter_swap` et `dflow_aggregator_v4`.
## Décisions de clôture
- `pump_fun` est clos côté decoder maximal local et validation corpus.
- Les prochaines interventions Pump.fun doivent être des corrections de bugs ou des adaptations à un changement externe prouvé.
- La suite logique est `0.7.55 pump_fees` sur nouvelle base SQLite.
- La politique reste : tout ce qui peut être décodé doit l'être ; tout ce qui peut être matérialisé de manière fiable doit l'être ; aucun trade/candle artificiel ne doit être créé.

View File

@@ -1,3 +1,5 @@
<!-- file: docs/reports/PUMP_SWAP_EVENT_COVERAGE_REPORT.md -->
# PumpSwap event coverage report — `0.7.53`
## Scope

View File

@@ -1,4 +1,4 @@
# file: docs/reports/RAYDIUM_CLMM_UPSTREAM_COVERAGE_REVIEW_PRE19.md
<!-- file: docs/reports/RAYDIUM_CLMM_UPSTREAM_COVERAGE_REVIEW_PRE19.md -->
# Raydium CLMM upstream coverage review — `0.7.49-pre.19`

View File

@@ -1,3 +1,5 @@
<!-- file: docs/reports/RAYDIUM_CPMM_CLMM_RECHECK_REPORT_0_7_50_PRE_R2.md -->
# Raydium CPMM/CLMM re-check report — `0.7.50-pre-r2`
## Scope

View File

@@ -1,3 +1,5 @@
<!-- file: docs/reports/RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md -->
# Rapport `0.7.48` — Raydium CPMM event coverage
## Scope

View File

@@ -1,3 +1,5 @@
<!-- file: docs/reports/RAYDIUM_CPMM_UPSTREAM_COVERAGE_REVIEW_PRE22.md -->
# Raydium CPMM upstream coverage review — 0.7.49-pre.22
## Scope

View File

@@ -1,3 +1,5 @@
<!-- file: docs/reports/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md -->
# Raydium Launchpad event coverage report — `0.7.50`
## Scope

View File

@@ -1,3 +1,5 @@
<!-- file: docs/reports/RAYDIUM_STABLE_SWAP_EVENT_COVERAGE_REPORT.md -->
# Raydium Stable Swap event coverage report — 0.7.52 final
## Scope

View File

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

View File

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

View File

@@ -62,6 +62,7 @@ pub use phoenix_v1::PhoenixV1AuditDecoded;
pub use phoenix_v1::PhoenixV1DecodedEvent;
pub use phoenix_v1::PhoenixV1Decoder;
pub use pump_fun::PumpFunCreateV2TokenDecoded;
pub use pump_fun::PumpFunInstructionAuditDecoded;
pub use pump_fun::PumpFunDecodedEvent;
pub use pump_fun::PumpFunDecoder;
pub use pump_fun::PumpFunTradeDecoded;

File diff suppressed because it is too large Load Diff

View File

@@ -1627,7 +1627,83 @@ impl DexDecodeService {
)
.await;
},
crate::PumpFunDecodedEvent::InstructionAudit(event) => {
let pool_account = Self::pump_fun_payload_string(
&event.payload_json,
&["poolAccount", "bondingCurve", "bonding_curve", "pool"],
);
let token_a_mint = Self::pump_fun_payload_string(
&event.payload_json,
&["tokenAMint", "token_a_mint", "tokenMint", "token_mint", "mint"],
);
let token_b_mint = match Self::pump_fun_payload_string(
&event.payload_json,
&["tokenBMint", "token_b_mint", "quoteMint", "quote_mint"],
) {
Some(token_b_mint) => Some(token_b_mint),
None => {
if token_a_mint.is_some() {
Some(crate::WSOL_MINT_ID.to_string())
} else {
None
}
},
};
let lp_mint = Self::pump_fun_payload_string(
&event.payload_json,
&[
"lpMint",
"lp_mint",
"associatedBondingCurve",
"associated_bonding_curve",
"poolBaseTokenAccount",
],
);
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"pump_fun",
event.program_id.clone(),
event.event_kind.as_str(),
pool_account,
None,
token_a_mint,
token_b_mint,
lp_mint,
event.payload_json.clone(),
)
.await;
},
}
}
fn pump_fun_payload_string(
payload: &serde_json::Value,
keys: &[&str],
) -> std::option::Option<std::string::String> {
if let Some(object) = payload.as_object() {
for key in keys {
let value = object.get(*key);
if let Some(value) = value {
if let Some(text) = value.as_str() {
if !text.trim().is_empty() {
return Some(text.to_string());
}
}
}
}
let decoded_arguments = object.get("decodedArguments");
if let Some(decoded_arguments) = decoded_arguments {
let nested = Self::pump_fun_payload_string(decoded_arguments, keys);
if nested.is_some() {
return nested;
}
}
}
return None;
}
async fn persist_pump_fun_trade_event(
@@ -2266,8 +2342,23 @@ impl DexDecodeService {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let decoded_events = pump_fun_enrich_trade_events_with_instruction_context(decoded_events);
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
if !pump_fun_decoded_event_is_trade_event(decoded_event) {
continue;
}
let persist_result = self.persist_pump_fun_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
for decoded_event in &decoded_events {
if pump_fun_decoded_event_is_trade_event(decoded_event) {
continue;
}
let persist_result = self.persist_pump_fun_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
@@ -4313,6 +4404,310 @@ fn dex_decode_extract_first_amount_string(
return dex_decode_extract_first_number_as_string(value, candidate_keys);
}
fn pump_fun_enrich_trade_events_with_instruction_context(
decoded_events: std::vec::Vec<crate::PumpFunDecodedEvent>,
) -> std::vec::Vec<crate::PumpFunDecodedEvent> {
let mut enriched_events = std::vec::Vec::new();
for decoded_event in &decoded_events {
let enriched_event = match decoded_event {
crate::PumpFunDecodedEvent::InstructionAudit(event) => {
if event.event_kind.as_str() == "pump_fun.trade_event" {
let mut enriched_event = event.clone();
pump_fun_merge_matching_instruction_context_into_trade_event(
&decoded_events,
&mut enriched_event,
);
pump_fun_mark_trade_event_duplicate_when_direct_instruction_exists(
&decoded_events,
&mut enriched_event,
);
crate::PumpFunDecodedEvent::InstructionAudit(enriched_event)
} else {
decoded_event.clone()
}
},
_ => decoded_event.clone(),
};
enriched_events.push(enriched_event);
}
return enriched_events;
}
fn pump_fun_merge_matching_instruction_context_into_trade_event(
decoded_events: &[crate::PumpFunDecodedEvent],
trade_event: &mut crate::PumpFunInstructionAuditDecoded,
) {
let trade_payload = trade_event.payload_json.clone();
let trade_instruction_id = dex_decode_extract_first_i64(
&trade_payload,
&["instructionId", "instruction_id"],
);
let trade_mint = dex_decode_extract_first_string(
&trade_payload,
&["mint", "tokenMint", "tokenAMint"],
);
let trade_actor = dex_decode_extract_first_string(
&trade_payload,
&["user", "actorWallet", "userWallet"],
);
for sibling in decoded_events {
let sibling_event = match sibling {
crate::PumpFunDecodedEvent::InstructionAudit(sibling_event) => sibling_event,
_ => continue,
};
if sibling_event.event_kind.as_str() == "pump_fun.trade_event" {
continue;
}
if !pump_fun_instruction_context_can_back_trade_event(sibling_event.event_kind.as_str()) {
continue;
}
let sibling_instruction_id = Some(sibling_event.instruction_id);
if trade_instruction_id.is_some()
&& sibling_instruction_id.is_some()
&& trade_instruction_id != sibling_instruction_id
{
continue;
}
let sibling_mint = dex_decode_extract_first_string(
&sibling_event.payload_json,
&["mint", "tokenMint", "tokenAMint"],
);
if !dex_decode_optional_strings_match(trade_mint.as_deref(), sibling_mint.as_deref()) {
continue;
}
let sibling_actor = dex_decode_extract_first_string(
&sibling_event.payload_json,
&["user", "actorWallet", "userWallet"],
);
if !dex_decode_optional_strings_match(trade_actor.as_deref(), sibling_actor.as_deref()) {
continue;
}
pump_fun_merge_instruction_context_payload(
&sibling_event.payload_json,
&mut trade_event.payload_json,
);
return;
}
}
fn pump_fun_instruction_context_can_back_trade_event(event_kind: &str) -> bool {
match event_kind {
"pump_fun.buy_exact_quote_in_v2" => return true,
"pump_fun.buy_v2" => return true,
"pump_fun.sell_v2" => return true,
"pump_fun.buy_exact_sol_in" => return true,
_ => return false,
}
}
fn pump_fun_mark_trade_event_duplicate_when_direct_instruction_exists(
decoded_events: &[crate::PumpFunDecodedEvent],
trade_event: &mut crate::PumpFunInstructionAuditDecoded,
) {
let trade_payload = trade_event.payload_json.clone();
let trade_instruction_id = dex_decode_extract_first_i64(
&trade_payload,
&["instructionId", "instruction_id"],
);
let trade_mint = dex_decode_extract_first_string(
&trade_payload,
&["mint", "tokenMint", "tokenAMint"],
);
let trade_actor = dex_decode_extract_first_string(
&trade_payload,
&["user", "actorWallet", "userWallet"],
);
for sibling in decoded_events {
let direct_match = match sibling {
crate::PumpFunDecodedEvent::BuyTrade(event) => {
pump_fun_direct_trade_matches_anchor_trade_event(
event.instruction_id,
event.mint.as_deref(),
event.user.as_deref(),
trade_instruction_id,
trade_mint.as_deref(),
trade_actor.as_deref(),
)
},
crate::PumpFunDecodedEvent::SellTrade(event) => {
pump_fun_direct_trade_matches_anchor_trade_event(
event.instruction_id,
event.mint.as_deref(),
event.user.as_deref(),
trade_instruction_id,
trade_mint.as_deref(),
trade_actor.as_deref(),
)
},
crate::PumpFunDecodedEvent::InstructionAudit(event) => {
if event.event_kind.as_str() != "pump_fun.buy_exact_sol_in" {
false
} else {
let instruction_mint = dex_decode_extract_first_string(
&event.payload_json,
&["mint", "tokenMint", "tokenAMint"],
);
let instruction_actor = dex_decode_extract_first_string(
&event.payload_json,
&["user", "actorWallet", "userWallet"],
);
pump_fun_direct_trade_matches_anchor_trade_event(
event.instruction_id,
instruction_mint.as_deref(),
instruction_actor.as_deref(),
trade_instruction_id,
trade_mint.as_deref(),
trade_actor.as_deref(),
)
}
},
_ => false,
};
if !direct_match {
continue;
}
let object = match trade_event.payload_json.as_object_mut() {
Some(object) => object,
None => return,
};
object.insert(
"skipTradeReason".to_string(),
serde_json::Value::String(
"pump_fun_trade_event_covered_by_direct_instruction_trade".to_string(),
),
);
object.insert(
"skipCandleReason".to_string(),
serde_json::Value::String(
"pump_fun_trade_event_covered_by_direct_instruction_trade".to_string(),
),
);
object.insert(
"anchorTradeEventCoveredByDirectInstructionTrade".to_string(),
serde_json::Value::Bool(true),
);
return;
}
}
fn pump_fun_direct_trade_matches_anchor_trade_event(
direct_instruction_id: i64,
direct_mint: std::option::Option<&str>,
direct_actor: std::option::Option<&str>,
trade_instruction_id: std::option::Option<i64>,
trade_mint: std::option::Option<&str>,
trade_actor: std::option::Option<&str>,
) -> bool {
if let Some(trade_instruction_id) = trade_instruction_id {
if direct_instruction_id != trade_instruction_id {
return false;
}
}
if !dex_decode_optional_strings_match(trade_mint, direct_mint) {
return false;
}
if !dex_decode_optional_strings_match(trade_actor, direct_actor) {
return false;
}
return true;
}
fn pump_fun_merge_instruction_context_payload(
instruction_payload: &serde_json::Value,
trade_payload: &mut serde_json::Value,
) {
let trade_object = match trade_payload.as_object_mut() {
Some(trade_object) => trade_object,
None => return,
};
let instruction_object = match instruction_payload.as_object() {
Some(instruction_object) => instruction_object,
None => return,
};
let copy_keys = [
"poolAccount",
"bondingCurve",
"associatedBondingCurve",
"poolBaseTokenAccount",
"poolQuoteTokenAccount",
"associatedQuoteBondingCurve",
"lpMint",
"tokenAMint",
"tokenBMint",
"quoteMint",
"feeRecipient",
"creatorVault",
"associatedCreatorVault",
];
for key in copy_keys {
if trade_object.contains_key(key) {
continue;
}
let value = match instruction_object.get(key) {
Some(value) => value.clone(),
None => continue,
};
trade_object.insert(key.to_string(), value);
}
trade_object.insert(
"amountSource".to_string(),
serde_json::Value::String("pump_fun_anchor_trade_event".to_string()),
);
trade_object.insert(
"anchorTradeEventInstructionContextSource".to_string(),
serde_json::Value::String("matching_instruction_audit".to_string()),
);
trade_object.remove("skipTradeReason");
trade_object.remove("skipCandleReason");
trade_object.remove("skipCatalogReason");
}
fn dex_decode_extract_first_i64(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<i64> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let candidate_value = match object.get(*candidate_key) {
Some(candidate_value) => candidate_value,
None => continue,
};
if let Some(number) = candidate_value.as_i64() {
return Some(number);
}
if let Some(text) = candidate_value.as_str() {
let parsed = text.parse::<i64>();
match parsed {
Ok(parsed) => return Some(parsed),
Err(_) => {},
}
}
}
}
return None;
}
fn dex_decode_optional_strings_match(
left: std::option::Option<&str>,
right: std::option::Option<&str>,
) -> bool {
match (left, right) {
(Some(left), Some(right)) => return left == right,
_ => return true,
}
}
fn pump_fun_decoded_event_is_trade_event(decoded_event: &crate::PumpFunDecodedEvent) -> bool {
match decoded_event {
crate::PumpFunDecodedEvent::InstructionAudit(event) => {
return event.event_kind.as_str() == "pump_fun.trade_event";
},
_ => return false,
}
}
fn dex_decode_extract_first_string(
value: &serde_json::Value,
candidate_keys: &[&str],

View File

@@ -119,6 +119,21 @@ pub(crate) fn dex_detection_route(
("pump_fun", "pump_fun.sell") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.buy_exact_sol_in") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.buy_exact_quote_in_v2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.buy_v2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.sell_v2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.trade_event") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_swap", "pump_swap.buy") => {
if crate::dex_detection_route::is_incomplete_pump_swap_decoded_event(decoded_event) {
return Some(

View File

@@ -323,6 +323,15 @@ pub fn is_dex_trade_event_kind(event_kind: &str) -> bool {
if event_kind == "raydium_launchpad.trade_event" {
return true;
}
if event_kind == "pump_fun.trade_event" {
return true;
}
if event_kind.ends_with(".buy_v2") {
return true;
}
if event_kind.ends_with(".sell_v2") {
return true;
}
if event_kind.ends_with(".buy") {
return true;
}
@@ -520,6 +529,15 @@ pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
if event_kind.contains("partner_claim_fee") {
return true;
}
if event_kind.contains("distribute_creator_fees") {
return true;
}
if event_kind.contains("minimum_distributable_fee") {
return true;
}
if event_kind.contains("reserved_fee_recipients_event") {
return false;
}
return false;
}
@@ -828,6 +846,15 @@ pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".extend_account") {
return true;
}
if event_kind.contains(".add_quote_mint") {
return true;
}
if event_kind.contains(".remove_quote_mint") {
return true;
}
if event_kind.contains("reserved_fee_recipients") {
return true;
}
if event_kind.contains("update_") {
return true;
}
@@ -1165,6 +1192,9 @@ mod tests {
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.exact_output"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.buy"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.buy_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.sell_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.trade_event"), "trade");
assert!(super::is_dex_trade_event_kind("raydium_cpmm.swap_base_input"));
assert!(super::is_dex_candle_candidate_event_kind("raydium_cpmm.swap_base_input"));
}

View File

@@ -318,6 +318,9 @@ fn infer_expected_db_target_for_entry(
if decoder_code == "pump_swap" {
return infer_pump_swap_expected_db_target(entry_name, entry_kind);
}
if decoder_code == "pump_fun" {
return infer_pump_fun_expected_db_target(entry_name, entry_kind);
}
if decoder_code == "raydium_cpmm"
&& (entry_name == "swap_event" || entry_name == "anchor_idl_instruction")
{
@@ -524,6 +527,104 @@ fn infer_expected_db_target(
return Some(target.to_string());
}
fn infer_pump_fun_expected_db_target(
entry_name: &str,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if entry_kind == crate::ENTRY_KIND_PROGRAM {
return None;
}
if entry_name == "buy"
|| entry_name == "sell"
|| entry_name == "buy_v2"
|| entry_name == "sell_v2"
|| entry_name == "buy_exact_sol_in"
|| entry_name == "buy_exact_quote_in_v2"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string());
}
if entry_name == "trade_event" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string());
}
if entry_name == "create"
|| entry_name == "create_event"
|| entry_name == "create_v2"
|| entry_name == "create_v2_token"
|| entry_name == "complete_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_LAUNCH_EVENTS.to_string());
}
if entry_name == "initialize" {
return Some(
crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string(),
);
}
if entry_name == "migrate"
|| entry_name == "migrate_v2"
|| entry_name == "migrate_bonding_curve_creator"
|| entry_name == "migrate_bonding_curve_creator_event"
|| entry_name == "complete_pump_amm_migration_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_LAUNCH_EVENTS.to_string());
}
if entry_name == "collect_creator_fee"
|| entry_name == "collect_creator_fee_v2"
|| entry_name == "collect_creator_fee_event"
|| entry_name == "distribute_creator_fees"
|| entry_name == "distribute_creator_fees_v2"
|| entry_name == "distribute_creator_fees_event"
|| entry_name == "get_minimum_distributable_fee"
|| entry_name == "minimum_distributable_fee_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_FEE_EVENTS.to_string());
}
if entry_name == "claim_cashback"
|| entry_name == "claim_cashback_v2"
|| entry_name == "claim_cashback_event"
|| entry_name == "claim_token_incentives"
|| entry_name == "claim_token_incentives_event"
|| entry_name == "admin_update_token_incentives"
|| entry_name == "admin_update_token_incentives_event"
|| entry_name == "init_user_volume_accumulator"
|| entry_name == "init_user_volume_accumulator_event"
|| entry_name == "sync_user_volume_accumulator"
|| entry_name == "sync_user_volume_accumulator_event"
|| entry_name == "close_user_volume_accumulator"
|| entry_name == "close_user_volume_accumulator_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_REWARD_EVENTS.to_string());
}
if entry_name == "admin_set_creator"
|| entry_name == "admin_set_creator_event"
|| entry_name == "admin_set_idl_authority"
|| entry_name == "admin_set_idl_authority_event"
|| entry_name == "add_quote_mint"
|| entry_name == "remove_quote_mint"
|| entry_name == "extend_account"
|| entry_name == "extend_account_event"
|| entry_name == "set_creator"
|| entry_name == "set_creator_event"
|| entry_name == "set_mayhem_virtual_params"
|| entry_name == "update_mayhem_virtual_params_event"
|| entry_name == "set_metaplex_creator"
|| entry_name == "set_metaplex_creator_event"
|| entry_name == "set_params"
|| entry_name == "set_params_event"
|| entry_name == "set_reserved_fee_recipients"
|| entry_name == "reserved_fee_recipients_event"
|| entry_name == "set_virtual_quote_reserves"
|| entry_name == "toggle_cashback_enabled"
|| entry_name == "toggle_create_v2"
|| entry_name == "toggle_mayhem_mode"
|| entry_name == "update_buyback_config"
|| entry_name == "update_global_authority"
|| entry_name == "update_global_authority_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string());
}
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
fn infer_pump_swap_expected_db_target(
entry_name: &str,
entry_kind: &str,
@@ -654,6 +755,161 @@ fn infer_pump_swap_event_family(
return infer_event_family(entry_name, entry_kind);
}
fn infer_pump_fun_event_family(
entry_name: &str,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if entry_kind == crate::ENTRY_KIND_PROGRAM {
return None;
}
match entry_name {
"buy"
| "sell"
| "buy_v2"
| "sell_v2"
| "buy_exact_quote_in_v2"
| "buy_exact_sol_in"
| "trade_event" => return Some("swap".to_string()),
"create" | "create_event" | "create_v2" | "create_v2_token" | "complete_event" => {
return Some("launch".to_string());
},
"migrate"
| "migrate_v2"
| "migrate_bonding_curve_creator"
| "migrate_bonding_curve_creator_event"
| "complete_pump_amm_migration_event" => return Some("migration".to_string()),
"claim_cashback"
| "claim_cashback_v2"
| "claim_cashback_event"
| "claim_token_incentives"
| "claim_token_incentives_event"
| "admin_update_token_incentives"
| "admin_update_token_incentives_event"
| "init_user_volume_accumulator"
| "init_user_volume_accumulator_event"
| "sync_user_volume_accumulator"
| "sync_user_volume_accumulator_event"
| "close_user_volume_accumulator"
| "close_user_volume_accumulator_event" => return Some("reward".to_string()),
"collect_creator_fee"
| "collect_creator_fee_v2"
| "collect_creator_fee_event"
| "distribute_creator_fees"
| "distribute_creator_fees_v2"
| "distribute_creator_fees_event"
| "get_minimum_distributable_fee"
| "minimum_distributable_fee_event" => return Some("fee".to_string()),
"add_quote_mint"
| "remove_quote_mint"
| "admin_set_creator"
| "admin_set_creator_event"
| "admin_set_idl_authority"
| "admin_set_idl_authority_event"
| "extend_account"
| "extend_account_event"
| "set_creator"
| "set_creator_event"
| "set_mayhem_virtual_params"
| "update_mayhem_virtual_params_event"
| "set_metaplex_creator"
| "set_metaplex_creator_event"
| "set_params"
| "set_params_event"
| "set_reserved_fee_recipients"
| "reserved_fee_recipients_event"
| "set_virtual_quote_reserves"
| "toggle_cashback_enabled"
| "toggle_create_v2"
| "toggle_mayhem_mode"
| "update_buyback_config"
| "update_global_authority"
| "update_global_authority_event" => return Some("admin_config".to_string()),
"initialize" => return Some("pool_create".to_string()),
_ => return infer_event_family(entry_name, entry_kind),
}
}
fn pump_fun_local_event_kind(entry_name: &str) -> std::option::Option<std::string::String> {
if entry_name.ends_with("_event") {
return Some(format!("pump_fun.{}", entry_name));
}
match entry_name {
"buy" => return Some("pump_fun.buy".to_string()),
"sell" => return Some("pump_fun.sell".to_string()),
"create_v2" => return Some("pump_fun.create_v2_token".to_string()),
"add_quote_mint" => return Some("pump_fun.add_quote_mint".to_string()),
"admin_set_creator" => return Some("pump_fun.admin_set_creator".to_string()),
"admin_set_idl_authority" => {
return Some("pump_fun.admin_set_idl_authority".to_string());
},
"admin_update_token_incentives" => {
return Some("pump_fun.admin_update_token_incentives".to_string());
},
"buy_exact_quote_in_v2" => {
return Some("pump_fun.buy_exact_quote_in_v2".to_string());
},
"buy_exact_sol_in" => return Some("pump_fun.buy_exact_sol_in".to_string()),
"buy_v2" => return Some("pump_fun.buy_v2".to_string()),
"claim_cashback" => return Some("pump_fun.claim_cashback".to_string()),
"claim_cashback_v2" => return Some("pump_fun.claim_cashback_v2".to_string()),
"claim_token_incentives" => {
return Some("pump_fun.claim_token_incentives".to_string());
},
"close_user_volume_accumulator" => {
return Some("pump_fun.close_user_volume_accumulator".to_string());
},
"collect_creator_fee" => return Some("pump_fun.collect_creator_fee".to_string()),
"collect_creator_fee_v2" => return Some("pump_fun.collect_creator_fee_v2".to_string()),
"create" => return Some("pump_fun.create".to_string()),
"distribute_creator_fees" => {
return Some("pump_fun.distribute_creator_fees".to_string());
},
"distribute_creator_fees_v2" => {
return Some("pump_fun.distribute_creator_fees_v2".to_string());
},
"extend_account" => return Some("pump_fun.extend_account".to_string()),
"get_minimum_distributable_fee" => {
return Some("pump_fun.get_minimum_distributable_fee".to_string());
},
"init_user_volume_accumulator" => {
return Some("pump_fun.init_user_volume_accumulator".to_string());
},
"initialize" => return Some("pump_fun.initialize".to_string()),
"migrate" => return Some("pump_fun.migrate".to_string()),
"migrate_bonding_curve_creator" => {
return Some("pump_fun.migrate_bonding_curve_creator".to_string());
},
"migrate_v2" => return Some("pump_fun.migrate_v2".to_string()),
"remove_quote_mint" => return Some("pump_fun.remove_quote_mint".to_string()),
"sell_v2" => return Some("pump_fun.sell_v2".to_string()),
"set_creator" => return Some("pump_fun.set_creator".to_string()),
"set_mayhem_virtual_params" => {
return Some("pump_fun.set_mayhem_virtual_params".to_string());
},
"set_metaplex_creator" => return Some("pump_fun.set_metaplex_creator".to_string()),
"set_params" => return Some("pump_fun.set_params".to_string()),
"set_reserved_fee_recipients" => {
return Some("pump_fun.set_reserved_fee_recipients".to_string());
},
"set_virtual_quote_reserves" => {
return Some("pump_fun.set_virtual_quote_reserves".to_string());
},
"sync_user_volume_accumulator" => {
return Some("pump_fun.sync_user_volume_accumulator".to_string());
},
"toggle_cashback_enabled" => {
return Some("pump_fun.toggle_cashback_enabled".to_string());
},
"toggle_create_v2" => return Some("pump_fun.toggle_create_v2".to_string()),
"toggle_mayhem_mode" => return Some("pump_fun.toggle_mayhem_mode".to_string()),
"update_buyback_config" => return Some("pump_fun.update_buyback_config".to_string()),
"update_global_authority" => {
return Some("pump_fun.update_global_authority".to_string());
},
_ => return None,
}
}
fn pump_swap_local_event_kind(entry_name: &str) -> std::option::Option<std::string::String> {
if entry_name.ends_with("_event") {
return Some(format!("pump_swap.{}", entry_name));
@@ -716,6 +972,9 @@ fn infer_event_family_for_entry(
entry_name: &str,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if decoder_code == "pump_fun" {
return infer_pump_fun_event_family(entry_name, entry_kind);
}
if decoder_code == "pump_swap" {
return infer_pump_swap_event_family(entry_name, entry_kind);
}
@@ -1110,6 +1369,9 @@ pub(crate) fn known_local_event_kind(
decoder_code: &str,
entry_name: &str,
) -> std::option::Option<std::string::String> {
if decoder_code == "pump_fun" {
return pump_fun_local_event_kind(entry_name);
}
if decoder_code == "pump_swap" {
return pump_swap_local_event_kind(entry_name);
}
@@ -1488,6 +1750,85 @@ mod tests {
Some("raydium_clmm.pool_created_event".to_string())
);
}
#[test]
fn pump_fun_coverage_maps_local_idl_and_audit_entries() {
assert_eq!(
super::known_local_event_kind("pump_fun", "buy"),
Some("pump_fun.buy".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "create_v2"),
Some("pump_fun.create_v2_token".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "buy_v2"),
Some("pump_fun.buy_v2".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "collect_creator_fee_v2"),
Some("pump_fun.collect_creator_fee_v2".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "trade_event"),
Some("pump_fun.trade_event".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "claim_cashback_event"),
Some("pump_fun.claim_cashback_event".to_string())
);
assert_eq!(
super::infer_event_family_for_entry("pump_fun", "create_event", crate::ENTRY_KIND_EVENT),
Some("launch".to_string())
);
assert_eq!(
super::infer_event_family_for_entry(
"pump_fun",
"set_metaplex_creator_event",
crate::ENTRY_KIND_EVENT,
),
Some("admin_config".to_string())
);
assert_eq!(
super::infer_event_family_for_entry(
"pump_fun",
"claim_token_incentives_event",
crate::ENTRY_KIND_EVENT,
),
Some("reward".to_string())
);
assert_eq!(
super::infer_event_family_for_entry("pump_fun", "buy_v2", crate::ENTRY_KIND_INSTRUCTION),
Some("swap".to_string())
);
assert_eq!(
super::infer_expected_db_target_for_entry(
"pump_fun",
"buy",
Some("swap"),
crate::ENTRY_KIND_INSTRUCTION,
),
Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string())
);
assert_eq!(
super::infer_expected_db_target_for_entry(
"pump_fun",
"buy_v2",
Some("swap"),
crate::ENTRY_KIND_INSTRUCTION,
),
Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string())
);
assert_eq!(
super::infer_expected_db_target_for_entry(
"pump_fun",
"create_v2",
Some("launch"),
crate::ENTRY_KIND_INSTRUCTION,
),
Some(crate::DexEventCoverageEntryDto::DB_TARGET_LAUNCH_EVENTS.to_string())
);
}
#[test]
fn launchpad_swap_instructions_materialize_as_launch_events_without_duplicate_trades() {
assert_eq!(

View File

@@ -319,6 +319,53 @@ fn resolve_instruction_name(
};
return Some(format!("raydium_launchpad.{}", layout.instruction_name));
}
if program_id == crate::PUMP_FUN_PROGRAM_ID || decoder_code == Some("pump_fun") {
let name = match discriminator_hex {
"e445a52e51cb9a1d" => "anchor_self_cpi_log",
"6f79153828185ed1" => "add_quote_mint",
"4519ab8e39ef0d04" => "admin_set_creator",
"08d960e79068c005" => "admin_set_idl_authority",
"d10b7357d5177ccc" => "admin_update_token_incentives",
"66063d1201daebea" => "buy",
"c2ab1c46684d5b2f" => "buy_exact_quote_in_v2",
"38fc74089edfcd5f" => "buy_exact_sol_in",
"b817ee6167c5d33d" => "buy_v2",
"253a237ebe35e4c5" => "claim_cashback",
"7af3cc415e741d37" => "claim_cashback_v2",
"1004471ccc01281b" => "claim_token_incentives",
"f945a4da9667548a" => "close_user_volume_accumulator",
"1416567bc61cdb84" => "collect_creator_fee",
"cf118af204221338" => "collect_creator_fee_v2",
"181ec828051c0777" => "create",
"d6904cec5f8b31b4" => "create_v2",
"a572670079cef751" => "distribute_creator_fees",
"ffcb134ff444089f" => "distribute_creator_fees_v2",
"ea66c2cb96483ee5" => "extend_account",
"75e17fca865f4423" => "get_minimum_distributable_fee",
"5e06ca73ff60e8b7" => "init_user_volume_accumulator",
"afaf6d1f0d989bed" => "initialize",
"9beae792ec9ea21e" => "migrate",
"577c34bf3426d6e8" => "migrate_bonding_curve_creator",
"bbcb121fceedfe29" => "migrate_v2",
"b141df2658d19e9b" => "remove_quote_mint",
"33e685a4017f83ad" => "sell",
"5df6823ce7e940b2" => "sell_v2",
"fe94ff70cf8eaaa5" => "set_creator",
"3da9bcbf99952a61" => "set_mayhem_virtual_params",
"8a60aed93055c5f6" => "set_metaplex_creator",
"1beab2349302bb8d" => "set_params",
"6faca2e87259d58e" => "set_reserved_fee_recipients",
"6587bf6809581460" => "set_virtual_quote_reserves",
"561fc057a3574fee" => "sync_user_volume_accumulator",
"7367e0ffbd5956c3" => "toggle_cashback_enabled",
"1cffe6f0ac6bcbab" => "toggle_create_v2",
"01096fd0641fffa3" => "toggle_mayhem_mode",
"fbe0ab92a01a71e9" => "update_buyback_config",
"e3b54ac4d01561d5" => "update_global_authority",
_ => return None,
};
return Some(name.to_string());
}
if program_id == crate::PUMP_SWAP_PROGRAM_ID || decoder_code == Some("pump_swap") {
let name = match discriminator_hex {
"e445a52e51cb9a1d" => "anchor_self_cpi_log",

View File

@@ -1177,6 +1177,8 @@ pub use dex::PumpFunCreateV2TokenDecoded;
pub use dex::PumpFunDecodedEvent;
/// Pump.fun decoder.
pub use dex::PumpFunDecoder;
/// Decoded Pump.fun audit-only instruction event.
pub use dex::PumpFunInstructionAuditDecoded;
/// Decoded Pump.fun bonding-curve trade event.
pub use dex::PumpFunTradeDecoded;
/// Decoded PumpSwap event.

View File

@@ -112,6 +112,15 @@ impl NonTradeEventMaterializationService {
if is_anchor_event_audit_only(&payload) {
continue;
}
if should_skip_pump_fun_duplicate_non_trade_event(decoded_event, &decoded_events) {
tracing::debug!(
event_kind = %decoded_event.event_kind,
decoded_event_id = ?decoded_event.id,
signature = %transaction.signature,
"skipping duplicate pump_fun non-trade materialization"
);
continue;
}
if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str()) {
let cleanup_result =
self.delete_stale_pool_admin_event_for_lifecycle(decoded_event).await;
@@ -140,7 +149,9 @@ impl NonTradeEventMaterializationService {
Err(error) => return Err(error),
}
}
if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str()) {
if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str())
&& !is_launchpad_launch_event_materializable(decoded_event.event_kind.as_str())
{
let materialized = self
.materialize_pool_lifecycle_event(&transaction, transaction_id, decoded_event)
.await;
@@ -672,6 +683,10 @@ impl NonTradeEventMaterializationService {
"poolState",
"pool_state",
"poolAccount",
"bondingCurve",
"bonding_curve",
"sharingConfig",
"sharing_config",
],
);
let related_mint = extract_first_string(
@@ -737,9 +752,8 @@ impl NonTradeEventMaterializationService {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(false),
};
let context = self
.resolve_liquidity_context(transaction, transaction_id, decoded_event)
.await;
let context =
self.resolve_liquidity_context(transaction, transaction_id, decoded_event).await;
let context = match context {
Ok(context) => context,
Err(error) => return Err(error),
@@ -1018,9 +1032,8 @@ impl NonTradeEventMaterializationService {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(()),
};
let payload_result = serde_json::from_str::<serde_json::Value>(
decoded_event.payload_json.as_str(),
);
let payload_result =
serde_json::from_str::<serde_json::Value>(decoded_event.payload_json.as_str());
let mut object = match payload_result {
Ok(serde_json::Value::Object(object)) => object,
Ok(other) => {
@@ -1179,9 +1192,8 @@ impl NonTradeEventMaterializationService {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let target_payload_result = serde_json::from_str::<serde_json::Value>(
decoded_event.payload_json.as_str(),
);
let target_payload_result =
serde_json::from_str::<serde_json::Value>(decoded_event.payload_json.as_str());
let target_payload = match target_payload_result {
Ok(target_payload) => target_payload,
Err(_) => serde_json::Value::Object(serde_json::Map::new()),
@@ -1193,9 +1205,8 @@ impl NonTradeEventMaterializationService {
if !candidate.event_kind.starts_with("raydium_clmm.") {
continue;
}
let candidate_payload_result = serde_json::from_str::<serde_json::Value>(
candidate.payload_json.as_str(),
);
let candidate_payload_result =
serde_json::from_str::<serde_json::Value>(candidate.payload_json.as_str());
let candidate_payload = match candidate_payload_result {
Ok(candidate_payload) => candidate_payload,
Err(_) => serde_json::Value::Object(serde_json::Map::new()),
@@ -1425,9 +1436,8 @@ struct MaterializationAccountKeyInfo {
fn token_mints_by_account_from_transaction(
transaction: &crate::ChainTransactionDto,
) -> std::collections::HashMap<std::string::String, std::string::String> {
let transaction_json = serde_json::from_str::<serde_json::Value>(
transaction.transaction_json.as_str(),
);
let transaction_json =
serde_json::from_str::<serde_json::Value>(transaction.transaction_json.as_str());
let transaction_json = match transaction_json {
Ok(transaction_json) => transaction_json,
Err(_) => return std::collections::HashMap::new(),
@@ -1475,10 +1485,7 @@ fn materialization_account_keys(
value.get("pubkey").and_then(serde_json::Value::as_str).map(str::to_string)
};
if let Some(address) = address {
account_keys.push(MaterializationAccountKeyInfo {
index: index as i64,
address,
});
account_keys.push(MaterializationAccountKeyInfo { index: index as i64, address });
}
index += 1;
}
@@ -1507,10 +1514,7 @@ fn append_materialization_loaded_addresses(
None => continue,
};
let index = account_keys.len() as i64;
account_keys.push(MaterializationAccountKeyInfo {
index,
address: address.to_string(),
});
account_keys.push(MaterializationAccountKeyInfo { index, address: address.to_string() });
}
}
@@ -1567,21 +1571,15 @@ fn infer_raydium_clmm_pair_mints_from_payload_accounts(
Some(accounts) => accounts,
None => return None,
};
let instruction_name = payload
.get("instructionName")
.and_then(serde_json::Value::as_str);
let instruction_name = payload.get("instructionName").and_then(serde_json::Value::as_str);
let instruction_name = match instruction_name {
Some(instruction_name) => instruction_name,
None => "",
};
let candidate_pairs = raydium_clmm_token_account_candidate_pairs(instruction_name);
for pair in candidate_pairs {
let inferred = infer_mints_from_account_pair(
accounts,
pair.0,
pair.1,
token_mints_by_account,
);
let inferred =
infer_mints_from_account_pair(accounts, pair.0, pair.1, token_mints_by_account);
if let Some(inferred) = inferred {
return Some(inferred);
}
@@ -1610,7 +1608,17 @@ fn raydium_clmm_token_account_candidate_pairs(
if instruction_name == "increase_liquidity_v2" {
return vec![(13, 14), (9, 10), (7, 8)];
}
return vec![(12, 13), (13, 14), (9, 10), (7, 8), (5, 6), (10, 11), (14, 15), (18, 19), (20, 21)];
return vec![
(12, 13),
(13, 14),
(9, 10),
(7, 8),
(5, 6),
(10, 11),
(14, 15),
(18, 19),
(20, 21),
];
}
fn infer_mints_from_account_pair(
@@ -1732,6 +1740,21 @@ fn extract_account_string(
}
fn is_launchpad_launch_event_materializable(event_kind: &str) -> bool {
if event_kind.contains("pump_fun.create_v2_token") {
return true;
}
if event_kind == "pump_fun.create" || event_kind == "pump_fun.create_event" {
return true;
}
if event_kind == "pump_fun.migrate"
|| event_kind == "pump_fun.migrate_v2"
|| event_kind == "pump_fun.migrate_bonding_curve_creator"
|| event_kind == "pump_fun.migrate_bonding_curve_creator_event"
|| event_kind == "pump_fun.complete_event"
|| event_kind == "pump_fun.complete_pump_amm_migration_event"
{
return true;
}
if event_kind.contains("raydium_launchpad.buy_exact_in") {
return true;
}
@@ -1793,6 +1816,17 @@ fn launchpad_launch_event_role(event_kind: &str) -> std::string::String {
if event_kind.contains("migrate_to_cpswap") {
return "migration_to_cpswap".to_string();
}
if event_kind.contains("pump_fun.migrate")
|| event_kind.contains("pump_fun.complete_pump_amm_migration")
{
return "pump_fun_migration".to_string();
}
if event_kind.contains("pump_fun.complete_event") {
return "pump_fun_completion".to_string();
}
if event_kind.contains("pump_fun.create") {
return "pump_fun_launch".to_string();
}
return "launch".to_string();
}
@@ -1938,7 +1972,108 @@ fn extract_first_bool(
return None;
}
fn should_skip_pump_fun_duplicate_non_trade_event(
decoded_event: &crate::DexDecodedEventDto,
decoded_events: &[crate::DexDecodedEventDto],
) -> bool {
if !decoded_event.event_kind.starts_with("pump_fun.") {
return false;
}
let preferred_siblings =
pump_fun_preferred_non_trade_siblings(decoded_event.event_kind.as_str());
if preferred_siblings.is_empty() {
return false;
}
for sibling in decoded_events {
if sibling.id == decoded_event.id {
continue;
}
for preferred in &preferred_siblings {
if sibling.event_kind.as_str() == *preferred {
return true;
}
}
}
return false;
}
fn pump_fun_preferred_non_trade_siblings(event_kind: &str) -> std::vec::Vec<&'static str> {
match event_kind {
"pump_fun.admin_set_creator" => return vec!["pump_fun.admin_set_creator_event"],
"pump_fun.admin_set_idl_authority" => {
return vec!["pump_fun.admin_set_idl_authority_event"];
},
"pump_fun.admin_update_token_incentives" => {
return vec!["pump_fun.admin_update_token_incentives_event"];
},
"pump_fun.claim_cashback" | "pump_fun.claim_cashback_v2" => {
return vec!["pump_fun.claim_cashback_event"];
},
"pump_fun.claim_token_incentives" => return vec!["pump_fun.claim_token_incentives_event"],
"pump_fun.close_user_volume_accumulator" => {
return vec!["pump_fun.close_user_volume_accumulator_event"];
},
"pump_fun.collect_creator_fee" | "pump_fun.collect_creator_fee_v2" => {
return vec!["pump_fun.collect_creator_fee_event"];
},
"pump_fun.create" => return vec!["pump_fun.create_v2_token", "pump_fun.create_event"],
"pump_fun.create_event" => return vec!["pump_fun.create_v2_token"],
"pump_fun.distribute_creator_fees" | "pump_fun.distribute_creator_fees_v2" => {
return vec!["pump_fun.distribute_creator_fees_event"];
},
"pump_fun.extend_account" => return vec!["pump_fun.extend_account_event"],
"pump_fun.get_minimum_distributable_fee" => {
return vec!["pump_fun.minimum_distributable_fee_event"];
},
"pump_fun.init_user_volume_accumulator" => {
return vec!["pump_fun.init_user_volume_accumulator_event"];
},
"pump_fun.migrate_bonding_curve_creator" => {
return vec!["pump_fun.migrate_bonding_curve_creator_event"];
},
"pump_fun.set_creator" => return vec!["pump_fun.set_creator_event"],
"pump_fun.set_metaplex_creator" => return vec!["pump_fun.set_metaplex_creator_event"],
"pump_fun.set_params" => return vec!["pump_fun.set_params_event"],
"pump_fun.set_reserved_fee_recipients" => {
return vec!["pump_fun.reserved_fee_recipients_event"];
},
"pump_fun.sync_user_volume_accumulator" => {
return vec!["pump_fun.sync_user_volume_accumulator_event"];
},
"pump_fun.update_global_authority" => {
return vec!["pump_fun.update_global_authority_event"];
},
"pump_fun.set_mayhem_virtual_params" => {
return vec!["pump_fun.update_mayhem_virtual_params_event"];
},
_ => return std::vec::Vec::new(),
}
}
fn is_pump_fun_payload(payload: &serde_json::Value) -> bool {
if let Some(object) = payload.as_object() {
let protocol_name = object.get("protocolName").and_then(serde_json::Value::as_str);
if protocol_name == Some("pump_fun") {
return true;
}
let decoder = object.get("decoder").and_then(serde_json::Value::as_str);
if decoder == Some("pump_fun") {
return true;
}
let event_kind = object.get("eventKind").and_then(serde_json::Value::as_str);
if let Some(event_kind) = event_kind {
if event_kind.starts_with("pump_fun.") {
return true;
}
}
}
return false;
}
fn is_anchor_event_audit_only(payload: &serde_json::Value) -> bool {
if is_pump_fun_payload(payload) {
return false;
}
if let Some(object) = payload.as_object() {
let flag = object.get("anchorEventAuditOnly");
if let Some(flag) = flag {
@@ -1946,6 +2081,12 @@ fn is_anchor_event_audit_only(payload: &serde_json::Value) -> bool {
return true;
}
}
let flag = object.get("instructionAuditOnly");
if let Some(flag) = flag {
if flag.as_bool() == Some(true) {
return true;
}
}
}
return false;
}

View File

@@ -62,6 +62,18 @@ impl TradeAggregationService {
if !crate::is_dex_trade_event_kind(decoded_event.event_kind.as_str()) {
continue;
}
if crate::trade_aggregation::should_skip_pump_fun_duplicate_trade_event(
decoded_event,
&decoded_events,
) {
tracing::debug!(
event_kind = %decoded_event.event_kind,
decoded_event_id = ?decoded_event.id,
transaction_signature = %transaction.signature,
"skipping duplicate pump_fun trade_event because an instruction trade exists"
);
continue;
}
let event_context =
crate::trade_aggregation_context::load_trade_aggregation_decoded_event_context(
self.database.as_ref(),
@@ -200,6 +212,68 @@ impl TradeAggregationService {
}
}
fn should_skip_pump_fun_duplicate_trade_event(
decoded_event: &crate::DexDecodedEventDto,
decoded_events: &[crate::DexDecodedEventDto],
) -> bool {
if decoded_event.event_kind.as_str() != "pump_fun.trade_event" {
return false;
}
let trade_instruction_id = pump_fun_payload_instruction_id(decoded_event.payload_json.as_str());
for sibling in decoded_events {
if sibling.id == decoded_event.id {
continue;
}
if !is_direct_materialized_pump_fun_instruction_trade_kind(sibling.event_kind.as_str()) {
continue;
}
let sibling_instruction_id = pump_fun_payload_instruction_id(sibling.payload_json.as_str());
if trade_instruction_id.is_some()
&& sibling_instruction_id.is_some()
&& trade_instruction_id != sibling_instruction_id
{
continue;
}
return true;
}
return false;
}
fn is_direct_materialized_pump_fun_instruction_trade_kind(event_kind: &str) -> bool {
match event_kind {
"pump_fun.buy" => return true,
"pump_fun.sell" => return true,
"pump_fun.buy_exact_sol_in" => return true,
_ => return false,
}
}
fn pump_fun_payload_instruction_id(payload_json: &str) -> std::option::Option<i64> {
let parsed_result = serde_json::from_str::<serde_json::Value>(payload_json);
let parsed = match parsed_result {
Ok(parsed) => parsed,
Err(_) => return None,
};
let object = match parsed.as_object() {
Some(object) => object,
None => return None,
};
let value = match object.get("instructionId") {
Some(value) => value,
None => return None,
};
if let Some(number) = value.as_i64() {
return Some(number);
}
if let Some(text) = value.as_str() {
let parsed_number = text.parse::<i64>();
match parsed_number {
Ok(parsed_number) => return Some(parsed_number),
Err(_) => return None,
}
}
return None;
}
fn transaction_has_effective_error(transaction: &crate::ChainTransactionDto) -> bool {
let err_json = match transaction.err_json.as_ref() {

View File

@@ -91,7 +91,8 @@ pub(crate) async fn resolve_trade_amounts(
&mut base_amount_raw,
&mut quote_amount_raw,
&mut price_quote_per_base,
);
)
.await;
if let Err(error) = resolution_result {
return Err(error);
}
@@ -788,7 +789,7 @@ fn apply_raydium_launchpad_side_amount_mapping(
}
}
fn apply_pump_fun_amount_fallback(
async fn apply_pump_fun_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>,
@@ -813,9 +814,183 @@ fn apply_pump_fun_amount_fallback(
if price_quote_per_base.is_none() {
*price_quote_per_base = inferred.2;
}
if base_amount_raw.is_none() || quote_amount_raw.is_none() || price_quote_per_base.is_none() {
let sibling_result = crate::trade_amount_resolution::apply_pump_fun_trade_event_sibling_amount_fallback(
input,
base_amount_raw,
quote_amount_raw,
price_quote_per_base,
)
.await;
if let Err(error) = sibling_result {
return Err(error);
}
}
return Ok(());
}
async fn apply_pump_fun_trade_event_sibling_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>,
price_quote_per_base: &mut std::option::Option<f64>,
) -> Result<(), crate::Error> {
if !crate::trade_amount_resolution::pump_fun_instruction_trade_can_use_trade_event_fallback(
input.decoded_event.event_kind.as_str(),
) {
return Ok(());
}
let sibling_events_result = crate::query_dex_decoded_events_list_by_transaction_id(
input.database,
input.decoded_event.transaction_id,
)
.await;
let sibling_events = match sibling_events_result {
Ok(sibling_events) => sibling_events,
Err(error) => return Err(error),
};
for sibling_event in sibling_events {
if sibling_event.id == input.decoded_event.id {
continue;
}
if sibling_event.protocol_name.as_str() != "pump_fun" {
continue;
}
if sibling_event.event_kind.as_str() != "pump_fun.trade_event" {
continue;
}
let sibling_payload_result =
serde_json::from_str::<serde_json::Value>(sibling_event.payload_json.as_str());
let sibling_payload = match sibling_payload_result {
Ok(sibling_payload) => sibling_payload,
Err(error) => {
tracing::debug!(
decoded_event_id = ?sibling_event.id,
error = %error,
"cannot parse pump_fun trade_event sibling payload for amount fallback"
);
continue;
},
};
if !crate::trade_amount_resolution::pump_fun_trade_event_sibling_matches_instruction(
input.decoded_event.event_kind.as_str(),
input.payload,
&sibling_payload,
) {
continue;
}
let sibling_base_amount = crate::trade_amount_resolution::extract_amount_string(
&sibling_payload,
&["baseAmountRaw", "baseAmount", "token_amount", "tokenAmount"],
);
let sibling_quote_amount = crate::trade_amount_resolution::extract_amount_string(
&sibling_payload,
&["quoteAmountRaw", "quoteAmount", "sol_amount", "solAmount", "quote_amount"],
);
if base_amount_raw.is_none() {
*base_amount_raw = sibling_base_amount;
}
if quote_amount_raw.is_none() {
*quote_amount_raw = sibling_quote_amount;
}
if price_quote_per_base.is_none() {
*price_quote_per_base = crate::trade_metric_update::compute_price_quote_per_base_from_raw_amounts_with_decimals(
base_amount_raw.as_deref(),
quote_amount_raw.as_deref(),
input.base_token_decimals,
input.quote_token_decimals,
);
}
tracing::debug!(
event_kind = %input.decoded_event.event_kind,
decoded_event_id = ?input.decoded_event.id,
sibling_decoded_event_id = ?sibling_event.id,
base_amount_raw = ?base_amount_raw,
quote_amount_raw = ?quote_amount_raw,
price_quote_per_base = ?price_quote_per_base,
"pump_fun instruction amounts recovered from sibling trade_event"
);
if base_amount_raw.is_some() && quote_amount_raw.is_some() {
return Ok(());
}
}
return Ok(());
}
fn pump_fun_instruction_trade_can_use_trade_event_fallback(event_kind: &str) -> bool {
match event_kind {
"pump_fun.buy_exact_quote_in_v2" => return true,
"pump_fun.buy_exact_sol_in" => return true,
"pump_fun.buy_v2" => return true,
"pump_fun.sell_v2" => return true,
_ => return false,
}
}
fn pump_fun_trade_event_sibling_matches_instruction(
instruction_event_kind: &str,
instruction_payload: &serde_json::Value,
trade_event_payload: &serde_json::Value,
) -> bool {
let expected_is_buy = match instruction_event_kind {
"pump_fun.buy_exact_quote_in_v2" => Some(true),
"pump_fun.buy_exact_sol_in" => Some(true),
"pump_fun.buy_v2" => Some(true),
"pump_fun.sell_v2" => Some(false),
_ => None,
};
if let Some(expected_is_buy) = expected_is_buy {
let actual_is_buy = crate::trade_amount_resolution::extract_bool_by_candidate_keys(
trade_event_payload,
&["is_buy", "isBuy"],
);
match actual_is_buy {
Some(actual_is_buy) if actual_is_buy == expected_is_buy => {},
Some(_) => return false,
None => {},
}
}
let instruction_mint = crate::trade_amount_resolution::extract_string_by_candidate_keys(
instruction_payload,
&["mint", "tokenMint", "tokenAMint"],
);
let trade_event_mint = crate::trade_amount_resolution::extract_string_by_candidate_keys(
trade_event_payload,
&["mint", "tokenMint", "tokenAMint"],
);
if !crate::trade_amount_resolution::optional_string_values_match(
instruction_mint.as_deref(),
trade_event_mint.as_deref(),
) {
return false;
}
let instruction_user = crate::trade_amount_resolution::extract_string_by_candidate_keys(
instruction_payload,
&["user", "actorWallet"],
);
let trade_event_user = crate::trade_amount_resolution::extract_string_by_candidate_keys(
trade_event_payload,
&["user", "actorWallet"],
);
if !crate::trade_amount_resolution::optional_string_values_match(
instruction_user.as_deref(),
trade_event_user.as_deref(),
) {
return false;
}
return true;
}
fn optional_string_values_match(
left: std::option::Option<&str>,
right: std::option::Option<&str>,
) -> bool {
match (left, right) {
(Some(left), Some(right)) => return left == right,
_ => return true,
}
}
async fn apply_raydium_instruction_amount_fallback(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
base_amount_raw: &mut std::option::Option<std::string::String>,
@@ -1492,6 +1667,44 @@ fn extract_amount_string(
);
}
fn extract_bool_by_candidate_keys(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<bool> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let direct_option = object.get(*candidate_key);
if let Some(direct) = direct_option {
if let Some(bool_value) = direct.as_bool() {
return Some(bool_value);
}
}
}
for nested_value in object.values() {
let nested_result = crate::trade_amount_resolution::extract_bool_by_candidate_keys(
nested_value,
candidate_keys,
);
if nested_result.is_some() {
return nested_result;
}
}
return None;
}
if let Some(array) = value.as_array() {
for nested_value in array {
let nested_result = crate::trade_amount_resolution::extract_bool_by_candidate_keys(
nested_value,
candidate_keys,
);
if nested_result.is_some() {
return nested_result;
}
}
}
return None;
}
fn extract_string_by_candidate_keys(
value: &serde_json::Value,
candidate_keys: &[&str],

View File

@@ -11042,6 +11042,127 @@ pub(crate) const UPSTREAM_REGISTRY_ENTRIES: &[crate::UpstreamRegistryEntry] = &[
8,
"decoders/pumpfun-decoder/src/instructions/admin_update_token_incentives.rs",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"add_quote_mint",
"6f79153828185ed1",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"buy_exact_quote_in_v2",
"c2ab1c46684d5b2f",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"buy_v2",
"b817ee6167c5d33d",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"claim_cashback_v2",
"7af3cc415e741d37",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"collect_creator_fee_v2",
"cf118af204221338",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"distribute_creator_fees_v2",
"ffcb134ff444089f",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"migrate_v2",
"bbcb121fceedfe29",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"remove_quote_mint",
"b141df2658d19e9b",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"sell_v2",
"5df6823ce7e940b2",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"set_virtual_quote_reserves",
"6587bf6809581460",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"update_buyback_config",
"fbe0ab92a01a71e9",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
upstream_git_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),

View File

@@ -0,0 +1,456 @@
-- file: validation_sql/SQL_VALIDATION_PUMP_FUN_0_7_54.sql
-- 0.7.54 pump_fun validation and corpus-seed checklist.
-- Run on a dedicated fresh SQLite database for the Pump.fun 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,
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') = 'pump_fun'
GROUP BY upstream_entry_name, upstream_discriminator_hex
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 = 'pump_fun'
GROUP BY instruction_name, discriminator_hex
ORDER BY observed_count DESC, instruction_name, discriminator_hex;
-- 02. Coverage pump_fun.
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 = 'pump_fun'
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 = 'pump_fun'
GROUP BY de.event_kind
ORDER BY decoded_count DESC, de.event_kind;
-- 04. Decoded pump_fun events without coverage.
-- Target after closure: empty for all locally decoded pump_fun 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 = 'pump_fun'
AND ce.local_event_kind = de.event_kind
WHERE de.protocol_name = 'pump_fun'
AND ce.id IS NULL
GROUP BY de.event_kind
ORDER BY decoded_count DESC, de.event_kind;
-- 05. Residual upstream fallback for covered local entries.
-- 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 ce.discriminator_hex = 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') = 'pump_fun'
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, or documented exceptions with explicit skip reason in payload_json.
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 = 'pump_fun'
AND (
tx.err_json IS NULL
OR tx.err_json = ''
OR tx.err_json = 'null'
)
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')), '') = ''
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 = 'pump_fun'
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 de.event_kind;
-- 08. Multi-target materialization safety.
-- Target after closure: empty. One decoded event must not feed multiple business targets.
SELECT
de.event_kind,
COUNT(DISTINCT de.id) AS decoded_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,
(
CASE WHEN COUNT(DISTINCT te.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT lae.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT lie.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT ple.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT fee.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT rew.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT adm.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT obe.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT tae.id) > 0 THEN 1 ELSE 0 END
) AS materialized_target_count
FROM k_sol_dex_decoded_events de
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 = 'pump_fun'
GROUP BY de.event_kind
HAVING materialized_target_count > 1
ORDER BY materialized_target_count DESC, de.event_kind;
-- 09. Materialization summary.
SELECT
de.event_kind,
COUNT(DISTINCT de.id) AS decoded_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
FROM k_sol_dex_decoded_events de
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 = 'pump_fun'
GROUP BY de.event_kind
ORDER BY de.event_kind;
-- 10. Instruction observation versus coverage.
-- Target after closure: every observed non-transport discriminator is covered or documented.
WITH normalized_io AS (
SELECT
io.decoder_code,
io.instruction_name,
CASE
WHEN io.instruction_name LIKE 'pump_fun.%'
THEN SUBSTR(io.instruction_name, LENGTH('pump_fun') + 2)
ELSE io.instruction_name
END AS normalized_entry_name,
io.discriminator_hex,
io.signature
FROM k_sol_instruction_observations io
WHERE io.decoder_code = 'pump_fun'
AND io.discriminator_hex IS NOT NULL
AND io.discriminator_hex <> ''
AND io.discriminator_hex <> 'e445a52e51cb9a1d'
)
SELECT
nio.instruction_name,
nio.normalized_entry_name,
nio.discriminator_hex,
COUNT(*) AS observed_count,
COUNT(DISTINCT nio.signature) AS tx_count,
MIN(nio.signature) AS sample_signature,
CASE
WHEN ce.id IS NULL THEN 'coverage_gap'
ELSE 'covered'
END AS observation_coverage_status,
ce.local_event_kind,
ce.expected_db_target,
ce.proof_status
FROM normalized_io nio
LEFT JOIN k_sol_dex_event_coverage_entries ce
ON ce.decoder_code = 'pump_fun'
AND COALESCE(ce.discriminator_hex, '') = COALESCE(nio.discriminator_hex, '')
AND (
COALESCE(TRIM(nio.instruction_name), '') = ''
OR ce.entry_name = nio.instruction_name
OR ce.entry_name = nio.normalized_entry_name
OR ce.local_event_kind = nio.instruction_name
OR ce.local_event_kind = ('pump_fun.' || nio.normalized_entry_name)
)
GROUP BY
nio.instruction_name,
nio.normalized_entry_name,
nio.discriminator_hex,
observation_coverage_status,
ce.local_event_kind,
ce.expected_db_target,
ce.proof_status
ORDER BY observed_count DESC, nio.instruction_name, nio.discriminator_hex;
-- 11. Pump.fun successful trade candidates without materialized trade.
-- Target after closure: only rows with explicit skipTradeReason when exact amounts/direction are not proven.
SELECT
de.event_kind,
json_extract(de.payload_json, '$.amountSource') AS amount_source,
json_extract(de.payload_json, '$.skipTradeReason') AS skip_trade_reason,
COUNT(*) AS decoded_count,
COUNT(te.id) AS trade_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
WHERE de.protocol_name = 'pump_fun'
AND de.event_kind IN (
'pump_fun.buy',
'pump_fun.sell',
'pump_fun.buy_v2',
'pump_fun.sell_v2',
'pump_fun.buy_exact_sol_in',
'pump_fun.buy_exact_quote_in_v2',
'pump_fun.trade_event'
)
AND (
tx.err_json IS NULL
OR tx.err_json = ''
OR tx.err_json = 'null'
)
GROUP BY de.event_kind, amount_source, skip_trade_reason
HAVING COUNT(te.id) = 0
AND COALESCE(TRIM(skip_trade_reason), '') = ''
ORDER BY decoded_count DESC, de.event_kind, amount_source;
-- 12. Global watchlist after pump_fun replay.
-- Expected after local promotion: pump_fun rows should no longer dominate this list unless explicitly deferred.
SELECT
json_extract(de.payload_json, '$.upstreamDecoderCode') AS upstream_decoder_code,
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 = 'upstream_git'
AND de.event_kind = 'upstream_git.instruction_match'
GROUP BY upstream_decoder_code, upstream_entry_name, upstream_discriminator_hex
ORDER BY decoded_count DESC, upstream_decoder_code, upstream_entry_name;
-- 13. Pump.fun Solscan-IDL-only instruction coverage.
-- Target after the first Rust delta: all rows below must be covered after coverage sync.
SELECT
ce.entry_name,
ce.discriminator_hex,
ce.source_repo,
ce.source_path,
ce.local_event_kind,
ce.expected_db_target,
ce.proof_status,
ce.observed_count,
ce.materialized_count,
ce.trade_count
FROM k_sol_dex_event_coverage_entries ce
WHERE ce.decoder_code = 'pump_fun'
AND ce.source_repo = 'manual-solscan'
ORDER BY ce.entry_name;
-- 14. Pump.fun Anchor event coverage local kind check.
-- Target after full decoder delta: every Pump.fun event registry row has a local_event_kind.
SELECT
ce.entry_name,
ce.discriminator_hex,
ce.local_event_kind,
ce.expected_db_target,
ce.proof_status,
ce.observed_count,
ce.materialized_count
FROM k_sol_dex_event_coverage_entries ce
WHERE ce.decoder_code = 'pump_fun'
AND ce.entry_kind = 'event'
AND (
ce.local_event_kind IS NULL
OR TRIM(ce.local_event_kind) = ''
)
ORDER BY ce.entry_name;
-- 15. Pump.fun decoded Anchor events summary.
-- Informational: real corpus may be empty until an Anchor event log/self-CPI appears.
SELECT
de.event_kind,
json_extract(de.payload_json, '$.anchorEventName') AS anchor_event_name,
json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') AS anchor_event_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 = 'pump_fun'
AND COALESCE(TRIM(json_extract(de.payload_json, '$.anchorEventName')), '') <> ''
GROUP BY de.event_kind, anchor_event_name, anchor_event_discriminator_hex
ORDER BY decoded_count DESC, de.event_kind;

View File

@@ -0,0 +1,146 @@
-- Pump.fun 0.7.54 materialization follow-up validation.
-- Run after a forced replay with skipDexDecode=no, forceDexDecode=yes, deferInstructionObservations=yes.
-- 01. Materialization summary by decoded event kind.
SELECT
de.event_kind,
COUNT(DISTINCT de.id) AS decoded_count,
COUNT(DISTINCT te.id) AS trade_count,
COUNT(DISTINCT lae.id) AS launch_count,
COUNT(DISTINCT fee.id) AS fee_count,
COUNT(DISTINCT rew.id) AS reward_count,
COUNT(DISTINCT adm.id) AS admin_count,
COUNT(DISTINCT ple.id) AS lifecycle_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_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_pool_lifecycle_events ple ON ple.decoded_event_id = de.id
LEFT JOIN k_sol_token_account_events tae ON tae.decoded_event_id = de.id
WHERE de.protocol_name = 'pump_fun'
GROUP BY de.event_kind
ORDER BY de.event_kind;
-- 02. Hard safety: no materialization on failed transactions.
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 fee.id) AS fee_count,
COUNT(DISTINCT rew.id) AS reward_count,
COUNT(DISTINCT adm.id) AS admin_count,
COUNT(DISTINCT ple.id) AS lifecycle_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_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_pool_lifecycle_events ple ON ple.decoded_event_id = de.id
WHERE de.protocol_name = 'pump_fun'
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 fee_count > 0
OR reward_count > 0
OR admin_count > 0
OR lifecycle_count > 0
ORDER BY de.event_kind;
-- 03. Hard safety: one decoded event must not feed multiple business targets.
SELECT
de.event_kind,
COUNT(DISTINCT de.id) AS decoded_count,
COUNT(DISTINCT te.id) AS trade_count,
COUNT(DISTINCT lae.id) AS launch_count,
COUNT(DISTINCT fee.id) AS fee_count,
COUNT(DISTINCT rew.id) AS reward_count,
COUNT(DISTINCT adm.id) AS admin_count,
COUNT(DISTINCT ple.id) AS lifecycle_count,
(
CASE WHEN COUNT(DISTINCT te.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT lae.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT fee.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT rew.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT adm.id) > 0 THEN 1 ELSE 0 END
+ CASE WHEN COUNT(DISTINCT ple.id) > 0 THEN 1 ELSE 0 END
) AS target_count
FROM k_sol_dex_decoded_events de
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_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_pool_lifecycle_events ple ON ple.decoded_event_id = de.id
WHERE de.protocol_name = 'pump_fun'
GROUP BY de.event_kind
HAVING target_count > 1
ORDER BY target_count DESC, de.event_kind;
-- 04. Trade duplicate safety: do not materialize trade_event when an instruction trade in the same tx was materialized.
WITH materialized_pump_fun_trades AS (
SELECT
tx.signature,
de.event_kind,
COUNT(te.id) AS trade_count
FROM k_sol_trade_events te
JOIN k_sol_dex_decoded_events de ON de.id = te.decoded_event_id
JOIN k_sol_chain_transactions tx ON tx.id = de.transaction_id
WHERE de.protocol_name = 'pump_fun'
GROUP BY tx.signature, de.event_kind
)
SELECT
signature,
SUM(CASE WHEN event_kind = 'pump_fun.trade_event' THEN trade_count ELSE 0 END) AS trade_event_materialized,
SUM(CASE WHEN event_kind IN (
'pump_fun.buy',
'pump_fun.sell',
'pump_fun.buy_v2',
'pump_fun.sell_v2',
'pump_fun.buy_exact_sol_in',
'pump_fun.buy_exact_quote_in_v2'
) THEN trade_count ELSE 0 END) AS instruction_trade_materialized
FROM materialized_pump_fun_trades
GROUP BY signature
HAVING trade_event_materialized > 0
AND instruction_trade_materialized > 0
ORDER BY signature;
-- 05. Residual successful materializable rows without a business target and without skip reason.
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_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_pool_lifecycle_events ple ON ple.decoded_event_id = de.id
WHERE de.protocol_name = 'pump_fun'
AND (tx.err_json IS NULL OR tx.err_json = '' OR tx.err_json = 'null')
AND te.id IS NULL
AND lae.id IS NULL
AND fee.id IS NULL
AND rew.id IS NULL
AND adm.id IS NULL
AND ple.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, '$.skipLifecycleReason')), '') = ''
AND COALESCE(TRIM(json_extract(de.payload_json, '$.skipCatalogReason')), '') = ''
GROUP BY de.event_kind
ORDER BY unexplained_count DESC, de.event_kind;