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