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