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