Skip to main content

sol_parser_sdk/instr/
pump_inner.rs

1//! PumpFun Inner Instruction 解析器
2//!
3//! Inner instructions 使用 16 字节的 discriminator(与 8 字节的 instruction 不同)
4//! 这些是程序内部通过 CPI (Cross-Program Invocation) 触发的事件
5//!
6//! ## 解析器插件系统
7//!
8//! 本模块提供两种可插拔的解析器实现:
9//!
10//! ### 1. Borsh 反序列化解析器(默认,推荐)
11//! - **启用**: `cargo build --features parse-borsh` (默认)
12//! - **优点**: 类型安全、代码简洁、易维护、自动验证
13//! - **适用**: 一般场景、需要稳定性和可维护性的项目
14//!
15//! ### 2. 零拷贝解析器(高性能)
16//! - **启用**: `cargo build --features parse-zero-copy --no-default-features`
17//! - **优点**: 最快、零拷贝、无验证开销、适合超高频场景
18//! - **适用**: 性能关键路径、每秒数万次解析的场景
19//!
20//! ## 使用示例
21//!
22//! ```bash
23//! # 使用 Borsh 解析器(推荐,默认)
24//! cargo build --release
25//!
26//! # 使用零拷贝解析器(极致性能)
27//! cargo build --release --features parse-zero-copy --no-default-features
28//! ```
29
30use crate::core::events::*;
31
32
33// ============================================================================
34// Inner Instruction Discriminators (16 bytes)
35// ============================================================================
36
37/// PumpFun inner instruction discriminators
38pub mod discriminators {
39    /// TradeEvent discriminator (CPI log event)
40    /// discriminator = sha256("event:TradeEvent")[..16]
41    pub const TRADE_EVENT: [u8; 16] = [
42        189, 219, 127, 211, 78, 230, 97, 238, // 前8字节
43        155, 167, 108, 32, 122, 76, 173, 64,  // 后8字节
44    ];
45
46    /// CreateTokenEvent discriminator
47    pub const CREATE_TOKEN_EVENT: [u8; 16] = [
48        27, 114, 169, 77, 222, 235, 99, 118,
49        155, 167, 108, 32, 122, 76, 173, 64,
50    ];
51
52    /// MigrateEvent discriminator (PumpAmm migration)
53    pub const COMPLETE_PUMP_AMM_MIGRATION_EVENT: [u8; 16] = [
54        189, 233, 93, 185, 92, 148, 234, 148,
55        155, 167, 108, 32, 122, 76, 173, 64,
56    ];
57}
58
59// ============================================================================
60// 零拷贝读取函数(仅用于 zero-copy 解析器)
61// ============================================================================
62
63#[cfg(feature = "parse-zero-copy")]
64#[inline(always)]
65unsafe fn read_u64_unchecked(data: &[u8], offset: usize) -> u64 {
66    let ptr = data.as_ptr().add(offset) as *const u64;
67    u64::from_le(ptr.read_unaligned())
68}
69
70#[cfg(feature = "parse-zero-copy")]
71#[inline(always)]
72unsafe fn read_i64_unchecked(data: &[u8], offset: usize) -> i64 {
73    let ptr = data.as_ptr().add(offset) as *const i64;
74    i64::from_le(ptr.read_unaligned())
75}
76
77#[cfg(feature = "parse-zero-copy")]
78#[inline(always)]
79unsafe fn read_bool_unchecked(data: &[u8], offset: usize) -> bool {
80    *data.get_unchecked(offset) == 1
81}
82
83#[cfg(feature = "parse-zero-copy")]
84#[inline(always)]
85unsafe fn read_pubkey_unchecked(data: &[u8], offset: usize) -> solana_sdk::pubkey::Pubkey {
86    use solana_sdk::pubkey::Pubkey;
87    let ptr = data.as_ptr().add(offset);
88    let mut bytes = [0u8; 32];
89    std::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 32);
90    Pubkey::new_from_array(bytes)
91}
92
93#[cfg(feature = "parse-zero-copy")]
94#[inline(always)]
95unsafe fn read_str_unchecked(data: &[u8], offset: usize) -> Option<(&str, usize)> {
96    if data.len() < offset + 4 {
97        return None;
98    }
99
100    let len = read_u32_unchecked(data, offset) as usize;
101    if data.len() < offset + 4 + len {
102        return None;
103    }
104
105    let string_bytes = &data[offset + 4..offset + 4 + len];
106    let s = std::str::from_utf8_unchecked(string_bytes);
107    Some((s, 4 + len))
108}
109
110#[cfg(feature = "parse-zero-copy")]
111#[inline(always)]
112unsafe fn read_u32_unchecked(data: &[u8], offset: usize) -> u32 {
113    let ptr = data.as_ptr().add(offset) as *const u32;
114    u32::from_le(ptr.read_unaligned())
115}
116
117// ============================================================================
118// Inner Instruction 解析函数
119// ============================================================================
120
121/// 解析 PumpFun inner instruction (统一入口)
122///
123/// # 参数
124/// - `discriminator`: 16 字节的 inner instruction discriminator
125/// - `data`: inner instruction 数据(不含 discriminator)
126/// - `metadata`: 事件元数据
127///
128/// # 返回
129/// 解析成功返回 `Some(DexEvent)`,否则返回 `None`
130///
131/// # is_created_buy
132/// 当同笔交易内存在 PumpFun create 时由外层传入 true,表示创建者首次买入,与 log 解析行为一致
133#[inline]
134pub fn parse_pumpfun_inner_instruction(
135    discriminator: &[u8; 16],
136    data: &[u8],
137    metadata: EventMetadata,
138    is_created_buy: bool,
139) -> Option<DexEvent> {
140    match discriminator {
141        &discriminators::TRADE_EVENT => parse_trade_event_inner(data, metadata, is_created_buy),
142        &discriminators::CREATE_TOKEN_EVENT => parse_create_event_inner(data, metadata),
143        &discriminators::COMPLETE_PUMP_AMM_MIGRATION_EVENT => parse_migrate_event_inner(data, metadata),
144        _ => None,
145    }
146}
147
148// ============================================================================
149// Trade 事件解析器
150// ============================================================================
151
152/// 解析 TradeEvent(统一入口)
153///
154/// 根据编译时的 feature flag 自动选择解析器实现
155#[inline(always)]
156fn parse_trade_event_inner(data: &[u8], metadata: EventMetadata, is_created_buy: bool) -> Option<DexEvent> {
157    #[cfg(feature = "parse-borsh")]
158    {
159        parse_trade_event_inner_borsh(data, metadata, is_created_buy)
160    }
161
162    #[cfg(feature = "parse-zero-copy")]
163    {
164        parse_trade_event_inner_zero_copy(data, metadata, is_created_buy)
165    }
166}
167
168/// Borsh 反序列化解析器 - Trade 事件
169///
170/// **优点**: 类型安全、代码简洁、自动验证
171#[cfg(feature = "parse-borsh")]
172#[inline(always)]
173fn parse_trade_event_inner_borsh(data: &[u8], metadata: EventMetadata, is_created_buy: bool) -> Option<DexEvent> {
174    // PumpFun TradeEvent 不是固定大小,因为包含 String 字段
175    // 我们需要解析整个数据
176    let mut event = borsh::from_slice::<PumpFunTradeEvent>(data).ok()?;
177    event.metadata = metadata;
178    event.is_created_buy = is_created_buy;
179    event.is_cashback_coin = event.cashback_fee_basis_points > 0;
180
181    // 根据 ix_name 返回不同的事件类型
182    match event.ix_name.as_str() {
183        "buy" => Some(DexEvent::PumpFunBuy(event)),
184        "sell" => Some(DexEvent::PumpFunSell(event)),
185        "buy_exact_sol_in" => Some(DexEvent::PumpFunBuyExactSolIn(event)),
186        _ => Some(DexEvent::PumpFunTrade(event)),
187    }
188}
189
190/// 零拷贝解析器 - Trade 事件
191///
192/// **优点**: 最快、零拷贝、无验证开销
193#[cfg(feature = "parse-zero-copy")]
194#[inline(always)]
195fn parse_trade_event_inner_zero_copy(data: &[u8], metadata: EventMetadata, is_created_buy: bool) -> Option<DexEvent> {
196    unsafe {
197        // 快速边界检查
198        if data.len() < 32 + 8 + 8 + 1 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 8 + 8 + 32 + 8 + 8 {
199            return None;
200        }
201
202        let mut offset = 0;
203
204        let mint = read_pubkey_unchecked(data, offset);
205        offset += 32;
206
207        let sol_amount = read_u64_unchecked(data, offset);
208        offset += 8;
209
210        let token_amount = read_u64_unchecked(data, offset);
211        offset += 8;
212
213        let is_buy = read_bool_unchecked(data, offset);
214        offset += 1;
215
216        let user = read_pubkey_unchecked(data, offset);
217        offset += 32;
218
219        let timestamp = read_i64_unchecked(data, offset);
220        offset += 8;
221
222        let virtual_sol_reserves = read_u64_unchecked(data, offset);
223        offset += 8;
224
225        let virtual_token_reserves = read_u64_unchecked(data, offset);
226        offset += 8;
227
228        let real_sol_reserves = read_u64_unchecked(data, offset);
229        offset += 8;
230
231        let real_token_reserves = read_u64_unchecked(data, offset);
232        offset += 8;
233
234        let fee_recipient = read_pubkey_unchecked(data, offset);
235        offset += 32;
236
237        let fee_basis_points = read_u64_unchecked(data, offset);
238        offset += 8;
239
240        let fee = read_u64_unchecked(data, offset);
241        offset += 8;
242
243        let creator = read_pubkey_unchecked(data, offset);
244        offset += 32;
245
246        let creator_fee_basis_points = read_u64_unchecked(data, offset);
247        offset += 8;
248
249        let creator_fee = read_u64_unchecked(data, offset);
250        offset += 8;
251
252        // 可选字段
253        let track_volume = if offset < data.len() {
254            read_bool_unchecked(data, offset)
255        } else {
256            false
257        };
258        offset += 1;
259
260        let total_unclaimed_tokens = if offset + 8 <= data.len() {
261            read_u64_unchecked(data, offset)
262        } else {
263            0
264        };
265        offset += 8;
266
267        let total_claimed_tokens = if offset + 8 <= data.len() {
268            read_u64_unchecked(data, offset)
269        } else {
270            0
271        };
272        offset += 8;
273
274        let current_sol_volume = if offset + 8 <= data.len() {
275            read_u64_unchecked(data, offset)
276        } else {
277            0
278        };
279        offset += 8;
280
281        let last_update_timestamp = if offset + 8 <= data.len() {
282            read_i64_unchecked(data, offset)
283        } else {
284            0
285        };
286        offset += 8;
287
288        let (ix_name, ix_name_len) = if offset + 4 <= data.len() {
289            if let Some((s, consumed)) = read_str_unchecked(data, offset) {
290                (s.to_string(), consumed)
291            } else {
292                (String::new(), 0)
293            }
294        } else {
295            (String::new(), 0)
296        };
297        offset += ix_name_len;
298
299        // TradeEvent 新增字段 (PUMP_CASHBACK_README): mayhem_mode, cashback_fee_basis_points, cashback
300        let mayhem_mode = if offset + 1 <= data.len() {
301            read_bool_unchecked(data, offset)
302        } else {
303            false
304        };
305        offset += 1;
306        let cashback_fee_basis_points = if offset + 8 <= data.len() {
307            read_u64_unchecked(data, offset)
308        } else {
309            0
310        };
311        offset += 8;
312        let cashback = if offset + 8 <= data.len() {
313            read_u64_unchecked(data, offset)
314        } else {
315            0
316        };
317
318        // Inner instruction 只包含日志数据,不含指令上下文账户;is_created_buy 由外层根据同 tx 是否含 create 传入
319        let trade_event = PumpFunTradeEvent {
320            metadata,
321            mint,
322            sol_amount,
323            token_amount,
324            is_buy,
325            is_created_buy,
326            user,
327            timestamp,
328            virtual_sol_reserves,
329            virtual_token_reserves,
330            real_sol_reserves,
331            real_token_reserves,
332            fee_recipient,
333            fee_basis_points,
334            fee,
335            creator,
336            creator_fee_basis_points,
337            creator_fee,
338            track_volume,
339            total_unclaimed_tokens,
340            total_claimed_tokens,
341            current_sol_volume,
342            last_update_timestamp,
343            ix_name: ix_name.clone(),
344            mayhem_mode,
345            cashback_fee_basis_points,
346            cashback,
347            is_cashback_coin: cashback_fee_basis_points > 0,
348            ..Default::default() // 其他账户字段由 instruction 提供
349        };
350
351        // 根据 ix_name 返回不同的事件类型
352        match ix_name.as_str() {
353            "buy" => Some(DexEvent::PumpFunBuy(trade_event)),
354            "sell" => Some(DexEvent::PumpFunSell(trade_event)),
355            "buy_exact_sol_in" => Some(DexEvent::PumpFunBuyExactSolIn(trade_event)),
356            _ => Some(DexEvent::PumpFunTrade(trade_event)),
357        }
358    }
359}
360
361// ============================================================================
362// Create 事件解析器
363// ============================================================================
364
365/// 解析 CreateTokenEvent(统一入口)
366///
367/// 根据编译时的 feature flag 自动选择解析器实现
368#[inline(always)]
369fn parse_create_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
370    #[cfg(feature = "parse-borsh")]
371    {
372        parse_create_event_inner_borsh(data, metadata)
373    }
374
375    #[cfg(feature = "parse-zero-copy")]
376    {
377        parse_create_event_inner_zero_copy(data, metadata)
378    }
379}
380
381/// Borsh 反序列化解析器 - Create 事件
382///
383/// **优点**: 类型安全、代码简洁、自动验证
384#[cfg(feature = "parse-borsh")]
385#[inline(always)]
386fn parse_create_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
387    // CreateTokenEvent 包含多个 String 字段,不是固定大小
388    let mut event = borsh::from_slice::<PumpFunCreateTokenEvent>(data).ok()?;
389    event.metadata = metadata;
390    Some(DexEvent::PumpFunCreate(event))
391}
392
393/// 零拷贝解析器 - Create 事件
394///
395/// **优点**: 最快、零拷贝、无验证开销
396#[cfg(feature = "parse-zero-copy")]
397#[inline(always)]
398fn parse_create_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
399    unsafe {
400        let mut offset = 0;
401
402        let (name, name_len) = read_str_unchecked(data, offset)?;
403        offset += name_len;
404
405        let (symbol, symbol_len) = read_str_unchecked(data, offset)?;
406        offset += symbol_len;
407
408        let (uri, uri_len) = read_str_unchecked(data, offset)?;
409        offset += uri_len;
410
411        if data.len() < offset + 32 + 32 + 32 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 1 {
412            return None;
413        }
414
415        let mint = read_pubkey_unchecked(data, offset);
416        offset += 32;
417
418        let bonding_curve = read_pubkey_unchecked(data, offset);
419        offset += 32;
420
421        let user = read_pubkey_unchecked(data, offset);
422        offset += 32;
423
424        let creator = read_pubkey_unchecked(data, offset);
425        offset += 32;
426
427        let timestamp = read_i64_unchecked(data, offset);
428        offset += 8;
429
430        let virtual_token_reserves = read_u64_unchecked(data, offset);
431        offset += 8;
432
433        let virtual_sol_reserves = read_u64_unchecked(data, offset);
434        offset += 8;
435
436        let real_token_reserves = read_u64_unchecked(data, offset);
437        offset += 8;
438
439        let token_total_supply = read_u64_unchecked(data, offset);
440        offset += 8;
441
442        let token_program = if offset + 32 <= data.len() {
443            read_pubkey_unchecked(data, offset)
444        } else {
445            solana_sdk::pubkey::Pubkey::default()
446        };
447        offset += 32;
448
449        let is_mayhem_mode = if offset < data.len() {
450            read_bool_unchecked(data, offset)
451        } else {
452            false
453        };
454        offset += 1;
455
456        // IDL CreateEvent 最后一列: is_cashback_enabled
457        let is_cashback_enabled = if offset < data.len() {
458            read_bool_unchecked(data, offset)
459        } else {
460            false
461        };
462
463        Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
464            metadata,
465            name: name.to_string(),
466            symbol: symbol.to_string(),
467            uri: uri.to_string(),
468            mint,
469            bonding_curve,
470            user,
471            creator,
472            timestamp,
473            virtual_token_reserves,
474            virtual_sol_reserves,
475            real_token_reserves,
476            token_total_supply,
477            token_program,
478            is_mayhem_mode,
479            is_cashback_enabled,
480        }))
481    }
482}
483
484// ============================================================================
485// Migrate 事件解析器
486// ============================================================================
487
488/// 解析 MigrateEvent(统一入口)
489///
490/// 根据编译时的 feature flag 自动选择解析器实现
491#[inline(always)]
492fn parse_migrate_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
493    #[cfg(feature = "parse-borsh")]
494    {
495        parse_migrate_event_inner_borsh(data, metadata)
496    }
497
498    #[cfg(feature = "parse-zero-copy")]
499    {
500        parse_migrate_event_inner_zero_copy(data, metadata)
501    }
502}
503
504/// Borsh 反序列化解析器 - Migrate 事件
505///
506/// **优点**: 类型安全、代码简洁、自动验证
507#[cfg(feature = "parse-borsh")]
508#[inline(always)]
509fn parse_migrate_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
510    // MigrateEvent 固定大小
511    const MIGRATE_EVENT_SIZE: usize = 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32; // 200 bytes
512
513    if data.len() < MIGRATE_EVENT_SIZE {
514        return None;
515    }
516
517    let mut event = borsh::from_slice::<PumpFunMigrateEvent>(&data[..MIGRATE_EVENT_SIZE]).ok()?;
518    event.metadata = metadata;
519    Some(DexEvent::PumpFunMigrate(event))
520}
521
522/// 零拷贝解析器 - Migrate 事件
523///
524/// **优点**: 最快、零拷贝、无验证开销
525#[cfg(feature = "parse-zero-copy")]
526#[inline(always)]
527fn parse_migrate_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
528    unsafe {
529        if data.len() < 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32 {
530            return None;
531        }
532
533        let mut offset = 0;
534
535        let user = read_pubkey_unchecked(data, offset);
536        offset += 32;
537
538        let mint = read_pubkey_unchecked(data, offset);
539        offset += 32;
540
541        let mint_amount = read_u64_unchecked(data, offset);
542        offset += 8;
543
544        let sol_amount = read_u64_unchecked(data, offset);
545        offset += 8;
546
547        let pool_migration_fee = read_u64_unchecked(data, offset);
548        offset += 8;
549
550        let bonding_curve = read_pubkey_unchecked(data, offset);
551        offset += 32;
552
553        let timestamp = read_i64_unchecked(data, offset);
554        offset += 8;
555
556        let pool = read_pubkey_unchecked(data, offset);
557
558        Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
559            metadata,
560            user,
561            mint,
562            mint_amount,
563            sol_amount,
564            pool_migration_fee,
565            bonding_curve,
566            timestamp,
567            pool,
568        }))
569    }
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575    use solana_sdk::signature::Signature;
576
577    #[test]
578    fn test_discriminator_match() {
579        // 验证 discriminator 匹配
580        let disc = discriminators::TRADE_EVENT;
581        assert_eq!(disc.len(), 16);
582    }
583
584    #[test]
585    fn test_parse_trade_event_boundary() {
586        // 测试边界条件 - 数据不足
587        let metadata = EventMetadata {
588            signature: Signature::default(),
589            slot: 0,
590            tx_index: 0,
591            block_time_us: 0,
592            grpc_recv_us: 0,
593        };
594
595        let short_data = vec![0u8; 10];
596        let result = parse_trade_event_inner(&short_data, metadata);
597        assert!(result.is_none());
598    }
599}