Skip to main content

sol_parser_sdk/core/
pumpfun_fee_enrich.rs

1//! 同笔交易内 Pump 事件后处理(**零 RPC**,供 gRPC/Shred 热路径):
2//! - Create/CreateV2 + 后续 Buy 分离时,将 Buy 的 fee recipient 回填到 `observed_fee_recipient`
3//! - 将 **Create / CreateV2 指令**里的 `is_cashback_enabled`、`is_mayhem_mode` 合并进同一 mint 的 Trade 事件,
4//!   避免仅有外层指令、TradeEvent 日志缺字段时 `sol-trade-sdk` 误判返现与 Mayhem fee 池
5
6use std::collections::HashMap;
7
8use solana_sdk::pubkey::Pubkey;
9
10use crate::core::events::{
11    is_pumpfun_solscan_sol_quote_mint, normalize_pumpfun_quote_mint, DexEvent,
12    PumpFunCreateTokenEvent, PumpFunCreateV2TokenEvent, PumpFunTradeEvent,
13};
14
15fn pumpfun_buy_like_mint_fee(e: &DexEvent) -> Option<(Pubkey, Pubkey)> {
16    match e {
17        DexEvent::PumpFunTrade(t) if t.is_buy && t.mint != Pubkey::default() => {
18            Some((t.mint, t.fee_recipient))
19        }
20        DexEvent::PumpFunBuy(t) if t.mint != Pubkey::default() => Some((t.mint, t.fee_recipient)),
21        DexEvent::PumpFunBuyExactSolIn(t) if t.mint != Pubkey::default() => {
22            Some((t.mint, t.fee_recipient))
23        }
24        _ => None,
25    }
26}
27
28fn pumpfun_trade_mint_quote(e: &DexEvent) -> Option<(Pubkey, Pubkey)> {
29    let t = match e {
30        DexEvent::PumpFunTrade(t)
31        | DexEvent::PumpFunBuy(t)
32        | DexEvent::PumpFunSell(t)
33        | DexEvent::PumpFunBuyExactSolIn(t) => t,
34        _ => return None,
35    };
36    if t.mint == Pubkey::default()
37        || t.quote_mint == Pubkey::default()
38        || is_pumpfun_solscan_sol_quote_mint(t.quote_mint)
39    {
40        return None;
41    }
42    Some((t.mint, t.quote_mint))
43}
44
45/// 扫描同签名下的买入类事件,按 mint 记录 `fee_recipient`(ShredStream 外层的 buy 已从 accounts[1] 解析)。
46pub fn enrich_create_observed_fee_recipient(events: &mut [DexEvent]) {
47    let mut mint_to_fee: HashMap<Pubkey, Pubkey> = HashMap::new();
48    for e in events.iter() {
49        if let Some((mint, fee)) = pumpfun_buy_like_mint_fee(e) {
50            if fee != Pubkey::default() {
51                mint_to_fee.entry(mint).or_insert(fee);
52            }
53        }
54    }
55    if mint_to_fee.is_empty() {
56        return;
57    }
58    for e in events.iter_mut() {
59        match e {
60            DexEvent::PumpFunCreate(c) => {
61                if c.observed_fee_recipient == Pubkey::default() {
62                    if let Some(&f) = mint_to_fee.get(&c.mint) {
63                        c.observed_fee_recipient = f;
64                    }
65                }
66            }
67            DexEvent::PumpFunCreateV2(c) => {
68                if c.observed_fee_recipient == Pubkey::default() {
69                    if let Some(&f) = mint_to_fee.get(&c.mint) {
70                        c.observed_fee_recipient = f;
71                    }
72                }
73            }
74            _ => {}
75        }
76    }
77}
78
79pub fn enrich_create_quote_mint_from_trades(events: &mut [DexEvent]) {
80    let mut mint_to_quote: HashMap<Pubkey, Pubkey> = HashMap::new();
81    for e in events.iter() {
82        if let Some((mint, quote_mint)) = pumpfun_trade_mint_quote(e) {
83            mint_to_quote.entry(mint).or_insert(quote_mint);
84        }
85    }
86    if mint_to_quote.is_empty() {
87        return;
88    }
89    for e in events.iter_mut() {
90        match e {
91            DexEvent::PumpFunCreate(c) => {
92                if (c.quote_mint == Pubkey::default()
93                    || is_pumpfun_solscan_sol_quote_mint(c.quote_mint))
94                    && mint_to_quote.contains_key(&c.mint)
95                {
96                    c.quote_mint = mint_to_quote[&c.mint];
97                }
98            }
99            DexEvent::PumpFunCreateV2(c) => {
100                if (c.quote_mint == Pubkey::default()
101                    || is_pumpfun_solscan_sol_quote_mint(c.quote_mint))
102                    && mint_to_quote.contains_key(&c.mint)
103                {
104                    c.quote_mint = mint_to_quote[&c.mint];
105                }
106            }
107            _ => {}
108        }
109    }
110}
111
112#[inline]
113fn collect_create_cashback_and_mayhem(events: &[DexEvent]) -> HashMap<Pubkey, (bool, bool)> {
114    let mut m: HashMap<Pubkey, (bool, bool)> = HashMap::new();
115    for e in events {
116        match e {
117            DexEvent::PumpFunCreateV2(c) if c.mint != Pubkey::default() => {
118                m.entry(c.mint).or_insert((c.is_cashback_enabled, c.is_mayhem_mode));
119            }
120            DexEvent::PumpFunCreate(c) if c.mint != Pubkey::default() => {
121                m.entry(c.mint).or_insert((c.is_cashback_enabled, c.is_mayhem_mode));
122            }
123            _ => {}
124        }
125    }
126    m
127}
128
129#[inline]
130fn collect_create_events_by_mint(events: &[DexEvent]) -> HashMap<Pubkey, PumpFunCreateTokenEvent> {
131    let mut m: HashMap<Pubkey, PumpFunCreateTokenEvent> = HashMap::new();
132    for e in events {
133        if let DexEvent::PumpFunCreate(c) = e {
134            if c.mint != Pubkey::default() {
135                m.entry(c.mint).or_insert_with(|| c.clone());
136            }
137        }
138    }
139    m
140}
141
142#[inline]
143fn fill_str_if_empty(to: &mut String, from: &str) {
144    if to.is_empty() && !from.is_empty() {
145        to.push_str(from);
146    }
147}
148
149#[inline]
150fn fill_pk_if_default(to: &mut Pubkey, from: Pubkey) {
151    if *to == Pubkey::default() && from != Pubkey::default() {
152        *to = from;
153    }
154}
155
156#[inline]
157fn fill_pumpfun_quote_mint_if_default(to: &mut Pubkey, from: Pubkey) {
158    let from = normalize_pumpfun_quote_mint(from);
159    if (*to == Pubkey::default() || is_pumpfun_solscan_sol_quote_mint(*to))
160        && from != Pubkey::default()
161    {
162        *to = from;
163    }
164}
165
166#[inline]
167fn fill_u64_if_zero(to: &mut u64, from: u64) {
168    if *to == 0 && from != 0 {
169        *to = from;
170    }
171}
172
173#[inline]
174fn fill_i64_if_zero(to: &mut i64, from: i64) {
175    if *to == 0 && from != 0 {
176        *to = from;
177    }
178}
179
180#[inline]
181fn fill_create_v2_from_create(
182    create_v2: &mut PumpFunCreateV2TokenEvent,
183    create: &PumpFunCreateTokenEvent,
184) {
185    fill_str_if_empty(&mut create_v2.name, &create.name);
186    fill_str_if_empty(&mut create_v2.symbol, &create.symbol);
187    fill_str_if_empty(&mut create_v2.uri, &create.uri);
188    fill_pk_if_default(&mut create_v2.bonding_curve, create.bonding_curve);
189    fill_pk_if_default(&mut create_v2.user, create.user);
190    fill_pk_if_default(&mut create_v2.creator, create.creator);
191    fill_pk_if_default(&mut create_v2.token_program, create.token_program);
192    fill_pumpfun_quote_mint_if_default(&mut create_v2.quote_mint, create.quote_mint);
193    fill_i64_if_zero(&mut create_v2.timestamp, create.timestamp);
194    fill_u64_if_zero(&mut create_v2.virtual_token_reserves, create.virtual_token_reserves);
195    fill_u64_if_zero(&mut create_v2.virtual_sol_reserves, create.virtual_sol_reserves);
196    fill_u64_if_zero(&mut create_v2.real_token_reserves, create.real_token_reserves);
197    fill_u64_if_zero(&mut create_v2.token_total_supply, create.token_total_supply);
198    fill_u64_if_zero(&mut create_v2.virtual_quote_reserves, create.virtual_quote_reserves);
199    create_v2.is_mayhem_mode |= create.is_mayhem_mode;
200    create_v2.is_cashback_enabled |= create.is_cashback_enabled;
201}
202
203/// Copy the official `CreateEvent` payload onto the same-mint `create_v2` instruction event.
204/// The `create_v2` instruction itself carries accounts and flags; the event payload carries the
205/// initial reserves and quote mint used by quote-denominated pools.
206pub fn enrich_create_v2_from_create_events(events: &mut [DexEvent]) {
207    let creates = collect_create_events_by_mint(events);
208    if creates.is_empty() {
209        return;
210    }
211
212    for e in events.iter_mut() {
213        if let DexEvent::PumpFunCreateV2(create_v2) = e {
214            if create_v2.mint == Pubkey::default() {
215                continue;
216            }
217            if let Some(create) = creates.get(&create_v2.mint) {
218                fill_create_v2_from_create(create_v2, create);
219            }
220        }
221    }
222}
223
224#[inline]
225fn trade_event_mut(e: &mut DexEvent) -> Option<&mut PumpFunTradeEvent> {
226    match e {
227        DexEvent::PumpFunTrade(t)
228        | DexEvent::PumpFunBuy(t)
229        | DexEvent::PumpFunSell(t)
230        | DexEvent::PumpFunBuyExactSolIn(t) => Some(t),
231        _ => None,
232    }
233}
234
235/// 将同笔交易中 Create / CreateV2 的 `is_cashback_enabled`、`is_mayhem_mode` 并入 Trade 事件(与官方 `create_v2` / bonding 语义一致)。
236pub fn enrich_pumpfun_trades_from_create_instructions(events: &mut [DexEvent]) {
237    let flags = collect_create_cashback_and_mayhem(events);
238    if flags.is_empty() {
239        return;
240    }
241    for e in events.iter_mut() {
242        if let Some(t) = trade_event_mut(e) {
243            if t.mint == Pubkey::default() {
244                continue;
245            }
246            let Some(&(cb_en, mayhem_create)) = flags.get(&t.mint) else {
247                continue;
248            };
249            t.is_cashback_coin |= cb_en;
250            t.mayhem_mode |= mayhem_create;
251            if cb_en {
252                t.track_volume = true;
253            }
254        }
255    }
256}
257
258/// 合并调用:fee 回填 + Create→Trade 标志(gRPC / Shred 在 `merge` 之后调用一次即可)。
259pub fn enrich_pumpfun_same_tx_post_merge(events: &mut [DexEvent]) {
260    enrich_create_v2_from_create_events(events);
261    enrich_create_observed_fee_recipient(events);
262    enrich_create_quote_mint_from_trades(events);
263    enrich_pumpfun_trades_from_create_instructions(events);
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::core::events::{EventMetadata, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT};
270    use solana_sdk::signature::Signature;
271
272    #[test]
273    fn enrich_fills_create_from_same_tx_buy() {
274        let sig = Signature::default();
275        let mint = Pubkey::new_unique();
276        let fee = Pubkey::new_unique();
277        let meta = EventMetadata {
278            signature: sig,
279            slot: 1,
280            tx_index: 0,
281            block_time_us: 0,
282            grpc_recv_us: 0,
283            recent_blockhash: None,
284        };
285        let mut events: Vec<DexEvent> = vec![
286            DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
287                metadata: meta.clone(),
288                mint,
289                ..Default::default()
290            }),
291            DexEvent::PumpFunTrade(PumpFunTradeEvent {
292                metadata: meta,
293                mint,
294                fee_recipient: fee,
295                is_buy: true,
296                ..Default::default()
297            }),
298        ];
299        enrich_create_observed_fee_recipient(&mut events);
300        if let DexEvent::PumpFunCreate(c) = &events[0] {
301            assert_eq!(c.observed_fee_recipient, fee);
302        } else {
303            panic!("expected Create");
304        }
305    }
306
307    #[test]
308    fn enrich_merges_cashback_and_mayhem_from_create_v2_to_trade() {
309        let sig = Signature::default();
310        let mint = Pubkey::new_unique();
311        let meta = EventMetadata {
312            signature: sig,
313            slot: 1,
314            tx_index: 0,
315            block_time_us: 0,
316            grpc_recv_us: 0,
317            recent_blockhash: None,
318        };
319        let mut events: Vec<DexEvent> = vec![
320            DexEvent::PumpFunCreateV2(PumpFunCreateV2TokenEvent {
321                metadata: meta.clone(),
322                mint,
323                is_mayhem_mode: true,
324                is_cashback_enabled: true,
325                ..Default::default()
326            }),
327            DexEvent::PumpFunTrade(PumpFunTradeEvent {
328                metadata: meta,
329                mint,
330                mayhem_mode: false,
331                is_cashback_coin: false,
332                track_volume: false,
333                ..Default::default()
334            }),
335        ];
336        enrich_pumpfun_same_tx_post_merge(&mut events);
337        if let DexEvent::PumpFunTrade(t) = &events[1] {
338            assert!(t.mayhem_mode);
339            assert!(t.is_cashback_coin);
340            assert!(t.track_volume);
341        } else {
342            panic!("expected trade");
343        }
344    }
345
346    #[test]
347    fn enrich_copies_quote_pool_create_payload_to_create_v2() {
348        let sig = Signature::default();
349        let mint = Pubkey::new_unique();
350        let quote_mint = Pubkey::new_unique();
351        let token_program = Pubkey::new_unique();
352        let meta = EventMetadata {
353            signature: sig,
354            slot: 1,
355            tx_index: 0,
356            block_time_us: 0,
357            grpc_recv_us: 0,
358            recent_blockhash: None,
359        };
360        let mut events: Vec<DexEvent> = vec![
361            DexEvent::PumpFunCreateV2(PumpFunCreateV2TokenEvent {
362                metadata: meta.clone(),
363                mint,
364                ..Default::default()
365            }),
366            DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
367                metadata: meta,
368                name: "USD Coin Pool".to_string(),
369                symbol: "USDCX".to_string(),
370                uri: "https://example.invalid/token.json".to_string(),
371                mint,
372                timestamp: 42,
373                virtual_token_reserves: 1_073_000_000_000_000,
374                virtual_sol_reserves: 0,
375                real_token_reserves: 793_100_000_000_000,
376                token_total_supply: 1_000_000_000_000_000,
377                token_program,
378                is_cashback_enabled: true,
379                quote_mint,
380                virtual_quote_reserves: 4_292_000_000,
381                ..Default::default()
382            }),
383        ];
384
385        enrich_create_v2_from_create_events(&mut events);
386
387        if let DexEvent::PumpFunCreateV2(c) = &events[0] {
388            assert_eq!(c.quote_mint, quote_mint);
389            assert_eq!(c.virtual_quote_reserves, 4_292_000_000);
390            assert_eq!(c.token_program, token_program);
391            assert!(c.is_cashback_enabled);
392            assert_eq!(c.name, "USD Coin Pool");
393        } else {
394            panic!("expected CreateV2");
395        }
396    }
397
398    #[test]
399    fn enrich_replaces_create_sol_placeholder_with_trade_quote_mint() {
400        let sig = Signature::default();
401        let mint = Pubkey::new_unique();
402        let quote_mint = Pubkey::new_unique();
403        let meta = EventMetadata {
404            signature: sig,
405            slot: 1,
406            tx_index: 0,
407            block_time_us: 0,
408            grpc_recv_us: 0,
409            recent_blockhash: None,
410        };
411        let mut events: Vec<DexEvent> = vec![
412            DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
413                metadata: meta.clone(),
414                mint,
415                quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
416                ..Default::default()
417            }),
418            DexEvent::PumpFunBuy(PumpFunTradeEvent {
419                metadata: meta,
420                mint,
421                quote_mint,
422                ..Default::default()
423            }),
424        ];
425
426        enrich_create_quote_mint_from_trades(&mut events);
427
428        if let DexEvent::PumpFunCreate(c) = &events[0] {
429            assert_eq!(c.quote_mint, quote_mint);
430        } else {
431            panic!("expected Create");
432        }
433    }
434}