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