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