Skip to main content

sol_parser_sdk/instr/
pump.rs

1//! PumpFun instruction parser
2//!
3//! Parse PumpFun instructions using discriminator pattern matching
4
5use super::program_ids;
6use super::utils::*;
7use crate::core::events::*;
8use solana_sdk::{pubkey::Pubkey, signature::Signature};
9
10/// PumpFun discriminator constants
11pub mod discriminators {
12    /// Buy instruction: buy tokens with SOL (legacy)
13    pub const BUY: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
14    /// Sell instruction: sell tokens for SOL (legacy)
15    pub const SELL: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
16    /// Create instruction: create a new bonding curve
17    pub const CREATE: [u8; 8] = [24, 30, 200, 40, 5, 28, 7, 119];
18    /// CreateV2 instruction: SPL-22 / Mayhem mode (idl create_v2)
19    pub const CREATE_V2: [u8; 8] = [214, 144, 76, 236, 95, 139, 49, 180];
20    /// buy_exact_sol_in: Given a budget of spendable SOL, buy at least min_tokens_out (legacy)
21    pub const BUY_EXACT_SOL_IN: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
22    /// Migrate event log discriminator (CPI)
23    pub const MIGRATE_EVENT_LOG: [u8; 8] = [189, 233, 93, 185, 92, 148, 234, 148];
24    /// `migrate_bonding_curve_creator` 外层 ix(`idls/pumpfun.json`)
25    pub const MIGRATE_BONDING_CURVE_CREATOR: [u8; 8] = [87, 124, 52, 191, 52, 38, 214, 232];
26    /// buy_v2: unified buy with quote_mint support (SOL + USDC)
27    pub const BUY_V2: [u8; 8] = [184, 23, 238, 97, 103, 197, 211, 61];
28    /// sell_v2: unified sell with quote_mint support (SOL + USDC)
29    pub const SELL_V2: [u8; 8] = [93, 246, 130, 60, 231, 233, 64, 178];
30    /// buy_exact_quote_in_v2: spend exact quote amount for min tokens out (SOL + USDC)
31    pub const BUY_EXACT_QUOTE_IN_V2: [u8; 8] = [194, 171, 28, 70, 104, 77, 91, 47];
32}
33
34/// PumpFun Program ID
35pub const PROGRAM_ID_PUBKEY: Pubkey = program_ids::PUMPFUN_PROGRAM_ID;
36
37/// Main PumpFun instruction parser
38///
39/// Outer instructions (8-byte discriminator): CREATE, CREATE_V2 从指令解析并返回事件;
40/// BUY/SELL 仍以 log 为主。Inner CPI: MIGRATE_EVENT_LOG 仅在此解析。
41pub fn parse_instruction(
42    instruction_data: &[u8],
43    accounts: &[Pubkey],
44    signature: Signature,
45    slot: u64,
46    tx_index: u64,
47    block_time_us: Option<i64>,
48    grpc_recv_us: i64,
49) -> Option<DexEvent> {
50    if instruction_data.len() < 8 {
51        return None;
52    }
53    let outer_disc: [u8; 8] = instruction_data[0..8].try_into().ok()?;
54    let data = &instruction_data[8..];
55
56    // 外层指令:Create / CreateV2(与 solana-streamer 功能对齐)
57    if outer_disc == discriminators::CREATE_V2 {
58        return parse_create_v2_instruction(
59            data,
60            accounts,
61            signature,
62            slot,
63            tx_index,
64            block_time_us,
65            grpc_recv_us,
66        );
67    }
68    if outer_disc == discriminators::CREATE {
69        return parse_create_instruction(
70            data,
71            accounts,
72            signature,
73            slot,
74            tx_index,
75            block_time_us,
76            grpc_recv_us,
77        );
78    }
79    if outer_disc == discriminators::BUY {
80        return parse_buy_instruction(
81            data,
82            accounts,
83            signature,
84            slot,
85            tx_index,
86            block_time_us,
87            grpc_recv_us,
88            "buy",
89            false,
90        );
91    }
92    if outer_disc == discriminators::BUY_EXACT_SOL_IN {
93        return parse_buy_instruction(
94            data,
95            accounts,
96            signature,
97            slot,
98            tx_index,
99            block_time_us,
100            grpc_recv_us,
101            "buy_exact_sol_in",
102            true,
103        );
104    }
105    if outer_disc == discriminators::SELL {
106        return parse_sell_instruction(
107            data,
108            accounts,
109            signature,
110            slot,
111            tx_index,
112            block_time_us,
113            grpc_recv_us,
114            "sell",
115            false,
116        );
117    }
118    if outer_disc == discriminators::BUY_V2 {
119        return parse_buy_v2_instruction(
120            data,
121            accounts,
122            signature,
123            slot,
124            tx_index,
125            block_time_us,
126            grpc_recv_us,
127            "buy_v2",
128            false,
129        );
130    }
131    if outer_disc == discriminators::BUY_EXACT_QUOTE_IN_V2 {
132        return parse_buy_v2_instruction(
133            data,
134            accounts,
135            signature,
136            slot,
137            tx_index,
138            block_time_us,
139            grpc_recv_us,
140            "buy_exact_quote_in_v2",
141            true,
142        );
143    }
144    if outer_disc == discriminators::SELL_V2 {
145        return parse_sell_v2_instruction(
146            data,
147            accounts,
148            signature,
149            slot,
150            tx_index,
151            block_time_us,
152            grpc_recv_us,
153            "sell_v2",
154        );
155    }
156
157    // Inner CPI:仅 MIGRATE 在此解析
158    if instruction_data.len() >= 16 {
159        let cpi_disc: [u8; 8] = instruction_data[8..16].try_into().ok()?;
160        if cpi_disc == discriminators::MIGRATE_EVENT_LOG {
161            return parse_migrate_log_instruction(
162                &instruction_data[16..],
163                accounts,
164                signature,
165                slot,
166                tx_index,
167                block_time_us,
168                grpc_recv_us,
169            );
170        }
171    }
172    None
173}
174
175/// Parse buy/buy_exact_sol_in instruction
176///
177/// Account indices (from pump.json IDL), 15 个固定账户:
178/// 0: global, 1: fee_recipient, 2: mint, 3: bonding_curve,
179/// 4: associated_bonding_curve, 5: associated_user, 6: user,
180/// 7: system_program, 8: token_program, 9: creator_vault,
181/// 10: event_authority, 11: program, 12: global_volume_accumulator,
182/// 13: user_volume_accumulator, 14: fee_config.
183/// remaining_accounts 可能含 bonding_curve_v2 等。
184fn parse_buy_instruction(
185    data: &[u8],
186    accounts: &[Pubkey],
187    signature: Signature,
188    slot: u64,
189    tx_index: u64,
190    block_time_us: Option<i64>,
191    grpc_recv_us: i64,
192    ix_name: &'static str,
193    exact_quote_in: bool,
194) -> Option<DexEvent> {
195    if accounts.len() < 7 {
196        return None;
197    }
198
199    // buy: amount, max_sol_cost. buy_exact_sol_in: spendable_sol_in, min_tokens_out.
200    let (first_arg, second_arg) = if data.len() >= 16 {
201        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
202    } else {
203        (0, 0)
204    };
205    let (token_amount, sol_amount, amount, max_sol_cost) = if exact_quote_in {
206        (second_arg, first_arg, 0, 0)
207    } else {
208        (first_arg, second_arg, first_arg, second_arg)
209    };
210
211    let mint = get_account(accounts, 2)?;
212    let metadata =
213        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
214
215    let trade_event = PumpFunTradeEvent {
216        metadata,
217        mint,
218        is_buy: true,
219        bonding_curve: get_account(accounts, 3).unwrap_or_default(),
220        user: get_account(accounts, 6).unwrap_or_default(),
221        sol_amount,
222        token_amount,
223        amount,
224        max_sol_cost,
225        fee_recipient: get_account(accounts, 1).unwrap_or_default(),
226        associated_bonding_curve: get_account(accounts, 4).unwrap_or_default(),
227        token_program: get_account(accounts, 8).unwrap_or_default(),
228        creator_vault: get_account(accounts, 9).unwrap_or_default(),
229        ix_name: ix_name.to_string(),
230        ..Default::default()
231    };
232
233    if exact_quote_in {
234        Some(DexEvent::PumpFunBuyExactSolIn(trade_event))
235    } else {
236        Some(DexEvent::PumpFunBuy(trade_event))
237    }
238}
239
240/// Parse sell instruction
241///
242/// Account indices (from pump.json IDL), 14 个固定账户:
243/// 0: global, 1: fee_recipient, 2: mint, 3: bonding_curve,
244/// 4: associated_bonding_curve, 5: associated_user, 6: user,
245/// 7: system_program, 8: creator_vault, 9: token_program,
246/// 10: event_authority, 11: program, 12: fee_config, 13: fee_program.
247/// remaining_accounts 可能含 user_volume_accumulator(返现)、bonding_curve_v2 等。
248fn parse_sell_instruction(
249    data: &[u8],
250    accounts: &[Pubkey],
251    signature: Signature,
252    slot: u64,
253    tx_index: u64,
254    block_time_us: Option<i64>,
255    grpc_recv_us: i64,
256    ix_name: &'static str,
257    v2_accounts: bool,
258) -> Option<DexEvent> {
259    let min_accounts = if v2_accounts { 26 } else { 7 };
260    if accounts.len() < min_accounts {
261        return None;
262    }
263
264    // Parse args: amount (u64), min_sol_output (u64)
265    let (amount, min_sol_output) = if data.len() >= 16 {
266        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
267    } else {
268        (0, 0)
269    };
270    let token_amount = amount;
271    let sol_amount = min_sol_output;
272
273    let (
274        mint_idx,
275        bonding_curve_idx,
276        associated_bonding_curve_idx,
277        user_idx,
278        fee_recipient_idx,
279        token_program_idx,
280        creator_vault_idx,
281    ) = if v2_accounts { (1, 10, 11, 13, 6, 3, 16) } else { (2, 3, 4, 6, 1, 9, 8) };
282    let mint = get_account(accounts, mint_idx)?;
283    let metadata =
284        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
285
286    Some(DexEvent::PumpFunSell(PumpFunTradeEvent {
287        metadata,
288        mint,
289        is_buy: false,
290        bonding_curve: get_account(accounts, bonding_curve_idx).unwrap_or_default(),
291        user: get_account(accounts, user_idx).unwrap_or_default(),
292        sol_amount,
293        token_amount,
294        amount,
295        min_sol_output,
296        fee_recipient: get_account(accounts, fee_recipient_idx).unwrap_or_default(),
297        associated_bonding_curve: get_account(accounts, associated_bonding_curve_idx)
298            .unwrap_or_default(),
299        token_program: get_account(accounts, token_program_idx).unwrap_or_default(),
300        creator_vault: get_account(accounts, creator_vault_idx).unwrap_or_default(),
301        ix_name: ix_name.to_string(),
302        ..Default::default()
303    }))
304}
305
306fn parse_buy_v2_instruction(
307    data: &[u8],
308    accounts: &[Pubkey],
309    signature: Signature,
310    slot: u64,
311    tx_index: u64,
312    block_time_us: Option<i64>,
313    grpc_recv_us: i64,
314    ix_name: &'static str,
315    exact_quote_in: bool,
316) -> Option<DexEvent> {
317    const MIN_ACC: usize = 27;
318    if accounts.len() < MIN_ACC {
319        return None;
320    }
321
322    // buy_v2: amount, max_sol_cost. buy_exact_quote_in_v2: spendable quote in, min_tokens_out.
323    let (first_arg, second_arg) = if data.len() >= 16 {
324        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
325    } else {
326        (0, 0)
327    };
328    let (token_amount, sol_amount, amount, max_sol_cost) = if exact_quote_in {
329        (second_arg, first_arg, 0, 0)
330    } else {
331        (first_arg, second_arg, first_arg, second_arg)
332    };
333
334    let metadata =
335        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
336    let trade_event = PumpFunTradeEvent {
337        metadata,
338        mint: accounts[1],
339        is_buy: true,
340        bonding_curve: accounts[10],
341        associated_bonding_curve: accounts[11],
342        user: accounts[13],
343        sol_amount,
344        token_amount,
345        amount,
346        max_sol_cost,
347        fee_recipient: accounts[6],
348        token_program: accounts[3],
349        creator_vault: accounts[16],
350        ix_name: ix_name.to_string(),
351        ..Default::default()
352    };
353
354    if exact_quote_in {
355        Some(DexEvent::PumpFunBuyExactSolIn(trade_event))
356    } else {
357        Some(DexEvent::PumpFunBuy(trade_event))
358    }
359}
360
361fn parse_sell_v2_instruction(
362    data: &[u8],
363    accounts: &[Pubkey],
364    signature: Signature,
365    slot: u64,
366    tx_index: u64,
367    block_time_us: Option<i64>,
368    grpc_recv_us: i64,
369    ix_name: &'static str,
370) -> Option<DexEvent> {
371    parse_sell_instruction(
372        data,
373        accounts,
374        signature,
375        slot,
376        tx_index,
377        block_time_us,
378        grpc_recv_us,
379        ix_name,
380        true,
381    )
382}
383
384/// Parse create instruction (legacy)
385///
386/// Account indices (from pump.json):
387/// 0: mint, 1: mint_authority, 2: bonding_curve, 3: associated_bonding_curve,
388/// 4: global, 5: mpl_token_metadata, 6: metadata, 7: user. 共至少 8 个账户。
389fn parse_create_instruction(
390    data: &[u8],
391    accounts: &[Pubkey],
392    signature: Signature,
393    slot: u64,
394    tx_index: u64,
395    block_time_us: Option<i64>,
396    grpc_recv_us: i64,
397) -> Option<DexEvent> {
398    if accounts.len() < 8 {
399        return None;
400    }
401
402    let mut offset = 0;
403
404    // Parse args: name (string), symbol (string), uri (string), creator (pubkey)
405    // String format: 4-byte length prefix + content
406    let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
407        offset += len;
408        s.to_string()
409    } else {
410        String::new()
411    };
412
413    let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
414        offset += len;
415        s.to_string()
416    } else {
417        String::new()
418    };
419
420    let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
421        offset += len;
422        s.to_string()
423    } else {
424        String::new()
425    };
426
427    // 读取 mint, bonding_curve, user, creator (在 name, symbol, uri 之后)
428    if data.len() < offset + 32 + 32 + 32 + 32 {
429        return None;
430    }
431
432    let mint = read_pubkey(data, offset).unwrap_or_default();
433    offset += 32;
434
435    let bonding_curve = read_pubkey(data, offset).unwrap_or_default();
436    offset += 32;
437
438    let user = read_pubkey(data, offset).unwrap_or_default();
439    offset += 32;
440
441    let creator = read_pubkey(data, offset).unwrap_or_default();
442
443    let metadata =
444        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
445
446    Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
447        metadata,
448        name,
449        symbol,
450        uri,
451        mint,
452        bonding_curve,
453        user,
454        creator,
455        ..Default::default()
456    }))
457}
458
459/// Parse create_v2 instruction (SPL-22;Mayhem 由 **data** 中 `is_mayhem_mode` 决定,不要用 mayhem 程序账户是否非空推断)
460///
461/// Account indices (idl pumpfun.json create_v2): 0 mint, 1 mint_authority, 2 bonding_curve,
462/// 3 associated_bonding_curve, 4 global, 5 user, 6 system_program, 7 token_program,
463/// 8 associated_token_program, 9 mayhem_program_id, 10 global_params, 11 sol_vault,
464/// 12 mayhem_state, 13 mayhem_token_vault, 14 event_authority, 15 program. 共 16 个账户。
465/// Instruction args (after disc): name, symbol, uri, creator, is_mayhem_mode (`bool`), is_cashback_enabled (`OptionBool` = 1-byte bool on wire)。
466/// Guard: return None when accounts.len() < 16 to avoid index out of bounds (e.g. ALT-loaded tx).
467fn parse_create_v2_instruction(
468    data: &[u8],
469    accounts: &[Pubkey],
470    signature: Signature,
471    slot: u64,
472    tx_index: u64,
473    block_time_us: Option<i64>,
474    grpc_recv_us: i64,
475) -> Option<DexEvent> {
476    const CREATE_V2_MIN_ACCOUNTS: usize = 16;
477    if accounts.len() < CREATE_V2_MIN_ACCOUNTS {
478        return None;
479    }
480    let acc = &accounts[0..CREATE_V2_MIN_ACCOUNTS];
481
482    // IDL args: name, symbol, uri, creator, is_mayhem_mode, is_cashback_enabled — mint/bc/user 仅在 accounts
483    let mut offset = 0usize;
484    let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
485        offset += len;
486        s.to_string()
487    } else {
488        String::new()
489    };
490    let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
491        offset += len;
492        s.to_string()
493    } else {
494        String::new()
495    };
496    let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
497        offset += len;
498        s.to_string()
499    } else {
500        String::new()
501    };
502    if data.len() < offset + 32 + 1 {
503        return None;
504    }
505    let creator = read_pubkey(data, offset)?;
506    offset += 32;
507    let is_mayhem_mode = read_bool(data, offset)?;
508    offset += 1;
509    let is_cashback_enabled = read_option_bool_idl(data, offset).unwrap_or(false);
510
511    let mint = acc[0];
512    let bonding_curve = acc[2];
513    let user = acc[5];
514
515    let metadata =
516        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
517
518    Some(DexEvent::PumpFunCreateV2(PumpFunCreateV2TokenEvent {
519        metadata,
520        name,
521        symbol,
522        uri,
523        mint,
524        bonding_curve,
525        user,
526        creator,
527        mint_authority: acc[1],
528        associated_bonding_curve: acc[3],
529        global: acc[4],
530        system_program: acc[6],
531        token_program: acc[7],
532        associated_token_program: acc[8],
533        mayhem_program_id: acc[9],
534        global_params: acc[10],
535        sol_vault: acc[11],
536        mayhem_state: acc[12],
537        mayhem_token_vault: acc[13],
538        event_authority: acc[14],
539        program: acc[15],
540        is_mayhem_mode,
541        is_cashback_enabled,
542        ..Default::default()
543    }))
544}
545
546/// Parse Migrate CPI instruction
547#[allow(unused_variables)]
548fn parse_migrate_log_instruction(
549    data: &[u8],
550    accounts: &[Pubkey],
551    signature: Signature,
552    slot: u64,
553    tx_index: u64,
554    block_time_us: Option<i64>,
555    rpc_recv_us: i64,
556) -> Option<DexEvent> {
557    let mut offset = 0;
558
559    // user (Pubkey - 32 bytes)
560    let user = read_pubkey(data, offset)?;
561    offset += 32;
562
563    // mint (Pubkey - 32 bytes)
564    let mint = read_pubkey(data, offset)?;
565    offset += 32;
566
567    // mintAmount (u64 - 8 bytes)
568    let mint_amount = read_u64_le(data, offset)?;
569    offset += 8;
570
571    // solAmount (u64 - 8 bytes)
572    let sol_amount = read_u64_le(data, offset)?;
573    offset += 8;
574
575    // poolMigrationFee (u64 - 8 bytes)
576    let pool_migration_fee = read_u64_le(data, offset)?;
577    offset += 8;
578
579    // bondingCurve (Pubkey - 32 bytes)
580    let bonding_curve = read_pubkey(data, offset)?;
581    offset += 32;
582
583    // timestamp (i64 - 8 bytes)
584    let timestamp = read_u64_le(data, offset)? as i64;
585    offset += 8;
586
587    // pool (Pubkey - 32 bytes)
588    let pool = read_pubkey(data, offset)?;
589
590    let metadata =
591        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), rpc_recv_us);
592
593    Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
594        metadata,
595        user,
596        mint,
597        mint_amount,
598        sol_amount,
599        pool_migration_fee,
600        bonding_curve,
601        timestamp,
602        pool,
603    }))
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    fn instruction_data(discriminator: [u8; 8], first: u64, second: u64) -> Vec<u8> {
611        let mut data = Vec::with_capacity(24);
612        data.extend_from_slice(&discriminator);
613        data.extend_from_slice(&first.to_le_bytes());
614        data.extend_from_slice(&second.to_le_bytes());
615        data
616    }
617
618    fn accounts(n: usize) -> Vec<Pubkey> {
619        (0..n).map(|_| Pubkey::new_unique()).collect()
620    }
621
622    #[test]
623    fn pumpfun_buy_instruction_exposes_raw_args() {
624        let data = instruction_data(discriminators::BUY, 123, 456);
625        let acc = accounts(15);
626        let event =
627            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
628
629        match event {
630            DexEvent::PumpFunBuy(t) => {
631                assert_eq!(t.amount, 123);
632                assert_eq!(t.max_sol_cost, 456);
633                assert_eq!(t.min_sol_output, 0);
634                assert_eq!(t.token_amount, 123);
635                assert_eq!(t.sol_amount, 456);
636                assert_eq!(t.ix_name, "buy");
637            }
638            other => panic!("expected PumpFunBuy, got {other:?}"),
639        }
640    }
641
642    #[test]
643    fn pumpfun_sell_instruction_exposes_raw_args() {
644        let data = instruction_data(discriminators::SELL, 321, 654);
645        let acc = accounts(14);
646        let event =
647            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
648
649        match event {
650            DexEvent::PumpFunSell(t) => {
651                assert_eq!(t.amount, 321);
652                assert_eq!(t.max_sol_cost, 0);
653                assert_eq!(t.min_sol_output, 654);
654                assert_eq!(t.token_amount, 321);
655                assert_eq!(t.sol_amount, 654);
656                assert_eq!(t.ix_name, "sell");
657            }
658            other => panic!("expected PumpFunSell, got {other:?}"),
659        }
660    }
661
662    #[test]
663    fn pumpfun_v2_instruction_args_use_v2_account_layout() {
664        let data = instruction_data(discriminators::BUY_V2, 777, 888);
665        let acc = accounts(27);
666        let event =
667            parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
668
669        match event {
670            DexEvent::PumpFunBuy(t) => {
671                assert_eq!(t.amount, 777);
672                assert_eq!(t.max_sol_cost, 888);
673                assert_eq!(t.mint, acc[1]);
674                assert_eq!(t.bonding_curve, acc[10]);
675                assert_eq!(t.associated_bonding_curve, acc[11]);
676                assert_eq!(t.user, acc[13]);
677                assert_eq!(t.ix_name, "buy_v2");
678            }
679            other => panic!("expected PumpFunBuy, got {other:?}"),
680        }
681    }
682}