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        let ix_kind = crate::logs::pump::normalize_pumpfun_ix_name(&ix_name);
283
284        // TradeEvent 新增字段 (PUMP_CASHBACK_README): mayhem_mode, cashback_fee_basis_points, cashback
285        let mayhem_mode =
286            if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
287        offset += 1;
288        let cashback_fee_basis_points =
289            if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
290        offset += 8;
291        let cashback = if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
292        offset += 8;
293        let (
294            buyback_fee_basis_points,
295            buyback_fee,
296            shareholders,
297            quote_mint,
298            quote_amount,
299            virtual_quote_reserves,
300            real_quote_reserves,
301            holder_rewards_bps,
302            holder_rewards,
303        ) = crate::logs::pump::read_trade_event_extensions(data, &mut offset)?;
304
305        // Inner instruction 只包含日志数据,不含指令上下文账户;is_created_buy 由外层根据同 tx 是否含 create 传入
306        let trade_event = PumpFunTradeEvent {
307            metadata,
308            mint,
309            sol_amount,
310            token_amount,
311            is_buy,
312            is_created_buy,
313            user,
314            timestamp,
315            virtual_sol_reserves,
316            virtual_token_reserves,
317            real_sol_reserves,
318            real_token_reserves,
319            fee_recipient,
320            fee_basis_points,
321            fee,
322            creator,
323            creator_fee_basis_points,
324            creator_fee,
325            track_volume,
326            total_unclaimed_tokens,
327            total_claimed_tokens,
328            current_sol_volume,
329            last_update_timestamp,
330            ix_name: ix_name.clone(),
331            mayhem_mode,
332            cashback_fee_basis_points,
333            cashback,
334            buyback_fee_basis_points,
335            buyback_fee,
336            shareholders,
337            quote_mint,
338            quote_amount,
339            virtual_quote_reserves,
340            real_quote_reserves,
341            holder_rewards_bps,
342            holder_rewards,
343            is_cashback_coin: cashback_fee_basis_points > 0,
344            ..Default::default() // 其他账户字段由 instruction 提供
345        };
346
347        // 根据 ix_name 返回不同的事件类型
348        match ix_kind {
349            "buy" => Some(DexEvent::PumpFunBuy(trade_event)),
350            "sell" => Some(DexEvent::PumpFunSell(trade_event)),
351            "buy_exact_sol_in" => Some(DexEvent::PumpFunBuyExactSolIn(trade_event)),
352            "buy_exact_quote_in" => Some(DexEvent::PumpFunBuy(trade_event)),
353            _ => Some(DexEvent::PumpFunTrade(trade_event)),
354        }
355    }
356}
357
358// ============================================================================
359// Create 事件解析器
360// ============================================================================
361
362/// 解析 CreateTokenEvent(统一入口)
363///
364/// 根据编译时的 feature flag 自动选择解析器实现
365#[inline(always)]
366fn parse_create_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
367    #[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
368    {
369        parse_create_event_inner_compatible(data, metadata)
370    }
371
372    #[cfg(feature = "parse-zero-copy")]
373    {
374        parse_create_event_inner_zero_copy(data, metadata)
375    }
376}
377
378/// Compatible CreateEvent parser.
379///
380/// The IDL added `quote_mint` and `virtual_quote_reserves` to the tail of
381/// `CreateEvent`, so this parser accepts both old and new payload lengths.
382#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
383#[inline(always)]
384fn parse_create_event_inner_compatible(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
385    parse_create_event_fields(data, metadata)
386}
387
388#[inline(always)]
389#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
390fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
391    let bytes = data.get(offset..offset + 4)?;
392    Some(u32::from_le_bytes(bytes.try_into().ok()?))
393}
394
395#[inline(always)]
396#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
397fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
398    let bytes = data.get(offset..offset + 8)?;
399    Some(u64::from_le_bytes(bytes.try_into().ok()?))
400}
401
402#[inline(always)]
403#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
404fn read_i64_le(data: &[u8], offset: usize) -> Option<i64> {
405    let bytes = data.get(offset..offset + 8)?;
406    Some(i64::from_le_bytes(bytes.try_into().ok()?))
407}
408
409#[inline(always)]
410#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
411fn read_pubkey(data: &[u8], offset: usize) -> Option<solana_sdk::pubkey::Pubkey> {
412    let bytes = data.get(offset..offset + 32)?;
413    let mut out = [0u8; 32];
414    out.copy_from_slice(bytes);
415    Some(solana_sdk::pubkey::Pubkey::new_from_array(out))
416}
417
418#[inline(always)]
419#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
420fn read_string(data: &[u8], offset: &mut usize) -> Option<String> {
421    let len = read_u32_le(data, *offset)? as usize;
422    *offset = (*offset).checked_add(4)?;
423    let end = (*offset).checked_add(len)?;
424    let bytes = data.get(*offset..end)?;
425    *offset = end;
426    Some(std::str::from_utf8(bytes).ok()?.to_string())
427}
428
429#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
430fn parse_create_event_fields(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
431    let mut offset = 0;
432
433    let name = read_string(data, &mut offset)?;
434    let symbol = read_string(data, &mut offset)?;
435    let uri = read_string(data, &mut offset)?;
436
437    let mint = read_pubkey(data, offset)?;
438    offset += 32;
439    let bonding_curve = read_pubkey(data, offset)?;
440    offset += 32;
441    let user = read_pubkey(data, offset)?;
442    offset += 32;
443    let creator = read_pubkey(data, offset)?;
444    offset += 32;
445    let timestamp = read_i64_le(data, offset)?;
446    offset += 8;
447    let virtual_token_reserves = read_u64_le(data, offset)?;
448    offset += 8;
449    let virtual_sol_reserves = read_u64_le(data, offset)?;
450    offset += 8;
451    let real_token_reserves = read_u64_le(data, offset)?;
452    offset += 8;
453    let token_total_supply = read_u64_le(data, offset)?;
454    offset += 8;
455
456    let token_program = read_pubkey(data, offset).unwrap_or_default();
457    offset += 32;
458    let is_mayhem_mode = data.get(offset).copied().unwrap_or_default() == 1;
459    offset += 1;
460    let is_cashback_enabled = data.get(offset).copied().unwrap_or_default() == 1;
461    offset += 1;
462    let quote_mint = normalize_pumpfun_quote_mint(read_pubkey(data, offset).unwrap_or_default());
463    offset += 32;
464    let virtual_quote_reserves = read_u64_le(data, offset).unwrap_or_default();
465    offset += 8;
466    let creator_fee_bps = read_u64_le(data, offset).unwrap_or_default();
467    offset += 8;
468    let is_holder_reward = data.get(offset).copied().unwrap_or_default() == 1;
469
470    Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
471        metadata,
472        name,
473        symbol,
474        uri,
475        mint,
476        bonding_curve,
477        user,
478        creator,
479        timestamp,
480        virtual_token_reserves,
481        virtual_sol_reserves,
482        real_token_reserves,
483        token_total_supply,
484        token_program,
485        is_mayhem_mode,
486        is_cashback_enabled,
487        quote_mint,
488        virtual_quote_reserves,
489        creator_fee_bps,
490        is_holder_reward,
491        ix_name: "create".to_string(),
492        ..Default::default()
493    }))
494}
495
496/// 零拷贝解析器 - Create 事件
497///
498/// **优点**: 最快、零拷贝、无验证开销
499#[cfg(feature = "parse-zero-copy")]
500#[inline(always)]
501fn parse_create_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
502    unsafe {
503        let mut offset = 0;
504
505        let (name, name_len) = read_str_unchecked(data, offset)?;
506        offset += name_len;
507
508        let (symbol, symbol_len) = read_str_unchecked(data, offset)?;
509        offset += symbol_len;
510
511        let (uri, uri_len) = read_str_unchecked(data, offset)?;
512        offset += uri_len;
513
514        if data.len() < offset + 32 + 32 + 32 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 1 {
515            return None;
516        }
517
518        let mint = read_pubkey_unchecked(data, offset);
519        offset += 32;
520
521        let bonding_curve = read_pubkey_unchecked(data, offset);
522        offset += 32;
523
524        let user = read_pubkey_unchecked(data, offset);
525        offset += 32;
526
527        let creator = read_pubkey_unchecked(data, offset);
528        offset += 32;
529
530        let timestamp = read_i64_unchecked(data, offset);
531        offset += 8;
532
533        let virtual_token_reserves = read_u64_unchecked(data, offset);
534        offset += 8;
535
536        let virtual_sol_reserves = read_u64_unchecked(data, offset);
537        offset += 8;
538
539        let real_token_reserves = read_u64_unchecked(data, offset);
540        offset += 8;
541
542        let token_total_supply = read_u64_unchecked(data, offset);
543        offset += 8;
544
545        let token_program = if offset + 32 <= data.len() {
546            read_pubkey_unchecked(data, offset)
547        } else {
548            solana_sdk::pubkey::Pubkey::default()
549        };
550        offset += 32;
551
552        let is_mayhem_mode =
553            if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
554        offset += 1;
555
556        // IDL CreateEvent 最后一列: is_cashback_enabled
557        let is_cashback_enabled =
558            if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
559        offset += 1;
560        let quote_mint = normalize_pumpfun_quote_mint(if offset + 32 <= data.len() {
561            read_pubkey_unchecked(data, offset)
562        } else {
563            solana_sdk::pubkey::Pubkey::default()
564        });
565        offset += 32;
566        let virtual_quote_reserves =
567            if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
568        offset += 8;
569        let creator_fee_bps =
570            if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
571        offset += 8;
572        let is_holder_reward =
573            if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
574
575        Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
576            metadata,
577            name: name.to_string(),
578            symbol: symbol.to_string(),
579            uri: uri.to_string(),
580            mint,
581            bonding_curve,
582            user,
583            creator,
584            timestamp,
585            virtual_token_reserves,
586            virtual_sol_reserves,
587            real_token_reserves,
588            token_total_supply,
589            token_program,
590            is_mayhem_mode,
591            is_cashback_enabled,
592            quote_mint,
593            virtual_quote_reserves,
594            creator_fee_bps,
595            is_holder_reward,
596            ix_name: "create".to_string(),
597            ..Default::default()
598        }))
599    }
600}
601
602// ============================================================================
603// Migrate 事件解析器
604// ============================================================================
605
606/// 解析 MigrateEvent(统一入口)
607///
608/// 根据编译时的 feature flag 自动选择解析器实现
609#[inline(always)]
610fn parse_migrate_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
611    #[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
612    {
613        parse_migrate_event_inner_borsh(data, metadata)
614    }
615
616    #[cfg(feature = "parse-zero-copy")]
617    {
618        parse_migrate_event_inner_zero_copy(data, metadata)
619    }
620}
621
622/// Borsh 反序列化解析器 - Migrate 事件
623///
624/// **优点**: 类型安全、代码简洁、自动验证
625#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
626#[inline(always)]
627fn parse_migrate_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
628    // MigrateEvent 固定大小
629    const MIGRATE_EVENT_SIZE: usize = 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32; // 200 bytes
630
631    if data.len() < MIGRATE_EVENT_SIZE {
632        return None;
633    }
634
635    let mut event = borsh::from_slice::<PumpFunMigrateEvent>(&data[..MIGRATE_EVENT_SIZE]).ok()?;
636    event.metadata = metadata;
637    Some(DexEvent::PumpFunMigrate(event))
638}
639
640/// 零拷贝解析器 - Migrate 事件
641///
642/// **优点**: 最快、零拷贝、无验证开销
643#[cfg(feature = "parse-zero-copy")]
644#[inline(always)]
645fn parse_migrate_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
646    unsafe {
647        if data.len() < 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32 {
648            return None;
649        }
650
651        let mut offset = 0;
652
653        let user = read_pubkey_unchecked(data, offset);
654        offset += 32;
655
656        let mint = read_pubkey_unchecked(data, offset);
657        offset += 32;
658
659        let mint_amount = read_u64_unchecked(data, offset);
660        offset += 8;
661
662        let sol_amount = read_u64_unchecked(data, offset);
663        offset += 8;
664
665        let pool_migration_fee = read_u64_unchecked(data, offset);
666        offset += 8;
667
668        let bonding_curve = read_pubkey_unchecked(data, offset);
669        offset += 32;
670
671        let timestamp = read_i64_unchecked(data, offset);
672        offset += 8;
673
674        let pool = read_pubkey_unchecked(data, offset);
675
676        Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
677            metadata,
678            user,
679            mint,
680            mint_amount,
681            sol_amount,
682            pool_migration_fee,
683            bonding_curve,
684            timestamp,
685            pool,
686        }))
687    }
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693    use solana_sdk::{pubkey::Pubkey, signature::Signature};
694
695    fn push_u64(out: &mut Vec<u8>, value: u64) {
696        out.extend_from_slice(&value.to_le_bytes());
697    }
698
699    fn push_i64(out: &mut Vec<u8>, value: i64) {
700        out.extend_from_slice(&value.to_le_bytes());
701    }
702
703    fn push_pubkey(out: &mut Vec<u8>, value: Pubkey) {
704        out.extend_from_slice(value.as_ref());
705    }
706
707    fn trade_event_data_without_buyback_tail(ix_name: &str) -> Vec<u8> {
708        let mut data = Vec::new();
709        push_pubkey(&mut data, Pubkey::new_unique()); // mint
710        push_u64(&mut data, 1_000); // sol_amount
711        push_u64(&mut data, 2_000); // token_amount
712        data.push(1); // is_buy
713        push_pubkey(&mut data, Pubkey::new_unique()); // user
714        push_i64(&mut data, 123); // timestamp
715        push_u64(&mut data, 10); // virtual_sol_reserves
716        push_u64(&mut data, 20); // virtual_token_reserves
717        push_u64(&mut data, 30); // real_sol_reserves
718        push_u64(&mut data, 40); // real_token_reserves
719        push_pubkey(&mut data, Pubkey::new_unique()); // fee_recipient
720        push_u64(&mut data, 50); // fee_basis_points
721        push_u64(&mut data, 60); // fee
722        push_pubkey(&mut data, Pubkey::new_unique()); // creator
723        push_u64(&mut data, 70); // creator_fee_basis_points
724        push_u64(&mut data, 80); // creator_fee
725        data.push(1); // track_volume
726        push_u64(&mut data, 90); // total_unclaimed_tokens
727        push_u64(&mut data, 100); // total_claimed_tokens
728        push_u64(&mut data, 110); // current_sol_volume
729        push_i64(&mut data, 120); // last_update_timestamp
730        data.extend_from_slice(&(ix_name.len() as u32).to_le_bytes());
731        data.extend_from_slice(ix_name.as_bytes());
732        data.push(1); // mayhem_mode
733        push_u64(&mut data, 130); // cashback_fee_basis_points
734        push_u64(&mut data, 140); // cashback
735        data
736    }
737
738    #[test]
739    fn test_discriminator_match() {
740        // 验证 discriminator 匹配
741        let disc = discriminators::TRADE_EVENT;
742        assert_eq!(disc.len(), 16);
743    }
744
745    #[test]
746    fn test_parse_trade_event_boundary() {
747        // 测试边界条件 - 数据不足
748        let metadata = EventMetadata {
749            signature: Signature::default(),
750            slot: 0,
751            tx_index: 0,
752            block_time_us: 0,
753            grpc_recv_us: 0,
754            recent_blockhash: None,
755        };
756
757        let short_data = vec![0u8; 10];
758        let result = parse_trade_event_inner(&short_data, metadata, false);
759        assert!(result.is_none());
760    }
761
762    #[test]
763    fn trade_event_parser_accepts_payload_without_latest_tail() {
764        let metadata = EventMetadata {
765            signature: Signature::default(),
766            slot: 10,
767            tx_index: 0,
768            block_time_us: 0,
769            grpc_recv_us: 0,
770            recent_blockhash: None,
771        };
772        let data = trade_event_data_without_buyback_tail("buy_exact_sol_in");
773        let event =
774            parse_pumpfun_inner_instruction(&discriminators::TRADE_EVENT, &data, metadata, true)
775                .expect("legacy tail-compatible trade event");
776
777        match event {
778            DexEvent::PumpFunBuyExactSolIn(t) => {
779                assert_eq!(t.sol_amount, 1_000);
780                assert_eq!(t.token_amount, 2_000);
781                assert_eq!(t.ix_name, "buy_exact_sol_in");
782                assert!(t.track_volume);
783                assert!(t.mayhem_mode);
784                assert_eq!(t.cashback_fee_basis_points, 130);
785                assert_eq!(t.cashback, 140);
786                assert!(t.is_created_buy);
787                assert_eq!(t.buyback_fee_basis_points, 0);
788                assert!(t.shareholders.is_empty());
789                assert_eq!(t.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
790            }
791            other => panic!("expected exact buy trade, got {other:?}"),
792        }
793    }
794}