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
80    // Inner CPI:仅 MIGRATE 在此解析
81    if instruction_data.len() >= 16 {
82        let cpi_disc: [u8; 8] = instruction_data[8..16].try_into().ok()?;
83        if cpi_disc == discriminators::MIGRATE_EVENT_LOG {
84            return parse_migrate_log_instruction(
85                &instruction_data[16..],
86                accounts,
87                signature,
88                slot,
89                tx_index,
90                block_time_us,
91                grpc_recv_us,
92            );
93        }
94    }
95    None
96}
97
98/// Parse buy/buy_exact_sol_in instruction
99///
100/// Account indices (from pump.json IDL), 15 个固定账户:
101/// 0: global, 1: fee_recipient, 2: mint, 3: bonding_curve,
102/// 4: associated_bonding_curve, 5: associated_user, 6: user,
103/// 7: system_program, 8: token_program, 9: creator_vault,
104/// 10: event_authority, 11: program, 12: global_volume_accumulator,
105/// 13: user_volume_accumulator, 14: fee_config.
106/// remaining_accounts 可能含 bonding_curve_v2 等。
107#[allow(dead_code)]
108fn parse_buy_instruction(
109    data: &[u8],
110    accounts: &[Pubkey],
111    signature: Signature,
112    slot: u64,
113    tx_index: u64,
114    block_time_us: Option<i64>,
115    grpc_recv_us: i64,
116) -> Option<DexEvent> {
117    if accounts.len() < 7 {
118        return None;
119    }
120
121    // Parse args: amount/spendable_sol_in (u64), max_sol_cost/min_tokens_out (u64)
122    let (sol_amount, token_amount) = if data.len() >= 16 {
123        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
124    } else {
125        (0, 0)
126    };
127
128    let mint = get_account(accounts, 2)?;
129    let metadata =
130        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
131
132    Some(DexEvent::PumpFunTrade(PumpFunTradeEvent {
133        metadata,
134        mint,
135        is_buy: true,
136        bonding_curve: get_account(accounts, 3).unwrap_or_default(),
137        user: get_account(accounts, 6).unwrap_or_default(),
138        sol_amount,
139        token_amount,
140        fee_recipient: get_account(accounts, 1).unwrap_or_default(),
141        ..Default::default()
142    }))
143}
144
145/// Parse sell instruction
146///
147/// Account indices (from pump.json IDL), 14 个固定账户:
148/// 0: global, 1: fee_recipient, 2: mint, 3: bonding_curve,
149/// 4: associated_bonding_curve, 5: associated_user, 6: user,
150/// 7: system_program, 8: creator_vault, 9: token_program,
151/// 10: event_authority, 11: program, 12: fee_config, 13: fee_program.
152/// remaining_accounts 可能含 user_volume_accumulator(返现)、bonding_curve_v2 等。
153#[allow(dead_code)]
154fn parse_sell_instruction(
155    data: &[u8],
156    accounts: &[Pubkey],
157    signature: Signature,
158    slot: u64,
159    tx_index: u64,
160    block_time_us: Option<i64>,
161    grpc_recv_us: i64,
162) -> Option<DexEvent> {
163    if accounts.len() < 7 {
164        return None;
165    }
166
167    // Parse args: amount (u64), min_sol_output (u64)
168    let (token_amount, sol_amount) = if data.len() >= 16 {
169        (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
170    } else {
171        (0, 0)
172    };
173
174    let mint = get_account(accounts, 2)?;
175    let metadata =
176        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
177
178    Some(DexEvent::PumpFunTrade(PumpFunTradeEvent {
179        metadata,
180        mint,
181        is_buy: false,
182        bonding_curve: get_account(accounts, 3).unwrap_or_default(),
183        user: get_account(accounts, 6).unwrap_or_default(),
184        sol_amount,
185        token_amount,
186        fee_recipient: get_account(accounts, 1).unwrap_or_default(),
187        ..Default::default()
188    }))
189}
190
191/// Parse create instruction (legacy)
192///
193/// Account indices (from pump.json):
194/// 0: mint, 1: mint_authority, 2: bonding_curve, 3: associated_bonding_curve,
195/// 4: global, 5: mpl_token_metadata, 6: metadata, 7: user. 共至少 8 个账户。
196fn parse_create_instruction(
197    data: &[u8],
198    accounts: &[Pubkey],
199    signature: Signature,
200    slot: u64,
201    tx_index: u64,
202    block_time_us: Option<i64>,
203    grpc_recv_us: i64,
204) -> Option<DexEvent> {
205    if accounts.len() < 8 {
206        return None;
207    }
208
209    let mut offset = 0;
210
211    // Parse args: name (string), symbol (string), uri (string), creator (pubkey)
212    // String format: 4-byte length prefix + content
213    let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
214        offset += len;
215        s.to_string()
216    } else {
217        String::new()
218    };
219
220    let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
221        offset += len;
222        s.to_string()
223    } else {
224        String::new()
225    };
226
227    let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
228        offset += len;
229        s.to_string()
230    } else {
231        String::new()
232    };
233
234    // 读取 mint, bonding_curve, user, creator (在 name, symbol, uri 之后)
235    if data.len() < offset + 32 + 32 + 32 + 32 {
236        return None;
237    }
238
239    let mint = read_pubkey(data, offset).unwrap_or_default();
240    offset += 32;
241
242    let bonding_curve = read_pubkey(data, offset).unwrap_or_default();
243    offset += 32;
244
245    let user = read_pubkey(data, offset).unwrap_or_default();
246    offset += 32;
247
248    let creator = read_pubkey(data, offset).unwrap_or_default();
249
250    let metadata =
251        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
252
253    Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
254        metadata,
255        name,
256        symbol,
257        uri,
258        mint,
259        bonding_curve,
260        user,
261        creator,
262        ..Default::default()
263    }))
264}
265
266/// Parse create_v2 instruction (SPL-22;Mayhem 由 **data** 中 `is_mayhem_mode` 决定,不要用 mayhem 程序账户是否非空推断)
267///
268/// Account indices (idl pumpfun.json create_v2): 0 mint, 1 mint_authority, 2 bonding_curve,
269/// 3 associated_bonding_curve, 4 global, 5 user, 6 system_program, 7 token_program,
270/// 8 associated_token_program, 9 mayhem_program_id, 10 global_params, 11 sol_vault,
271/// 12 mayhem_state, 13 mayhem_token_vault, 14 event_authority, 15 program. 共 16 个账户。
272/// Instruction args (after disc): name, symbol, uri, creator, is_mayhem_mode (`bool`), is_cashback_enabled (`OptionBool` = 1-byte bool on wire)。
273/// Guard: return None when accounts.len() < 16 to avoid index out of bounds (e.g. ALT-loaded tx).
274fn parse_create_v2_instruction(
275    data: &[u8],
276    accounts: &[Pubkey],
277    signature: Signature,
278    slot: u64,
279    tx_index: u64,
280    block_time_us: Option<i64>,
281    grpc_recv_us: i64,
282) -> Option<DexEvent> {
283    const CREATE_V2_MIN_ACCOUNTS: usize = 16;
284    if accounts.len() < CREATE_V2_MIN_ACCOUNTS {
285        return None;
286    }
287    let acc = &accounts[0..CREATE_V2_MIN_ACCOUNTS];
288
289    // IDL args: name, symbol, uri, creator, is_mayhem_mode, is_cashback_enabled — mint/bc/user 仅在 accounts
290    let mut offset = 0usize;
291    let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
292        offset += len;
293        s.to_string()
294    } else {
295        String::new()
296    };
297    let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
298        offset += len;
299        s.to_string()
300    } else {
301        String::new()
302    };
303    let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
304        offset += len;
305        s.to_string()
306    } else {
307        String::new()
308    };
309    if data.len() < offset + 32 + 1 {
310        return None;
311    }
312    let creator = read_pubkey(data, offset)?;
313    offset += 32;
314    let is_mayhem_mode = read_bool(data, offset)?;
315    offset += 1;
316    let is_cashback_enabled = read_option_bool_idl(data, offset).unwrap_or(false);
317
318    let mint = acc[0];
319    let bonding_curve = acc[2];
320    let user = acc[5];
321
322    let metadata =
323        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
324
325    Some(DexEvent::PumpFunCreateV2(PumpFunCreateV2TokenEvent {
326        metadata,
327        name,
328        symbol,
329        uri,
330        mint,
331        bonding_curve,
332        user,
333        creator,
334        mint_authority: acc[1],
335        associated_bonding_curve: acc[3],
336        global: acc[4],
337        system_program: acc[6],
338        token_program: acc[7],
339        associated_token_program: acc[8],
340        mayhem_program_id: acc[9],
341        global_params: acc[10],
342        sol_vault: acc[11],
343        mayhem_state: acc[12],
344        mayhem_token_vault: acc[13],
345        event_authority: acc[14],
346        program: acc[15],
347        is_mayhem_mode,
348        is_cashback_enabled,
349        ..Default::default()
350    }))
351}
352
353/// Parse Migrate CPI instruction
354#[allow(unused_variables)]
355fn parse_migrate_log_instruction(
356    data: &[u8],
357    accounts: &[Pubkey],
358    signature: Signature,
359    slot: u64,
360    tx_index: u64,
361    block_time_us: Option<i64>,
362    rpc_recv_us: i64,
363) -> Option<DexEvent> {
364    let mut offset = 0;
365
366    // user (Pubkey - 32 bytes)
367    let user = read_pubkey(data, offset)?;
368    offset += 32;
369
370    // mint (Pubkey - 32 bytes)
371    let mint = read_pubkey(data, offset)?;
372    offset += 32;
373
374    // mintAmount (u64 - 8 bytes)
375    let mint_amount = read_u64_le(data, offset)?;
376    offset += 8;
377
378    // solAmount (u64 - 8 bytes)
379    let sol_amount = read_u64_le(data, offset)?;
380    offset += 8;
381
382    // poolMigrationFee (u64 - 8 bytes)
383    let pool_migration_fee = read_u64_le(data, offset)?;
384    offset += 8;
385
386    // bondingCurve (Pubkey - 32 bytes)
387    let bonding_curve = read_pubkey(data, offset)?;
388    offset += 32;
389
390    // timestamp (i64 - 8 bytes)
391    let timestamp = read_u64_le(data, offset)? as i64;
392    offset += 8;
393
394    // pool (Pubkey - 32 bytes)
395    let pool = read_pubkey(data, offset)?;
396
397    let metadata =
398        create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), rpc_recv_us);
399
400    Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
401        metadata,
402        user,
403        mint,
404        mint_amount,
405        sol_amount,
406        pool_migration_fee,
407        bonding_curve,
408        timestamp,
409        pool,
410    }))
411}