diff --git a/CHANGELOG.md b/CHANGELOG.md index 248525c..d596c5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,4 +85,5 @@ 0.7.50-pre-r2 - Clôture CPMM/CLMM post-Launchpad : ajout des entrées Carbon `cpi_event` pour `raydium_cpmm` et `raydium_clmm`, ajout de `raydium_clmm.update_dynamic_fee_config`, normalisation des Program-data events CLMM, ajout de la table `k_sol_token_account_events` et de la matérialisation `create_support_mint_associated`, reclassement des familles ambiguës (`cpi_transport`, `liquidity_calculation`, `liquidity_change`, `position_open`, `pool_create`, `admin_config`, `account_create`, `idl_management`), codage du discriminant CPMM `40f4bc78a7e9690a` comme `raydium_cpmm.anchor_idl_instruction` decoded-only après inspection Solscan, et contexte de secours pour matérialisation liquidity CLMM via événements frères quand possible. 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 d’instruction 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.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`. diff --git a/Cargo.toml b/Cargo.toml index 31adc18..35064e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 9b130fd..dcb47a1 100644 --- a/README.md +++ b/README.md @@ -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 l’objectif devient l’analyse 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. diff --git a/ROADMAP.md b/ROADMAP.md index 11d6a8f..82e8ccd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 l’ajout d’entré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é l’instruction sans faire apparaître l’event ; -- 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 : diff --git a/docs/DB_EVENT_MODEL_REVIEW.md b/docs/DB_EVENT_MODEL_REVIEW.md index 22b453b..fec53c0 100644 --- a/docs/DB_EVENT_MODEL_REVIEW.md +++ b/docs/DB_EVENT_MODEL_REVIEW.md @@ -1,3 +1,5 @@ + + # Database Event Model Review — `khadhroony-bobobot` `0.7.47-1FE5` ## Conclusion courte diff --git a/docs/DEX_DECODER_MATRIX.md b/docs/DEX_DECODER_MATRIX.md index faf4057..9ab3732 100644 --- a/docs/DEX_DECODER_MATRIX.md +++ b/docs/DEX_DECODER_MATRIX.md @@ -1,4 +1,22 @@ -# DEX Decoder Matrix — `khadhroony-bobobot` `0.7.53 final` + + +# 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 | diff --git a/docs/DEX_EVENT_COVERAGE_MATRIX.md b/docs/DEX_EVENT_COVERAGE_MATRIX.md index 8cf1688..c349ac1 100644 --- a/docs/DEX_EVENT_COVERAGE_MATRIX.md +++ b/docs/DEX_EVENT_COVERAGE_MATRIX.md @@ -1,4 +1,6 @@ -# DEX Event Coverage Matrix — `khadhroony-bobobot` `0.7.53 final` + + +# 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 d’instruction 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 n’est incluse dans cette clôture. diff --git a/docs/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md b/docs/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md index 63f1372..d8f7bd4 100644 --- a/docs/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md +++ b/docs/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md @@ -1,3 +1,5 @@ + + # Raydium Launchpad event coverage report — `0.7.50` ## Scope diff --git a/docs/SOLSCAN_ACCOUNT_SOURCE_MATRIX.md b/docs/SOLSCAN_ACCOUNT_SOURCE_MATRIX.md index 860abf2..c5ddcd7 100644 --- a/docs/SOLSCAN_ACCOUNT_SOURCE_MATRIX.md +++ b/docs/SOLSCAN_ACCOUNT_SOURCE_MATRIX.md @@ -1,3 +1,5 @@ + + # 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. diff --git a/docs/VALIDATION_STATUS_0_7_51.md b/docs/VALIDATION_STATUS_0_7_51.md index 6b3cac4..5e4ab40 100644 --- a/docs/VALIDATION_STATUS_0_7_51.md +++ b/docs/VALIDATION_STATUS_0_7_51.md @@ -1,4 +1,4 @@ - + # Validation status — `0.7.51 raydium_amm_v4` diff --git a/docs/VALIDATION_STATUS_0_7_51_MAX_DECODER.md b/docs/VALIDATION_STATUS_0_7_51_MAX_DECODER.md index a40e933..82c6c95 100644 --- a/docs/VALIDATION_STATUS_0_7_51_MAX_DECODER.md +++ b/docs/VALIDATION_STATUS_0_7_51_MAX_DECODER.md @@ -1,4 +1,4 @@ -# file: VALIDATION_STATUS_0_7_51_MAX_DECODER.md + # Validation status — `0.7.51 raydium_amm_v4 max-decoder` diff --git a/docs/VALIDATION_STATUS_0_7_52_FINAL.md b/docs/VALIDATION_STATUS_0_7_52_FINAL.md index 7ce4632..e0f817f 100644 --- a/docs/VALIDATION_STATUS_0_7_52_FINAL.md +++ b/docs/VALIDATION_STATUS_0_7_52_FINAL.md @@ -1,3 +1,5 @@ + + # Validation status — 0.7.52 Raydium Stable Swap final ## Scope diff --git a/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_1FE5_CONTINUATION_V2.md b/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_1FE5_CONTINUATION_V2.md index 6181af5..cc145f3 100644 --- a/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_1FE5_CONTINUATION_V2.md +++ b/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_1FE5_CONTINUATION_V2.md @@ -1,3 +1,5 @@ + + # Prompt de reprise — khadhroony-bobobot `0.7.47-1FE5` Reprise du projet `khadhroony-bobobot`. diff --git a/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_EVENT_COVERAGE_V3.md b/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_EVENT_COVERAGE_V3.md index e85cd45..bf74e8a 100644 --- a/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_EVENT_COVERAGE_V3.md +++ b/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_EVENT_COVERAGE_V3.md @@ -1,3 +1,5 @@ + + # Prompt de reprise — khadhroony-bobobot `0.7.47-1FE5` / Event coverage Reprise du projet `khadhroony-bobobot`. diff --git a/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_UPSTREAM_REGISTRY.md b/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_UPSTREAM_REGISTRY.md index 00baab7..962a589 100644 --- a/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_UPSTREAM_REGISTRY.md +++ b/docs/prompts/NEXT_SESSION_PROMPT_0.7.47_UPSTREAM_REGISTRY.md @@ -1,3 +1,5 @@ + + # Prompt de reprise — khadhroony-bobobot `0.7.47` Reprise du projet `khadhroony-bobobot`. diff --git a/docs/prompts/NEXT_SESSION_PROMPT_0.7.49_RAYDIUM_CLMM.md b/docs/prompts/NEXT_SESSION_PROMPT_0.7.49_RAYDIUM_CLMM.md index a7d8111..e52b435 100644 --- a/docs/prompts/NEXT_SESSION_PROMPT_0.7.49_RAYDIUM_CLMM.md +++ b/docs/prompts/NEXT_SESSION_PROMPT_0.7.49_RAYDIUM_CLMM.md @@ -1,3 +1,5 @@ + + # 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`. diff --git a/docs/prompts/PROMPT_0_7_54_PUMP_FUN.md b/docs/prompts/PROMPT_0_7_54_PUMP_FUN.md index 8810bba..eba49f6 100644 --- a/docs/prompts/PROMPT_0_7_54_PUMP_FUN.md +++ b/docs/prompts/PROMPT_0_7_54_PUMP_FUN.md @@ -269,4 +269,4 @@ Commencer par analyser l’archive 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. diff --git a/docs/prompts/PROMPT_0_7_55_PUMP_FEES.md b/docs/prompts/PROMPT_0_7_55_PUMP_FEES.md new file mode 100644 index 0000000..6632a37 --- /dev/null +++ b/docs/prompts/PROMPT_0_7_55_PUMP_FEES.md @@ -0,0 +1,347 @@ + + +# 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. diff --git a/docs/prompts/PROMPT_REPRISE_khadhroony-bobobot_0.7.48-raydium-cpmm.md b/docs/prompts/PROMPT_REPRISE_khadhroony-bobobot_0.7.48-raydium-cpmm.md index d1e81b4..7672492 100644 --- a/docs/prompts/PROMPT_REPRISE_khadhroony-bobobot_0.7.48-raydium-cpmm.md +++ b/docs/prompts/PROMPT_REPRISE_khadhroony-bobobot_0.7.48-raydium-cpmm.md @@ -1,3 +1,5 @@ + + # 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`. diff --git a/docs/reports/DEX_COVERAGE_GLOBAL_WATCHLIST_0_7_53.md b/docs/reports/DEX_COVERAGE_GLOBAL_WATCHLIST_0_7_53.md index 60e9a99..0086729 100644 --- a/docs/reports/DEX_COVERAGE_GLOBAL_WATCHLIST_0_7_53.md +++ b/docs/reports/DEX_COVERAGE_GLOBAL_WATCHLIST_0_7_53.md @@ -1,3 +1,5 @@ + + # DEX coverage global watchlist — `0.7.53` ## Objet diff --git a/docs/reports/PUMP_FUN_EVENT_COVERAGE_REPORT.md b/docs/reports/PUMP_FUN_EVENT_COVERAGE_REPORT.md new file mode 100644 index 0000000..b8d2c8e --- /dev/null +++ b/docs/reports/PUMP_FUN_EVENT_COVERAGE_REPORT.md @@ -0,0 +1,127 @@ + + +# 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éé. diff --git a/docs/reports/PUMP_SWAP_EVENT_COVERAGE_REPORT.md b/docs/reports/PUMP_SWAP_EVENT_COVERAGE_REPORT.md index 92a9a27..7f380c8 100644 --- a/docs/reports/PUMP_SWAP_EVENT_COVERAGE_REPORT.md +++ b/docs/reports/PUMP_SWAP_EVENT_COVERAGE_REPORT.md @@ -1,3 +1,5 @@ + + # PumpSwap event coverage report — `0.7.53` ## Scope diff --git a/docs/reports/RAYDIUM_CLMM_UPSTREAM_COVERAGE_REVIEW_PRE19.md b/docs/reports/RAYDIUM_CLMM_UPSTREAM_COVERAGE_REVIEW_PRE19.md index 84d3ae4..f16ee3d 100644 --- a/docs/reports/RAYDIUM_CLMM_UPSTREAM_COVERAGE_REVIEW_PRE19.md +++ b/docs/reports/RAYDIUM_CLMM_UPSTREAM_COVERAGE_REVIEW_PRE19.md @@ -1,4 +1,4 @@ -# file: docs/reports/RAYDIUM_CLMM_UPSTREAM_COVERAGE_REVIEW_PRE19.md + # Raydium CLMM upstream coverage review — `0.7.49-pre.19` diff --git a/docs/reports/RAYDIUM_CPMM_CLMM_RECHECK_REPORT_0_7_50_PRE_R2.md b/docs/reports/RAYDIUM_CPMM_CLMM_RECHECK_REPORT_0_7_50_PRE_R2.md index 68a445b..7f111f7 100644 --- a/docs/reports/RAYDIUM_CPMM_CLMM_RECHECK_REPORT_0_7_50_PRE_R2.md +++ b/docs/reports/RAYDIUM_CPMM_CLMM_RECHECK_REPORT_0_7_50_PRE_R2.md @@ -1,3 +1,5 @@ + + # Raydium CPMM/CLMM re-check report — `0.7.50-pre-r2` ## Scope diff --git a/docs/reports/RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md b/docs/reports/RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md index 6602979..c1d079d 100644 --- a/docs/reports/RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md +++ b/docs/reports/RAYDIUM_CPMM_EVENT_COVERAGE_REPORT.md @@ -1,3 +1,5 @@ + + # Rapport `0.7.48` — Raydium CPMM event coverage ## Scope diff --git a/docs/reports/RAYDIUM_CPMM_UPSTREAM_COVERAGE_REVIEW_PRE22.md b/docs/reports/RAYDIUM_CPMM_UPSTREAM_COVERAGE_REVIEW_PRE22.md index b02469d..d48e02f 100644 --- a/docs/reports/RAYDIUM_CPMM_UPSTREAM_COVERAGE_REVIEW_PRE22.md +++ b/docs/reports/RAYDIUM_CPMM_UPSTREAM_COVERAGE_REVIEW_PRE22.md @@ -1,3 +1,5 @@ + + # Raydium CPMM upstream coverage review — 0.7.49-pre.22 ## Scope diff --git a/docs/reports/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md b/docs/reports/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md index 76ed3d0..fea0c14 100644 --- a/docs/reports/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md +++ b/docs/reports/RAYDIUM_LAUNCHPAD_EVENT_COVERAGE_REPORT.md @@ -1,3 +1,5 @@ + + # Raydium Launchpad event coverage report — `0.7.50` ## Scope diff --git a/docs/reports/RAYDIUM_STABLE_SWAP_EVENT_COVERAGE_REPORT.md b/docs/reports/RAYDIUM_STABLE_SWAP_EVENT_COVERAGE_REPORT.md index c499ee2..6365d94 100644 --- a/docs/reports/RAYDIUM_STABLE_SWAP_EVENT_COVERAGE_REPORT.md +++ b/docs/reports/RAYDIUM_STABLE_SWAP_EVENT_COVERAGE_REPORT.md @@ -1,3 +1,5 @@ + + # Raydium Stable Swap event coverage report — 0.7.52 final ## Scope diff --git a/kb_demo_app/package.json b/kb_demo_app/package.json index 2cbff8d..7e89a9e 100644 --- a/kb_demo_app/package.json +++ b/kb_demo_app/package.json @@ -1,7 +1,7 @@ { "name": "kb-demo-app", "private": true, - "version": "0.7.53", + "version": "0.7.54", "type": "module", "scripts": { "dev": "vite", diff --git a/kb_demo_app/tauri.conf.json b/kb_demo_app/tauri.conf.json index 4d1d93d..a409885 100644 --- a/kb_demo_app/tauri.conf.json +++ b/kb_demo_app/tauri.conf.json @@ -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", diff --git a/kb_lib/src/dex.rs b/kb_lib/src/dex.rs index 4972c8a..ee4479e 100644 --- a/kb_lib/src/dex.rs +++ b/kb_lib/src/dex.rs @@ -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; diff --git a/kb_lib/src/dex/pump_fun.rs b/kb_lib/src/dex/pump_fun.rs index 93fe29a..8569267 100644 --- a/kb_lib/src/dex/pump_fun.rs +++ b/kb_lib/src/dex/pump_fun.rs @@ -4,6 +4,43 @@ const PUMP_FUN_BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234]; const PUMP_FUN_SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173]; +const PUMP_FUN_ANCHOR_SELF_CPI_LOG_DISCRIMINATOR: [u8; 8] = [228, 69, 165, 46, 81, 203, 154, 29]; + +#[derive(Debug, Clone, Copy)] +enum PumpFunBorshFieldKind { + U64, + I64, + Bool, + Pubkey, + String, + OptionBool, + OptionU64, + PubkeyArray(usize), + ShareholderVec, +} + +#[derive(Debug, Clone, Copy)] +struct PumpFunBorshFieldDescriptor { + name: &'static str, + kind: PumpFunBorshFieldKind, +} + +#[derive(Debug, Clone, Copy)] +struct PumpFunAnchorEventDescriptor { + event_name: &'static str, + event_kind: &'static str, + discriminator: [u8; 8], + fields: &'static [PumpFunBorshFieldDescriptor], +} + +#[derive(Debug, Clone)] +struct PumpFunAnchorEventDecodedPayload { + event_name: &'static str, + event_kind: &'static str, + discriminator_hex: std::string::String, + payload_size: usize, + payload_json: serde_json::Value, +} /// Decoded Pump.fun `create_v2` token event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -59,6 +96,27 @@ pub struct PumpFunTradeDecoded { pub payload_json: serde_json::Value, } +/// Decoded Pump.fun instruction audit event. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PumpFunInstructionAuditDecoded { + /// Parent transaction id. + pub transaction_id: i64, + /// Parent instruction id. + pub instruction_id: i64, + /// Transaction signature. + pub signature: std::string::String, + /// Program id. + pub program_id: std::string::String, + /// Stable Pump.fun instruction name. + pub instruction_name: std::string::String, + /// Stable local decoded event kind. + pub event_kind: std::string::String, + /// Optional instruction discriminator hex. + pub discriminator_hex: std::option::Option, + /// Decoded payload. + pub payload_json: serde_json::Value, +} + /// Decoded Pump.fun event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum PumpFunDecodedEvent { @@ -68,6 +126,8 @@ pub enum PumpFunDecodedEvent { BuyTrade(PumpFunTradeDecoded), /// Sell trade. SellTrade(PumpFunTradeDecoded), + /// Audit-only instruction. + InstructionAudit(PumpFunInstructionAuditDecoded), } /// Pump.fun decoder. @@ -114,6 +174,7 @@ impl PumpFunDecoder { let has_create_v2_log = log_messages_contain_keyword(&log_messages, "create_v2") || log_messages_contain_keyword(&log_messages, "createv2"); let mut decoded_events = std::vec::Vec::new(); + let mut pump_fun_instruction_ids = std::vec::Vec::::new(); for instruction in instructions { if instruction.parent_instruction_id.is_some() { continue; @@ -131,20 +192,38 @@ impl PumpFunDecoder { Some(instruction_id) => instruction_id, None => continue, }; + pump_fun_instruction_ids.push(instruction_id); let accounts_result = parse_accounts_json(instruction.accounts_json.as_str()); let accounts = match accounts_result { Ok(accounts) => accounts, Err(error) => return Err(error), }; let instruction_data = decode_optional_instruction_data(instruction.data_json.as_ref()); - let is_buy = instruction_data_starts_with( - instruction_data.as_deref(), - &PUMP_FUN_BUY_DISCRIMINATOR, - ); - let is_sell = instruction_data_starts_with( - instruction_data.as_deref(), - &PUMP_FUN_SELL_DISCRIMINATOR, - ); + let discriminator_hex = instruction_discriminator_hex(instruction_data.as_deref()); + let instruction_name = pump_fun_instruction_name(discriminator_hex.as_deref()); + if instruction_name == Some("anchor_self_cpi_log") { + if let Some(instruction_data) = instruction_data.as_deref() { + let anchor_event = decode_pump_fun_anchor_event_data(instruction_data); + if let Some(anchor_event) = anchor_event { + decoded_events.push(build_pump_fun_anchor_event_decoded( + transaction, + instruction_id, + &anchor_event, + )); + } + } + continue; + } + let is_buy = instruction_name == Some("buy") + || instruction_data_starts_with( + instruction_data.as_deref(), + &PUMP_FUN_BUY_DISCRIMINATOR, + ); + let is_sell = instruction_name == Some("sell") + || instruction_data_starts_with( + instruction_data.as_deref(), + &PUMP_FUN_SELL_DISCRIMINATOR, + ); if is_buy || is_sell { if accounts.len() < 7 { continue; @@ -164,6 +243,8 @@ impl PumpFunDecoder { "signature": transaction.signature, "instructionId": instruction_id, "instructionIndex": instruction.instruction_index, + "instructionName": if is_buy { "buy" } else { "sell" }, + "instructionDiscriminatorHex": discriminator_hex, "accounts": accounts, "logMessages": log_messages, "eventKind": if is_buy { "buy" } else { "sell" }, @@ -181,6 +262,10 @@ impl PumpFunDecoder { "poolQuoteNativeAccount": bonding_curve, "amountRaw": amount_raw, "solLimitRaw": sol_limit_raw, + "decodedArguments": decode_pump_fun_instruction_arguments( + if is_buy { "buy" } else { "sell" }, + instruction_data.as_deref() + ), "tradeSide": if is_buy { "BuyBase" } else { "SellBase" } }); let event = crate::PumpFunTradeDecoded { @@ -209,43 +294,101 @@ impl PumpFunDecoder { } continue; } - if !has_create_v2_log { + let should_decode_create_v2 = instruction_name == Some("create_v2") + || (instruction_name.is_none() && has_create_v2_log); + if should_decode_create_v2 { + if accounts.len() < 6 { + continue; + } + let mint = extract_account(&accounts, 0); + let bonding_curve = extract_account(&accounts, 2); + let associated_bonding_curve = extract_account(&accounts, 3); + let creator = extract_account(&accounts, 5); + let payload_json = serde_json::json!({ + "decoder": "pump_fun", + "eventKind": "create_v2_token", + "signature": transaction.signature, + "instructionId": instruction_id, + "instructionIndex": instruction.instruction_index, + "instructionName": "create_v2", + "instructionDiscriminatorHex": discriminator_hex, + "accounts": accounts, + "logMessages": log_messages, + "mint": mint, + "bondingCurve": bonding_curve, + "associatedBondingCurve": associated_bonding_curve, + "creator": creator, + "decodedArguments": decode_pump_fun_instruction_arguments( + "create_v2", + instruction_data.as_deref() + ) + }); + decoded_events.push(crate::PumpFunDecodedEvent::CreateV2Token( + crate::PumpFunCreateV2TokenDecoded { + transaction_id, + instruction_id, + signature: transaction.signature.clone(), + program_id: program_id.clone(), + mint, + bonding_curve, + associated_bonding_curve, + creator, + payload_json, + }, + )); continue; } - if accounts.len() < 6 { + let instruction_name = match instruction_name { + Some(instruction_name) => instruction_name, + None => continue, + }; + if instruction_name == "anchor_self_cpi_log" { continue; } - let mint = extract_account(&accounts, 0); - let bonding_curve = extract_account(&accounts, 2); - let associated_bonding_curve = extract_account(&accounts, 3); - let creator = extract_account(&accounts, 5); - let payload_json = serde_json::json!({ - "decoder": "pump_fun", - "eventKind": "create_v2_token", - "signature": transaction.signature, - "instructionId": instruction_id, - "instructionIndex": instruction.instruction_index, - "accounts": accounts, - "logMessages": log_messages, - "mint": mint, - "bondingCurve": bonding_curve, - "associatedBondingCurve": associated_bonding_curve, - "creator": creator - }); - decoded_events.push(crate::PumpFunDecodedEvent::CreateV2Token( - crate::PumpFunCreateV2TokenDecoded { + let event_kind = format!("pump_fun.{}", instruction_name); + let payload_json = build_instruction_audit_payload( + transaction, + instruction, + instruction_name, + discriminator_hex.clone(), + accounts, + &log_messages, + instruction_data.as_deref(), + ); + decoded_events.push(crate::PumpFunDecodedEvent::InstructionAudit( + crate::PumpFunInstructionAuditDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.clone(), - mint, - bonding_curve, - associated_bonding_curve, - creator, + instruction_name: instruction_name.to_string(), + event_kind, + discriminator_hex, payload_json, }, )); } + let fallback_instruction_id = match pump_fun_instruction_ids.first() { + Some(instruction_id) => Some(*instruction_id), + None => None, + }; + if let Some(instruction_id) = fallback_instruction_id { + for log_message in &log_messages { + let encoded = match extract_program_data_base64(log_message.as_str()) { + Some(encoded) => encoded, + None => continue, + }; + let anchor_event = match decode_pump_fun_anchor_event_from_base64(encoded) { + Some(anchor_event) => anchor_event, + None => continue, + }; + decoded_events.push(build_pump_fun_anchor_event_decoded( + transaction, + instruction_id, + &anchor_event, + )); + } + } return Ok(decoded_events); } } @@ -375,6 +518,2157 @@ fn extract_account( return Some(accounts[index].clone()); } +fn instruction_discriminator_hex( + instruction_data: std::option::Option<&[u8]>, +) -> std::option::Option { + let instruction_data = match instruction_data { + Some(instruction_data) => instruction_data, + None => return None, + }; + if instruction_data.len() < 8 { + return None; + } + return Some(bytes_to_hex(&instruction_data[0..8])); +} + +fn bytes_to_hex(bytes: &[u8]) -> std::string::String { + let mut text = std::string::String::new(); + for byte in bytes { + text.push_str(format!("{:02x}", byte).as_str()); + } + return text; +} + +fn pump_fun_instruction_name( + discriminator_hex: std::option::Option<&str>, +) -> std::option::Option<&'static str> { + let discriminator_hex = match discriminator_hex { + Some(discriminator_hex) => discriminator_hex, + None => return None, + }; + 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); +} + +fn build_instruction_audit_payload( + transaction: &crate::ChainTransactionDto, + instruction: &crate::ChainInstructionDto, + instruction_name: &str, + discriminator_hex: std::option::Option, + accounts: std::vec::Vec, + log_messages: &[std::string::String], + instruction_data: std::option::Option<&[u8]>, +) -> serde_json::Value { + let event_kind = format!("pump_fun.{}", instruction_name); + let decoded_arguments = decode_pump_fun_instruction_arguments(instruction_name, instruction_data); + let is_trade_instruction = pump_fun_instruction_is_direct_materializable_trade(instruction_name); + let mut payload = serde_json::json!({ + "decoder": "pump_fun", + "signature": transaction.signature, + "slot": transaction.slot, + "instructionId": instruction.id, + "instructionIndex": instruction.instruction_index, + "instructionName": instruction_name, + "instructionDiscriminatorHex": discriminator_hex, + "eventKind": event_kind, + "programId": crate::PUMP_FUN_PROGRAM_ID, + "accounts": accounts.clone(), + "logMessages": log_messages, + "decodedArguments": decoded_arguments, + "instructionAuditOnly": true, + "tradeCandidate": is_trade_instruction, + "candleCandidate": is_trade_instruction, + "nonTradeUseful": !is_trade_instruction, + }); + if let Some(object) = payload.as_object_mut() { + add_pump_fun_instruction_aliases(instruction_name, &accounts, object); + if let Some(trade_side) = pump_fun_instruction_trade_side(instruction_name) { + object.insert( + "tradeSide".to_string(), + serde_json::Value::String(trade_side.to_string()), + ); + } + if is_trade_instruction { + add_pump_fun_instruction_program_data_trade_amounts( + instruction_name, + log_messages, + object, + ); + } + if pump_fun_instruction_is_anchor_trade_sibling_backed(instruction_name) { + object.insert( + "instructionCoveredByAnchorTradeEvent".to_string(), + serde_json::Value::Bool(true), + ); + object.insert( + "skipTradeReason".to_string(), + serde_json::Value::String( + "pump_fun_instruction_covered_by_anchor_trade_event".to_string(), + ), + ); + object.insert( + "skipCandleReason".to_string(), + serde_json::Value::String( + "pump_fun_instruction_covered_by_anchor_trade_event".to_string(), + ), + ); + } else if !is_trade_instruction { + object.insert( + "skipTradeReason".to_string(), + serde_json::Value::String("non_trade_event".to_string()), + ); + object.insert( + "skipCandleReason".to_string(), + serde_json::Value::String("non_trade_event".to_string()), + ); + } + object.insert( + "skipCatalogReason".to_string(), + serde_json::Value::String("pump_fun_instruction_audit_only".to_string()), + ); + } + return payload; +} + +fn pump_fun_instruction_is_direct_materializable_trade(instruction_name: &str) -> bool { + match instruction_name { + "buy_exact_sol_in" => return true, + _ => return false, + } +} + +fn pump_fun_instruction_is_anchor_trade_sibling_backed(instruction_name: &str) -> bool { + match instruction_name { + "buy_exact_quote_in_v2" => return true, + "buy_v2" => return true, + "sell_v2" => return true, + _ => return false, + } +} + +fn pump_fun_instruction_trade_side( + instruction_name: &str, +) -> std::option::Option<&'static str> { + match instruction_name { + "buy_exact_quote_in_v2" => return Some("BuyBase"), + "buy_exact_sol_in" => return Some("BuyBase"), + "buy_v2" => return Some("BuyBase"), + "sell_v2" => return Some("SellBase"), + _ => return None, + } +} + +fn add_pump_fun_instruction_program_data_trade_amounts( + instruction_name: &str, + log_messages: &[std::string::String], + object: &mut serde_json::Map, +) { + let expected_is_buy = match instruction_name { + "buy_exact_sol_in" => Some(true), + _ => None, + }; + let expected_is_buy = match expected_is_buy { + Some(expected_is_buy) => expected_is_buy, + None => return, + }; + let instruction_mint = pump_fun_object_string(object, &["mint", "tokenMint", "tokenAMint"]); + let instruction_user = pump_fun_object_string(object, &["user", "actorWallet"]); + for log_message in log_messages { + let encoded = match extract_program_data_base64(log_message.as_str()) { + Some(encoded) => encoded, + None => continue, + }; + let anchor_event = match decode_pump_fun_anchor_event_from_base64(encoded) { + Some(anchor_event) => anchor_event, + None => continue, + }; + if anchor_event.event_name != "trade_event" { + continue; + } + let event_object = match anchor_event.payload_json.as_object() { + Some(event_object) => event_object, + None => continue, + }; + let event_is_buy = match event_object.get("is_buy") { + Some(value) => value.as_bool(), + None => None, + }; + match event_is_buy { + Some(event_is_buy) if event_is_buy == expected_is_buy => {}, + Some(_) => continue, + None => continue, + } + let event_mint = pump_fun_object_string(event_object, &["mint"]); + if !pump_fun_optional_strings_match(instruction_mint.as_deref(), event_mint.as_deref()) { + continue; + } + let event_user = pump_fun_object_string(event_object, &["user"]); + if !pump_fun_optional_strings_match(instruction_user.as_deref(), event_user.as_deref()) { + continue; + } + copy_pump_fun_anchor_event_value(event_object, object, "token_amount", "baseAmountRaw"); + copy_pump_fun_anchor_event_value(event_object, object, "sol_amount", "quoteAmountRaw"); + copy_pump_fun_anchor_event_value(event_object, object, "quote_amount", "quoteAmountRaw"); + copy_pump_fun_anchor_event_value(event_object, object, "fee", "feeAmountRaw"); + copy_pump_fun_anchor_event_value(event_object, object, "creator_fee", "creatorFeeAmountRaw"); + copy_pump_fun_anchor_event_value(event_object, object, "cashback", "cashbackAmountRaw"); + object.insert( + "amountSource".to_string(), + serde_json::Value::String("pump_fun_program_data_trade_event".to_string()), + ); + object.insert( + "canonicalAnchorEventName".to_string(), + serde_json::Value::String(anchor_event.event_name.to_string()), + ); + object.insert( + "canonicalAnchorEventDiscriminatorHex".to_string(), + serde_json::Value::String(anchor_event.discriminator_hex.clone()), + ); + return; + } +} + +fn copy_pump_fun_anchor_event_value( + source: &serde_json::Map, + target: &mut serde_json::Map, + source_key: &str, + target_key: &str, +) { + if target.contains_key(target_key) { + return; + } + let value = match source.get(source_key) { + Some(value) => value.clone(), + None => return, + }; + target.insert(target_key.to_string(), value); +} + +fn pump_fun_object_string( + object: &serde_json::Map, + keys: &[&str], +) -> std::option::Option { + for key in keys { + let value = match object.get(*key) { + Some(value) => value, + None => continue, + }; + if let Some(text) = value.as_str() { + let text = text.trim(); + if !text.is_empty() { + return Some(text.to_string()); + } + } + } + return None; +} + +fn pump_fun_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 add_pump_fun_instruction_aliases( + instruction_name: &str, + accounts: &[std::string::String], + object: &mut serde_json::Map, +) { + if instruction_name == "buy_exact_quote_in_v2" + || instruction_name == "buy_v2" + || instruction_name == "sell_v2" + { + insert_account_alias(object, accounts, 1, &["mint", "tokenMint", "tokenAMint"]); + insert_account_alias(object, accounts, 2, &["quoteMint", "tokenBMint"]); + insert_account_alias(object, accounts, 3, &["baseTokenProgram", "tokenProgram"]); + insert_account_alias(object, accounts, 4, &["quoteTokenProgram"]); + insert_account_alias(object, accounts, 5, &["associatedTokenProgram"]); + insert_account_alias(object, accounts, 6, &["feeRecipient"]); + insert_account_alias(object, accounts, 7, &["associatedQuoteFeeRecipient"]); + insert_account_alias(object, accounts, 8, &["buybackFeeRecipient"]); + insert_account_alias(object, accounts, 9, &["associatedQuoteBuybackFeeRecipient"]); + insert_account_alias(object, accounts, 10, &["bondingCurve", "poolAccount"]); + insert_account_alias( + object, + accounts, + 11, + &["associatedBondingCurve", "poolBaseTokenAccount", "lpMint"], + ); + insert_account_alias( + object, + accounts, + 12, + &["associatedQuoteBondingCurve", "poolQuoteTokenAccount"], + ); + insert_account_alias(object, accounts, 13, &["user", "actorWallet"]); + insert_account_alias(object, accounts, 14, &["associatedUser", "associatedUserBase"]); + insert_account_alias(object, accounts, 15, &["associatedUserQuote"]); + insert_account_alias(object, accounts, 16, &["creatorVault"]); + insert_account_alias(object, accounts, 17, &["associatedCreatorVault"]); + insert_account_alias(object, accounts, 18, &["sharingConfig"]); + if instruction_name == "sell_v2" { + insert_account_alias(object, accounts, 19, &["userVolumeAccumulator"]); + insert_account_alias(object, accounts, 20, &["associatedUserVolumeAccumulator"]); + insert_account_alias(object, accounts, 21, &["feeConfig"]); + insert_account_alias(object, accounts, 22, &["feeProgram"]); + insert_account_alias(object, accounts, 23, &["systemProgram"]); + insert_account_alias(object, accounts, 24, &["eventAuthority"]); + insert_account_alias(object, accounts, 25, &["program"]); + } else { + insert_account_alias(object, accounts, 19, &["globalVolumeAccumulator"]); + insert_account_alias(object, accounts, 20, &["userVolumeAccumulator"]); + insert_account_alias(object, accounts, 21, &["associatedUserVolumeAccumulator"]); + insert_account_alias(object, accounts, 22, &["feeConfig"]); + insert_account_alias(object, accounts, 23, &["feeProgram"]); + insert_account_alias(object, accounts, 24, &["systemProgram"]); + insert_account_alias(object, accounts, 25, &["eventAuthority"]); + insert_account_alias(object, accounts, 26, &["program"]); + } + let pool_quote_token_account = object + .get("poolQuoteTokenAccount") + .cloned() + .unwrap_or(serde_json::Value::String(crate::PUMP_FUN_PROGRAM_ID.to_string())); + object.insert("poolQuoteNativeAccount".to_string(), pool_quote_token_account); + return; + } + if instruction_name == "buy_exact_sol_in" { + insert_account_alias(object, accounts, 2, &["mint", "tokenMint", "tokenAMint"]); + insert_account_alias(object, accounts, 3, &["bondingCurve", "poolAccount"]); + insert_account_alias( + object, + accounts, + 4, + &["associatedBondingCurve", "poolBaseTokenAccount", "lpMint"], + ); + insert_account_alias(object, accounts, 5, &["associatedUser"]); + insert_account_alias(object, accounts, 6, &["user", "actorWallet"]); + insert_account_alias(object, accounts, 1, &["feeRecipient"]); + insert_account_alias(object, accounts, 8, &["tokenProgram"]); + insert_account_alias(object, accounts, 9, &["creatorVault"]); + object.insert( + "tokenBMint".to_string(), + serde_json::Value::String(crate::WSOL_MINT_ID.to_string()), + ); + let pool_quote_native_account = object + .get("bondingCurve") + .cloned() + .unwrap_or(serde_json::Value::String(crate::PUMP_FUN_PROGRAM_ID.to_string())); + object.insert("poolQuoteNativeAccount".to_string(), pool_quote_native_account); + return; + } + if instruction_name == "create" || instruction_name == "create_v2" { + insert_account_alias(object, accounts, 0, &["mint", "tokenMint", "tokenAMint"]); + insert_account_alias(object, accounts, 2, &["bondingCurve", "poolAccount"]); + insert_account_alias(object, accounts, 3, &["associatedBondingCurve", "lpMint"]); + insert_account_alias(object, accounts, 5, &["creator", "actorWallet"]); + object.insert( + "tokenBMint".to_string(), + serde_json::Value::String(crate::WSOL_MINT_ID.to_string()), + ); + return; + } + if instruction_name.contains("migrate") { + insert_account_alias(object, accounts, 0, &["actorWallet", "user"]); + insert_account_alias(object, accounts, 1, &["mint", "tokenMint", "tokenAMint"]); + insert_account_alias(object, accounts, 2, &["bondingCurve", "poolAccount"]); + object.insert( + "tokenBMint".to_string(), + serde_json::Value::String(crate::WSOL_MINT_ID.to_string()), + ); + return; + } + if instruction_name.contains("creator") + || instruction_name.contains("fee") + || instruction_name.contains("cashback") + || instruction_name.contains("volume_accumulator") + || instruction_name.contains("quote_mint") + || instruction_name.contains("reserved_fee_recipients") + || instruction_name.contains("mayhem") + || instruction_name.contains("global_authority") + || instruction_name.contains("buyback") + || instruction_name.contains("extend_account") + { + insert_account_alias(object, accounts, 0, &["actorWallet", "authority", "user"]); + insert_account_alias(object, accounts, 1, &["mint", "tokenMint", "tokenAMint", "quoteMint"]); + insert_account_alias(object, accounts, 2, &["bondingCurve", "poolAccount", "relatedAccount"]); + if instruction_name.contains("fee") { + clone_json_value(object, "quoteMint", "feeTokenMint"); + } + if instruction_name.contains("cashback") || instruction_name.contains("volume_accumulator") { + clone_json_value(object, "mint", "rewardTokenMint"); + } + object.insert( + "adminAction".to_string(), + serde_json::Value::String(instruction_name.to_string()), + ); + } +} + +fn insert_account_alias( + object: &mut serde_json::Map, + accounts: &[std::string::String], + index: usize, + keys: &[&str], +) { + let value = match accounts.get(index) { + Some(value) => value, + None => return, + }; + for key in keys { + if object.contains_key(*key) { + continue; + } + object.insert((*key).to_string(), serde_json::Value::String(value.clone())); + } +} + +const PUMP_FUN_ADMIN_SET_CREATOR_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "admin_set_creator_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "bonding_curve", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "old_creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "new_creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_ADMIN_SET_IDL_AUTHORITY_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "idl_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }]; + +const PUMP_FUN_ADMIN_UPDATE_TOKEN_INCENTIVES_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "start_time", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "end_time", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "day_number", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "token_supply_per_day", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "seconds_in_a_day", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, +]; + +const PUMP_FUN_CLAIM_CASHBACK_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "total_claimed", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "total_cashback_earned", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_CLAIM_TOKEN_INCENTIVES_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "total_claimed_tokens", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "current_sol_volume", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_CLOSE_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "total_unclaimed_tokens", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "total_claimed_tokens", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "current_sol_volume", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "last_update_timestamp", + kind: PumpFunBorshFieldKind::I64, + }, +]; + +const PUMP_FUN_COLLECT_CREATOR_FEE_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "creator_fee", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "quote_mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_COMPLETE_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "bonding_curve", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "quote_mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_COMPLETE_PUMP_AMM_MIGRATION_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "mint_amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "sol_amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "pool_migration_fee", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "bonding_curve", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "pool", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "quote_mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_CREATE_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "name", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "symbol", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "uri", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "bonding_curve", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "virtual_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "virtual_sol_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "real_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "token_total_supply", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "token_program", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "is_mayhem_mode", + kind: PumpFunBorshFieldKind::Bool, + }, + PumpFunBorshFieldDescriptor { + name: "is_cashback_enabled", + kind: PumpFunBorshFieldKind::Bool, + }, + PumpFunBorshFieldDescriptor { + name: "quote_mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "virtual_quote_reserves", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_DISTRIBUTE_CREATOR_FEES_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "bonding_curve", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "sharing_config", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "admin", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "shareholders", + kind: PumpFunBorshFieldKind::ShareholderVec, + }, + PumpFunBorshFieldDescriptor { + name: "distributed", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "quote_mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_EXTEND_ACCOUNT_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "account", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "current_size", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "new_size", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, +]; + +const PUMP_FUN_INIT_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "payer", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, +]; + +const PUMP_FUN_MIGRATE_BONDING_CURVE_CREATOR_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "bonding_curve", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "sharing_config", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "old_creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "new_creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_MINIMUM_DISTRIBUTABLE_FEE_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "minimum_required", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "distributable_fees", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "can_distribute", + kind: PumpFunBorshFieldKind::Bool, + }, +]; + +const PUMP_FUN_RESERVED_FEE_RECIPIENTS_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "reserved_fee_recipient", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "reserved_fee_recipients", + kind: PumpFunBorshFieldKind::PubkeyArray(7), + }, +]; + +const PUMP_FUN_SET_CREATOR_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "bonding_curve", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_SET_METAPLEX_CREATOR_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "bonding_curve", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "metadata", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_SET_PARAMS_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "initial_virtual_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "initial_virtual_sol_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "initial_real_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "final_real_sol_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "token_total_supply", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "fee_basis_points", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "withdraw_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "enable_migrate", + kind: PumpFunBorshFieldKind::Bool, + }, + PumpFunBorshFieldDescriptor { + name: "pool_migration_fee", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "creator_fee_basis_points", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "fee_recipients", + kind: PumpFunBorshFieldKind::PubkeyArray(8), + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "set_creator_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "admin_set_creator_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "total_claimed_tokens_before", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "total_claimed_tokens_after", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, +]; + +const PUMP_FUN_TRADE_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "sol_amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "token_amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "is_buy", + kind: PumpFunBorshFieldKind::Bool, + }, + PumpFunBorshFieldDescriptor { + name: "user", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "virtual_sol_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "virtual_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "real_sol_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "real_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "fee_recipient", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "fee_basis_points", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "fee", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "creator_fee_basis_points", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "creator_fee", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "track_volume", + kind: PumpFunBorshFieldKind::Bool, + }, + PumpFunBorshFieldDescriptor { + name: "total_unclaimed_tokens", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "total_claimed_tokens", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "current_sol_volume", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "last_update_timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "ix_name", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "mayhem_mode", + kind: PumpFunBorshFieldKind::Bool, + }, + PumpFunBorshFieldDescriptor { + name: "cashback_fee_basis_points", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "cashback", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "buyback_fee_basis_points", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "buyback_fee", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "shareholders", + kind: PumpFunBorshFieldKind::ShareholderVec, + }, + PumpFunBorshFieldDescriptor { + name: "quote_mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "quote_amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "virtual_quote_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "real_quote_reserves", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_UPDATE_GLOBAL_AUTHORITY_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "global", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "authority", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "new_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, +]; + +const PUMP_FUN_UPDATE_MAYHEM_VIRTUAL_PARAMS_EVENT_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "timestamp", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "mint", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "virtual_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "virtual_sol_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "new_virtual_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "new_virtual_sol_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "real_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "real_sol_reserves", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_IX_ADD_QUOTE_MINT_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "quote_mint", + kind: PumpFunBorshFieldKind::Pubkey, + }]; + +const PUMP_FUN_IX_ADMIN_SET_CREATOR_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "creator", + kind: PumpFunBorshFieldKind::Pubkey, + }]; + +const PUMP_FUN_IX_ADMIN_SET_IDL_AUTHORITY_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "idl_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }]; + +const PUMP_FUN_IX_ADMIN_UPDATE_TOKEN_INCENTIVES_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "start_time", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "end_time", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "seconds_in_a_day", + kind: PumpFunBorshFieldKind::I64, + }, + PumpFunBorshFieldDescriptor { + name: "day_number", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "pump_token_supply_per_day", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_IX_BUY_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "max_sol_cost", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "track_volume", + kind: PumpFunBorshFieldKind::OptionBool, + }, +]; + +const PUMP_FUN_IX_BUY_EXACT_QUOTE_IN_V2_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "spendable_quote_in", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "min_tokens_out", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_IX_BUY_EXACT_SOL_IN_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "spendable_sol_in", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "min_tokens_out", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "track_volume", + kind: PumpFunBorshFieldKind::OptionBool, + }, +]; + +const PUMP_FUN_IX_BUY_V2_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "max_sol_cost", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_IX_CREATE_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "name", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "symbol", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "uri", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_IX_CREATE_V2_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "name", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "symbol", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "uri", + kind: PumpFunBorshFieldKind::String, + }, + PumpFunBorshFieldDescriptor { + name: "creator", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "is_mayhem_mode", + kind: PumpFunBorshFieldKind::Bool, + }, + PumpFunBorshFieldDescriptor { + name: "is_cashback_enabled", + kind: PumpFunBorshFieldKind::OptionBool, + }, +]; + +const PUMP_FUN_IX_DISTRIBUTE_CREATOR_FEES_V2_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "initialize_ata", + kind: PumpFunBorshFieldKind::Bool, + }]; + +const PUMP_FUN_IX_REMOVE_QUOTE_MINT_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "quote_mint", + kind: PumpFunBorshFieldKind::Pubkey, + }]; + +const PUMP_FUN_IX_SELL_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "min_sol_output", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_IX_SELL_V2_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "amount", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "min_sol_output", + kind: PumpFunBorshFieldKind::U64, + }, +]; + +const PUMP_FUN_IX_SET_CREATOR_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "creator", + kind: PumpFunBorshFieldKind::Pubkey, + }]; + +const PUMP_FUN_IX_SET_PARAMS_FIELDS: &[PumpFunBorshFieldDescriptor] = &[ + PumpFunBorshFieldDescriptor { + name: "initial_virtual_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "initial_virtual_sol_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "initial_real_token_reserves", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "token_total_supply", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "fee_basis_points", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "withdraw_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "enable_migrate", + kind: PumpFunBorshFieldKind::Bool, + }, + PumpFunBorshFieldDescriptor { + name: "pool_migration_fee", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "creator_fee_basis_points", + kind: PumpFunBorshFieldKind::U64, + }, + PumpFunBorshFieldDescriptor { + name: "set_creator_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }, + PumpFunBorshFieldDescriptor { + name: "admin_set_creator_authority", + kind: PumpFunBorshFieldKind::Pubkey, + }, +]; + +const PUMP_FUN_IX_SET_RESERVED_FEE_RECIPIENTS_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "whitelist_pda", + kind: PumpFunBorshFieldKind::Pubkey, + }]; + +const PUMP_FUN_IX_SET_VIRTUAL_QUOTE_RESERVES_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "initial_virtual_quote_reserves", + kind: PumpFunBorshFieldKind::U64, + }]; + +const PUMP_FUN_IX_TOGGLE_CASHBACK_ENABLED_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "enabled", + kind: PumpFunBorshFieldKind::Bool, + }]; + +const PUMP_FUN_IX_TOGGLE_CREATE_V2_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "enabled", + kind: PumpFunBorshFieldKind::Bool, + }]; + +const PUMP_FUN_IX_TOGGLE_MAYHEM_MODE_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "enabled", + kind: PumpFunBorshFieldKind::Bool, + }]; + +const PUMP_FUN_IX_UPDATE_BUYBACK_CONFIG_FIELDS: &[PumpFunBorshFieldDescriptor] = + &[PumpFunBorshFieldDescriptor { + name: "buyback_basis_points", + kind: PumpFunBorshFieldKind::OptionU64, + }]; + +fn pump_fun_anchor_event_descriptor( + discriminator: [u8; 8], +) -> std::option::Option { + match discriminator { + [64, 69, 192, 104, 29, 30, 25, 107] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "admin_set_creator_event", + event_kind: "pump_fun.admin_set_creator_event", + discriminator: [64, 69, 192, 104, 29, 30, 25, 107], + fields: PUMP_FUN_ADMIN_SET_CREATOR_EVENT_FIELDS, + }); + }, + [245, 59, 70, 34, 75, 185, 109, 92] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "admin_set_idl_authority_event", + event_kind: "pump_fun.admin_set_idl_authority_event", + discriminator: [245, 59, 70, 34, 75, 185, 109, 92], + fields: PUMP_FUN_ADMIN_SET_IDL_AUTHORITY_EVENT_FIELDS, + }); + }, + [147, 250, 108, 120, 247, 29, 67, 222] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "admin_update_token_incentives_event", + event_kind: "pump_fun.admin_update_token_incentives_event", + discriminator: [147, 250, 108, 120, 247, 29, 67, 222], + fields: PUMP_FUN_ADMIN_UPDATE_TOKEN_INCENTIVES_EVENT_FIELDS, + }); + }, + [226, 214, 246, 33, 7, 242, 147, 229] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "claim_cashback_event", + event_kind: "pump_fun.claim_cashback_event", + discriminator: [226, 214, 246, 33, 7, 242, 147, 229], + fields: PUMP_FUN_CLAIM_CASHBACK_EVENT_FIELDS, + }); + }, + [79, 172, 246, 49, 205, 91, 206, 232] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "claim_token_incentives_event", + event_kind: "pump_fun.claim_token_incentives_event", + discriminator: [79, 172, 246, 49, 205, 91, 206, 232], + fields: PUMP_FUN_CLAIM_TOKEN_INCENTIVES_EVENT_FIELDS, + }); + }, + [146, 159, 189, 172, 146, 88, 56, 244] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "close_user_volume_accumulator_event", + event_kind: "pump_fun.close_user_volume_accumulator_event", + discriminator: [146, 159, 189, 172, 146, 88, 56, 244], + fields: PUMP_FUN_CLOSE_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS, + }); + }, + [122, 2, 127, 1, 14, 191, 12, 175] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "collect_creator_fee_event", + event_kind: "pump_fun.collect_creator_fee_event", + discriminator: [122, 2, 127, 1, 14, 191, 12, 175], + fields: PUMP_FUN_COLLECT_CREATOR_FEE_EVENT_FIELDS, + }); + }, + [95, 114, 97, 156, 212, 46, 152, 8] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "complete_event", + event_kind: "pump_fun.complete_event", + discriminator: [95, 114, 97, 156, 212, 46, 152, 8], + fields: PUMP_FUN_COMPLETE_EVENT_FIELDS, + }); + }, + [189, 233, 93, 185, 92, 148, 234, 148] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "complete_pump_amm_migration_event", + event_kind: "pump_fun.complete_pump_amm_migration_event", + discriminator: [189, 233, 93, 185, 92, 148, 234, 148], + fields: PUMP_FUN_COMPLETE_PUMP_AMM_MIGRATION_EVENT_FIELDS, + }); + }, + [27, 114, 169, 77, 222, 235, 99, 118] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "create_event", + event_kind: "pump_fun.create_event", + discriminator: [27, 114, 169, 77, 222, 235, 99, 118], + fields: PUMP_FUN_CREATE_EVENT_FIELDS, + }); + }, + [165, 55, 129, 112, 4, 179, 202, 40] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "distribute_creator_fees_event", + event_kind: "pump_fun.distribute_creator_fees_event", + discriminator: [165, 55, 129, 112, 4, 179, 202, 40], + fields: PUMP_FUN_DISTRIBUTE_CREATOR_FEES_EVENT_FIELDS, + }); + }, + [97, 97, 215, 144, 93, 146, 22, 124] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "extend_account_event", + event_kind: "pump_fun.extend_account_event", + discriminator: [97, 97, 215, 144, 93, 146, 22, 124], + fields: PUMP_FUN_EXTEND_ACCOUNT_EVENT_FIELDS, + }); + }, + [134, 36, 13, 72, 232, 101, 130, 216] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "init_user_volume_accumulator_event", + event_kind: "pump_fun.init_user_volume_accumulator_event", + discriminator: [134, 36, 13, 72, 232, 101, 130, 216], + fields: PUMP_FUN_INIT_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS, + }); + }, + [155, 167, 104, 220, 213, 108, 243, 3] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "migrate_bonding_curve_creator_event", + event_kind: "pump_fun.migrate_bonding_curve_creator_event", + discriminator: [155, 167, 104, 220, 213, 108, 243, 3], + fields: PUMP_FUN_MIGRATE_BONDING_CURVE_CREATOR_EVENT_FIELDS, + }); + }, + [168, 216, 132, 239, 235, 182, 49, 52] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "minimum_distributable_fee_event", + event_kind: "pump_fun.minimum_distributable_fee_event", + discriminator: [168, 216, 132, 239, 235, 182, 49, 52], + fields: PUMP_FUN_MINIMUM_DISTRIBUTABLE_FEE_EVENT_FIELDS, + }); + }, + [43, 188, 250, 18, 221, 75, 187, 95] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "reserved_fee_recipients_event", + event_kind: "pump_fun.reserved_fee_recipients_event", + discriminator: [43, 188, 250, 18, 221, 75, 187, 95], + fields: PUMP_FUN_RESERVED_FEE_RECIPIENTS_EVENT_FIELDS, + }); + }, + [237, 52, 123, 37, 245, 251, 72, 210] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "set_creator_event", + event_kind: "pump_fun.set_creator_event", + discriminator: [237, 52, 123, 37, 245, 251, 72, 210], + fields: PUMP_FUN_SET_CREATOR_EVENT_FIELDS, + }); + }, + [142, 203, 6, 32, 127, 105, 191, 162] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "set_metaplex_creator_event", + event_kind: "pump_fun.set_metaplex_creator_event", + discriminator: [142, 203, 6, 32, 127, 105, 191, 162], + fields: PUMP_FUN_SET_METAPLEX_CREATOR_EVENT_FIELDS, + }); + }, + [223, 195, 159, 246, 62, 48, 143, 131] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "set_params_event", + event_kind: "pump_fun.set_params_event", + discriminator: [223, 195, 159, 246, 62, 48, 143, 131], + fields: PUMP_FUN_SET_PARAMS_EVENT_FIELDS, + }); + }, + [197, 122, 167, 124, 116, 81, 91, 255] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "sync_user_volume_accumulator_event", + event_kind: "pump_fun.sync_user_volume_accumulator_event", + discriminator: [197, 122, 167, 124, 116, 81, 91, 255], + fields: PUMP_FUN_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS, + }); + }, + [189, 219, 127, 211, 78, 230, 97, 238] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "trade_event", + event_kind: "pump_fun.trade_event", + discriminator: [189, 219, 127, 211, 78, 230, 97, 238], + fields: PUMP_FUN_TRADE_EVENT_FIELDS, + }); + }, + [182, 195, 137, 42, 35, 206, 207, 247] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "update_global_authority_event", + event_kind: "pump_fun.update_global_authority_event", + discriminator: [182, 195, 137, 42, 35, 206, 207, 247], + fields: PUMP_FUN_UPDATE_GLOBAL_AUTHORITY_EVENT_FIELDS, + }); + }, + [117, 123, 228, 182, 161, 168, 220, 214] => { + return Some(PumpFunAnchorEventDescriptor { + event_name: "update_mayhem_virtual_params_event", + event_kind: "pump_fun.update_mayhem_virtual_params_event", + discriminator: [117, 123, 228, 182, 161, 168, 220, 214], + fields: PUMP_FUN_UPDATE_MAYHEM_VIRTUAL_PARAMS_EVENT_FIELDS, + }); + }, + _ => return None, + } +} + +fn pump_fun_instruction_arg_fields( + instruction_name: &str, +) -> std::option::Option<&'static [PumpFunBorshFieldDescriptor]> { + match instruction_name { + "add_quote_mint" => return Some(PUMP_FUN_IX_ADD_QUOTE_MINT_FIELDS), + "admin_set_creator" => return Some(PUMP_FUN_IX_ADMIN_SET_CREATOR_FIELDS), + "admin_set_idl_authority" => return Some(PUMP_FUN_IX_ADMIN_SET_IDL_AUTHORITY_FIELDS), + "admin_update_token_incentives" => { + return Some(PUMP_FUN_IX_ADMIN_UPDATE_TOKEN_INCENTIVES_FIELDS); + }, + "buy" => return Some(PUMP_FUN_IX_BUY_FIELDS), + "buy_exact_quote_in_v2" => return Some(PUMP_FUN_IX_BUY_EXACT_QUOTE_IN_V2_FIELDS), + "buy_exact_sol_in" => return Some(PUMP_FUN_IX_BUY_EXACT_SOL_IN_FIELDS), + "buy_v2" => return Some(PUMP_FUN_IX_BUY_V2_FIELDS), + "create" => return Some(PUMP_FUN_IX_CREATE_FIELDS), + "create_v2" => return Some(PUMP_FUN_IX_CREATE_V2_FIELDS), + "distribute_creator_fees_v2" => return Some(PUMP_FUN_IX_DISTRIBUTE_CREATOR_FEES_V2_FIELDS), + "remove_quote_mint" => return Some(PUMP_FUN_IX_REMOVE_QUOTE_MINT_FIELDS), + "sell" => return Some(PUMP_FUN_IX_SELL_FIELDS), + "sell_v2" => return Some(PUMP_FUN_IX_SELL_V2_FIELDS), + "set_creator" => return Some(PUMP_FUN_IX_SET_CREATOR_FIELDS), + "set_params" => return Some(PUMP_FUN_IX_SET_PARAMS_FIELDS), + "set_reserved_fee_recipients" => { + return Some(PUMP_FUN_IX_SET_RESERVED_FEE_RECIPIENTS_FIELDS); + }, + "set_virtual_quote_reserves" => return Some(PUMP_FUN_IX_SET_VIRTUAL_QUOTE_RESERVES_FIELDS), + "toggle_cashback_enabled" => return Some(PUMP_FUN_IX_TOGGLE_CASHBACK_ENABLED_FIELDS), + "toggle_create_v2" => return Some(PUMP_FUN_IX_TOGGLE_CREATE_V2_FIELDS), + "toggle_mayhem_mode" => return Some(PUMP_FUN_IX_TOGGLE_MAYHEM_MODE_FIELDS), + "update_buyback_config" => return Some(PUMP_FUN_IX_UPDATE_BUYBACK_CONFIG_FIELDS), + _ => return None, + } +} + +fn build_pump_fun_anchor_event_decoded( + transaction: &crate::ChainTransactionDto, + instruction_id: i64, + anchor_event: &PumpFunAnchorEventDecodedPayload, +) -> crate::PumpFunDecodedEvent { + let transaction_id = match transaction.id { + Some(transaction_id) => transaction_id, + None => 0, + }; + let payload_json = + build_pump_fun_anchor_event_payload(transaction, instruction_id, anchor_event); + return crate::PumpFunDecodedEvent::InstructionAudit(crate::PumpFunInstructionAuditDecoded { + transaction_id, + instruction_id, + signature: transaction.signature.clone(), + program_id: crate::PUMP_FUN_PROGRAM_ID.to_string(), + instruction_name: anchor_event.event_name.to_string(), + event_kind: anchor_event.event_kind.to_string(), + discriminator_hex: Some(anchor_event.discriminator_hex.clone()), + payload_json, + }); +} + +fn build_pump_fun_anchor_event_payload( + transaction: &crate::ChainTransactionDto, + instruction_id: i64, + anchor_event: &PumpFunAnchorEventDecodedPayload, +) -> serde_json::Value { + let mut object = match anchor_event.payload_json.clone() { + serde_json::Value::Object(object) => object, + other => { + let mut object = serde_json::Map::new(); + object.insert("rawAnchorEventPayload".to_string(), other); + object + }, + }; + object.insert("decoder".to_string(), serde_json::Value::String("pump_fun".to_string())); + object.insert( + "signature".to_string(), + serde_json::Value::String(transaction.signature.clone()), + ); + object.insert( + "instructionId".to_string(), + serde_json::Value::Number(serde_json::Number::from(instruction_id)), + ); + object.insert( + "eventKind".to_string(), + serde_json::Value::String(anchor_event.event_kind.to_string()), + ); + object.insert( + "anchorEventName".to_string(), + serde_json::Value::String(anchor_event.event_name.to_string()), + ); + object.insert( + "anchorEventDiscriminatorHex".to_string(), + serde_json::Value::String(anchor_event.discriminator_hex.clone()), + ); + object.insert( + "anchorEventPayloadSize".to_string(), + serde_json::json!(anchor_event.payload_size), + ); + object.insert( + "anchorEventSource".to_string(), + serde_json::Value::String("anchor_program_data_or_self_cpi".to_string()), + ); + object.insert("protocolName".to_string(), serde_json::Value::String("pump_fun".to_string())); + object.insert( + "programId".to_string(), + serde_json::Value::String(crate::PUMP_FUN_PROGRAM_ID.to_string()), + ); + add_pump_fun_anchor_event_aliases(anchor_event.event_name, &mut object); + object.insert("anchorEventAuditOnly".to_string(), serde_json::Value::Bool(true)); + object.insert( + "skipTradeReason".to_string(), + serde_json::Value::String("pump_fun_anchor_event_audit_only".to_string()), + ); + object.insert( + "skipCandleReason".to_string(), + serde_json::Value::String("pump_fun_anchor_event_audit_only".to_string()), + ); + object.insert( + "skipCatalogReason".to_string(), + serde_json::Value::String("pump_fun_anchor_event_audit_only".to_string()), + ); + return crate::enrich_dex_decoded_payload( + "pump_fun", + anchor_event.event_kind, + serde_json::Value::Object(object), + ); +} + +fn add_pump_fun_anchor_event_aliases( + event_name: &str, + object: &mut serde_json::Map, +) { + clone_json_value(object, "mint", "tokenMint"); + clone_json_value(object, "mint", "tokenAMint"); + clone_json_value(object, "quote_mint", "quoteMint"); + clone_json_value(object, "quote_mint", "tokenBMint"); + clone_json_value(object, "bonding_curve", "poolAccount"); + clone_json_value(object, "pool", "poolAccount"); + clone_json_value(object, "user", "actorWallet"); + clone_json_value(object, "creator", "actorWallet"); + clone_json_value(object, "admin", "actorWallet"); + clone_json_value(object, "authority", "actorWallet"); + clone_json_value(object, "creator_fee", "feeAmountRaw"); + clone_json_value(object, "distributed", "feeAmountRaw"); + clone_json_value(object, "distributable_fees", "feeAmountRaw"); + clone_json_value(object, "amount", "rewardAmountRaw"); + clone_json_value(object, "cashback", "rewardAmountRaw"); + clone_json_value(object, "total_unclaimed_tokens", "rewardAmountRaw"); + clone_json_value(object, "token_amount", "baseAmountRaw"); + clone_json_value(object, "sol_amount", "quoteAmountRaw"); + clone_json_value(object, "quote_amount", "quoteAmountRaw"); + if event_name == "claim_token_incentives_event" || event_name == "claim_cashback_event" { + object.insert( + "anchorEventMaterializationRole".to_string(), + serde_json::Value::String("reward".to_string()), + ); + } + if event_name == "collect_creator_fee_event" || event_name == "distribute_creator_fees_event" { + object.insert( + "anchorEventMaterializationRole".to_string(), + serde_json::Value::String("fee".to_string()), + ); + } +} + +fn clone_json_value( + object: &mut serde_json::Map, + source_key: &str, + target_key: &str, +) { + if object.contains_key(target_key) { + return; + } + let value = match object.get(source_key) { + Some(value) => value.clone(), + None => return, + }; + object.insert(target_key.to_string(), value); +} + +fn decode_pump_fun_anchor_event_from_base64( + encoded: &str, +) -> std::option::Option { + let decoded = match decode_base64_standard(encoded) { + Some(decoded) => decoded, + None => return None, + }; + return decode_pump_fun_anchor_event_data(decoded.as_slice()); +} + +fn decode_pump_fun_anchor_event_data( + data: &[u8], +) -> std::option::Option { + let first_discriminator = read_8_byte_array(data, 0); + if data.len() >= 16 && first_discriminator == Some(PUMP_FUN_ANCHOR_SELF_CPI_LOG_DISCRIMINATOR) { + return decode_pump_fun_anchor_event_payload(&data[8..]); + } + return decode_pump_fun_anchor_event_payload(data); +} + +fn decode_pump_fun_anchor_event_payload( + data: &[u8], +) -> std::option::Option { + let discriminator = match read_8_byte_array(data, 0) { + Some(discriminator) => discriminator, + None => return None, + }; + let descriptor = match pump_fun_anchor_event_descriptor(discriminator) { + Some(descriptor) => descriptor, + None => return None, + }; + let payload_json = if descriptor.event_name == "trade_event" { + match decode_pump_fun_trade_event_borsh_fields(data, 8, descriptor.fields) { + Some(payload_json) => payload_json, + None => return None, + } + } else { + match decode_borsh_fields(data, 8, descriptor.fields) { + Some(payload_json) => payload_json, + None => return None, + } + }; + return Some(PumpFunAnchorEventDecodedPayload { + event_name: descriptor.event_name, + event_kind: descriptor.event_kind, + discriminator_hex: bytes_to_hex(&descriptor.discriminator), + payload_size: data.len().saturating_sub(8), + payload_json, + }); +} + +fn decode_pump_fun_trade_event_borsh_fields( + data: &[u8], + start_offset: usize, + fields: &[PumpFunBorshFieldDescriptor], +) -> std::option::Option { + let mut object = serde_json::Map::new(); + let mut offset = start_offset; + for field in fields { + let before_offset = offset; + let value = match read_borsh_field_value(data, &mut offset, field.kind) { + Some(value) => value, + None => { + offset = before_offset; + if object.contains_key("ix_name") { + object.insert( + "layoutTruncatedAfterIxName".to_string(), + serde_json::Value::Bool(true), + ); + object.insert( + "layoutConsumedBytes".to_string(), + serde_json::Value::Number(serde_json::Number::from(offset as u64)), + ); + object.insert( + "layoutPayloadBytes".to_string(), + serde_json::Value::Number(serde_json::Number::from(data.len() as u64)), + ); + return Some(serde_json::Value::Object(object)); + } + return None; + }, + }; + object.insert(field.name.to_string(), value); + } + return Some(serde_json::Value::Object(object)); +} + +fn decode_pump_fun_instruction_arguments( + instruction_name: &str, + instruction_data: std::option::Option<&[u8]>, +) -> serde_json::Value { + let instruction_data = match instruction_data { + Some(instruction_data) => instruction_data, + None => return serde_json::Value::Object(serde_json::Map::new()), + }; + let fields = match pump_fun_instruction_arg_fields(instruction_name) { + Some(fields) => fields, + None => return serde_json::Value::Object(serde_json::Map::new()), + }; + let decoded = decode_borsh_fields(instruction_data, 8, fields); + match decoded { + Some(decoded) => return decoded, + None => return serde_json::Value::Object(serde_json::Map::new()), + } +} + +fn decode_borsh_fields( + data: &[u8], + start_offset: usize, + fields: &[PumpFunBorshFieldDescriptor], +) -> std::option::Option { + let mut object = serde_json::Map::new(); + let mut offset = start_offset; + for field in fields { + let value = match read_borsh_field_value(data, &mut offset, field.kind) { + Some(value) => value, + None => return None, + }; + object.insert(field.name.to_string(), value); + } + return Some(serde_json::Value::Object(object)); +} + +fn read_borsh_field_value( + data: &[u8], + offset: &mut usize, + kind: PumpFunBorshFieldKind, +) -> std::option::Option { + match kind { + PumpFunBorshFieldKind::U64 => { + let value = match decode_u64_le_as_string(data, *offset) { + Some(value) => value, + None => return None, + }; + *offset = (*offset).saturating_add(8); + return Some(serde_json::Value::String(value)); + }, + PumpFunBorshFieldKind::I64 => { + let value = match decode_i64_le_as_string(data, *offset) { + Some(value) => value, + None => return None, + }; + *offset = (*offset).saturating_add(8); + return Some(serde_json::Value::String(value)); + }, + PumpFunBorshFieldKind::Bool => { + let value = match read_bool(data, *offset) { + Some(value) => value, + None => return None, + }; + *offset = (*offset).saturating_add(1); + return Some(serde_json::Value::Bool(value)); + }, + PumpFunBorshFieldKind::Pubkey => { + let value = match decode_pubkey32_as_string(data, *offset) { + Some(value) => value, + None => return None, + }; + *offset = (*offset).saturating_add(32); + return Some(serde_json::Value::String(value)); + }, + PumpFunBorshFieldKind::String => { + let result = match read_borsh_string(data, *offset) { + Some(result) => result, + None => return None, + }; + *offset = result.1; + return Some(serde_json::Value::String(result.0)); + }, + PumpFunBorshFieldKind::OptionBool => { + let value = match read_bool(data, *offset) { + Some(value) => value, + None => return None, + }; + *offset = (*offset).saturating_add(1); + return Some(serde_json::Value::Bool(value)); + }, + PumpFunBorshFieldKind::OptionU64 => { + let tag = match read_u8(data, *offset) { + Some(tag) => tag, + None => return None, + }; + *offset = (*offset).saturating_add(1); + if tag == 0 { + return Some(serde_json::Value::Null); + } + let value = match decode_u64_le_as_string(data, *offset) { + Some(value) => value, + None => return None, + }; + *offset = (*offset).saturating_add(8); + return Some(serde_json::Value::String(value)); + }, + PumpFunBorshFieldKind::PubkeyArray(count) => { + let mut values = std::vec::Vec::new(); + let mut index = 0_usize; + while index < count { + let value = match decode_pubkey32_as_string(data, *offset) { + Some(value) => value, + None => return None, + }; + values.push(serde_json::Value::String(value)); + *offset = (*offset).saturating_add(32); + index = index.saturating_add(1); + } + return Some(serde_json::Value::Array(values)); + }, + PumpFunBorshFieldKind::ShareholderVec => { + if data.len() < (*offset).saturating_add(4) { + return None; + } + let count = u32::from_le_bytes([ + data[*offset], + data[(*offset).saturating_add(1)], + data[(*offset).saturating_add(2)], + data[(*offset).saturating_add(3)], + ]) as usize; + *offset = (*offset).saturating_add(4); + let mut values = std::vec::Vec::new(); + let mut index = 0_usize; + while index < count { + let address = match decode_pubkey32_as_string(data, *offset) { + Some(value) => value, + None => return None, + }; + *offset = (*offset).saturating_add(32); + if data.len() < (*offset).saturating_add(2) { + return None; + } + let share_bps = + u16::from_le_bytes([data[*offset], data[(*offset).saturating_add(1)]]); + *offset = (*offset).saturating_add(2); + values.push(serde_json::json!({ + "address": address, + "share_bps": share_bps.to_string() + })); + index = index.saturating_add(1); + } + return Some(serde_json::Value::Array(values)); + }, + } +} + +fn read_u8(data: &[u8], offset: usize) -> std::option::Option { + if data.len() <= offset { + return None; + } + return Some(data[offset]); +} + +fn read_8_byte_array(data: &[u8], offset: usize) -> std::option::Option<[u8; 8]> { + if data.len() < offset.saturating_add(8) { + return None; + } + return Some([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + data[offset + 5], + data[offset + 6], + data[offset + 7], + ]); +} + +fn read_bool(data: &[u8], offset: usize) -> std::option::Option { + if data.len() <= offset { + return None; + } + return Some(data[offset] != 0); +} + +fn read_borsh_string( + data: &[u8], + offset: usize, +) -> std::option::Option<(std::string::String, usize)> { + if data.len() < offset.saturating_add(4) { + return None; + } + let len = + u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]); + let start = offset.saturating_add(4); + let end = start.saturating_add(len as usize); + if data.len() < end { + return None; + } + let text_result = std::str::from_utf8(&data[start..end]); + let text = match text_result { + Ok(text) => text, + Err(_) => return None, + }; + return Some((text.to_string(), end)); +} + +fn decode_u64_le_as_string(data: &[u8], offset: usize) -> std::option::Option { + if data.len() < offset.saturating_add(8) { + return None; + } + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&data[offset..offset + 8]); + return Some(u64::from_le_bytes(bytes).to_string()); +} + +fn decode_i64_le_as_string(data: &[u8], offset: usize) -> std::option::Option { + if data.len() < offset.saturating_add(8) { + return None; + } + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&data[offset..offset + 8]); + return Some(i64::from_le_bytes(bytes).to_string()); +} + +fn decode_pubkey32_as_string( + data: &[u8], + offset: usize, +) -> std::option::Option { + if data.len() < offset.saturating_add(32) { + return None; + } + return Some(bs58::encode(&data[offset..offset + 32]).into_string()); +} + +fn extract_program_data_base64(message: &str) -> std::option::Option<&str> { + let trimmed = message.trim(); + if let Some(rest) = trimmed.strip_prefix("Program data: ") { + return Some(rest.trim()); + } + return None; +} + +fn decode_base64_standard(encoded: &str) -> std::option::Option> { + use base64::Engine as _; + let decoded = base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()); + match decoded { + Ok(decoded) => return Some(decoded), + Err(_) => return None, + } +} + fn normalize_log_text(value: &str) -> std::string::String { let mut normalized = std::string::String::new(); for character in value.chars() { @@ -444,6 +2738,15 @@ mod tests { return dto; } + fn make_instruction_with_data(discriminator: [u8; 8]) -> crate::ChainInstructionDto { + let mut instruction = make_instruction(); + let mut data = std::vec::Vec::new(); + data.extend_from_slice(&discriminator); + let encoded = bs58::encode(data).into_string(); + instruction.data_json = Some(serde_json::json!(encoded).to_string()); + return instruction; + } + #[test] fn pump_fun_create_v2_is_detected() { let decoder = crate::PumpFunDecoder::new(); @@ -473,6 +2776,9 @@ mod tests { crate::PumpFunDecodedEvent::SellTrade(_) => { panic!("unexpected pump_fun sell trade event"); }, + crate::PumpFunDecodedEvent::InstructionAudit(_) => { + panic!("unexpected pump_fun instruction audit event"); + }, } } @@ -502,4 +2808,297 @@ mod tests { }; assert_eq!(decoded.len(), 0); } + + #[test] + fn pump_fun_buy_v2_is_decoded_as_anchor_trade_sibling_backed_audit() { + let decoder = crate::PumpFunDecoder::new(); + let mut transaction = make_transaction(); + transaction.transaction_json = serde_json::json!({ + "slot": 777001, + "meta": { + "logMessages": [ + "Program log: Instruction: BuyV2" + ] + }, + "transaction": { + "message": { + "instructions": [] + } + } + }) + .to_string(); + let mut instruction = make_instruction_with_data([184, 23, 238, 97, 103, 197, 211, 61]); + instruction.accounts_json = serde_json::json!([ + "Global111", + "BaseMint111", + "QuoteMint111", + "BaseTokenProgram111", + "QuoteTokenProgram111", + "AssociatedTokenProgram111", + "FeeRecipient111", + "AssociatedQuoteFeeRecipient111", + "BuybackFeeRecipient111", + "AssociatedQuoteBuybackFeeRecipient111", + "BondingCurve111", + "AssociatedBaseBondingCurve111", + "AssociatedQuoteBondingCurve111", + "User111", + "AssociatedBaseUser111", + "AssociatedQuoteUser111", + "CreatorVault111", + "AssociatedCreatorVault111", + "SharingConfig111", + "GlobalVolumeAccumulator111", + "UserVolumeAccumulator111", + "AssociatedUserVolumeAccumulator111", + "FeeConfig111", + "FeeProgram111", + "SystemProgram111", + "EventAuthority111", + "PumpFunProgram111" + ]) + .to_string(); + let instructions = vec![instruction]; + let decoded_result = decoder.decode_transaction(&transaction, &instructions); + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => panic!("decode must succeed: {}", error), + }; + assert_eq!(decoded.len(), 1); + match &decoded[0] { + crate::PumpFunDecodedEvent::InstructionAudit(event) => { + assert_eq!(event.instruction_name, "buy_v2"); + assert_eq!(event.event_kind, "pump_fun.buy_v2"); + assert_eq!(event.discriminator_hex, Some("b817ee6167c5d33d".to_string())); + assert_eq!( + event.payload_json.get("instructionAuditOnly"), + Some(&serde_json::Value::Bool(true)) + ); + assert_eq!( + event.payload_json.get("tradeCandidate"), + Some(&serde_json::Value::Bool(false)) + ); + assert_eq!( + event.payload_json.get("instructionCoveredByAnchorTradeEvent"), + Some(&serde_json::Value::Bool(true)) + ); + assert_eq!( + event.payload_json.get("skipTradeReason"), + Some(&serde_json::Value::String( + "pump_fun_instruction_covered_by_anchor_trade_event".to_string() + )) + ); + assert_eq!( + event.payload_json.get("tokenMint"), + Some(&serde_json::Value::String("BaseMint111".to_string())) + ); + assert_eq!( + event.payload_json.get("quoteMint"), + Some(&serde_json::Value::String("QuoteMint111".to_string())) + ); + assert_eq!( + event.payload_json.get("bondingCurve"), + Some(&serde_json::Value::String("BondingCurve111".to_string())) + ); + assert_eq!( + event.payload_json.get("actorWallet"), + Some(&serde_json::Value::String("User111".to_string())) + ); + }, + crate::PumpFunDecodedEvent::CreateV2Token(_) => { + panic!("unexpected pump_fun create_v2 token event"); + }, + crate::PumpFunDecodedEvent::BuyTrade(_) => { + panic!("unexpected pump_fun buy trade event"); + }, + crate::PumpFunDecodedEvent::SellTrade(_) => { + panic!("unexpected pump_fun sell trade event"); + }, + } + } + + fn append_synthetic_field(data: &mut std::vec::Vec, kind: super::PumpFunBorshFieldKind) { + match kind { + super::PumpFunBorshFieldKind::U64 => data.extend_from_slice(&1234_u64.to_le_bytes()), + super::PumpFunBorshFieldKind::I64 => data.extend_from_slice(&1234_i64.to_le_bytes()), + super::PumpFunBorshFieldKind::Bool => data.push(1), + super::PumpFunBorshFieldKind::Pubkey => data.extend_from_slice(&[7_u8; 32]), + super::PumpFunBorshFieldKind::String => { + let text = b"synthetic"; + data.extend_from_slice(&(text.len() as u32).to_le_bytes()); + data.extend_from_slice(text); + }, + super::PumpFunBorshFieldKind::OptionBool => data.push(1), + super::PumpFunBorshFieldKind::OptionU64 => { + data.push(1); + data.extend_from_slice(&55_u64.to_le_bytes()); + }, + super::PumpFunBorshFieldKind::PubkeyArray(count) => { + let mut index = 0_usize; + while index < count { + data.extend_from_slice(&[8_u8; 32]); + index = index.saturating_add(1); + } + }, + super::PumpFunBorshFieldKind::ShareholderVec => { + data.extend_from_slice(&1_u32.to_le_bytes()); + data.extend_from_slice(&[9_u8; 32]); + data.extend_from_slice(&100_u16.to_le_bytes()); + }, + } + } + + fn synthetic_payload( + discriminator: [u8; 8], + fields: &[super::PumpFunBorshFieldDescriptor], + ) -> std::vec::Vec { + let mut data = std::vec::Vec::new(); + data.extend_from_slice(&discriminator); + for field in fields { + append_synthetic_field(&mut data, field.kind); + } + return data; + } + + fn make_instruction_with_raw_data(data: std::vec::Vec) -> crate::ChainInstructionDto { + let mut instruction = make_instruction(); + instruction.data_json = + Some(serde_json::json!(bs58::encode(data).into_string()).to_string()); + return instruction; + } + + #[test] + fn pump_fun_all_idl_instruction_discriminators_decode_synthetically() { + let decoder = crate::PumpFunDecoder::new(); + let mut transaction = make_transaction(); + transaction.transaction_json = serde_json::json!({ + "slot": 777001, + "meta": { "logMessages": ["Program log: Instruction: Synthetic"] }, + "transaction": { "message": { "instructions": [] } } + }) + .to_string(); + let cases: &[(&str, [u8; 8])] = &[ + ("add_quote_mint", [111, 121, 21, 56, 40, 24, 94, 209]), + ("admin_set_creator", [69, 25, 171, 142, 57, 239, 13, 4]), + ("admin_set_idl_authority", [8, 217, 96, 231, 144, 104, 192, 5]), + ("admin_update_token_incentives", [209, 11, 115, 87, 213, 23, 124, 204]), + ("buy", [102, 6, 61, 18, 1, 218, 235, 234]), + ("buy_exact_quote_in_v2", [194, 171, 28, 70, 104, 77, 91, 47]), + ("buy_exact_sol_in", [56, 252, 116, 8, 158, 223, 205, 95]), + ("buy_v2", [184, 23, 238, 97, 103, 197, 211, 61]), + ("claim_cashback", [37, 58, 35, 126, 190, 53, 228, 197]), + ("claim_cashback_v2", [122, 243, 204, 65, 94, 116, 29, 55]), + ("claim_token_incentives", [16, 4, 71, 28, 204, 1, 40, 27]), + ("close_user_volume_accumulator", [249, 69, 164, 218, 150, 103, 84, 138]), + ("collect_creator_fee", [20, 22, 86, 123, 198, 28, 219, 132]), + ("collect_creator_fee_v2", [207, 17, 138, 242, 4, 34, 19, 56]), + ("create", [24, 30, 200, 40, 5, 28, 7, 119]), + ("create_v2", [214, 144, 76, 236, 95, 139, 49, 180]), + ("distribute_creator_fees", [165, 114, 103, 0, 121, 206, 247, 81]), + ("distribute_creator_fees_v2", [255, 203, 19, 79, 244, 68, 8, 159]), + ("extend_account", [234, 102, 194, 203, 150, 72, 62, 229]), + ("get_minimum_distributable_fee", [117, 225, 127, 202, 134, 95, 68, 35]), + ("init_user_volume_accumulator", [94, 6, 202, 115, 255, 96, 232, 183]), + ("initialize", [175, 175, 109, 31, 13, 152, 155, 237]), + ("migrate", [155, 234, 231, 146, 236, 158, 162, 30]), + ("migrate_bonding_curve_creator", [87, 124, 52, 191, 52, 38, 214, 232]), + ("migrate_v2", [187, 203, 18, 31, 206, 237, 254, 41]), + ("remove_quote_mint", [177, 65, 223, 38, 88, 209, 158, 155]), + ("sell", [51, 230, 133, 164, 1, 127, 131, 173]), + ("sell_v2", [93, 246, 130, 60, 231, 233, 64, 178]), + ("set_creator", [254, 148, 255, 112, 207, 142, 170, 165]), + ("set_mayhem_virtual_params", [61, 169, 188, 191, 153, 149, 42, 97]), + ("set_metaplex_creator", [138, 96, 174, 217, 48, 85, 197, 246]), + ("set_params", [27, 234, 178, 52, 147, 2, 187, 141]), + ("set_reserved_fee_recipients", [111, 172, 162, 232, 114, 89, 213, 142]), + ("set_virtual_quote_reserves", [101, 135, 191, 104, 9, 88, 20, 96]), + ("sync_user_volume_accumulator", [86, 31, 192, 87, 163, 87, 79, 238]), + ("toggle_cashback_enabled", [115, 103, 224, 255, 189, 89, 86, 195]), + ("toggle_create_v2", [28, 255, 230, 240, 172, 107, 203, 171]), + ("toggle_mayhem_mode", [1, 9, 111, 208, 100, 31, 255, 163]), + ("update_buyback_config", [251, 224, 171, 146, 160, 26, 113, 233]), + ("update_global_authority", [227, 181, 74, 196, 208, 21, 97, 213]), + ]; + for (instruction_name, discriminator) in cases { + let fields = match super::pump_fun_instruction_arg_fields(instruction_name) { + Some(fields) => fields, + None => &[], + }; + let instruction = + make_instruction_with_raw_data(synthetic_payload(*discriminator, fields)); + let decoded_result = decoder.decode_transaction(&transaction, &[instruction]); + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => { + panic!("{} synthetic decode must succeed: {}", instruction_name, error) + }, + }; + assert_eq!(decoded.len(), 1, "{} must decode to one local event", instruction_name); + } + } + + #[test] + fn pump_fun_all_idl_anchor_events_decode_synthetically_from_self_cpi() { + let decoder = crate::PumpFunDecoder::new(); + let transaction = make_transaction(); + let cases: &[(&str, [u8; 8])] = &[ + ("admin_set_creator_event", [64, 69, 192, 104, 29, 30, 25, 107]), + ("admin_set_idl_authority_event", [245, 59, 70, 34, 75, 185, 109, 92]), + ("admin_update_token_incentives_event", [147, 250, 108, 120, 247, 29, 67, 222]), + ("claim_cashback_event", [226, 214, 246, 33, 7, 242, 147, 229]), + ("claim_token_incentives_event", [79, 172, 246, 49, 205, 91, 206, 232]), + ("close_user_volume_accumulator_event", [146, 159, 189, 172, 146, 88, 56, 244]), + ("collect_creator_fee_event", [122, 2, 127, 1, 14, 191, 12, 175]), + ("complete_event", [95, 114, 97, 156, 212, 46, 152, 8]), + ("complete_pump_amm_migration_event", [189, 233, 93, 185, 92, 148, 234, 148]), + ("create_event", [27, 114, 169, 77, 222, 235, 99, 118]), + ("distribute_creator_fees_event", [165, 55, 129, 112, 4, 179, 202, 40]), + ("extend_account_event", [97, 97, 215, 144, 93, 146, 22, 124]), + ("init_user_volume_accumulator_event", [134, 36, 13, 72, 232, 101, 130, 216]), + ("migrate_bonding_curve_creator_event", [155, 167, 104, 220, 213, 108, 243, 3]), + ("minimum_distributable_fee_event", [168, 216, 132, 239, 235, 182, 49, 52]), + ("reserved_fee_recipients_event", [43, 188, 250, 18, 221, 75, 187, 95]), + ("set_creator_event", [237, 52, 123, 37, 245, 251, 72, 210]), + ("set_metaplex_creator_event", [142, 203, 6, 32, 127, 105, 191, 162]), + ("set_params_event", [223, 195, 159, 246, 62, 48, 143, 131]), + ("sync_user_volume_accumulator_event", [197, 122, 167, 124, 116, 81, 91, 255]), + ("trade_event", [189, 219, 127, 211, 78, 230, 97, 238]), + ("update_global_authority_event", [182, 195, 137, 42, 35, 206, 207, 247]), + ("update_mayhem_virtual_params_event", [117, 123, 228, 182, 161, 168, 220, 214]), + ]; + for (event_name, discriminator) in cases { + let descriptor = match super::pump_fun_anchor_event_descriptor(*discriminator) { + Some(descriptor) => descriptor, + None => panic!("missing descriptor for {}", event_name), + }; + let event_data = synthetic_payload(*discriminator, descriptor.fields); + let mut self_cpi_data = std::vec::Vec::new(); + self_cpi_data.extend_from_slice(&super::PUMP_FUN_ANCHOR_SELF_CPI_LOG_DISCRIMINATOR); + self_cpi_data.extend_from_slice(&event_data); + let instruction = make_instruction_with_raw_data(self_cpi_data); + let decoded_result = decoder.decode_transaction(&transaction, &[instruction]); + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => { + panic!("{} synthetic anchor event decode must succeed: {}", event_name, error) + }, + }; + assert_eq!(decoded.len(), 1, "{} must decode to one local event", event_name); + match &decoded[0] { + crate::PumpFunDecodedEvent::InstructionAudit(event) => { + assert_eq!(event.event_kind, format!("pump_fun.{}", event_name)); + assert_eq!(event.instruction_name, *event_name); + assert_eq!( + event.payload_json.get("anchorEventAuditOnly"), + Some(&serde_json::Value::Bool(true)) + ); + }, + crate::PumpFunDecodedEvent::CreateV2Token(_) => { + panic!("unexpected create_v2 token event") + }, + crate::PumpFunDecodedEvent::BuyTrade(_) => panic!("unexpected buy trade event"), + crate::PumpFunDecodedEvent::SellTrade(_) => panic!("unexpected sell trade event"), + } + } + } } diff --git a/kb_lib/src/dex_decode.rs b/kb_lib/src/dex_decode.rs index ba5a1e3..d0ddc5e 100644 --- a/kb_lib/src/dex_decode.rs +++ b/kb_lib/src/dex_decode.rs @@ -1627,9 +1627,85 @@ 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 { + 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( &self, transaction: &crate::ChainTransactionDto, @@ -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, +) -> std::vec::Vec { + 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, + 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 { + 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::(); + 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], diff --git a/kb_lib/src/dex_detection_route.rs b/kb_lib/src/dex_detection_route.rs index 3d751d4..b2de670 100644 --- a/kb_lib/src/dex_detection_route.rs +++ b/kb_lib/src/dex_detection_route.rs @@ -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( diff --git a/kb_lib/src/dex_event_classification.rs b/kb_lib/src/dex_event_classification.rs index a3d9887..145bea9 100644 --- a/kb_lib/src/dex_event_classification.rs +++ b/kb_lib/src/dex_event_classification.rs @@ -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")); } diff --git a/kb_lib/src/dex_event_coverage.rs b/kb_lib/src/dex_event_coverage.rs index acf6533..6853f07 100644 --- a/kb_lib/src/dex_event_coverage.rs +++ b/kb_lib/src/dex_event_coverage.rs @@ -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 { + 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 { + 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 { + 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 { 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 { + 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 { + 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!( diff --git a/kb_lib/src/instruction_observation_index.rs b/kb_lib/src/instruction_observation_index.rs index 0c6c1ef..affeb92 100644 --- a/kb_lib/src/instruction_observation_index.rs +++ b/kb_lib/src/instruction_observation_index.rs @@ -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", diff --git a/kb_lib/src/lib.rs b/kb_lib/src/lib.rs index 62be54f..f53493c 100644 --- a/kb_lib/src/lib.rs +++ b/kb_lib/src/lib.rs @@ -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. diff --git a/kb_lib/src/non_trade_event_materialization.rs b/kb_lib/src/non_trade_event_materialization.rs index 730c621..5f5c846 100644 --- a/kb_lib/src/non_trade_event_materialization.rs +++ b/kb_lib/src/non_trade_event_materialization.rs @@ -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::( - decoded_event.payload_json.as_str(), - ); + let payload_result = + serde_json::from_str::(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::( - decoded_event.payload_json.as_str(), - ); + let target_payload_result = + serde_json::from_str::(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::( - candidate.payload_json.as_str(), - ); + let candidate_payload_result = + serde_json::from_str::(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 { - let transaction_json = serde_json::from_str::( - transaction.transaction_json.as_str(), - ); + let transaction_json = + serde_json::from_str::(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; } diff --git a/kb_lib/src/trade_aggregation.rs b/kb_lib/src/trade_aggregation.rs index e418ab9..06818c5 100644 --- a/kb_lib/src/trade_aggregation.rs +++ b/kb_lib/src/trade_aggregation.rs @@ -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 { + let parsed_result = serde_json::from_str::(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::(); + 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() { diff --git a/kb_lib/src/trade_amount_resolution.rs b/kb_lib/src/trade_amount_resolution.rs index de3a760..5094e94 100644 --- a/kb_lib/src/trade_amount_resolution.rs +++ b/kb_lib/src/trade_amount_resolution.rs @@ -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, quote_amount_raw: &mut std::option::Option, @@ -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, + quote_amount_raw: &mut std::option::Option, + price_quote_per_base: &mut std::option::Option, +) -> 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::(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, @@ -1492,6 +1667,44 @@ fn extract_amount_string( ); } +fn extract_bool_by_candidate_keys( + value: &serde_json::Value, + candidate_keys: &[&str], +) -> std::option::Option { + 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], diff --git a/kb_lib/src/upstream_registry_generated.rs b/kb_lib/src/upstream_registry_generated.rs index 9ea0ddc..d641089 100644 --- a/kb_lib/src/upstream_registry_generated.rs +++ b/kb_lib/src/upstream_registry_generated.rs @@ -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), diff --git a/validation_sql/SQL_VALIDATION_PUMP_FUN_0_7_54.sql b/validation_sql/SQL_VALIDATION_PUMP_FUN_0_7_54.sql new file mode 100644 index 0000000..b6096f6 --- /dev/null +++ b/validation_sql/SQL_VALIDATION_PUMP_FUN_0_7_54.sql @@ -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; diff --git a/validation_sql/SQL_VALIDATION_PUMP_FUN_MATERIALIZATION_0_7_54.sql b/validation_sql/SQL_VALIDATION_PUMP_FUN_MATERIALIZATION_0_7_54.sql new file mode 100644 index 0000000..232a4d0 --- /dev/null +++ b/validation_sql/SQL_VALIDATION_PUMP_FUN_MATERIALIZATION_0_7_54.sql @@ -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;