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