1use 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
45pub 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
203pub 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
235pub 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
258pub 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}