Skip to main content

sol_parser_sdk/instr/
pump.rs

1//! PumpFun instruction parser
2//!
3//! Parse PumpFun instructions using discriminator pattern matching
4
5use super::program_ids;
6use super::utils::*;
7use crate::core::events::*;
8use solana_sdk::{pubkey::Pubkey, signature::Signature};
9
10/// PumpFun discriminator constants
11pub mod discriminators {
12    /// Buy instruction: buy tokens with SOL (legacy)
13    pub const BUY: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
14    /// Sell instruction: sell tokens for SOL (legacy)
15    pub const SELL: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
16    /// Create instruction: create a new bonding curve
17    pub const CREATE: [u8; 8] = [24, 30, 200, 40, 5, 28, 7, 119];
18    /// CreateV2 instruction: SPL-22 / Mayhem mode (idl create_v2)
19    pub const CREATE_V2: [u8; 8] = [214, 144, 76, 236, 95, 139, 49, 180];
20    /// buy_exact_sol_in: Given a budget of spendable SOL, buy at least min_tokens_out (legacy)
21    pub const BUY_EXACT_SOL_IN: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
22    /// Migrate event log discriminator (CPI)
23    pub const MIGRATE_EVENT_LOG: [u8; 8] = [189, 233, 93, 185, 92, 148, 234, 148];
24    /// `migrate_bonding_curve_creator` 外层 ix(`idls/pumpfun.json`)
25    pub const MIGRATE_BONDING_CURVE_CREATOR: [u8; 8] = [87, 124, 52, 191, 52, 38, 214, 232];
26    /// buy_v2: unified buy with quote_mint support (SOL + USDC)
27    pub const BUY_V2: [u8; 8] = [184, 23, 238, 97, 103, 197, 211, 61];
28    /// sell_v2: unified sell with quote_mint support (SOL + USDC)
29    pub const SELL_V2: [u8; 8] = [93, 246, 130, 60, 231, 233, 64, 178];
30    /// buy_exact_quote_in_v2: spend exact quote amount for min tokens out (SOL + USDC)
31    pub const BUY_EXACT_QUOTE_IN_V2: [u8; 8] = [194, 171, 28, 70, 104, 77, 91, 47];
32}
33
34/// PumpFun Program ID
35pub const PROGRAM_ID_PUBKEY: Pubkey = program_ids::PUMPFUN_PROGRAM_ID;
36
37/// Main PumpFun instruction parser
38///
39/// Outer instructions (8-byte discriminator): CREATE, CREATE_V2 从指令解析并返回事件;
40/// BUY/SELL 仍以 log 为主。Inner CPI: MIGRATE_EVENT_LOG 仅在此解析。
41pub fn parse_instruction(
42    instruction_data: &[u8],
43    accounts: &[Pubkey],
44    signature: Signature,
45    slot: u64,
46    tx_index: u64,
47    block_time_us: Option<i64>,
48    grpc_recv_us: i64,
49) -> Option<DexEvent> {
50    if instruction_data.len() < 8 {
51        return None;
52    }
53    let outer_disc: [u8; 8] = instruction_data[0..8].try_into().ok()?;
54    let data = &instruction_data[8..];
55
56    // 外层指令:Create / CreateV2(与 solana-streamer 功能对齐)
57    if outer_disc == discriminators::CREATE_V2 {
58        return parse_create_v2_instruction(
59            data,
60            accounts,
61            signature,
62            slot,
63            tx_index,
64            block_time_us,
65            grpc_recv_us,
66        );
67    }
68    if outer_disc == discriminators::CREATE {
69        return parse_create_instruction(
70            data,
71            accounts,
72            signature,
73            slot,
74            tx_index,
75            block_time_us,
76            grpc_recv_us,
77        );
78    }
79    if outer_disc == discriminators::BUY {
80        return parse_buy_instruction(
81            data,
82            accounts,
83            signature,
84            slot,
85            tx_index,
86            block_time_us,
87            grpc_recv_us,
88            "buy",
89            false,
90        );
91    }
92    if outer_disc == discriminators::BUY_EXACT_SOL_IN {
93        return parse_buy_instruction(
94            data,
95            accounts,
96            signature,
97            slot,
98            tx_index,
99            block_time_us,
100            grpc_recv_us,
101            "buy_exact_sol_in",
102            true,
103        );
104    }
105    if outer_disc == discriminators::SELL {
106        return parse_sell_instruction(
107            data,
108            accounts,
109            signature,
110            slot,
111            tx_index,
112            block_time_us,
113            grpc_recv_us,
114            "sell",
115            false,
116        );
117    }
118    if outer_disc == discriminators::BUY_V2 {
119        return parse_buy_v2_instruction(
120            data,
121            accounts,
122            signature,
123            slot,
124            tx_index,
125            block_time_us,
126            grpc_recv_us,
127            "buy_v2",
128            false,
129        );
130    }
131    if outer_disc == discriminators::BUY_EXACT_QUOTE_IN_V2 {
132        return parse_buy_v2_instruction(
133            data,
134            accounts,
135            signature,
136            slot,
137            tx_index,
138            block_time_us,
139            grpc_recv_us,
140            "buy_exact_quote_in_v2",
141            true,
142        );
143    }
144    if outer_disc == discriminators::SELL_V2 {
145        return parse_sell_v2_instruction(
146            data,
147            accounts,
148            signature,
149            slot,
150            tx_index,
151            block_time_us,
152            grpc_recv_us,
153            "sell_v2",
154        );
155    }
156
157    // Inner CPI:仅 MIGRATE 在此解析
158    if instruction_data.len() >= 16 {
159        let cpi_disc: [u8; 8] = instruction_data[8..16].try_into().ok()?;
160        if cpi_disc == discriminators::MIGRATE_EVENT_LOG {
161            return parse_migrate_log_instruction(
162                &instruction_data[16..],
163                accounts,
164                signature,
165                slot,
166                tx_index,
167                block_time_us,
168                grpc_recv_us,
169            );
170        }
171    }
172    None
173}
174
175/// Parse buy/buy_exact_sol_in instruction
176///
177/// Account indices (from pump.json IDL), 16 个固定账户:
178/// 0: global, 1: fee_recipient, 2: mint, 3: bonding_curve,
179/// 4: associated_bonding_curve, 5: associated_user, 6: user,
180/// 7: system_program, 8: token_program, 9: creator_vault,
181/// 10: event_authority, 11: program, 12: global_volume_accumulator,
182/// 13: user_volume_accumulator, 14: fee_config, 15: fee_program.
183/// Post-upgrade remaining accounts: 16 bonding_curve_v2, 17 buyback_fee_recipient.
184fn parse_buy_instruction(
185    data: &[u8],
186    accounts: &[Pubkey],
187    signature: Signature,
188    slot: u64,
189    tx_index: u64,
190    block_time_us: Option<i64>,
191    grpc_recv_us: i64,
192    ix_name: &'static str,
193    exact_quote_in: bool,
194) -> Option<DexEvent> {
195    const LEGACY_BUY_ACCOUNTS: usize = 16;
196    if accounts.len() < LEGACY_BUY_ACCOUNTS {
197        return None;
198    }
199
200    // buy: amount, max_sol_cost. buy_exact_sol_in: spendable_sol_in, min_tokens_out.
201    let (first_arg, second_arg) = if data.len() >= 16 {
202        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
203    } else {
204        (0, 0)
205    };
206    let track_volume = data.get(16).copied().map(|b| b != 0).unwrap_or(false);
207    let (
208        token_amount,
209        sol_amount,
210        amount,
211        max_sol_cost,
212        spendable_sol_in,
213        spendable_quote_in,
214        min_tokens_out,
215    ) = if exact_quote_in {
216        (second_arg, first_arg, second_arg, first_arg, first_arg, 0, second_arg)
217    } else {
218        (first_arg, second_arg, first_arg, second_arg, 0, 0, 0)
219    };
220    let bonding_curve_v2 = get_account(accounts, 16).unwrap_or_default();
221    let buyback_fee_recipient = get_account(accounts, 17).unwrap_or_default();
222    let account =
223        if buyback_fee_recipient != Pubkey::default() { Some(buyback_fee_recipient) } else { None };
224    let fee_program = get_account(accounts, 15).unwrap_or_default();
225    let mint = get_account(accounts, 2)?;
226    let metadata =
227        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
228
229    let trade_event = PumpFunTradeEvent {
230        metadata,
231        mint,
232        quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
233        is_buy: true,
234        global: get_account(accounts, 0).unwrap_or_default(),
235        fee_recipient: get_account(accounts, 1).unwrap_or_default(),
236        bonding_curve: get_account(accounts, 3).unwrap_or_default(),
237        bonding_curve_v2,
238        associated_bonding_curve: get_account(accounts, 4).unwrap_or_default(),
239        associated_user: get_account(accounts, 5).unwrap_or_default(),
240        user: get_account(accounts, 6).unwrap_or_default(),
241        system_program: get_account(accounts, 7).unwrap_or_default(),
242        token_program: get_account(accounts, 8).unwrap_or_default(),
243        creator_vault: get_account(accounts, 9).unwrap_or_default(),
244        event_authority: get_account(accounts, 10).unwrap_or_default(),
245        program: get_account(accounts, 11).unwrap_or_default(),
246        global_volume_accumulator: get_account(accounts, 12).unwrap_or_default(),
247        user_volume_accumulator: get_account(accounts, 13).unwrap_or_default(),
248        fee_config: get_account(accounts, 14).unwrap_or_default(),
249        fee_program,
250        buyback_fee_recipient,
251        account,
252        sol_amount,
253        token_amount,
254        amount,
255        max_sol_cost,
256        spendable_sol_in,
257        spendable_quote_in,
258        min_tokens_out,
259        track_volume,
260        ix_name: ix_name.to_string(),
261        ..Default::default()
262    };
263
264    if exact_quote_in {
265        Some(DexEvent::PumpFunBuyExactSolIn(trade_event))
266    } else {
267        Some(DexEvent::PumpFunBuy(trade_event))
268    }
269}
270
271/// Parse sell instruction
272///
273/// Account indices (from pump.json IDL), 14 个固定账户:
274/// 0: global, 1: fee_recipient, 2: mint, 3: bonding_curve,
275/// 4: associated_bonding_curve, 5: associated_user, 6: user,
276/// 7: system_program, 8: creator_vault, 9: token_program,
277/// 10: event_authority, 11: program, 12: fee_config, 13: fee_program.
278/// Post-upgrade non-cashback: 14 bonding_curve_v2, 15 buyback_fee_recipient.
279/// Post-upgrade cashback: 14 user_volume_accumulator, 15 bonding_curve_v2, 16 buyback_fee_recipient.
280fn parse_sell_instruction(
281    data: &[u8],
282    accounts: &[Pubkey],
283    signature: Signature,
284    slot: u64,
285    tx_index: u64,
286    block_time_us: Option<i64>,
287    grpc_recv_us: i64,
288    ix_name: &'static str,
289    v2_accounts: bool,
290) -> Option<DexEvent> {
291    let min_accounts = if v2_accounts { 26 } else { 14 };
292    if accounts.len() < min_accounts {
293        return None;
294    }
295
296    // Parse args: amount (u64), min_sol_output (u64)
297    let (amount, min_sol_output) = if data.len() >= 16 {
298        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
299    } else {
300        (0, 0)
301    };
302    let token_amount = amount;
303    let sol_amount = min_sol_output;
304
305    let (
306        global_idx,
307        mint_idx,
308        bonding_curve_idx,
309        associated_bonding_curve_idx,
310        associated_user_idx,
311        user_idx,
312        system_program_idx,
313        fee_recipient_idx,
314        token_program_idx,
315        creator_vault_idx,
316        event_authority_idx,
317        program_idx,
318        user_volume_accumulator_idx,
319        fee_config_idx,
320        fee_program_idx,
321    ) = if v2_accounts {
322        (0, 1, 10, 11, 14, 13, 23, 6, 3, 16, 24, 25, 19, 21, 22)
323    } else {
324        (0, 2, 3, 4, 5, 6, 7, 1, 9, 8, 10, 11, usize::MAX, 12, 13)
325    };
326    let mint = get_account(accounts, mint_idx)?;
327    let (legacy_user_volume_accumulator, legacy_bonding_curve_v2, legacy_buyback_fee_recipient) =
328        if v2_accounts {
329            (Pubkey::default(), Pubkey::default(), Pubkey::default())
330        } else if accounts.len() >= 17 {
331            (
332                get_account(accounts, 14).unwrap_or_default(),
333                get_account(accounts, 15).unwrap_or_default(),
334                get_account(accounts, 16).unwrap_or_default(),
335            )
336        } else if accounts.len() >= 16 {
337            (
338                Pubkey::default(),
339                get_account(accounts, 14).unwrap_or_default(),
340                get_account(accounts, 15).unwrap_or_default(),
341            )
342        } else {
343            (Pubkey::default(), get_account(accounts, 14).unwrap_or_default(), Pubkey::default())
344        };
345    let account = if legacy_buyback_fee_recipient != Pubkey::default() {
346        Some(legacy_buyback_fee_recipient)
347    } else {
348        None
349    };
350    let metadata =
351        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
352
353    Some(DexEvent::PumpFunSell(PumpFunTradeEvent {
354        metadata,
355        mint,
356        quote_mint: normalize_pumpfun_quote_mint(if v2_accounts {
357            get_account(accounts, 2).unwrap_or_default()
358        } else {
359            Pubkey::default()
360        }),
361        is_buy: false,
362        global: get_account(accounts, global_idx).unwrap_or_default(),
363        bonding_curve: get_account(accounts, bonding_curve_idx).unwrap_or_default(),
364        bonding_curve_v2: legacy_bonding_curve_v2,
365        associated_bonding_curve: get_account(accounts, associated_bonding_curve_idx)
366            .unwrap_or_default(),
367        associated_user: get_account(accounts, associated_user_idx).unwrap_or_default(),
368        user: get_account(accounts, user_idx).unwrap_or_default(),
369        system_program: get_account(accounts, system_program_idx).unwrap_or_default(),
370        fee_recipient: get_account(accounts, fee_recipient_idx).unwrap_or_default(),
371        token_program: get_account(accounts, token_program_idx).unwrap_or_default(),
372        quote_token_program: if v2_accounts {
373            get_account(accounts, 4).unwrap_or_default()
374        } else {
375            Pubkey::default()
376        },
377        associated_token_program: if v2_accounts {
378            get_account(accounts, 5).unwrap_or_default()
379        } else {
380            Pubkey::default()
381        },
382        creator_vault: get_account(accounts, creator_vault_idx).unwrap_or_default(),
383        associated_quote_fee_recipient: if v2_accounts {
384            get_account(accounts, 7).unwrap_or_default()
385        } else {
386            Pubkey::default()
387        },
388        associated_quote_buyback_fee_recipient: if v2_accounts {
389            get_account(accounts, 9).unwrap_or_default()
390        } else {
391            Pubkey::default()
392        },
393        associated_quote_bonding_curve: if v2_accounts {
394            get_account(accounts, 12).unwrap_or_default()
395        } else {
396            Pubkey::default()
397        },
398        associated_quote_user: if v2_accounts {
399            get_account(accounts, 15).unwrap_or_default()
400        } else {
401            Pubkey::default()
402        },
403        associated_creator_vault: if v2_accounts {
404            get_account(accounts, 17).unwrap_or_default()
405        } else {
406            Pubkey::default()
407        },
408        sharing_config: if v2_accounts {
409            get_account(accounts, 18).unwrap_or_default()
410        } else {
411            Pubkey::default()
412        },
413        event_authority: get_account(accounts, event_authority_idx).unwrap_or_default(),
414        program: get_account(accounts, program_idx).unwrap_or_default(),
415        user_volume_accumulator: if v2_accounts {
416            get_account(accounts, user_volume_accumulator_idx).unwrap_or_default()
417        } else {
418            legacy_user_volume_accumulator
419        },
420        associated_user_volume_accumulator: if v2_accounts {
421            get_account(accounts, 20).unwrap_or_default()
422        } else {
423            Pubkey::default()
424        },
425        fee_config: get_account(accounts, fee_config_idx).unwrap_or_default(),
426        fee_program: get_account(accounts, fee_program_idx).unwrap_or_default(),
427        buyback_fee_recipient: if v2_accounts {
428            get_account(accounts, 8).unwrap_or_default()
429        } else {
430            legacy_buyback_fee_recipient
431        },
432        account,
433        sol_amount,
434        token_amount,
435        amount,
436        min_sol_output,
437        ix_name: ix_name.to_string(),
438        ..Default::default()
439    }))
440}
441
442fn parse_buy_v2_instruction(
443    data: &[u8],
444    accounts: &[Pubkey],
445    signature: Signature,
446    slot: u64,
447    tx_index: u64,
448    block_time_us: Option<i64>,
449    grpc_recv_us: i64,
450    ix_name: &'static str,
451    exact_quote_in: bool,
452) -> Option<DexEvent> {
453    const MIN_ACC: usize = 27;
454    if accounts.len() < MIN_ACC {
455        return None;
456    }
457
458    // buy_v2: amount, max_sol_cost. buy_exact_quote_in_v2: spendable quote in, min_tokens_out.
459    let (first_arg, second_arg) = if data.len() >= 16 {
460        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
461    } else {
462        (0, 0)
463    };
464    let (
465        token_amount,
466        sol_amount,
467        amount,
468        max_sol_cost,
469        quote_amount,
470        spendable_quote_in,
471        min_tokens_out,
472    ) = if exact_quote_in {
473        (second_arg, first_arg, second_arg, 0, first_arg, first_arg, second_arg)
474    } else {
475        (first_arg, second_arg, first_arg, second_arg, 0, 0, 0)
476    };
477
478    let metadata =
479        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
480    let trade_event = PumpFunTradeEvent {
481        metadata,
482        mint: accounts[1],
483        quote_mint: normalize_pumpfun_quote_mint(accounts[2]),
484        is_buy: true,
485        global: accounts[0],
486        bonding_curve: accounts[10],
487        associated_bonding_curve: accounts[11],
488        associated_user: accounts[14],
489        user: accounts[13],
490        system_program: accounts[24],
491        quote_token_program: accounts[4],
492        associated_token_program: accounts[5],
493        sol_amount,
494        token_amount,
495        amount,
496        max_sol_cost,
497        quote_amount,
498        spendable_sol_in: 0,
499        spendable_quote_in,
500        min_tokens_out,
501        fee_recipient: accounts[6],
502        token_program: accounts[3],
503        creator_vault: accounts[16],
504        associated_quote_fee_recipient: accounts[7],
505        buyback_fee_recipient: accounts[8],
506        associated_quote_buyback_fee_recipient: accounts[9],
507        associated_quote_bonding_curve: accounts[12],
508        associated_quote_user: accounts[15],
509        associated_creator_vault: accounts[17],
510        sharing_config: accounts[18],
511        event_authority: accounts[25],
512        program: accounts[26],
513        global_volume_accumulator: accounts[19],
514        user_volume_accumulator: accounts[20],
515        associated_user_volume_accumulator: accounts[21],
516        fee_config: accounts[22],
517        fee_program: accounts[23],
518        ix_name: ix_name.to_string(),
519        ..Default::default()
520    };
521
522    Some(DexEvent::PumpFunBuy(trade_event))
523}
524
525fn parse_sell_v2_instruction(
526    data: &[u8],
527    accounts: &[Pubkey],
528    signature: Signature,
529    slot: u64,
530    tx_index: u64,
531    block_time_us: Option<i64>,
532    grpc_recv_us: i64,
533    ix_name: &'static str,
534) -> Option<DexEvent> {
535    parse_sell_instruction(
536        data,
537        accounts,
538        signature,
539        slot,
540        tx_index,
541        block_time_us,
542        grpc_recv_us,
543        ix_name,
544        true,
545    )
546}
547
548/// Parse create instruction (legacy)
549///
550/// Account indices (from pump.json):
551/// 0: mint, 1: mint_authority, 2: bonding_curve, 3: associated_bonding_curve,
552/// 4: global, 5: mpl_token_metadata, 6: metadata, 7: user. 共至少 8 个账户。
553fn parse_create_instruction(
554    data: &[u8],
555    accounts: &[Pubkey],
556    signature: Signature,
557    slot: u64,
558    tx_index: u64,
559    block_time_us: Option<i64>,
560    grpc_recv_us: i64,
561) -> Option<DexEvent> {
562    if accounts.len() < 8 {
563        return None;
564    }
565
566    let mut offset = 0;
567
568    // Parse args: name (string), symbol (string), uri (string), creator (pubkey)
569    // String format: 4-byte length prefix + content
570    let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
571        offset += len;
572        s.to_string()
573    } else {
574        String::new()
575    };
576
577    let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
578        offset += len;
579        s.to_string()
580    } else {
581        String::new()
582    };
583
584    let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
585        offset += len;
586        s.to_string()
587    } else {
588        String::new()
589    };
590
591    // 读取 mint, bonding_curve, user, creator (在 name, symbol, uri 之后)
592    if data.len() < offset + 32 + 32 + 32 + 32 {
593        return None;
594    }
595
596    let mint = read_pubkey(data, offset).unwrap_or_default();
597    offset += 32;
598
599    let bonding_curve = read_pubkey(data, offset).unwrap_or_default();
600    offset += 32;
601
602    let user = read_pubkey(data, offset).unwrap_or_default();
603    offset += 32;
604
605    let creator = read_pubkey(data, offset).unwrap_or_default();
606
607    let metadata =
608        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
609
610    Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
611        metadata,
612        name,
613        symbol,
614        uri,
615        mint,
616        bonding_curve,
617        user,
618        creator,
619        quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
620        ..Default::default()
621    }))
622}
623
624/// Parse create_v2 instruction (SPL-22;Mayhem 由 **data** 中 `is_mayhem_mode` 决定,不要用 mayhem 程序账户是否非空推断)
625///
626/// Account indices (idl pumpfun.json create_v2): 0 mint, 1 mint_authority, 2 bonding_curve,
627/// 3 associated_bonding_curve, 4 global, 5 user, 6 system_program, 7 token_program,
628/// 8 associated_token_program, 9 mayhem_program_id, 10 global_params, 11 sol_vault,
629/// 12 mayhem_state, 13 mayhem_token_vault, 14 event_authority, 15 program. 共 16 个账户。
630/// Instruction args (after disc): name, symbol, uri, creator, is_mayhem_mode (`bool`), is_cashback_enabled (`OptionBool` = 1-byte bool on wire)。
631/// Guard: return None when accounts.len() < 16 to avoid index out of bounds (e.g. ALT-loaded tx).
632fn parse_create_v2_instruction(
633    data: &[u8],
634    accounts: &[Pubkey],
635    signature: Signature,
636    slot: u64,
637    tx_index: u64,
638    block_time_us: Option<i64>,
639    grpc_recv_us: i64,
640) -> Option<DexEvent> {
641    const CREATE_V2_MIN_ACCOUNTS: usize = 16;
642    if accounts.len() < CREATE_V2_MIN_ACCOUNTS {
643        return None;
644    }
645    let acc = &accounts[0..CREATE_V2_MIN_ACCOUNTS];
646
647    // IDL args: name, symbol, uri, creator, is_mayhem_mode, is_cashback_enabled — mint/bc/user 仅在 accounts
648    let mut offset = 0usize;
649    let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
650        offset += len;
651        s.to_string()
652    } else {
653        String::new()
654    };
655    let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
656        offset += len;
657        s.to_string()
658    } else {
659        String::new()
660    };
661    let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
662        offset += len;
663        s.to_string()
664    } else {
665        String::new()
666    };
667    if data.len() < offset + 32 + 1 {
668        return None;
669    }
670    let creator = read_pubkey(data, offset)?;
671    offset += 32;
672    let is_mayhem_mode = read_bool(data, offset)?;
673    offset += 1;
674    let is_cashback_enabled = read_option_bool_idl(data, offset).unwrap_or(false);
675
676    let mint = acc[0];
677    let bonding_curve = acc[2];
678    let user = acc[5];
679
680    let metadata =
681        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
682
683    Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
684        metadata,
685        name,
686        symbol,
687        uri,
688        mint,
689        bonding_curve,
690        user,
691        creator,
692        mint_authority: acc[1],
693        associated_bonding_curve: acc[3],
694        global: acc[4],
695        system_program: acc[6],
696        token_program: acc[7],
697        associated_token_program: acc[8],
698        mayhem_program_id: acc[9],
699        global_params: acc[10],
700        sol_vault: acc[11],
701        mayhem_state: acc[12],
702        mayhem_token_vault: acc[13],
703        event_authority: acc[14],
704        program: acc[15],
705        is_mayhem_mode,
706        is_cashback_enabled,
707        quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
708        ..Default::default()
709    }))
710}
711
712/// Parse Migrate CPI instruction
713#[allow(unused_variables)]
714fn parse_migrate_log_instruction(
715    data: &[u8],
716    accounts: &[Pubkey],
717    signature: Signature,
718    slot: u64,
719    tx_index: u64,
720    block_time_us: Option<i64>,
721    rpc_recv_us: i64,
722) -> Option<DexEvent> {
723    let mut offset = 0;
724
725    // user (Pubkey - 32 bytes)
726    let user = read_pubkey(data, offset)?;
727    offset += 32;
728
729    // mint (Pubkey - 32 bytes)
730    let mint = read_pubkey(data, offset)?;
731    offset += 32;
732
733    // mintAmount (u64 - 8 bytes)
734    let mint_amount = read_u64_le(data, offset)?;
735    offset += 8;
736
737    // solAmount (u64 - 8 bytes)
738    let sol_amount = read_u64_le(data, offset)?;
739    offset += 8;
740
741    // poolMigrationFee (u64 - 8 bytes)
742    let pool_migration_fee = read_u64_le(data, offset)?;
743    offset += 8;
744
745    // bondingCurve (Pubkey - 32 bytes)
746    let bonding_curve = read_pubkey(data, offset)?;
747    offset += 32;
748
749    // timestamp (i64 - 8 bytes)
750    let timestamp = read_u64_le(data, offset)? as i64;
751    offset += 8;
752
753    // pool (Pubkey - 32 bytes)
754    let pool = read_pubkey(data, offset)?;
755
756    let metadata =
757        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), rpc_recv_us);
758
759    Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
760        metadata,
761        user,
762        mint,
763        mint_amount,
764        sol_amount,
765        pool_migration_fee,
766        bonding_curve,
767        timestamp,
768        pool,
769    }))
770}
771
772#[cfg(test)]
773mod tests {
774    use super::*;
775
776    fn instruction_data(discriminator: [u8; 8], first: u64, second: u64) -> Vec<u8> {
777        let mut data = Vec::with_capacity(24);
778        data.extend_from_slice(&discriminator);
779        data.extend_from_slice(&first.to_le_bytes());
780        data.extend_from_slice(&second.to_le_bytes());
781        data
782    }
783
784    fn accounts(n: usize) -> Vec<Pubkey> {
785        (0..n).map(|_| Pubkey::new_unique()).collect()
786    }
787
788    fn str_arg(s: &str, out: &mut Vec<u8>) {
789        out.extend_from_slice(&(s.len() as u32).to_le_bytes());
790        out.extend_from_slice(s.as_bytes());
791    }
792
793    fn create_v2_data() -> Vec<u8> {
794        let mut data = Vec::new();
795        data.extend_from_slice(&discriminators::CREATE_V2);
796        str_arg("Token", &mut data);
797        str_arg("TOK", &mut data);
798        str_arg("https://example.invalid/token.json", &mut data);
799        data.extend_from_slice(Pubkey::new_unique().as_ref());
800        data.push(1);
801        data.push(1);
802        data
803    }
804
805    #[test]
806    fn pumpfun_create_v2_instruction_emits_canonical_create() {
807        let acc = accounts(16);
808        let event =
809            parse_instruction(&create_v2_data(), &acc, Signature::default(), 1, 0, None, 99)
810                .expect("event");
811
812        match event {
813            DexEvent::PumpFunCreate(c) => {
814                assert_eq!(c.name, "Token");
815                assert_eq!(c.symbol, "TOK");
816                assert_eq!(c.mint, acc[0]);
817                assert_eq!(c.mint_authority, acc[1]);
818                assert_eq!(c.bonding_curve, acc[2]);
819                assert_eq!(c.associated_bonding_curve, acc[3]);
820                assert_eq!(c.user, acc[5]);
821                assert_eq!(c.token_program, acc[7]);
822                assert_eq!(c.mayhem_program_id, acc[9]);
823                assert_eq!(c.program, acc[15]);
824                assert!(c.is_mayhem_mode);
825                assert!(c.is_cashback_enabled);
826                assert_eq!(c.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
827            }
828            other => panic!("expected canonical PumpFunCreate, got {other:?}"),
829        }
830    }
831
832    #[test]
833    fn pumpfun_buy_instruction_exposes_raw_args() {
834        let data = instruction_data(discriminators::BUY, 123, 456);
835        let acc = accounts(18);
836        let event =
837            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
838
839        match event {
840            DexEvent::PumpFunBuy(t) => {
841                assert_eq!(t.amount, 123);
842                assert_eq!(t.max_sol_cost, 456);
843                assert_eq!(t.min_sol_output, 0);
844                assert_eq!(t.spendable_sol_in, 0);
845                assert_eq!(t.min_tokens_out, 0);
846                assert_eq!(t.token_amount, 123);
847                assert_eq!(t.sol_amount, 456);
848                assert_eq!(t.bonding_curve_v2, acc[16]);
849                assert_eq!(t.buyback_fee_recipient, acc[17]);
850                assert_eq!(t.ix_name, "buy");
851            }
852            other => panic!("expected PumpFunBuy, got {other:?}"),
853        }
854    }
855
856    #[test]
857    fn pumpfun_legacy_trade_rejects_short_account_lists() {
858        let buy_data = instruction_data(discriminators::BUY, 123, 456);
859        assert!(parse_instruction(&buy_data, &accounts(15), Signature::default(), 1, 0, None, 99)
860            .is_none());
861
862        let sell_data = instruction_data(discriminators::SELL, 321, 654);
863        assert!(parse_instruction(&sell_data, &accounts(13), Signature::default(), 1, 0, None, 99)
864            .is_none());
865    }
866
867    #[test]
868    fn pumpfun_sell_instruction_exposes_raw_args() {
869        let data = instruction_data(discriminators::SELL, 321, 654);
870        let acc = accounts(16);
871        let event =
872            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
873
874        match event {
875            DexEvent::PumpFunSell(t) => {
876                assert_eq!(t.amount, 321);
877                assert_eq!(t.max_sol_cost, 0);
878                assert_eq!(t.min_sol_output, 654);
879                assert_eq!(t.spendable_sol_in, 0);
880                assert_eq!(t.min_tokens_out, 0);
881                assert_eq!(t.token_amount, 321);
882                assert_eq!(t.sol_amount, 654);
883                assert_eq!(t.user_volume_accumulator, Pubkey::default());
884                assert_eq!(t.bonding_curve_v2, acc[14]);
885                assert_eq!(t.buyback_fee_recipient, acc[15]);
886                assert_eq!(t.ix_name, "sell");
887            }
888            other => panic!("expected PumpFunSell, got {other:?}"),
889        }
890    }
891
892    #[test]
893    fn pumpfun_cashback_sell_uses_17_account_layout() {
894        let data = instruction_data(discriminators::SELL, 321, 654);
895        let acc = accounts(17);
896        let event =
897            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
898
899        match event {
900            DexEvent::PumpFunSell(t) => {
901                assert_eq!(t.user_volume_accumulator, acc[14]);
902                assert_eq!(t.bonding_curve_v2, acc[15]);
903                assert_eq!(t.buyback_fee_recipient, acc[16]);
904            }
905            other => panic!("expected PumpFunSell, got {other:?}"),
906        }
907    }
908
909    #[test]
910    fn pumpfun_buy_exact_sol_in_exposes_exact_args() {
911        let data = instruction_data(discriminators::BUY_EXACT_SOL_IN, 1_111, 2_222);
912        let acc = accounts(18);
913        let event =
914            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
915
916        match event {
917            DexEvent::PumpFunBuyExactSolIn(t) => {
918                assert_eq!(t.spendable_sol_in, 1_111);
919                assert_eq!(t.spendable_quote_in, 0);
920                assert_eq!(t.min_tokens_out, 2_222);
921                assert_eq!(t.sol_amount, 1_111);
922                assert_eq!(t.token_amount, 2_222);
923                assert_eq!(t.global, acc[0]);
924                assert_eq!(t.associated_user, acc[5]);
925                assert_eq!(t.event_authority, acc[10]);
926                assert_eq!(t.fee_program, acc[15]);
927                assert_eq!(t.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
928                assert_eq!(t.bonding_curve_v2, acc[16]);
929                assert_eq!(t.buyback_fee_recipient, acc[17]);
930                assert_eq!(t.ix_name, "buy_exact_sol_in");
931            }
932            other => panic!("expected PumpFunBuyExactSolIn, got {other:?}"),
933        }
934    }
935
936    #[test]
937    fn pumpfun_v2_instruction_args_use_v2_account_layout() {
938        let data = instruction_data(discriminators::BUY_V2, 777, 888);
939        let acc = accounts(27);
940        let event =
941            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
942
943        match event {
944            DexEvent::PumpFunBuy(t) => {
945                assert_eq!(t.amount, 777);
946                assert_eq!(t.max_sol_cost, 888);
947                assert_eq!(t.mint, acc[1]);
948                assert_eq!(t.quote_mint, acc[2]);
949                assert_eq!(t.bonding_curve, acc[10]);
950                assert_eq!(t.associated_bonding_curve, acc[11]);
951                assert_eq!(t.associated_quote_bonding_curve, acc[12]);
952                assert_eq!(t.user, acc[13]);
953                assert_eq!(t.associated_quote_user, acc[15]);
954                assert_eq!(t.quote_token_program, acc[4]);
955                assert_eq!(t.associated_token_program, acc[5]);
956                assert_eq!(t.associated_quote_fee_recipient, acc[7]);
957                assert_eq!(t.buyback_fee_recipient, acc[8]);
958                assert_eq!(t.associated_quote_buyback_fee_recipient, acc[9]);
959                assert_eq!(t.associated_creator_vault, acc[17]);
960                assert_eq!(t.sharing_config, acc[18]);
961                assert_eq!(t.global_volume_accumulator, acc[19]);
962                assert_eq!(t.associated_user_volume_accumulator, acc[21]);
963                assert_eq!(t.ix_name, "buy_v2");
964            }
965            other => panic!("expected PumpFunBuy, got {other:?}"),
966        }
967    }
968
969    #[test]
970    fn pumpfun_buy_exact_quote_in_v2_uses_quote_amount_fields() {
971        let data = instruction_data(discriminators::BUY_EXACT_QUOTE_IN_V2, 777, 888);
972        let acc = accounts(27);
973        let event =
974            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
975
976        match event {
977            DexEvent::PumpFunBuy(t) => {
978                assert_eq!(t.ix_name, "buy_exact_quote_in_v2");
979                assert_eq!(t.amount, 888);
980                assert_eq!(t.max_sol_cost, 0);
981                assert_eq!(t.quote_amount, 777);
982                assert_eq!(t.spendable_quote_in, 777);
983                assert_eq!(t.min_tokens_out, 888);
984                assert_eq!(t.quote_mint, acc[2]);
985            }
986            other => panic!("expected PumpFunBuy, got {other:?}"),
987        }
988    }
989}