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