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