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",
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",
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",
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        is_buy: true,
233        global: get_account(accounts, 0).unwrap_or_default(),
234        fee_recipient: get_account(accounts, 1).unwrap_or_default(),
235        bonding_curve: get_account(accounts, 3).unwrap_or_default(),
236        bonding_curve_v2,
237        associated_bonding_curve: get_account(accounts, 4).unwrap_or_default(),
238        associated_user: get_account(accounts, 5).unwrap_or_default(),
239        user: get_account(accounts, 6).unwrap_or_default(),
240        system_program: get_account(accounts, 7).unwrap_or_default(),
241        token_program: get_account(accounts, 8).unwrap_or_default(),
242        creator_vault: get_account(accounts, 9).unwrap_or_default(),
243        event_authority: get_account(accounts, 10).unwrap_or_default(),
244        program: get_account(accounts, 11).unwrap_or_default(),
245        global_volume_accumulator: get_account(accounts, 12).unwrap_or_default(),
246        user_volume_accumulator: get_account(accounts, 13).unwrap_or_default(),
247        fee_config: get_account(accounts, 14).unwrap_or_default(),
248        fee_program,
249        buyback_fee_recipient,
250        account,
251        sol_amount,
252        token_amount,
253        amount,
254        max_sol_cost,
255        spendable_sol_in,
256        spendable_quote_in,
257        min_tokens_out,
258        track_volume,
259        ix_name: ix_name.to_string(),
260        ..Default::default()
261    };
262
263    if exact_quote_in {
264        Some(DexEvent::PumpFunBuyExactSolIn(trade_event))
265    } else {
266        Some(DexEvent::PumpFunBuy(trade_event))
267    }
268}
269
270/// Parse sell instruction
271///
272/// Account indices (from pump.json IDL), 14 个固定账户:
273/// 0: global, 1: fee_recipient, 2: mint, 3: bonding_curve,
274/// 4: associated_bonding_curve, 5: associated_user, 6: user,
275/// 7: system_program, 8: creator_vault, 9: token_program,
276/// 10: event_authority, 11: program, 12: fee_config, 13: fee_program.
277/// Post-upgrade non-cashback: 14 bonding_curve_v2, 15 buyback_fee_recipient.
278/// Post-upgrade cashback: 14 user_volume_accumulator, 15 bonding_curve_v2, 16 buyback_fee_recipient.
279fn parse_sell_instruction(
280    data: &[u8],
281    accounts: &[Pubkey],
282    signature: Signature,
283    slot: u64,
284    tx_index: u64,
285    block_time_us: Option<i64>,
286    grpc_recv_us: i64,
287    ix_name: &'static str,
288    v2_accounts: bool,
289) -> Option<DexEvent> {
290    let min_accounts = if v2_accounts { 26 } else { 14 };
291    if accounts.len() < min_accounts {
292        return None;
293    }
294
295    // Parse args: amount (u64), min_sol_output (u64)
296    let (amount, min_sol_output) = if data.len() >= 16 {
297        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
298    } else {
299        (0, 0)
300    };
301    let token_amount = amount;
302    let sol_amount = min_sol_output;
303
304    let (
305        global_idx,
306        mint_idx,
307        bonding_curve_idx,
308        associated_bonding_curve_idx,
309        associated_user_idx,
310        user_idx,
311        system_program_idx,
312        fee_recipient_idx,
313        token_program_idx,
314        creator_vault_idx,
315        event_authority_idx,
316        program_idx,
317        user_volume_accumulator_idx,
318        fee_config_idx,
319        fee_program_idx,
320    ) = if v2_accounts {
321        (0, 1, 10, 11, 14, 13, 23, 6, 3, 16, 24, 25, 19, 21, 22)
322    } else {
323        (0, 2, 3, 4, 5, 6, 7, 1, 9, 8, 10, 11, usize::MAX, 12, 13)
324    };
325    let mint = get_account(accounts, mint_idx)?;
326    let (legacy_user_volume_accumulator, legacy_bonding_curve_v2, legacy_buyback_fee_recipient) =
327        if v2_accounts {
328            (Pubkey::default(), Pubkey::default(), Pubkey::default())
329        } else if accounts.len() >= 17 {
330            (
331                get_account(accounts, 14).unwrap_or_default(),
332                get_account(accounts, 15).unwrap_or_default(),
333                get_account(accounts, 16).unwrap_or_default(),
334            )
335        } else if accounts.len() >= 16 {
336            (
337                Pubkey::default(),
338                get_account(accounts, 14).unwrap_or_default(),
339                get_account(accounts, 15).unwrap_or_default(),
340            )
341        } else {
342            (Pubkey::default(), get_account(accounts, 14).unwrap_or_default(), Pubkey::default())
343        };
344    let account = if legacy_buyback_fee_recipient != Pubkey::default() {
345        Some(legacy_buyback_fee_recipient)
346    } else {
347        None
348    };
349    let metadata =
350        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
351
352    Some(DexEvent::PumpFunSell(PumpFunTradeEvent {
353        metadata,
354        mint,
355        quote_mint: if v2_accounts {
356            get_account(accounts, 2).unwrap_or_default()
357        } else {
358            Pubkey::default()
359        },
360        is_buy: false,
361        global: get_account(accounts, global_idx).unwrap_or_default(),
362        bonding_curve: get_account(accounts, bonding_curve_idx).unwrap_or_default(),
363        bonding_curve_v2: legacy_bonding_curve_v2,
364        associated_bonding_curve: get_account(accounts, associated_bonding_curve_idx)
365            .unwrap_or_default(),
366        associated_user: get_account(accounts, associated_user_idx).unwrap_or_default(),
367        user: get_account(accounts, user_idx).unwrap_or_default(),
368        system_program: get_account(accounts, system_program_idx).unwrap_or_default(),
369        fee_recipient: get_account(accounts, fee_recipient_idx).unwrap_or_default(),
370        token_program: get_account(accounts, token_program_idx).unwrap_or_default(),
371        quote_token_program: if v2_accounts {
372            get_account(accounts, 4).unwrap_or_default()
373        } else {
374            Pubkey::default()
375        },
376        associated_token_program: if v2_accounts {
377            get_account(accounts, 5).unwrap_or_default()
378        } else {
379            Pubkey::default()
380        },
381        creator_vault: get_account(accounts, creator_vault_idx).unwrap_or_default(),
382        associated_quote_fee_recipient: if v2_accounts {
383            get_account(accounts, 7).unwrap_or_default()
384        } else {
385            Pubkey::default()
386        },
387        associated_quote_buyback_fee_recipient: if v2_accounts {
388            get_account(accounts, 9).unwrap_or_default()
389        } else {
390            Pubkey::default()
391        },
392        associated_quote_bonding_curve: if v2_accounts {
393            get_account(accounts, 12).unwrap_or_default()
394        } else {
395            Pubkey::default()
396        },
397        associated_quote_user: if v2_accounts {
398            get_account(accounts, 15).unwrap_or_default()
399        } else {
400            Pubkey::default()
401        },
402        associated_creator_vault: if v2_accounts {
403            get_account(accounts, 17).unwrap_or_default()
404        } else {
405            Pubkey::default()
406        },
407        sharing_config: if v2_accounts {
408            get_account(accounts, 18).unwrap_or_default()
409        } else {
410            Pubkey::default()
411        },
412        event_authority: get_account(accounts, event_authority_idx).unwrap_or_default(),
413        program: get_account(accounts, program_idx).unwrap_or_default(),
414        user_volume_accumulator: if v2_accounts {
415            get_account(accounts, user_volume_accumulator_idx).unwrap_or_default()
416        } else {
417            legacy_user_volume_accumulator
418        },
419        associated_user_volume_accumulator: if v2_accounts {
420            get_account(accounts, 20).unwrap_or_default()
421        } else {
422            Pubkey::default()
423        },
424        fee_config: get_account(accounts, fee_config_idx).unwrap_or_default(),
425        fee_program: get_account(accounts, fee_program_idx).unwrap_or_default(),
426        buyback_fee_recipient: if v2_accounts {
427            get_account(accounts, 8).unwrap_or_default()
428        } else {
429            legacy_buyback_fee_recipient
430        },
431        account,
432        sol_amount,
433        token_amount,
434        amount,
435        min_sol_output,
436        ix_name: ix_name.to_string(),
437        ..Default::default()
438    }))
439}
440
441fn parse_buy_v2_instruction(
442    data: &[u8],
443    accounts: &[Pubkey],
444    signature: Signature,
445    slot: u64,
446    tx_index: u64,
447    block_time_us: Option<i64>,
448    grpc_recv_us: i64,
449    ix_name: &'static str,
450    exact_quote_in: bool,
451) -> Option<DexEvent> {
452    const MIN_ACC: usize = 27;
453    if accounts.len() < MIN_ACC {
454        return None;
455    }
456
457    // buy_v2: amount, max_sol_cost. buy_exact_quote_in_v2: spendable quote in, min_tokens_out.
458    let (first_arg, second_arg) = if data.len() >= 16 {
459        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
460    } else {
461        (0, 0)
462    };
463    let (
464        token_amount,
465        sol_amount,
466        amount,
467        max_sol_cost,
468        quote_amount,
469        spendable_quote_in,
470        min_tokens_out,
471    ) = if exact_quote_in {
472        (second_arg, first_arg, second_arg, 0, first_arg, first_arg, second_arg)
473    } else {
474        (first_arg, second_arg, first_arg, second_arg, 0, 0, 0)
475    };
476
477    let metadata =
478        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
479    let trade_event = PumpFunTradeEvent {
480        metadata,
481        mint: accounts[1],
482        quote_mint: accounts[2],
483        is_buy: true,
484        global: accounts[0],
485        bonding_curve: accounts[10],
486        associated_bonding_curve: accounts[11],
487        associated_user: accounts[14],
488        user: accounts[13],
489        system_program: accounts[24],
490        quote_token_program: accounts[4],
491        associated_token_program: accounts[5],
492        sol_amount,
493        token_amount,
494        amount,
495        max_sol_cost,
496        quote_amount,
497        spendable_sol_in: 0,
498        spendable_quote_in,
499        min_tokens_out,
500        fee_recipient: accounts[6],
501        token_program: accounts[3],
502        creator_vault: accounts[16],
503        associated_quote_fee_recipient: accounts[7],
504        buyback_fee_recipient: accounts[8],
505        associated_quote_buyback_fee_recipient: accounts[9],
506        associated_quote_bonding_curve: accounts[12],
507        associated_quote_user: accounts[15],
508        associated_creator_vault: accounts[17],
509        sharing_config: accounts[18],
510        event_authority: accounts[25],
511        program: accounts[26],
512        global_volume_accumulator: accounts[19],
513        user_volume_accumulator: accounts[20],
514        associated_user_volume_accumulator: accounts[21],
515        fee_config: accounts[22],
516        fee_program: accounts[23],
517        ix_name: ix_name.to_string(),
518        ..Default::default()
519    };
520
521    Some(DexEvent::PumpFunBuy(trade_event))
522}
523
524fn parse_sell_v2_instruction(
525    data: &[u8],
526    accounts: &[Pubkey],
527    signature: Signature,
528    slot: u64,
529    tx_index: u64,
530    block_time_us: Option<i64>,
531    grpc_recv_us: i64,
532    ix_name: &'static str,
533) -> Option<DexEvent> {
534    parse_sell_instruction(
535        data,
536        accounts,
537        signature,
538        slot,
539        tx_index,
540        block_time_us,
541        grpc_recv_us,
542        ix_name,
543        true,
544    )
545}
546
547/// Parse create instruction (legacy)
548///
549/// Account indices (from pump.json):
550/// 0: mint, 1: mint_authority, 2: bonding_curve, 3: associated_bonding_curve,
551/// 4: global, 5: mpl_token_metadata, 6: metadata, 7: user. 共至少 8 个账户。
552fn parse_create_instruction(
553    data: &[u8],
554    accounts: &[Pubkey],
555    signature: Signature,
556    slot: u64,
557    tx_index: u64,
558    block_time_us: Option<i64>,
559    grpc_recv_us: i64,
560) -> Option<DexEvent> {
561    if accounts.len() < 8 {
562        return None;
563    }
564
565    let mut offset = 0;
566
567    // Parse args: name (string), symbol (string), uri (string), creator (pubkey)
568    // String format: 4-byte length prefix + content
569    let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
570        offset += len;
571        s.to_string()
572    } else {
573        String::new()
574    };
575
576    let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
577        offset += len;
578        s.to_string()
579    } else {
580        String::new()
581    };
582
583    let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
584        offset += len;
585        s.to_string()
586    } else {
587        String::new()
588    };
589
590    // 读取 mint, bonding_curve, user, creator (在 name, symbol, uri 之后)
591    if data.len() < offset + 32 + 32 + 32 + 32 {
592        return None;
593    }
594
595    let mint = read_pubkey(data, offset).unwrap_or_default();
596    offset += 32;
597
598    let bonding_curve = read_pubkey(data, offset).unwrap_or_default();
599    offset += 32;
600
601    let user = read_pubkey(data, offset).unwrap_or_default();
602    offset += 32;
603
604    let creator = read_pubkey(data, offset).unwrap_or_default();
605
606    let metadata =
607        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
608
609    Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
610        metadata,
611        name,
612        symbol,
613        uri,
614        mint,
615        bonding_curve,
616        user,
617        creator,
618        ..Default::default()
619    }))
620}
621
622/// Parse create_v2 instruction (SPL-22;Mayhem 由 **data** 中 `is_mayhem_mode` 决定,不要用 mayhem 程序账户是否非空推断)
623///
624/// Account indices (idl pumpfun.json create_v2): 0 mint, 1 mint_authority, 2 bonding_curve,
625/// 3 associated_bonding_curve, 4 global, 5 user, 6 system_program, 7 token_program,
626/// 8 associated_token_program, 9 mayhem_program_id, 10 global_params, 11 sol_vault,
627/// 12 mayhem_state, 13 mayhem_token_vault, 14 event_authority, 15 program. 共 16 个账户。
628/// Instruction args (after disc): name, symbol, uri, creator, is_mayhem_mode (`bool`), is_cashback_enabled (`OptionBool` = 1-byte bool on wire)。
629/// Guard: return None when accounts.len() < 16 to avoid index out of bounds (e.g. ALT-loaded tx).
630fn parse_create_v2_instruction(
631    data: &[u8],
632    accounts: &[Pubkey],
633    signature: Signature,
634    slot: u64,
635    tx_index: u64,
636    block_time_us: Option<i64>,
637    grpc_recv_us: i64,
638) -> Option<DexEvent> {
639    const CREATE_V2_MIN_ACCOUNTS: usize = 16;
640    if accounts.len() < CREATE_V2_MIN_ACCOUNTS {
641        return None;
642    }
643    let acc = &accounts[0..CREATE_V2_MIN_ACCOUNTS];
644
645    // IDL args: name, symbol, uri, creator, is_mayhem_mode, is_cashback_enabled — mint/bc/user 仅在 accounts
646    let mut offset = 0usize;
647    let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
648        offset += len;
649        s.to_string()
650    } else {
651        String::new()
652    };
653    let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
654        offset += len;
655        s.to_string()
656    } else {
657        String::new()
658    };
659    let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
660        offset += len;
661        s.to_string()
662    } else {
663        String::new()
664    };
665    if data.len() < offset + 32 + 1 {
666        return None;
667    }
668    let creator = read_pubkey(data, offset)?;
669    offset += 32;
670    let is_mayhem_mode = read_bool(data, offset)?;
671    offset += 1;
672    let is_cashback_enabled = read_option_bool_idl(data, offset).unwrap_or(false);
673
674    let mint = acc[0];
675    let bonding_curve = acc[2];
676    let user = acc[5];
677
678    let metadata =
679        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
680
681    Some(DexEvent::PumpFunCreateV2(PumpFunCreateV2TokenEvent {
682        metadata,
683        name,
684        symbol,
685        uri,
686        mint,
687        bonding_curve,
688        user,
689        creator,
690        mint_authority: acc[1],
691        associated_bonding_curve: acc[3],
692        global: acc[4],
693        system_program: acc[6],
694        token_program: acc[7],
695        associated_token_program: acc[8],
696        mayhem_program_id: acc[9],
697        global_params: acc[10],
698        sol_vault: acc[11],
699        mayhem_state: acc[12],
700        mayhem_token_vault: acc[13],
701        event_authority: acc[14],
702        program: acc[15],
703        is_mayhem_mode,
704        is_cashback_enabled,
705        ..Default::default()
706    }))
707}
708
709/// Parse Migrate CPI instruction
710#[allow(unused_variables)]
711fn parse_migrate_log_instruction(
712    data: &[u8],
713    accounts: &[Pubkey],
714    signature: Signature,
715    slot: u64,
716    tx_index: u64,
717    block_time_us: Option<i64>,
718    rpc_recv_us: i64,
719) -> Option<DexEvent> {
720    let mut offset = 0;
721
722    // user (Pubkey - 32 bytes)
723    let user = read_pubkey(data, offset)?;
724    offset += 32;
725
726    // mint (Pubkey - 32 bytes)
727    let mint = read_pubkey(data, offset)?;
728    offset += 32;
729
730    // mintAmount (u64 - 8 bytes)
731    let mint_amount = read_u64_le(data, offset)?;
732    offset += 8;
733
734    // solAmount (u64 - 8 bytes)
735    let sol_amount = read_u64_le(data, offset)?;
736    offset += 8;
737
738    // poolMigrationFee (u64 - 8 bytes)
739    let pool_migration_fee = read_u64_le(data, offset)?;
740    offset += 8;
741
742    // bondingCurve (Pubkey - 32 bytes)
743    let bonding_curve = read_pubkey(data, offset)?;
744    offset += 32;
745
746    // timestamp (i64 - 8 bytes)
747    let timestamp = read_u64_le(data, offset)? as i64;
748    offset += 8;
749
750    // pool (Pubkey - 32 bytes)
751    let pool = read_pubkey(data, offset)?;
752
753    let metadata =
754        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), rpc_recv_us);
755
756    Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
757        metadata,
758        user,
759        mint,
760        mint_amount,
761        sol_amount,
762        pool_migration_fee,
763        bonding_curve,
764        timestamp,
765        pool,
766    }))
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772
773    fn instruction_data(discriminator: [u8; 8], first: u64, second: u64) -> Vec<u8> {
774        let mut data = Vec::with_capacity(24);
775        data.extend_from_slice(&discriminator);
776        data.extend_from_slice(&first.to_le_bytes());
777        data.extend_from_slice(&second.to_le_bytes());
778        data
779    }
780
781    fn accounts(n: usize) -> Vec<Pubkey> {
782        (0..n).map(|_| Pubkey::new_unique()).collect()
783    }
784
785    #[test]
786    fn pumpfun_buy_instruction_exposes_raw_args() {
787        let data = instruction_data(discriminators::BUY, 123, 456);
788        let acc = accounts(18);
789        let event =
790            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
791
792        match event {
793            DexEvent::PumpFunBuy(t) => {
794                assert_eq!(t.amount, 123);
795                assert_eq!(t.max_sol_cost, 456);
796                assert_eq!(t.min_sol_output, 0);
797                assert_eq!(t.spendable_sol_in, 0);
798                assert_eq!(t.min_tokens_out, 0);
799                assert_eq!(t.token_amount, 123);
800                assert_eq!(t.sol_amount, 456);
801                assert_eq!(t.bonding_curve_v2, acc[16]);
802                assert_eq!(t.buyback_fee_recipient, acc[17]);
803                assert_eq!(t.ix_name, "buy");
804            }
805            other => panic!("expected PumpFunBuy, got {other:?}"),
806        }
807    }
808
809    #[test]
810    fn pumpfun_legacy_trade_rejects_short_account_lists() {
811        let buy_data = instruction_data(discriminators::BUY, 123, 456);
812        assert!(parse_instruction(&buy_data, &accounts(15), Signature::default(), 1, 0, None, 99)
813            .is_none());
814
815        let sell_data = instruction_data(discriminators::SELL, 321, 654);
816        assert!(parse_instruction(&sell_data, &accounts(13), Signature::default(), 1, 0, None, 99)
817            .is_none());
818    }
819
820    #[test]
821    fn pumpfun_sell_instruction_exposes_raw_args() {
822        let data = instruction_data(discriminators::SELL, 321, 654);
823        let acc = accounts(16);
824        let event =
825            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
826
827        match event {
828            DexEvent::PumpFunSell(t) => {
829                assert_eq!(t.amount, 321);
830                assert_eq!(t.max_sol_cost, 0);
831                assert_eq!(t.min_sol_output, 654);
832                assert_eq!(t.spendable_sol_in, 0);
833                assert_eq!(t.min_tokens_out, 0);
834                assert_eq!(t.token_amount, 321);
835                assert_eq!(t.sol_amount, 654);
836                assert_eq!(t.user_volume_accumulator, Pubkey::default());
837                assert_eq!(t.bonding_curve_v2, acc[14]);
838                assert_eq!(t.buyback_fee_recipient, acc[15]);
839                assert_eq!(t.ix_name, "sell");
840            }
841            other => panic!("expected PumpFunSell, got {other:?}"),
842        }
843    }
844
845    #[test]
846    fn pumpfun_cashback_sell_uses_17_account_layout() {
847        let data = instruction_data(discriminators::SELL, 321, 654);
848        let acc = accounts(17);
849        let event =
850            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
851
852        match event {
853            DexEvent::PumpFunSell(t) => {
854                assert_eq!(t.user_volume_accumulator, acc[14]);
855                assert_eq!(t.bonding_curve_v2, acc[15]);
856                assert_eq!(t.buyback_fee_recipient, acc[16]);
857            }
858            other => panic!("expected PumpFunSell, got {other:?}"),
859        }
860    }
861
862    #[test]
863    fn pumpfun_buy_exact_sol_in_exposes_exact_args() {
864        let data = instruction_data(discriminators::BUY_EXACT_SOL_IN, 1_111, 2_222);
865        let acc = accounts(18);
866        let event =
867            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
868
869        match event {
870            DexEvent::PumpFunBuyExactSolIn(t) => {
871                assert_eq!(t.spendable_sol_in, 1_111);
872                assert_eq!(t.spendable_quote_in, 0);
873                assert_eq!(t.min_tokens_out, 2_222);
874                assert_eq!(t.sol_amount, 1_111);
875                assert_eq!(t.token_amount, 2_222);
876                assert_eq!(t.global, acc[0]);
877                assert_eq!(t.associated_user, acc[5]);
878                assert_eq!(t.event_authority, acc[10]);
879                assert_eq!(t.fee_program, acc[15]);
880                assert_eq!(t.bonding_curve_v2, acc[16]);
881                assert_eq!(t.buyback_fee_recipient, acc[17]);
882                assert_eq!(t.ix_name, "buy_exact_sol_in");
883            }
884            other => panic!("expected PumpFunBuyExactSolIn, got {other:?}"),
885        }
886    }
887
888    #[test]
889    fn pumpfun_v2_instruction_args_use_v2_account_layout() {
890        let data = instruction_data(discriminators::BUY_V2, 777, 888);
891        let acc = accounts(27);
892        let event =
893            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
894
895        match event {
896            DexEvent::PumpFunBuy(t) => {
897                assert_eq!(t.amount, 777);
898                assert_eq!(t.max_sol_cost, 888);
899                assert_eq!(t.mint, acc[1]);
900                assert_eq!(t.quote_mint, acc[2]);
901                assert_eq!(t.bonding_curve, acc[10]);
902                assert_eq!(t.associated_bonding_curve, acc[11]);
903                assert_eq!(t.associated_quote_bonding_curve, acc[12]);
904                assert_eq!(t.user, acc[13]);
905                assert_eq!(t.associated_quote_user, acc[15]);
906                assert_eq!(t.quote_token_program, acc[4]);
907                assert_eq!(t.associated_token_program, acc[5]);
908                assert_eq!(t.associated_quote_fee_recipient, acc[7]);
909                assert_eq!(t.buyback_fee_recipient, acc[8]);
910                assert_eq!(t.associated_quote_buyback_fee_recipient, acc[9]);
911                assert_eq!(t.associated_creator_vault, acc[17]);
912                assert_eq!(t.sharing_config, acc[18]);
913                assert_eq!(t.global_volume_accumulator, acc[19]);
914                assert_eq!(t.associated_user_volume_accumulator, acc[21]);
915                assert_eq!(t.ix_name, "buy");
916            }
917            other => panic!("expected PumpFunBuy, got {other:?}"),
918        }
919    }
920
921    #[test]
922    fn pumpfun_buy_exact_quote_in_v2_uses_quote_amount_fields() {
923        let data = instruction_data(discriminators::BUY_EXACT_QUOTE_IN_V2, 777, 888);
924        let acc = accounts(27);
925        let event =
926            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
927
928        match event {
929            DexEvent::PumpFunBuy(t) => {
930                assert_eq!(t.ix_name, "buy_exact_quote_in");
931                assert_eq!(t.amount, 888);
932                assert_eq!(t.max_sol_cost, 0);
933                assert_eq!(t.quote_amount, 777);
934                assert_eq!(t.spendable_quote_in, 777);
935                assert_eq!(t.min_tokens_out, 888);
936                assert_eq!(t.quote_mint, acc[2]);
937            }
938            other => panic!("expected PumpFunBuy, got {other:?}"),
939        }
940    }
941}