Skip to main content

sol_parser_sdk/logs/
optimized_matcher.rs

1//! Optimized log matcher with early discriminator filtering
2//!
3//! Performance strategy:
4//! 1. SIMD-based log type detection (~50ns)
5//! 2. Extract discriminator BEFORE full parsing (~50ns)
6//! 3. Check filter at discriminator level - skip parsing if not needed
7//! 4. Only parse events user actually configured
8//! 5. Compiler-optimized base64 decoding (auto-vectorized with target-cpu=native)
9
10use super::perf_hints::{likely, unlikely};
11use crate::core::events::{DexEvent, EventMetadata};
12use crate::grpc::types::{EventType, EventTypeFilter};
13use crate::instr::program_ids;
14use memchr::memmem;
15use once_cell::sync::Lazy;
16use solana_sdk::pubkey::Pubkey;
17use solana_sdk::signature::Signature;
18
19/// SIMD 优化的字符串查找器 - 预编译一次,重复使用
20static PUMPFUN_FINDER: Lazy<memmem::Finder> =
21    Lazy::new(|| memmem::Finder::new(b"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"));
22static RAYDIUM_AMM_FINDER: Lazy<memmem::Finder> =
23    Lazy::new(|| memmem::Finder::new(b"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"));
24static RAYDIUM_CLMM_FINDER: Lazy<memmem::Finder> =
25    Lazy::new(|| memmem::Finder::new(b"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"));
26static RAYDIUM_CPMM_FINDER: Lazy<memmem::Finder> =
27    Lazy::new(|| memmem::Finder::new(b"CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"));
28static BONK_FINDER: Lazy<memmem::Finder> =
29    Lazy::new(|| memmem::Finder::new(b"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"));
30static PROGRAM_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"Program"));
31static PROGRAM_DATA_FINDER: Lazy<memmem::Finder> =
32    Lazy::new(|| memmem::Finder::new(b"Program data: "));
33static PUMPFUN_CREATE_FINDER: Lazy<memmem::Finder> =
34    Lazy::new(|| memmem::Finder::new(b"Program data: G3KpTd7rY3Y"));
35static WHIRL_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"whirL"));
36static METEORA_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"meteora"));
37static METEORA_LB_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"LB"));
38static METEORA_DLMM_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"DLMM"));
39static PUMPSWAP_LOWER_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"pumpswap"));
40static PUMPSWAP_UPPER_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"PumpSwap"));
41
42/// 预计算的程序 ID 字符串常量
43pub mod program_id_strings {
44    pub const PUMPFUN_INVOKE: &str = "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke";
45    pub const PUMPFUN_SUCCESS: &str = "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success";
46    pub const PUMPFUN_ID: &str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
47
48    pub const BONK_INVOKE: &str = "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke";
49    pub const BONK_SUCCESS: &str = "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success";
50    pub const BONK_ID: &str = "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj";
51
52    pub const RAYDIUM_CLMM_INVOKE: &str =
53        "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK invoke";
54    pub const RAYDIUM_CLMM_SUCCESS: &str =
55        "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK success";
56    pub const RAYDIUM_CLMM_ID: &str = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
57
58    pub const RAYDIUM_CPMM_INVOKE: &str =
59        "Program CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C invoke";
60    pub const RAYDIUM_CPMM_SUCCESS: &str =
61        "Program CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C success";
62    pub const RAYDIUM_CPMM_ID: &str = "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C";
63
64    pub const RAYDIUM_AMM_V4_ID: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
65
66    // 常用的日志模式
67    pub const PROGRAM_DATA: &str = "Program data: ";
68    pub const PROGRAM_LOG: &str = "Program log: ";
69
70    // PumpFun 事件 discriminator (base64)
71    pub const PUMPFUN_CREATE_DISCRIMINATOR: &str = "GB7IKAUcB3c"; // [24, 30, 200, 40, 5, 28, 7, 119]
72}
73
74/// 快速日志类型枚举
75#[derive(Debug, Copy, Clone, PartialEq)]
76pub enum LogType {
77    PumpFun,
78    RaydiumLaunchpad,
79    PumpAmm,
80    RaydiumClmm,
81    RaydiumCpmm,
82    RaydiumAmm,
83    OrcaWhirlpool,
84    MeteoraAmm,
85    MeteoraDamm,
86    MeteoraDlmm,
87    Unknown,
88}
89
90/// SIMD 优化的日志类型检测器 - 激进早期退出
91#[inline(always)]
92pub fn detect_log_type(log: &str) -> LogType {
93    let log_bytes = log.as_bytes();
94
95    // 第一步:快速长度检查 - 太短的日志直接跳过
96    if log_bytes.len() < 20 {
97        return LogType::Unknown;
98    }
99
100    // 第二步:检查是否有 "Program data:" - 这是事件日志的标志
101    let has_program_data = PROGRAM_DATA_FINDER.find(log_bytes).is_some();
102
103    // 只有 "Program data:" 日志才可能是交易事件
104    if unlikely(!has_program_data) {
105        return LogType::Unknown;
106    }
107
108    // 第三步:使用 SIMD 快速检测具体协议
109    // Raydium AMM - 高频,有明确程序ID(最常见)
110    if likely(RAYDIUM_AMM_FINDER.find(log_bytes).is_some()) {
111        return LogType::RaydiumAmm;
112    }
113
114    // Raydium CLMM
115    if RAYDIUM_CLMM_FINDER.find(log_bytes).is_some() {
116        return LogType::RaydiumClmm;
117    }
118
119    // Raydium CPMM
120    if RAYDIUM_CPMM_FINDER.find(log_bytes).is_some() {
121        return LogType::RaydiumCpmm;
122    }
123
124    // Raydium Launchpad (Bonk)
125    if BONK_FINDER.find(log_bytes).is_some() {
126        return LogType::RaydiumLaunchpad;
127    }
128
129    // Orca Whirlpool
130    if WHIRL_FINDER.find(log_bytes).is_some() {
131        return LogType::OrcaWhirlpool;
132    }
133
134    // Meteora - SIMD 优化
135    if let Some(pos) = METEORA_FINDER.find(log_bytes) {
136        let rest = &log_bytes[pos..];
137        if METEORA_LB_FINDER.find(rest).is_some() {
138            return LogType::MeteoraDamm;
139        } else if METEORA_DLMM_FINDER.find(rest).is_some() {
140            return LogType::MeteoraDlmm;
141        } else {
142            return LogType::MeteoraAmm;
143        }
144    }
145
146    // Pump AMM
147    if PUMPSWAP_LOWER_FINDER.find(log_bytes).is_some()
148        || PUMPSWAP_UPPER_FINDER.find(log_bytes).is_some()
149    {
150        return LogType::PumpAmm;
151    }
152
153    // PumpFun - 特殊处理:可能有程序ID,也可能直接是base64数据
154    // 1. 先检查是否包含程序ID(高频事件)
155    if likely(PUMPFUN_FINDER.find(log_bytes).is_some()) {
156        return LogType::PumpFun;
157    }
158
159    // 2. 兜底:有 "Program data:" 但无法识别协议的,尝试作为 PumpFun 解析
160    // PumpFun的日志格式:Program data: <base64>
161    // 只要日志够长且包含Program data,就认为可能是PumpFun
162    if log.len() > 30 {
163        return LogType::PumpFun;
164    }
165
166    LogType::Unknown
167}
168
169// ============================================================================
170// Discriminator constants (compile-time computed) - All protocols
171// ============================================================================
172mod discriminators {
173    // PumpFun discriminators
174    pub const PUMPFUN_CREATE: u64 = u64::from_le_bytes([27, 114, 169, 77, 222, 235, 99, 118]);
175    pub const PUMPFUN_TRADE: u64 = u64::from_le_bytes([189, 219, 127, 211, 78, 230, 97, 238]);
176    pub const PUMPFUN_MIGRATE: u64 = u64::from_le_bytes([189, 233, 93, 185, 92, 148, 234, 148]);
177    pub const PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR: u64 =
178        u64::from_le_bytes([155, 167, 104, 220, 213, 108, 243, 3]);
179    // Raydium Launchpad event discriminators. `TRADE` intentionally equals
180    // PumpFun's TradeEvent discriminator, so gRPC must route logs with program
181    // context instead of discriminator alone.
182    pub const RAYDIUM_LAUNCHPAD_POOL_CREATE: u64 =
183        u64::from_le_bytes([151, 215, 226, 9, 118, 161, 115, 174]);
184    pub const RAYDIUM_LAUNCHPAD_TRADE: u64 =
185        u64::from_le_bytes([189, 219, 127, 211, 78, 230, 97, 238]);
186    // Pump fees (`idls/pump_fees.json` event discriminators)
187    pub const PUMP_FEES_CREATE_FEE_SHARING_CONFIG: u64 =
188        u64::from_le_bytes([133, 105, 170, 200, 184, 116, 251, 88]);
189    pub const PUMP_FEES_INITIALIZE_FEE_CONFIG: u64 =
190        u64::from_le_bytes([89, 138, 244, 230, 10, 56, 226, 126]);
191    pub const PUMP_FEES_RESET_FEE_SHARING_CONFIG: u64 =
192        u64::from_le_bytes([203, 204, 151, 226, 120, 55, 214, 243]);
193    pub const PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY: u64 =
194        u64::from_le_bytes([114, 23, 101, 60, 14, 190, 153, 62]);
195    pub const PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY: u64 =
196        u64::from_le_bytes([124, 143, 198, 245, 77, 184, 8, 236]);
197    pub const PUMP_FEES_UPDATE_ADMIN: u64 =
198        u64::from_le_bytes([225, 152, 171, 87, 246, 63, 66, 234]);
199    pub const PUMP_FEES_UPDATE_FEE_CONFIG: u64 =
200        u64::from_le_bytes([90, 23, 65, 35, 62, 244, 188, 208]);
201    pub const PUMP_FEES_UPDATE_FEE_SHARES: u64 =
202        u64::from_le_bytes([21, 186, 196, 184, 91, 228, 225, 203]);
203    pub const PUMP_FEES_UPSERT_FEE_TIERS: u64 =
204        u64::from_le_bytes([171, 89, 169, 187, 122, 186, 33, 204]);
205
206    // PumpSwap discriminators
207    pub const PUMPSWAP_BUY: u64 = u64::from_le_bytes([103, 244, 82, 31, 44, 245, 119, 119]);
208    pub const PUMPSWAP_SELL: u64 = u64::from_le_bytes([62, 47, 55, 10, 165, 3, 220, 42]);
209    pub const PUMPSWAP_CREATE_POOL: u64 =
210        u64::from_le_bytes([177, 49, 12, 210, 160, 118, 167, 116]);
211    pub const PUMPSWAP_ADD_LIQUIDITY: u64 =
212        u64::from_le_bytes([120, 248, 61, 83, 31, 142, 107, 144]);
213    pub const PUMPSWAP_REMOVE_LIQUIDITY: u64 =
214        u64::from_le_bytes([22, 9, 133, 26, 160, 44, 71, 192]);
215
216    // Raydium CLMM discriminators
217    pub const RAYDIUM_CLMM_SWAP: u64 = u64::from_le_bytes([248, 198, 158, 145, 225, 117, 135, 200]);
218    pub const RAYDIUM_CLMM_INCREASE_LIQUIDITY: u64 =
219        u64::from_le_bytes([133, 29, 89, 223, 69, 238, 176, 10]);
220    pub const RAYDIUM_CLMM_DECREASE_LIQUIDITY: u64 =
221        u64::from_le_bytes([160, 38, 208, 111, 104, 91, 44, 1]);
222    pub const RAYDIUM_CLMM_CREATE_POOL: u64 =
223        u64::from_le_bytes([233, 146, 209, 142, 207, 104, 64, 188]);
224    pub const RAYDIUM_CLMM_COLLECT_FEE: u64 =
225        u64::from_le_bytes([164, 152, 207, 99, 187, 104, 171, 119]);
226
227    // Raydium CPMM discriminators
228    pub const RAYDIUM_CPMM_SWAP_BASE_IN: u64 =
229        u64::from_le_bytes([143, 190, 90, 218, 196, 30, 51, 222]);
230    pub const RAYDIUM_CPMM_SWAP_BASE_OUT: u64 =
231        u64::from_le_bytes([55, 217, 98, 86, 163, 74, 180, 173]);
232    pub const RAYDIUM_CPMM_CREATE_POOL: u64 =
233        u64::from_le_bytes([233, 146, 209, 142, 207, 104, 64, 188]);
234    pub const RAYDIUM_CPMM_DEPOSIT: u64 =
235        u64::from_le_bytes([242, 35, 198, 137, 82, 225, 242, 182]);
236    pub const RAYDIUM_CPMM_WITHDRAW: u64 =
237        u64::from_le_bytes([183, 18, 70, 156, 148, 109, 161, 34]);
238
239    // Raydium AMM V4 discriminators
240    pub const RAYDIUM_AMM_SWAP_BASE_IN: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 9]);
241    pub const RAYDIUM_AMM_SWAP_BASE_OUT: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 11]);
242    pub const RAYDIUM_AMM_DEPOSIT: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 3]);
243    pub const RAYDIUM_AMM_WITHDRAW: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 4]);
244    pub const RAYDIUM_AMM_INITIALIZE2: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 1]);
245    pub const RAYDIUM_AMM_WITHDRAW_PNL: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 7]);
246
247    // Orca Whirlpool discriminators
248    pub const ORCA_TRADED: u64 = u64::from_le_bytes([225, 202, 73, 175, 147, 43, 160, 150]);
249    pub const ORCA_LIQUIDITY_INCREASED: u64 =
250        u64::from_le_bytes([30, 7, 144, 181, 102, 254, 155, 161]);
251    pub const ORCA_LIQUIDITY_DECREASED: u64 =
252        u64::from_le_bytes([166, 1, 36, 71, 112, 202, 181, 171]);
253    pub const ORCA_POOL_INITIALIZED: u64 =
254        u64::from_le_bytes([100, 118, 173, 87, 12, 198, 254, 229]);
255
256    // Meteora AMM discriminators
257    pub const METEORA_AMM_SWAP: u64 = u64::from_le_bytes([81, 108, 227, 190, 205, 208, 10, 196]);
258    pub const METEORA_AMM_ADD_LIQUIDITY: u64 =
259        u64::from_le_bytes([31, 94, 125, 90, 227, 52, 61, 186]);
260    pub const METEORA_AMM_REMOVE_LIQUIDITY: u64 =
261        u64::from_le_bytes([116, 244, 97, 232, 103, 31, 152, 58]);
262    pub const METEORA_AMM_BOOTSTRAP_LIQUIDITY: u64 =
263        u64::from_le_bytes([121, 127, 38, 136, 92, 55, 14, 247]);
264    pub const METEORA_AMM_POOL_CREATED: u64 =
265        u64::from_le_bytes([202, 44, 41, 88, 104, 220, 157, 82]);
266    pub const METEORA_AMM_SET_POOL_FEES: u64 =
267        u64::from_le_bytes([245, 26, 198, 164, 88, 18, 75, 9]);
268
269    // Meteora DAMM V2 discriminators
270    pub const METEORA_DAMM_SWAP: u64 = u64::from_le_bytes([27, 60, 21, 213, 138, 170, 187, 147]);
271    pub const METEORA_DAMM_SWAP2: u64 = u64::from_le_bytes([189, 66, 51, 168, 38, 80, 117, 153]);
272    pub const METEORA_DAMM_ADD_LIQUIDITY: u64 =
273        u64::from_le_bytes([175, 242, 8, 157, 30, 247, 185, 169]);
274    pub const METEORA_DAMM_REMOVE_LIQUIDITY: u64 =
275        u64::from_le_bytes([87, 46, 88, 98, 175, 96, 34, 91]);
276    pub const METEORA_DAMM_INITIALIZE_POOL: u64 =
277        u64::from_le_bytes([228, 50, 246, 85, 203, 66, 134, 37]);
278    pub const METEORA_DAMM_CREATE_POSITION: u64 =
279        u64::from_le_bytes([156, 15, 119, 198, 29, 181, 221, 55]);
280    pub const METEORA_DAMM_CLOSE_POSITION: u64 =
281        u64::from_le_bytes([20, 145, 144, 68, 143, 142, 214, 178]);
282
283    // Meteora DLMM discriminators
284    pub const METEORA_DLMM_SWAP: u64 = u64::from_le_bytes([143, 190, 90, 218, 196, 30, 51, 222]);
285    pub const METEORA_DLMM_ADD_LIQUIDITY: u64 =
286        u64::from_le_bytes([181, 157, 89, 67, 143, 182, 52, 72]);
287    pub const METEORA_DLMM_REMOVE_LIQUIDITY: u64 =
288        u64::from_le_bytes([80, 85, 209, 72, 24, 206, 35, 178]);
289    pub const METEORA_DLMM_INITIALIZE_POOL: u64 =
290        u64::from_le_bytes([95, 180, 10, 172, 84, 174, 232, 40]);
291    pub const METEORA_DLMM_INITIALIZE_BIN_ARRAY: u64 =
292        u64::from_le_bytes([11, 18, 155, 194, 33, 115, 238, 119]);
293    pub const METEORA_DLMM_CREATE_POSITION: u64 =
294        u64::from_le_bytes([123, 233, 11, 43, 146, 180, 97, 119]);
295    pub const METEORA_DLMM_CLOSE_POSITION: u64 =
296        u64::from_le_bytes([94, 168, 102, 45, 59, 122, 137, 54]);
297    pub const METEORA_DLMM_CLAIM_FEE: u64 = u64::from_le_bytes([152, 70, 208, 111, 104, 91, 44, 1]);
298}
299
300/// Optimized unified log parser with **discriminator predecode, decode-on-match** strategy
301///
302/// **Performance Strategy**:
303/// 1. Decode only the first 8 event bytes to read the discriminator
304/// 2. Check filter BEFORE decoding the full event payload
305/// 3. Decode matching payloads once to a stack buffer; rare large events use heap
306/// 4. Parse only the specific event type requested
307///
308/// **Key optimization**: narrow filters skip full base64 decoding entirely.
309/// Old: decode full payload -> check filter -> parse
310/// New: decode discriminator prefix -> check filter -> decode matching payload once
311#[inline(always)]
312/// `recent_blockhash`: pass as `Option<&[u8]>`; only cloned when an event is built (low latency).
313pub fn parse_log_optimized(
314    log: &str,
315    signature: Signature,
316    slot: u64,
317    tx_index: u64,
318    block_time_us: Option<i64>,
319    grpc_recv_us: i64,
320    event_type_filter: Option<&EventTypeFilter>,
321    is_created_buy: bool,
322    recent_blockhash: Option<&[u8]>,
323) -> Option<DexEvent> {
324    parse_log_optimized_inner(
325        log,
326        signature,
327        slot,
328        tx_index,
329        block_time_us,
330        grpc_recv_us,
331        event_type_filter,
332        is_created_buy,
333        recent_blockhash,
334        None,
335    )
336}
337
338/// Program-aware log parser for gRPC/RPC transaction logs.
339///
340/// `Program data:` lines do not carry the emitting program id. The caller should
341/// pass the current invoke stack's program id so discriminators shared by
342/// multiple Anchor programs are routed correctly.
343#[inline(always)]
344pub fn parse_log_optimized_with_program_id(
345    log: &str,
346    signature: Signature,
347    slot: u64,
348    tx_index: u64,
349    block_time_us: Option<i64>,
350    grpc_recv_us: i64,
351    event_type_filter: Option<&EventTypeFilter>,
352    is_created_buy: bool,
353    recent_blockhash: Option<&[u8]>,
354    program_id: Option<&Pubkey>,
355) -> Option<DexEvent> {
356    parse_log_optimized_inner(
357        log,
358        signature,
359        slot,
360        tx_index,
361        block_time_us,
362        grpc_recv_us,
363        event_type_filter,
364        is_created_buy,
365        recent_blockhash,
366        program_id,
367    )
368}
369
370#[inline(always)]
371fn decode_base64_discriminator(trimmed: &str) -> Option<u64> {
372    let bytes = trimmed.as_bytes();
373    if bytes.len() < 12 {
374        return None;
375    }
376
377    let mut discriminator_buf = [0u8; 9];
378    let decoded_len = {
379        use base64_simd::AsOut;
380        let decoded =
381            base64_simd::STANDARD.decode(&bytes[..12], discriminator_buf.as_mut().as_out()).ok()?;
382        decoded.len()
383    };
384    if decoded_len < 8 {
385        return None;
386    }
387
388    Some(unsafe { (discriminator_buf.as_ptr() as *const u64).read_unaligned() })
389}
390
391#[inline(always)]
392fn filter_includes_known_program(program_id: &Pubkey, filter: &EventTypeFilter) -> bool {
393    match *program_id {
394        program_ids::PUMPFUN_PROGRAM_ID => filter.includes_pumpfun(),
395        program_ids::PUMP_FEES_PROGRAM_ID => filter.includes_pump_fees(),
396        program_ids::PUMPSWAP_PROGRAM_ID => filter.includes_pumpswap(),
397        program_ids::BONK_PROGRAM_ID => filter.includes_raydium_launchpad(),
398        program_ids::RAYDIUM_CLMM_PROGRAM_ID => filter.includes_raydium_clmm(),
399        program_ids::RAYDIUM_CPMM_PROGRAM_ID => filter.includes_raydium_cpmm(),
400        program_ids::RAYDIUM_AMM_V4_PROGRAM_ID => filter.includes_raydium_amm_v4(),
401        program_ids::ORCA_WHIRLPOOL_PROGRAM_ID => filter.includes_orca_whirlpool(),
402        program_ids::METEORA_POOLS_PROGRAM_ID => filter.includes_meteora_pools(),
403        program_ids::METEORA_DAMM_V2_PROGRAM_ID => filter.includes_meteora_damm_v2(),
404        program_ids::METEORA_DLMM_PROGRAM_ID => filter.includes_meteora_dlmm(),
405        _ => true,
406    }
407}
408
409#[inline(always)]
410fn filter_wants_supported_logs(filter: &EventTypeFilter) -> bool {
411    filter.includes_pumpfun()
412        || filter.includes_pump_fees()
413        || filter.includes_pumpswap()
414        || filter.includes_raydium_launchpad()
415        || filter.includes_raydium_clmm()
416        || filter.includes_raydium_cpmm()
417        || filter.includes_raydium_amm_v4()
418        || filter.includes_orca_whirlpool()
419        || filter.includes_meteora_pools()
420        || filter.includes_meteora_damm_v2()
421        || filter.includes_meteora_dlmm()
422}
423
424#[inline(always)]
425fn unscoped_filter_allows_discriminator(discriminator: u64, filter: &EventTypeFilter) -> bool {
426    match discriminator {
427        // Shared by Pump.fun trade and Raydium Launchpad/Bonk trade.
428        discriminators::PUMPFUN_TRADE => {
429            filter.should_include(EventType::PumpFunTrade)
430                || filter.should_include(EventType::BonkTrade)
431        }
432        // Shared by Raydium CLMM create-pool and Raydium CPMM initialize.
433        discriminators::RAYDIUM_CLMM_CREATE_POOL => {
434            filter.should_include(EventType::RaydiumClmmCreatePool)
435                || filter.should_include(EventType::RaydiumCpmmInitialize)
436        }
437        // Shared by Raydium CPMM swap-base-in and Meteora DLMM swap.
438        discriminators::RAYDIUM_CPMM_SWAP_BASE_IN => {
439            filter.should_include(EventType::RaydiumCpmmSwap)
440                || filter.should_include(EventType::MeteoraDlmmSwap)
441        }
442        _ => discriminator_to_event_type(discriminator)
443            .map(|event_type| filter.should_include(event_type))
444            .unwrap_or_else(|| filter_wants_supported_logs(filter)),
445    }
446}
447
448#[inline(always)]
449fn filter_allows_discriminator(
450    program_id: Option<&Pubkey>,
451    discriminator: u64,
452    event_type_filter: Option<&EventTypeFilter>,
453) -> bool {
454    let Some(filter) = event_type_filter else {
455        return true;
456    };
457
458    if let Some(program_id) = program_id {
459        if let Some(event_type) =
460            program_scoped_discriminator_to_event_type(program_id, discriminator)
461        {
462            return filter.should_include(event_type);
463        }
464        return filter_includes_known_program(program_id, filter);
465    }
466
467    unscoped_filter_allows_discriminator(discriminator, filter)
468}
469
470#[inline(always)]
471fn apply_event_type_filter(
472    event: DexEvent,
473    event_type_filter: Option<&EventTypeFilter>,
474) -> Option<DexEvent> {
475    if let Some(filter) = event_type_filter {
476        if !filter.should_include_dex_event(&event) {
477            return None;
478        }
479    }
480    Some(event)
481}
482
483#[inline(always)]
484fn parse_log_optimized_inner(
485    log: &str,
486    signature: Signature,
487    slot: u64,
488    tx_index: u64,
489    block_time_us: Option<i64>,
490    grpc_recv_us: i64,
491    event_type_filter: Option<&EventTypeFilter>,
492    is_created_buy: bool,
493    recent_blockhash: Option<&[u8]>,
494    program_id: Option<&Pubkey>,
495) -> Option<DexEvent> {
496    // Step 1: Find "Program data: " prefix using SIMD
497    let log_bytes = log.as_bytes();
498    let pos = PROGRAM_DATA_FINDER.find(log_bytes)?;
499    let data_start = pos + 14; // "Program data: " length
500
501    if log_bytes.len() <= data_start {
502        return None;
503    }
504
505    // Step 2: Decode base64 ONCE. Normal swap logs stay on the stack; rare large
506    // IDL events (for example pump-fees vectors) fall back to heap instead of being dropped.
507    const STACK_DECODE_CAP: usize = 2048;
508    let data_part = &log[data_start..];
509    let trimmed = data_part.trim();
510
511    // Decode the discriminator prefix before touching the full payload. This is
512    // the fastest reject path for users subscribing to a narrow event set.
513    let discriminator = decode_base64_discriminator(trimmed)?;
514    if !filter_allows_discriminator(program_id, discriminator, event_type_filter) {
515        return None;
516    }
517
518    // SIMD-accelerated base64 decoding (AVX2/SSE4/NEON)
519    use base64_simd::AsOut;
520    let max_decoded_len = (trimmed.len() / 4).saturating_mul(3).saturating_add(3);
521    let mut stack_buf = [0u8; STACK_DECODE_CAP];
522    let heap_buf: Vec<u8>;
523    let program_data: &[u8] = if max_decoded_len <= STACK_DECODE_CAP {
524        let decoded_len = {
525            let decoded_slice = base64_simd::STANDARD
526                .decode(trimmed.as_bytes(), stack_buf.as_mut().as_out())
527                .ok()?;
528            decoded_slice.len()
529        };
530        &stack_buf[..decoded_len]
531    } else {
532        heap_buf = base64_simd::STANDARD.decode_to_vec(trimmed.as_bytes()).ok()?;
533        heap_buf.as_slice()
534    };
535
536    if program_data.len() < 8 {
537        return None;
538    }
539
540    debug_assert_eq!(discriminator, unsafe {
541        (program_data.as_ptr() as *const u64).read_unaligned()
542    });
543
544    // Step 6: Parse the specific event type (data already decoded)
545    let data = &program_data[8..]; // Skip discriminator
546
547    use crate::core::events::*;
548
549    let metadata = EventMetadata {
550        signature,
551        slot,
552        tx_index,
553        block_time_us: block_time_us.unwrap_or(0),
554        grpc_recv_us,
555        recent_blockhash: recent_blockhash.map(|s| bs58::encode(s).into_string()),
556    };
557
558    if let Some(program_id) = program_id {
559        return parse_program_scoped_event(
560            program_id,
561            discriminator,
562            data,
563            metadata,
564            log,
565            signature,
566            slot,
567            tx_index,
568            block_time_us,
569            grpc_recv_us,
570            event_type_filter,
571            is_created_buy,
572        );
573    }
574
575    // ========================================================================
576    // Hot-path optimization: Fast check for top 5 most common discriminators
577    // This avoids the large match statement for ~80% of events
578    // Expected savings: 5-20ns per hot event
579    // ========================================================================
580
581    // Check hot-path discriminators first (ordered by frequency)
582    if likely(discriminator == discriminators::PUMPFUN_TRADE) {
583        // PumpFun Trade - Most common (~40% of all events)
584        let event = crate::logs::pump::parse_trade_from_data(data, metadata, is_created_buy)?;
585        return apply_event_type_filter(event, event_type_filter);
586    }
587
588    if likely(discriminator == discriminators::RAYDIUM_CLMM_SWAP) {
589        // Raydium CLMM Swap - High frequency (~20% of events)
590        return apply_event_type_filter(
591            crate::logs::raydium_clmm::parse_swap_from_data(data, metadata)?,
592            event_type_filter,
593        );
594    }
595
596    if likely(discriminator == discriminators::RAYDIUM_AMM_SWAP_BASE_IN) {
597        // Raydium AMM Swap Base In - High frequency (~15% of events)
598        return apply_event_type_filter(
599            crate::logs::raydium_amm::parse_swap_base_in_from_data(data, metadata)?,
600            event_type_filter,
601        );
602    }
603
604    if likely(discriminator == discriminators::PUMPSWAP_BUY) {
605        // PumpSwap Buy - Medium frequency (~10% of events)
606        return apply_event_type_filter(
607            crate::logs::pump_amm::parse_buy_from_data(data, metadata)?,
608            event_type_filter,
609        );
610    }
611
612    if discriminator == discriminators::PUMPSWAP_SELL {
613        // PumpSwap Sell - Medium frequency (~5% of events)
614        return apply_event_type_filter(
615            crate::logs::pump_amm::parse_sell_from_data(data, metadata)?,
616            event_type_filter,
617        );
618    }
619
620    // ========================================================================
621    // Cold path: Handle remaining ~10% of events via match statement
622    // ========================================================================
623
624    let event = match discriminator {
625        // Note: Hot-path discriminators (PUMPFUN_TRADE, RAYDIUM_CLMM_SWAP, RAYDIUM_AMM_SWAP_BASE_IN,
626        // PUMPSWAP_BUY, PUMPSWAP_SELL) are handled above and never reach this match statement
627
628        // PumpFun events (cold path)
629        discriminators::PUMPFUN_CREATE => crate::logs::pump::parse_create_from_data(data, metadata),
630        discriminators::PUMPFUN_MIGRATE => {
631            crate::logs::pump::parse_migrate_from_data(data, metadata)
632        }
633        discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG => {
634            crate::logs::pump_fees::parse_create_fee_sharing_config_from_data(data, metadata)
635        }
636        discriminators::PUMP_FEES_INITIALIZE_FEE_CONFIG => {
637            crate::logs::pump_fees::parse_initialize_fee_config_from_data(data, metadata)
638        }
639        discriminators::PUMP_FEES_RESET_FEE_SHARING_CONFIG => {
640            crate::logs::pump_fees::parse_reset_fee_sharing_config_from_data(data, metadata)
641        }
642        discriminators::PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY => {
643            crate::logs::pump_fees::parse_revoke_fee_sharing_authority_from_data(data, metadata)
644        }
645        discriminators::PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY => {
646            crate::logs::pump_fees::parse_transfer_fee_sharing_authority_from_data(data, metadata)
647        }
648        discriminators::PUMP_FEES_UPDATE_ADMIN => {
649            crate::logs::pump_fees::parse_update_admin_from_data(data, metadata)
650        }
651        discriminators::PUMP_FEES_UPDATE_FEE_CONFIG => {
652            crate::logs::pump_fees::parse_update_fee_config_from_data(data, metadata)
653        }
654        discriminators::PUMP_FEES_UPDATE_FEE_SHARES => {
655            crate::logs::pump_fees::parse_update_fee_shares_from_data(data, metadata)
656        }
657        discriminators::PUMP_FEES_UPSERT_FEE_TIERS => {
658            crate::logs::pump_fees::parse_upsert_fee_tiers_from_data(data, metadata)
659        }
660        discriminators::PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR => {
661            crate::logs::pump::parse_migrate_bonding_curve_creator_from_data(data, metadata)
662        }
663        discriminators::PUMPSWAP_CREATE_POOL => {
664            crate::logs::pump_amm::parse_create_pool_from_data(data, metadata)
665        }
666        discriminators::PUMPSWAP_ADD_LIQUIDITY => {
667            crate::logs::pump_amm::parse_add_liquidity_from_data(data, metadata)
668        }
669        discriminators::PUMPSWAP_REMOVE_LIQUIDITY => {
670            crate::logs::pump_amm::parse_remove_liquidity_from_data(data, metadata)
671        }
672
673        // ========== Other protocols - route by discriminator ==========
674        // Raydium CLMM - use from_data functions (cold path)
675        discriminators::RAYDIUM_CLMM_INCREASE_LIQUIDITY => {
676            crate::logs::raydium_clmm::parse_increase_liquidity_from_data(data, metadata)
677        }
678        discriminators::RAYDIUM_CLMM_DECREASE_LIQUIDITY => {
679            crate::logs::raydium_clmm::parse_decrease_liquidity_from_data(data, metadata)
680        }
681        discriminators::RAYDIUM_CLMM_CREATE_POOL => {
682            crate::logs::raydium_clmm::parse_create_pool_from_data(data, metadata)
683        }
684        discriminators::RAYDIUM_CLMM_COLLECT_FEE => {
685            crate::logs::raydium_clmm::parse_collect_fee_from_data(data, metadata)
686        }
687
688        // Raydium CPMM - use from_data functions (single decode)
689        discriminators::RAYDIUM_CPMM_SWAP_BASE_IN => {
690            crate::logs::raydium_cpmm::parse_swap_base_in_from_data(data, metadata)
691        }
692        discriminators::RAYDIUM_CPMM_SWAP_BASE_OUT => {
693            crate::logs::raydium_cpmm::parse_swap_base_out_from_data(data, metadata)
694        }
695        // Note: RAYDIUM_CPMM_CREATE_POOL discriminator conflicts with RAYDIUM_CLMM_CREATE_POOL
696        // CPMM create pool is rare, handled via log content detection if needed
697        discriminators::RAYDIUM_CPMM_DEPOSIT => {
698            crate::logs::raydium_cpmm::parse_deposit_from_data(data, metadata)
699        }
700        discriminators::RAYDIUM_CPMM_WITHDRAW => {
701            crate::logs::raydium_cpmm::parse_withdraw_from_data(data, metadata)
702        }
703
704        // Raydium AMM V4 - use from_data functions (single decode)
705        discriminators::RAYDIUM_AMM_SWAP_BASE_IN => {
706            crate::logs::raydium_amm::parse_swap_base_in_from_data(data, metadata)
707        }
708        discriminators::RAYDIUM_AMM_SWAP_BASE_OUT => {
709            crate::logs::raydium_amm::parse_swap_base_out_from_data(data, metadata)
710        }
711        discriminators::RAYDIUM_AMM_DEPOSIT => {
712            crate::logs::raydium_amm::parse_deposit_from_data(data, metadata)
713        }
714        discriminators::RAYDIUM_AMM_WITHDRAW => {
715            crate::logs::raydium_amm::parse_withdraw_from_data(data, metadata)
716        }
717        discriminators::RAYDIUM_AMM_INITIALIZE2 => {
718            crate::logs::raydium_amm::parse_initialize2_from_data(data, metadata)
719        }
720        discriminators::RAYDIUM_AMM_WITHDRAW_PNL => {
721            crate::logs::raydium_amm::parse_withdraw_pnl_from_data(data, metadata)
722        }
723
724        // Orca Whirlpool - use from_data functions (single decode)
725        discriminators::ORCA_TRADED => {
726            crate::logs::orca_whirlpool::parse_traded_from_data(data, metadata)
727        }
728        discriminators::ORCA_LIQUIDITY_INCREASED => {
729            crate::logs::orca_whirlpool::parse_liquidity_increased_from_data(data, metadata)
730        }
731        discriminators::ORCA_LIQUIDITY_DECREASED => {
732            crate::logs::orca_whirlpool::parse_liquidity_decreased_from_data(data, metadata)
733        }
734        discriminators::ORCA_POOL_INITIALIZED => {
735            crate::logs::orca_whirlpool::parse_pool_initialized_from_data(data, metadata)
736        }
737
738        // Meteora AMM - use from_data functions (single decode)
739        discriminators::METEORA_AMM_SWAP => {
740            crate::logs::meteora_amm::parse_swap_from_data(data, metadata)
741        }
742        discriminators::METEORA_AMM_ADD_LIQUIDITY => {
743            crate::logs::meteora_amm::parse_add_liquidity_from_data(data, metadata)
744        }
745        discriminators::METEORA_AMM_REMOVE_LIQUIDITY => {
746            crate::logs::meteora_amm::parse_remove_liquidity_from_data(data, metadata)
747        }
748        discriminators::METEORA_AMM_BOOTSTRAP_LIQUIDITY => {
749            crate::logs::meteora_amm::parse_bootstrap_liquidity_from_data(data, metadata)
750        }
751        discriminators::METEORA_AMM_POOL_CREATED => {
752            crate::logs::meteora_amm::parse_pool_created_from_data(data, metadata)
753        }
754        discriminators::METEORA_AMM_SET_POOL_FEES => {
755            crate::logs::meteora_amm::parse_set_pool_fees_from_data(data, metadata)
756        }
757
758        // Meteora DAMM V2
759        discriminators::METEORA_DAMM_SWAP => {
760            crate::logs::meteora_damm::parse_swap_from_data(data, metadata)
761        }
762        discriminators::METEORA_DAMM_SWAP2 => {
763            crate::logs::meteora_damm::parse_swap2_from_data(data, metadata)
764        }
765        discriminators::METEORA_DAMM_ADD_LIQUIDITY => {
766            crate::logs::meteora_damm::parse_add_liquidity_from_data(data, metadata)
767        }
768        discriminators::METEORA_DAMM_REMOVE_LIQUIDITY => {
769            crate::logs::meteora_damm::parse_remove_liquidity_from_data(data, metadata)
770        }
771        discriminators::METEORA_DAMM_CREATE_POSITION => {
772            crate::logs::meteora_damm::parse_create_position_from_data(data, metadata)
773        }
774        discriminators::METEORA_DAMM_CLOSE_POSITION => {
775            crate::logs::meteora_damm::parse_close_position_from_data(data, metadata)
776        }
777
778        // NOTE: Meteora DLMM discriminators conflict with Raydium CPMM!
779        // METEORA_DLMM_SWAP == RAYDIUM_CPMM_SWAP_BASE_IN
780        // Handle DLMM in fallback using log content detection
781
782        // Unknown discriminator - try fallback protocols
783        _ => {
784            // Try Meteora DLMM (has discriminator conflict with Raydium CPMM)
785            if let Some(event) = crate::logs::parse_meteora_dlmm_log(
786                log,
787                signature,
788                slot,
789                tx_index,
790                block_time_us,
791                grpc_recv_us,
792            ) {
793                return apply_event_type_filter(event, event_type_filter);
794            }
795            None
796        }
797    }?;
798    apply_event_type_filter(event, event_type_filter)
799}
800
801#[inline(always)]
802fn program_scoped_discriminator_to_event_type(
803    program_id: &Pubkey,
804    discriminator: u64,
805) -> Option<EventType> {
806    match *program_id {
807        program_ids::PUMPFUN_PROGRAM_ID => match discriminator {
808            discriminators::PUMPFUN_CREATE => Some(EventType::PumpFunCreate),
809            discriminators::PUMPFUN_TRADE => Some(EventType::PumpFunTrade),
810            discriminators::PUMPFUN_MIGRATE => Some(EventType::PumpFunMigrate),
811            discriminators::PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR => {
812                Some(EventType::PumpFunMigrateBondingCurveCreator)
813            }
814            _ => None,
815        },
816        program_ids::PUMP_FEES_PROGRAM_ID => match discriminator {
817            discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG => {
818                Some(EventType::PumpFeesCreateFeeSharingConfig)
819            }
820            discriminators::PUMP_FEES_INITIALIZE_FEE_CONFIG => {
821                Some(EventType::PumpFeesInitializeFeeConfig)
822            }
823            discriminators::PUMP_FEES_RESET_FEE_SHARING_CONFIG => {
824                Some(EventType::PumpFeesResetFeeSharingConfig)
825            }
826            discriminators::PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY => {
827                Some(EventType::PumpFeesRevokeFeeSharingAuthority)
828            }
829            discriminators::PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY => {
830                Some(EventType::PumpFeesTransferFeeSharingAuthority)
831            }
832            discriminators::PUMP_FEES_UPDATE_ADMIN => Some(EventType::PumpFeesUpdateAdmin),
833            discriminators::PUMP_FEES_UPDATE_FEE_CONFIG => Some(EventType::PumpFeesUpdateFeeConfig),
834            discriminators::PUMP_FEES_UPDATE_FEE_SHARES => Some(EventType::PumpFeesUpdateFeeShares),
835            discriminators::PUMP_FEES_UPSERT_FEE_TIERS => Some(EventType::PumpFeesUpsertFeeTiers),
836            _ => None,
837        },
838        program_ids::PUMPSWAP_PROGRAM_ID => match discriminator {
839            discriminators::PUMPSWAP_BUY => Some(EventType::PumpSwapBuy),
840            discriminators::PUMPSWAP_SELL => Some(EventType::PumpSwapSell),
841            discriminators::PUMPSWAP_CREATE_POOL => Some(EventType::PumpSwapCreatePool),
842            discriminators::PUMPSWAP_ADD_LIQUIDITY => Some(EventType::PumpSwapLiquidityAdded),
843            discriminators::PUMPSWAP_REMOVE_LIQUIDITY => Some(EventType::PumpSwapLiquidityRemoved),
844            _ => None,
845        },
846        program_ids::BONK_PROGRAM_ID => match discriminator {
847            discriminators::RAYDIUM_LAUNCHPAD_TRADE => Some(EventType::BonkTrade),
848            discriminators::RAYDIUM_LAUNCHPAD_POOL_CREATE => Some(EventType::BonkPoolCreate),
849            _ => None,
850        },
851        program_ids::RAYDIUM_CLMM_PROGRAM_ID => match discriminator {
852            discriminators::RAYDIUM_CLMM_SWAP => Some(EventType::RaydiumClmmSwap),
853            discriminators::RAYDIUM_CLMM_INCREASE_LIQUIDITY => {
854                Some(EventType::RaydiumClmmIncreaseLiquidity)
855            }
856            discriminators::RAYDIUM_CLMM_DECREASE_LIQUIDITY => {
857                Some(EventType::RaydiumClmmDecreaseLiquidity)
858            }
859            discriminators::RAYDIUM_CLMM_CREATE_POOL => Some(EventType::RaydiumClmmCreatePool),
860            discriminators::RAYDIUM_CLMM_COLLECT_FEE => Some(EventType::RaydiumClmmCollectFee),
861            _ => None,
862        },
863        program_ids::RAYDIUM_CPMM_PROGRAM_ID => match discriminator {
864            discriminators::RAYDIUM_CPMM_SWAP_BASE_IN
865            | discriminators::RAYDIUM_CPMM_SWAP_BASE_OUT => Some(EventType::RaydiumCpmmSwap),
866            discriminators::RAYDIUM_CPMM_CREATE_POOL => Some(EventType::RaydiumCpmmInitialize),
867            discriminators::RAYDIUM_CPMM_DEPOSIT => Some(EventType::RaydiumCpmmDeposit),
868            discriminators::RAYDIUM_CPMM_WITHDRAW => Some(EventType::RaydiumCpmmWithdraw),
869            _ => None,
870        },
871        program_ids::RAYDIUM_AMM_V4_PROGRAM_ID => match discriminator {
872            discriminators::RAYDIUM_AMM_SWAP_BASE_IN
873            | discriminators::RAYDIUM_AMM_SWAP_BASE_OUT => Some(EventType::RaydiumAmmV4Swap),
874            discriminators::RAYDIUM_AMM_DEPOSIT => Some(EventType::RaydiumAmmV4Deposit),
875            discriminators::RAYDIUM_AMM_WITHDRAW => Some(EventType::RaydiumAmmV4Withdraw),
876            discriminators::RAYDIUM_AMM_INITIALIZE2 => Some(EventType::RaydiumAmmV4Initialize2),
877            discriminators::RAYDIUM_AMM_WITHDRAW_PNL => Some(EventType::RaydiumAmmV4WithdrawPnl),
878            _ => None,
879        },
880        program_ids::ORCA_WHIRLPOOL_PROGRAM_ID => match discriminator {
881            discriminators::ORCA_TRADED => Some(EventType::OrcaWhirlpoolSwap),
882            discriminators::ORCA_LIQUIDITY_INCREASED => {
883                Some(EventType::OrcaWhirlpoolLiquidityIncreased)
884            }
885            discriminators::ORCA_LIQUIDITY_DECREASED => {
886                Some(EventType::OrcaWhirlpoolLiquidityDecreased)
887            }
888            discriminators::ORCA_POOL_INITIALIZED => Some(EventType::OrcaWhirlpoolPoolInitialized),
889            _ => None,
890        },
891        program_ids::METEORA_POOLS_PROGRAM_ID => match discriminator {
892            discriminators::METEORA_AMM_SWAP => Some(EventType::MeteoraPoolsSwap),
893            discriminators::METEORA_AMM_ADD_LIQUIDITY => Some(EventType::MeteoraPoolsAddLiquidity),
894            discriminators::METEORA_AMM_REMOVE_LIQUIDITY => {
895                Some(EventType::MeteoraPoolsRemoveLiquidity)
896            }
897            discriminators::METEORA_AMM_BOOTSTRAP_LIQUIDITY => {
898                Some(EventType::MeteoraPoolsBootstrapLiquidity)
899            }
900            discriminators::METEORA_AMM_POOL_CREATED => Some(EventType::MeteoraPoolsPoolCreated),
901            discriminators::METEORA_AMM_SET_POOL_FEES => Some(EventType::MeteoraPoolsSetPoolFees),
902            _ => None,
903        },
904        program_ids::METEORA_DAMM_V2_PROGRAM_ID => match discriminator {
905            discriminators::METEORA_DAMM_SWAP | discriminators::METEORA_DAMM_SWAP2 => {
906                Some(EventType::MeteoraDammV2Swap)
907            }
908            discriminators::METEORA_DAMM_ADD_LIQUIDITY => {
909                Some(EventType::MeteoraDammV2AddLiquidity)
910            }
911            discriminators::METEORA_DAMM_REMOVE_LIQUIDITY => {
912                Some(EventType::MeteoraDammV2RemoveLiquidity)
913            }
914            discriminators::METEORA_DAMM_CREATE_POSITION => {
915                Some(EventType::MeteoraDammV2CreatePosition)
916            }
917            discriminators::METEORA_DAMM_CLOSE_POSITION => {
918                Some(EventType::MeteoraDammV2ClosePosition)
919            }
920            _ => None,
921        },
922        program_ids::METEORA_DLMM_PROGRAM_ID => match discriminator {
923            discriminators::METEORA_DLMM_SWAP => Some(EventType::MeteoraDlmmSwap),
924            discriminators::METEORA_DLMM_ADD_LIQUIDITY => Some(EventType::MeteoraDlmmAddLiquidity),
925            discriminators::METEORA_DLMM_REMOVE_LIQUIDITY => {
926                Some(EventType::MeteoraDlmmRemoveLiquidity)
927            }
928            discriminators::METEORA_DLMM_INITIALIZE_POOL => {
929                Some(EventType::MeteoraDlmmInitializePool)
930            }
931            discriminators::METEORA_DLMM_INITIALIZE_BIN_ARRAY => {
932                Some(EventType::MeteoraDlmmInitializeBinArray)
933            }
934            discriminators::METEORA_DLMM_CREATE_POSITION => {
935                Some(EventType::MeteoraDlmmCreatePosition)
936            }
937            discriminators::METEORA_DLMM_CLOSE_POSITION => {
938                Some(EventType::MeteoraDlmmClosePosition)
939            }
940            discriminators::METEORA_DLMM_CLAIM_FEE => Some(EventType::MeteoraDlmmClaimFee),
941            _ => None,
942        },
943        _ => None,
944    }
945}
946
947#[inline(always)]
948fn parse_program_scoped_event(
949    program_id: &Pubkey,
950    discriminator: u64,
951    data: &[u8],
952    metadata: EventMetadata,
953    log: &str,
954    signature: Signature,
955    slot: u64,
956    tx_index: u64,
957    block_time_us: Option<i64>,
958    grpc_recv_us: i64,
959    event_type_filter: Option<&EventTypeFilter>,
960    is_created_buy: bool,
961) -> Option<DexEvent> {
962    if let Some(filter) = event_type_filter {
963        if let Some(event_type) =
964            program_scoped_discriminator_to_event_type(program_id, discriminator)
965        {
966            if !filter.should_include(event_type) {
967                return None;
968            }
969        }
970    }
971
972    match *program_id {
973        program_ids::PUMPFUN_PROGRAM_ID => {
974            if let Some(filter) = event_type_filter {
975                if !filter.includes_pumpfun() {
976                    return None;
977                }
978            }
979            match discriminator {
980                discriminators::PUMPFUN_TRADE => {
981                    let event =
982                        crate::logs::pump::parse_trade_from_data(data, metadata, is_created_buy)?;
983                    filter_pumpfun_trade_variant(event, event_type_filter)
984                }
985                discriminators::PUMPFUN_CREATE => {
986                    crate::logs::pump::parse_create_from_data(data, metadata)
987                }
988                discriminators::PUMPFUN_MIGRATE => {
989                    crate::logs::pump::parse_migrate_from_data(data, metadata)
990                }
991                discriminators::PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR => {
992                    crate::logs::pump::parse_migrate_bonding_curve_creator_from_data(data, metadata)
993                }
994                _ => None,
995            }
996        }
997        program_ids::PUMP_FEES_PROGRAM_ID => {
998            if let Some(filter) = event_type_filter {
999                if !filter.includes_pump_fees() {
1000                    return None;
1001                }
1002            }
1003            match discriminator {
1004                discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG => {
1005                    crate::logs::pump_fees::parse_create_fee_sharing_config_from_data(
1006                        data, metadata,
1007                    )
1008                }
1009                discriminators::PUMP_FEES_INITIALIZE_FEE_CONFIG => {
1010                    crate::logs::pump_fees::parse_initialize_fee_config_from_data(data, metadata)
1011                }
1012                discriminators::PUMP_FEES_RESET_FEE_SHARING_CONFIG => {
1013                    crate::logs::pump_fees::parse_reset_fee_sharing_config_from_data(data, metadata)
1014                }
1015                discriminators::PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY => {
1016                    crate::logs::pump_fees::parse_revoke_fee_sharing_authority_from_data(
1017                        data, metadata,
1018                    )
1019                }
1020                discriminators::PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY => {
1021                    crate::logs::pump_fees::parse_transfer_fee_sharing_authority_from_data(
1022                        data, metadata,
1023                    )
1024                }
1025                discriminators::PUMP_FEES_UPDATE_ADMIN => {
1026                    crate::logs::pump_fees::parse_update_admin_from_data(data, metadata)
1027                }
1028                discriminators::PUMP_FEES_UPDATE_FEE_CONFIG => {
1029                    crate::logs::pump_fees::parse_update_fee_config_from_data(data, metadata)
1030                }
1031                discriminators::PUMP_FEES_UPDATE_FEE_SHARES => {
1032                    crate::logs::pump_fees::parse_update_fee_shares_from_data(data, metadata)
1033                }
1034                discriminators::PUMP_FEES_UPSERT_FEE_TIERS => {
1035                    crate::logs::pump_fees::parse_upsert_fee_tiers_from_data(data, metadata)
1036                }
1037                _ => None,
1038            }
1039        }
1040        program_ids::PUMPSWAP_PROGRAM_ID => {
1041            if let Some(filter) = event_type_filter {
1042                if !filter.includes_pumpswap() {
1043                    return None;
1044                }
1045            }
1046            match discriminator {
1047                discriminators::PUMPSWAP_BUY => {
1048                    crate::logs::pump_amm::parse_buy_from_data(data, metadata)
1049                }
1050                discriminators::PUMPSWAP_SELL => {
1051                    crate::logs::pump_amm::parse_sell_from_data(data, metadata)
1052                }
1053                discriminators::PUMPSWAP_CREATE_POOL => {
1054                    crate::logs::pump_amm::parse_create_pool_from_data(data, metadata)
1055                }
1056                discriminators::PUMPSWAP_ADD_LIQUIDITY => {
1057                    crate::logs::pump_amm::parse_add_liquidity_from_data(data, metadata)
1058                }
1059                discriminators::PUMPSWAP_REMOVE_LIQUIDITY => {
1060                    crate::logs::pump_amm::parse_remove_liquidity_from_data(data, metadata)
1061                }
1062                _ => None,
1063            }
1064        }
1065        program_ids::BONK_PROGRAM_ID => {
1066            if let Some(filter) = event_type_filter {
1067                if !filter.includes_raydium_launchpad() {
1068                    return None;
1069                }
1070            }
1071            match discriminator {
1072                discriminators::RAYDIUM_LAUNCHPAD_TRADE => {
1073                    crate::logs::raydium_launchpad::parse_trade_from_data(data, metadata)
1074                }
1075                discriminators::RAYDIUM_LAUNCHPAD_POOL_CREATE => {
1076                    crate::logs::raydium_launchpad::parse_pool_create_from_data(data, metadata)
1077                }
1078                _ => None,
1079            }
1080        }
1081        program_ids::RAYDIUM_CLMM_PROGRAM_ID => {
1082            if let Some(filter) = event_type_filter {
1083                if !filter.includes_raydium_clmm() {
1084                    return None;
1085                }
1086            }
1087            match discriminator {
1088                discriminators::RAYDIUM_CLMM_SWAP => {
1089                    crate::logs::raydium_clmm::parse_swap_from_data(data, metadata)
1090                }
1091                discriminators::RAYDIUM_CLMM_INCREASE_LIQUIDITY => {
1092                    crate::logs::raydium_clmm::parse_increase_liquidity_from_data(data, metadata)
1093                }
1094                discriminators::RAYDIUM_CLMM_DECREASE_LIQUIDITY => {
1095                    crate::logs::raydium_clmm::parse_decrease_liquidity_from_data(data, metadata)
1096                }
1097                discriminators::RAYDIUM_CLMM_CREATE_POOL => {
1098                    crate::logs::raydium_clmm::parse_create_pool_from_data(data, metadata)
1099                }
1100                discriminators::RAYDIUM_CLMM_COLLECT_FEE => {
1101                    crate::logs::raydium_clmm::parse_collect_fee_from_data(data, metadata)
1102                }
1103                _ => None,
1104            }
1105        }
1106        program_ids::RAYDIUM_CPMM_PROGRAM_ID => {
1107            if let Some(filter) = event_type_filter {
1108                if !filter.includes_raydium_cpmm() {
1109                    return None;
1110                }
1111            }
1112            match discriminator {
1113                discriminators::RAYDIUM_CPMM_SWAP_BASE_IN => {
1114                    crate::logs::raydium_cpmm::parse_swap_base_in_from_data(data, metadata)
1115                }
1116                discriminators::RAYDIUM_CPMM_SWAP_BASE_OUT => {
1117                    crate::logs::raydium_cpmm::parse_swap_base_out_from_data(data, metadata)
1118                }
1119                discriminators::RAYDIUM_CPMM_CREATE_POOL => {
1120                    crate::logs::raydium_cpmm::parse_create_pool_from_data(data, metadata)
1121                }
1122                discriminators::RAYDIUM_CPMM_DEPOSIT => {
1123                    crate::logs::raydium_cpmm::parse_deposit_from_data(data, metadata)
1124                }
1125                discriminators::RAYDIUM_CPMM_WITHDRAW => {
1126                    crate::logs::raydium_cpmm::parse_withdraw_from_data(data, metadata)
1127                }
1128                _ => None,
1129            }
1130        }
1131        program_ids::RAYDIUM_AMM_V4_PROGRAM_ID => {
1132            if let Some(filter) = event_type_filter {
1133                if !filter.includes_raydium_amm_v4() {
1134                    return None;
1135                }
1136            }
1137            match discriminator {
1138                discriminators::RAYDIUM_AMM_SWAP_BASE_IN => {
1139                    crate::logs::raydium_amm::parse_swap_base_in_from_data(data, metadata)
1140                }
1141                discriminators::RAYDIUM_AMM_SWAP_BASE_OUT => {
1142                    crate::logs::raydium_amm::parse_swap_base_out_from_data(data, metadata)
1143                }
1144                discriminators::RAYDIUM_AMM_DEPOSIT => {
1145                    crate::logs::raydium_amm::parse_deposit_from_data(data, metadata)
1146                }
1147                discriminators::RAYDIUM_AMM_WITHDRAW => {
1148                    crate::logs::raydium_amm::parse_withdraw_from_data(data, metadata)
1149                }
1150                discriminators::RAYDIUM_AMM_INITIALIZE2 => {
1151                    crate::logs::raydium_amm::parse_initialize2_from_data(data, metadata)
1152                }
1153                discriminators::RAYDIUM_AMM_WITHDRAW_PNL => {
1154                    crate::logs::raydium_amm::parse_withdraw_pnl_from_data(data, metadata)
1155                }
1156                _ => None,
1157            }
1158        }
1159        program_ids::ORCA_WHIRLPOOL_PROGRAM_ID => {
1160            if let Some(filter) = event_type_filter {
1161                if !filter.includes_orca_whirlpool() {
1162                    return None;
1163                }
1164            }
1165            match discriminator {
1166                discriminators::ORCA_TRADED => {
1167                    crate::logs::orca_whirlpool::parse_traded_from_data(data, metadata)
1168                }
1169                discriminators::ORCA_LIQUIDITY_INCREASED => {
1170                    crate::logs::orca_whirlpool::parse_liquidity_increased_from_data(data, metadata)
1171                }
1172                discriminators::ORCA_LIQUIDITY_DECREASED => {
1173                    crate::logs::orca_whirlpool::parse_liquidity_decreased_from_data(data, metadata)
1174                }
1175                discriminators::ORCA_POOL_INITIALIZED => {
1176                    crate::logs::orca_whirlpool::parse_pool_initialized_from_data(data, metadata)
1177                }
1178                _ => None,
1179            }
1180        }
1181        program_ids::METEORA_POOLS_PROGRAM_ID => {
1182            if let Some(filter) = event_type_filter {
1183                if !filter.includes_meteora_pools() {
1184                    return None;
1185                }
1186            }
1187            match discriminator {
1188                discriminators::METEORA_AMM_SWAP => {
1189                    crate::logs::meteora_amm::parse_swap_from_data(data, metadata)
1190                }
1191                discriminators::METEORA_AMM_ADD_LIQUIDITY => {
1192                    crate::logs::meteora_amm::parse_add_liquidity_from_data(data, metadata)
1193                }
1194                discriminators::METEORA_AMM_REMOVE_LIQUIDITY => {
1195                    crate::logs::meteora_amm::parse_remove_liquidity_from_data(data, metadata)
1196                }
1197                discriminators::METEORA_AMM_BOOTSTRAP_LIQUIDITY => {
1198                    crate::logs::meteora_amm::parse_bootstrap_liquidity_from_data(data, metadata)
1199                }
1200                discriminators::METEORA_AMM_POOL_CREATED => {
1201                    crate::logs::meteora_amm::parse_pool_created_from_data(data, metadata)
1202                }
1203                discriminators::METEORA_AMM_SET_POOL_FEES => {
1204                    crate::logs::meteora_amm::parse_set_pool_fees_from_data(data, metadata)
1205                }
1206                _ => None,
1207            }
1208        }
1209        program_ids::METEORA_DAMM_V2_PROGRAM_ID => {
1210            if let Some(filter) = event_type_filter {
1211                if !filter.includes_meteora_damm_v2() {
1212                    return None;
1213                }
1214            }
1215            match discriminator {
1216                discriminators::METEORA_DAMM_SWAP => {
1217                    crate::logs::meteora_damm::parse_swap_from_data(data, metadata)
1218                }
1219                discriminators::METEORA_DAMM_SWAP2 => {
1220                    crate::logs::meteora_damm::parse_swap2_from_data(data, metadata)
1221                }
1222                discriminators::METEORA_DAMM_ADD_LIQUIDITY => {
1223                    crate::logs::meteora_damm::parse_add_liquidity_from_data(data, metadata)
1224                }
1225                discriminators::METEORA_DAMM_REMOVE_LIQUIDITY => {
1226                    crate::logs::meteora_damm::parse_remove_liquidity_from_data(data, metadata)
1227                }
1228                discriminators::METEORA_DAMM_CREATE_POSITION => {
1229                    crate::logs::meteora_damm::parse_create_position_from_data(data, metadata)
1230                }
1231                discriminators::METEORA_DAMM_CLOSE_POSITION => {
1232                    crate::logs::meteora_damm::parse_close_position_from_data(data, metadata)
1233                }
1234                _ => None,
1235            }
1236        }
1237        program_ids::METEORA_DLMM_PROGRAM_ID => {
1238            if let Some(filter) = event_type_filter {
1239                if !filter.includes_meteora_dlmm() {
1240                    return None;
1241                }
1242            }
1243            match discriminator {
1244                discriminators::METEORA_DLMM_SWAP => {
1245                    crate::logs::meteora_dlmm::parse_swap_from_data(data, metadata)
1246                }
1247                discriminators::METEORA_DLMM_ADD_LIQUIDITY => {
1248                    crate::logs::meteora_dlmm::parse_add_liquidity_from_data(data, metadata)
1249                }
1250                discriminators::METEORA_DLMM_REMOVE_LIQUIDITY => {
1251                    crate::logs::meteora_dlmm::parse_remove_liquidity_from_data(data, metadata)
1252                }
1253                discriminators::METEORA_DLMM_INITIALIZE_POOL => {
1254                    crate::logs::meteora_dlmm::parse_initialize_pool_from_data(data, metadata)
1255                }
1256                discriminators::METEORA_DLMM_INITIALIZE_BIN_ARRAY => {
1257                    crate::logs::meteora_dlmm::parse_initialize_bin_array_from_data(data, metadata)
1258                }
1259                discriminators::METEORA_DLMM_CREATE_POSITION => {
1260                    crate::logs::meteora_dlmm::parse_create_position_from_data(data, metadata)
1261                }
1262                discriminators::METEORA_DLMM_CLOSE_POSITION => {
1263                    crate::logs::meteora_dlmm::parse_close_position_from_data(data, metadata)
1264                }
1265                discriminators::METEORA_DLMM_CLAIM_FEE => {
1266                    crate::logs::meteora_dlmm::parse_claim_fee_from_data(data, metadata)
1267                }
1268                _ => None,
1269            }
1270        }
1271        _ => None,
1272    }
1273}
1274
1275#[inline(always)]
1276fn filter_pumpfun_trade_variant(
1277    event: DexEvent,
1278    event_type_filter: Option<&EventTypeFilter>,
1279) -> Option<DexEvent> {
1280    if let Some(filter) = event_type_filter {
1281        if let Some(ref include_only) = filter.include_only {
1282            let has_specific_filter = !include_only.contains(&EventType::PumpFunTrade)
1283                && include_only.iter().any(|t| {
1284                    matches!(
1285                        t,
1286                        EventType::PumpFunBuy
1287                            | EventType::PumpFunSell
1288                            | EventType::PumpFunBuyExactSolIn
1289                            | EventType::PumpFunCreate
1290                            | EventType::PumpFunCreateV2
1291                    )
1292                });
1293            if has_specific_filter {
1294                let event_type_matches = match &event {
1295                    DexEvent::PumpFunBuy(_) => include_only.contains(&EventType::PumpFunBuy),
1296                    DexEvent::PumpFunSell(_) => include_only.contains(&EventType::PumpFunSell),
1297                    DexEvent::PumpFunBuyExactSolIn(_) => {
1298                        include_only.contains(&EventType::PumpFunBuyExactSolIn)
1299                    }
1300                    DexEvent::PumpFunTrade(_) => include_only.contains(&EventType::PumpFunTrade),
1301                    DexEvent::PumpFunCreate(_) => include_only.contains(&EventType::PumpFunCreate),
1302                    DexEvent::PumpFunCreateV2(_) => {
1303                        include_only.contains(&EventType::PumpFunCreateV2)
1304                    }
1305                    _ => false,
1306                };
1307                if !event_type_matches {
1308                    return None;
1309                }
1310            }
1311        }
1312        if filter.exclude_types.is_some() && !filter.should_include_dex_event(&event) {
1313            return None;
1314        }
1315    }
1316    Some(event)
1317}
1318
1319/// Map discriminator to EventType (compile-time optimized match)
1320#[inline(always)]
1321fn discriminator_to_event_type(discriminator: u64) -> Option<EventType> {
1322    match discriminator {
1323        discriminators::PUMPFUN_CREATE => Some(EventType::PumpFunCreate),
1324        discriminators::PUMPFUN_TRADE => Some(EventType::PumpFunTrade),
1325        discriminators::PUMPFUN_MIGRATE => Some(EventType::PumpFunMigrate),
1326        discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG => {
1327            Some(EventType::PumpFeesCreateFeeSharingConfig)
1328        }
1329        discriminators::PUMP_FEES_INITIALIZE_FEE_CONFIG => {
1330            Some(EventType::PumpFeesInitializeFeeConfig)
1331        }
1332        discriminators::PUMP_FEES_RESET_FEE_SHARING_CONFIG => {
1333            Some(EventType::PumpFeesResetFeeSharingConfig)
1334        }
1335        discriminators::PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY => {
1336            Some(EventType::PumpFeesRevokeFeeSharingAuthority)
1337        }
1338        discriminators::PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY => {
1339            Some(EventType::PumpFeesTransferFeeSharingAuthority)
1340        }
1341        discriminators::PUMP_FEES_UPDATE_ADMIN => Some(EventType::PumpFeesUpdateAdmin),
1342        discriminators::PUMP_FEES_UPDATE_FEE_CONFIG => Some(EventType::PumpFeesUpdateFeeConfig),
1343        discriminators::PUMP_FEES_UPDATE_FEE_SHARES => Some(EventType::PumpFeesUpdateFeeShares),
1344        discriminators::PUMP_FEES_UPSERT_FEE_TIERS => Some(EventType::PumpFeesUpsertFeeTiers),
1345        discriminators::PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR => {
1346            Some(EventType::PumpFunMigrateBondingCurveCreator)
1347        }
1348        discriminators::PUMPSWAP_BUY => Some(EventType::PumpSwapBuy),
1349        discriminators::PUMPSWAP_SELL => Some(EventType::PumpSwapSell),
1350        discriminators::PUMPSWAP_CREATE_POOL => Some(EventType::PumpSwapCreatePool),
1351        discriminators::PUMPSWAP_ADD_LIQUIDITY => Some(EventType::PumpSwapLiquidityAdded),
1352        discriminators::PUMPSWAP_REMOVE_LIQUIDITY => Some(EventType::PumpSwapLiquidityRemoved),
1353        discriminators::RAYDIUM_LAUNCHPAD_POOL_CREATE => Some(EventType::BonkPoolCreate),
1354        discriminators::RAYDIUM_CLMM_SWAP => Some(EventType::RaydiumClmmSwap),
1355        discriminators::RAYDIUM_CLMM_INCREASE_LIQUIDITY => {
1356            Some(EventType::RaydiumClmmIncreaseLiquidity)
1357        }
1358        discriminators::RAYDIUM_CLMM_DECREASE_LIQUIDITY => {
1359            Some(EventType::RaydiumClmmDecreaseLiquidity)
1360        }
1361        discriminators::RAYDIUM_CLMM_CREATE_POOL => Some(EventType::RaydiumClmmCreatePool),
1362        discriminators::RAYDIUM_CLMM_COLLECT_FEE => Some(EventType::RaydiumClmmCollectFee),
1363        discriminators::RAYDIUM_CPMM_SWAP_BASE_IN | discriminators::RAYDIUM_CPMM_SWAP_BASE_OUT => {
1364            Some(EventType::RaydiumCpmmSwap)
1365        }
1366        discriminators::RAYDIUM_CPMM_DEPOSIT => Some(EventType::RaydiumCpmmDeposit),
1367        discriminators::RAYDIUM_CPMM_WITHDRAW => Some(EventType::RaydiumCpmmWithdraw),
1368        discriminators::RAYDIUM_AMM_SWAP_BASE_IN | discriminators::RAYDIUM_AMM_SWAP_BASE_OUT => {
1369            Some(EventType::RaydiumAmmV4Swap)
1370        }
1371        discriminators::RAYDIUM_AMM_DEPOSIT => Some(EventType::RaydiumAmmV4Deposit),
1372        discriminators::RAYDIUM_AMM_WITHDRAW => Some(EventType::RaydiumAmmV4Withdraw),
1373        discriminators::RAYDIUM_AMM_INITIALIZE2 => Some(EventType::RaydiumAmmV4Initialize2),
1374        discriminators::RAYDIUM_AMM_WITHDRAW_PNL => Some(EventType::RaydiumAmmV4WithdrawPnl),
1375        discriminators::ORCA_TRADED => Some(EventType::OrcaWhirlpoolSwap),
1376        discriminators::ORCA_LIQUIDITY_INCREASED => {
1377            Some(EventType::OrcaWhirlpoolLiquidityIncreased)
1378        }
1379        discriminators::ORCA_LIQUIDITY_DECREASED => {
1380            Some(EventType::OrcaWhirlpoolLiquidityDecreased)
1381        }
1382        discriminators::ORCA_POOL_INITIALIZED => Some(EventType::OrcaWhirlpoolPoolInitialized),
1383        discriminators::METEORA_AMM_SWAP => Some(EventType::MeteoraPoolsSwap),
1384        discriminators::METEORA_AMM_ADD_LIQUIDITY => Some(EventType::MeteoraPoolsAddLiquidity),
1385        discriminators::METEORA_AMM_REMOVE_LIQUIDITY => {
1386            Some(EventType::MeteoraPoolsRemoveLiquidity)
1387        }
1388        discriminators::METEORA_AMM_BOOTSTRAP_LIQUIDITY => {
1389            Some(EventType::MeteoraPoolsBootstrapLiquidity)
1390        }
1391        discriminators::METEORA_AMM_POOL_CREATED => Some(EventType::MeteoraPoolsPoolCreated),
1392        discriminators::METEORA_AMM_SET_POOL_FEES => Some(EventType::MeteoraPoolsSetPoolFees),
1393        discriminators::METEORA_DAMM_SWAP | discriminators::METEORA_DAMM_SWAP2 => {
1394            Some(EventType::MeteoraDammV2Swap)
1395        }
1396        discriminators::METEORA_DAMM_ADD_LIQUIDITY => Some(EventType::MeteoraDammV2AddLiquidity),
1397        discriminators::METEORA_DAMM_REMOVE_LIQUIDITY => {
1398            Some(EventType::MeteoraDammV2RemoveLiquidity)
1399        }
1400        discriminators::METEORA_DAMM_CREATE_POSITION => {
1401            Some(EventType::MeteoraDammV2CreatePosition)
1402        }
1403        discriminators::METEORA_DAMM_CLOSE_POSITION => Some(EventType::MeteoraDammV2ClosePosition),
1404        _ => None,
1405    }
1406}
1407
1408// ============================================================================
1409// SIMD utilities for log detection
1410// ============================================================================
1411#[inline]
1412pub fn detect_pumpfun_create(logs: &[String]) -> bool {
1413    logs.iter().any(|log| PUMPFUN_CREATE_FINDER.find(log.as_bytes()).is_some())
1414}
1415
1416/// SIMD 优化的 "invoke [" 查找器
1417static INVOKE_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"invoke ["));
1418
1419/// 从日志中解析指令调用信息 (SIMD 优化版本)
1420/// 返回 (program_id, depth)
1421#[inline]
1422pub fn parse_invoke_info(log: &str) -> Option<(&str, usize)> {
1423    let log_bytes = log.as_bytes();
1424
1425    // SIMD 快速查找 "invoke ["
1426    let invoke_start = INVOKE_FINDER.find(log_bytes)?;
1427    let bracket_start = invoke_start + 8; // "invoke [" 长度
1428
1429    // 边界检查
1430    if bracket_start >= log_bytes.len() {
1431        return None;
1432    }
1433
1434    // 解析深度数字,直到遇到 ']'
1435    let mut depth = 0usize;
1436    for &byte in &log_bytes[bracket_start..] {
1437        match byte {
1438            b'0'..=b'9' => {
1439                depth = depth * 10 + (byte - b'0') as usize;
1440            }
1441            b']' => break,
1442            _ => return None, // 遇到非数字非']'字符,解析失败
1443        }
1444    }
1445
1446    // 提取程序ID:从 "Program " 开始到 " invoke" 结束
1447    if invoke_start < 8 {
1448        return None; // 没有足够空间放 "Program "
1449    }
1450
1451    let program_start = 8; // "Program " 的长度
1452    let program_end = invoke_start - 1; // " invoke" 前面的空格位置
1453
1454    if program_end <= program_start {
1455        return None;
1456    }
1457
1458    let program_id = std::str::from_utf8(&log_bytes[program_start..program_end]).ok()?;
1459
1460    Some((program_id, depth))
1461}
1462
1463/// Parse `Program <id> success` or `Program <id> failed: ...` completion lines.
1464#[inline]
1465pub fn parse_program_complete_info(log: &str) -> Option<&str> {
1466    let rest = log.strip_prefix("Program ")?;
1467    if let Some(pos) = rest.find(" success") {
1468        return Some(&rest[..pos]);
1469    }
1470    if let Some(pos) = rest.find(" failed:") {
1471        return Some(&rest[..pos]);
1472    }
1473    None
1474}
1475
1476#[cfg(test)]
1477mod tests {
1478    use super::*;
1479    use crate::core::events::PumpFunTradeEvent;
1480    use base64::{engine::general_purpose::STANDARD, Engine as _};
1481    use solana_sdk::{pubkey::Pubkey, signature::Signature};
1482
1483    #[test]
1484    fn program_scoped_launchpad_trade_is_not_parsed_as_pumpfun() {
1485        let pool = Pubkey::new_unique();
1486        let mut raw = Vec::new();
1487        raw.extend_from_slice(&discriminators::RAYDIUM_LAUNCHPAD_TRADE.to_le_bytes());
1488        raw.extend_from_slice(pool.as_ref());
1489        for value in 0u64..13 {
1490            raw.extend_from_slice(&(100 + value).to_le_bytes());
1491        }
1492        raw.push(1); // TradeDirection::Sell
1493        raw.push(2); // PoolStatus::Trade
1494        raw.push(1); // exact_in
1495
1496        let log = format!("Program data: {}", STANDARD.encode(raw));
1497        let filter = EventTypeFilter::include_only(vec![EventType::BonkTrade]);
1498        let event = parse_log_optimized_with_program_id(
1499            &log,
1500            Signature::default(),
1501            1,
1502            2,
1503            Some(3),
1504            4,
1505            Some(&filter),
1506            false,
1507            None,
1508            Some(&program_ids::BONK_PROGRAM_ID),
1509        )
1510        .expect("launchpad trade should parse");
1511
1512        match event {
1513            DexEvent::BonkTrade(trade) => {
1514                assert_eq!(trade.pool_state, pool);
1515                assert_eq!(trade.amount_in, 107);
1516                assert_eq!(trade.amount_out, 108);
1517                assert!(!trade.is_buy);
1518                assert!(trade.exact_in);
1519            }
1520            other => panic!("expected BonkTrade, got {other:?}"),
1521        }
1522    }
1523
1524    #[test]
1525    fn program_scoped_dlmm_initialize_bin_array_parses_and_filters() {
1526        let pool = Pubkey::new_unique();
1527        let bin_array = Pubkey::new_unique();
1528        let mut raw = Vec::new();
1529        raw.extend_from_slice(&discriminators::METEORA_DLMM_INITIALIZE_BIN_ARRAY.to_le_bytes());
1530        raw.extend_from_slice(pool.as_ref());
1531        raw.extend_from_slice(bin_array.as_ref());
1532        raw.extend_from_slice(&(-12i64).to_le_bytes());
1533
1534        let log = format!("Program data: {}", STANDARD.encode(raw));
1535        let matching_filter =
1536            EventTypeFilter::include_only(vec![EventType::MeteoraDlmmInitializeBinArray]);
1537        let event = parse_log_optimized_with_program_id(
1538            &log,
1539            Signature::default(),
1540            1,
1541            2,
1542            Some(3),
1543            4,
1544            Some(&matching_filter),
1545            false,
1546            None,
1547            Some(&program_ids::METEORA_DLMM_PROGRAM_ID),
1548        )
1549        .expect("DLMM initialize bin array should parse");
1550
1551        match event {
1552            DexEvent::MeteoraDlmmInitializeBinArray(event) => {
1553                assert_eq!(event.pool, pool);
1554                assert_eq!(event.bin_array, bin_array);
1555                assert_eq!(event.index, -12);
1556            }
1557            other => panic!("expected MeteoraDlmmInitializeBinArray, got {other:?}"),
1558        }
1559
1560        let non_matching_filter = EventTypeFilter::include_only(vec![EventType::MeteoraDlmmSwap]);
1561        assert!(parse_log_optimized_with_program_id(
1562            &log,
1563            Signature::default(),
1564            1,
1565            2,
1566            Some(3),
1567            4,
1568            Some(&non_matching_filter),
1569            false,
1570            None,
1571            Some(&program_ids::METEORA_DLMM_PROGRAM_ID),
1572        )
1573        .is_none());
1574    }
1575
1576    #[test]
1577    fn pumpfun_trade_filter_remains_generic_when_combined_with_specific_type() {
1578        let filter =
1579            EventTypeFilter::include_only(vec![EventType::PumpFunTrade, EventType::PumpFunBuy]);
1580        let event = DexEvent::PumpFunSell(PumpFunTradeEvent {
1581            metadata: EventMetadata::default(),
1582            is_buy: false,
1583            ix_name: "sell".to_string(),
1584            ..Default::default()
1585        });
1586
1587        assert!(filter_pumpfun_trade_variant(event, Some(&filter)).is_some());
1588    }
1589
1590    #[test]
1591    fn discriminator_prefix_filter_handles_program_scoped_collisions() {
1592        let dlmm_filter = EventTypeFilter::include_only(vec![EventType::MeteoraDlmmSwap]);
1593        assert!(filter_allows_discriminator(
1594            Some(&program_ids::METEORA_DLMM_PROGRAM_ID),
1595            discriminators::METEORA_DLMM_SWAP,
1596            Some(&dlmm_filter),
1597        ));
1598        assert!(!filter_allows_discriminator(
1599            Some(&program_ids::RAYDIUM_CPMM_PROGRAM_ID),
1600            discriminators::METEORA_DLMM_SWAP,
1601            Some(&dlmm_filter),
1602        ));
1603
1604        let bonk_filter = EventTypeFilter::include_only(vec![EventType::BonkTrade]);
1605        assert!(filter_allows_discriminator(
1606            Some(&program_ids::BONK_PROGRAM_ID),
1607            discriminators::RAYDIUM_LAUNCHPAD_TRADE,
1608            Some(&bonk_filter),
1609        ));
1610        assert!(!filter_allows_discriminator(
1611            Some(&program_ids::PUMPFUN_PROGRAM_ID),
1612            discriminators::PUMPFUN_TRADE,
1613            Some(&bonk_filter),
1614        ));
1615    }
1616
1617    #[test]
1618    fn discriminator_prefix_filter_keeps_unscoped_collision_candidates() {
1619        let dlmm_filter = EventTypeFilter::include_only(vec![EventType::MeteoraDlmmSwap]);
1620        assert!(filter_allows_discriminator(
1621            None,
1622            discriminators::METEORA_DLMM_SWAP,
1623            Some(&dlmm_filter),
1624        ));
1625
1626        let cpmm_filter = EventTypeFilter::include_only(vec![EventType::RaydiumCpmmInitialize]);
1627        assert!(filter_allows_discriminator(
1628            None,
1629            discriminators::RAYDIUM_CPMM_CREATE_POOL,
1630            Some(&cpmm_filter),
1631        ));
1632    }
1633
1634    #[test]
1635    fn unscoped_collision_does_not_emit_wrong_protocol_event_after_filter() {
1636        let mut raw = Vec::new();
1637        raw.extend_from_slice(&discriminators::METEORA_DLMM_SWAP.to_le_bytes());
1638        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // CPMM pool_state shape
1639        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // CPMM user shape
1640        raw.extend_from_slice(&1u64.to_le_bytes());
1641        raw.extend_from_slice(&2u64.to_le_bytes());
1642        raw.extend_from_slice(&3u64.to_le_bytes());
1643        raw.push(1);
1644
1645        let log = format!("Program data: {}", STANDARD.encode(raw));
1646        let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDlmmSwap]);
1647        assert!(parse_log_optimized(
1648            &log,
1649            Signature::default(),
1650            1,
1651            2,
1652            Some(3),
1653            4,
1654            Some(&filter),
1655            false,
1656            None,
1657        )
1658        .is_none());
1659    }
1660
1661    #[test]
1662    fn unscoped_pumpfun_launchpad_collision_does_not_emit_wrong_protocol_event() {
1663        let mut raw = Vec::new();
1664        raw.extend_from_slice(&discriminators::PUMPFUN_TRADE.to_le_bytes());
1665        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // mint
1666        raw.extend_from_slice(&1u64.to_le_bytes()); // sol_amount
1667        raw.extend_from_slice(&2u64.to_le_bytes()); // token_amount
1668        raw.push(1); // is_buy
1669        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // user
1670        raw.extend_from_slice(&3i64.to_le_bytes()); // timestamp
1671        for value in 4u64..=9 {
1672            raw.extend_from_slice(&value.to_le_bytes());
1673        }
1674        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // fee_recipient
1675        raw.extend_from_slice(&10u64.to_le_bytes()); // fee_basis_points
1676        raw.extend_from_slice(&11u64.to_le_bytes()); // fee
1677        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // creator
1678        raw.extend_from_slice(&12u64.to_le_bytes()); // creator_fee_basis_points
1679        raw.extend_from_slice(&13u64.to_le_bytes()); // creator_fee
1680
1681        let log = format!("Program data: {}", STANDARD.encode(raw));
1682        let filter = EventTypeFilter::include_only(vec![EventType::BonkTrade]);
1683        assert!(parse_log_optimized(
1684            &log,
1685            Signature::default(),
1686            1,
1687            2,
1688            Some(3),
1689            4,
1690            Some(&filter),
1691            false,
1692            None,
1693        )
1694        .is_none());
1695    }
1696
1697    #[test]
1698    fn discriminator_prefix_decode_reads_first_event_bytes() {
1699        let mut raw = Vec::new();
1700        raw.extend_from_slice(&discriminators::PUMP_FEES_UPDATE_ADMIN.to_le_bytes());
1701        raw.extend_from_slice(Pubkey::new_unique().as_ref());
1702        raw.extend_from_slice(Pubkey::new_unique().as_ref());
1703
1704        let encoded = STANDARD.encode(raw);
1705        assert_eq!(
1706            decode_base64_discriminator(&encoded),
1707            Some(discriminators::PUMP_FEES_UPDATE_ADMIN)
1708        );
1709    }
1710
1711    #[test]
1712    fn program_scoped_damm_add_liquidity_parses_from_decoded_data() {
1713        let pool = Pubkey::new_unique();
1714        let position = Pubkey::new_unique();
1715        let owner = Pubkey::new_unique();
1716        let mut raw = Vec::new();
1717        raw.extend_from_slice(&discriminators::METEORA_DAMM_ADD_LIQUIDITY.to_le_bytes());
1718        raw.extend_from_slice(pool.as_ref());
1719        raw.extend_from_slice(position.as_ref());
1720        raw.extend_from_slice(owner.as_ref());
1721        raw.extend_from_slice(&123u128.to_le_bytes());
1722        raw.extend_from_slice(&10u64.to_le_bytes());
1723        raw.extend_from_slice(&20u64.to_le_bytes());
1724        raw.extend_from_slice(&30u64.to_le_bytes());
1725        raw.extend_from_slice(&40u64.to_le_bytes());
1726        raw.extend_from_slice(&50u64.to_le_bytes());
1727        raw.extend_from_slice(&60u64.to_le_bytes());
1728
1729        let log = format!("Program data: {}", STANDARD.encode(raw));
1730        let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDammV2AddLiquidity]);
1731        let event = parse_log_optimized_with_program_id(
1732            &log,
1733            Signature::default(),
1734            1,
1735            2,
1736            Some(3),
1737            4,
1738            Some(&filter),
1739            false,
1740            None,
1741            Some(&program_ids::METEORA_DAMM_V2_PROGRAM_ID),
1742        )
1743        .expect("DAMM V2 add-liquidity should parse");
1744
1745        match event {
1746            DexEvent::MeteoraDammV2AddLiquidity(event) => {
1747                assert_eq!(event.pool, pool);
1748                assert_eq!(event.position, position);
1749                assert_eq!(event.owner, owner);
1750                assert_eq!(event.liquidity_delta, 123);
1751                assert_eq!(event.token_a_amount, 30);
1752                assert_eq!(event.token_b_amount, 40);
1753                assert_eq!(event.total_amount_a, 50);
1754                assert_eq!(event.total_amount_b, 60);
1755            }
1756            other => panic!("expected MeteoraDammV2AddLiquidity, got {other:?}"),
1757        }
1758    }
1759
1760    #[test]
1761    fn large_program_data_uses_heap_fallback_without_dropping_event() {
1762        let mut raw = Vec::new();
1763        raw.extend_from_slice(&discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG.to_le_bytes());
1764        raw.extend_from_slice(&1_777_920_719i64.to_le_bytes());
1765        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // mint
1766        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // bonding_curve
1767        raw.push(0); // pool: None
1768        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // sharing_config
1769        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // admin
1770        raw.extend_from_slice(&64u32.to_le_bytes());
1771        for i in 0..64u16 {
1772            raw.extend_from_slice(Pubkey::new_unique().as_ref());
1773            raw.extend_from_slice(&i.to_le_bytes());
1774        }
1775        raw.push(1); // PumpFeesConfigStatus::Active
1776
1777        let encoded = STANDARD.encode(&raw);
1778        assert!(encoded.len() > 2700, "test must exceed the old fixed stack buffer limit");
1779        let log = format!("Program data: {encoded}");
1780
1781        let event = parse_log_optimized_with_program_id(
1782            &log,
1783            Signature::default(),
1784            1,
1785            2,
1786            Some(3),
1787            4,
1788            None,
1789            false,
1790            None,
1791            Some(&program_ids::PUMP_FEES_PROGRAM_ID),
1792        )
1793        .expect("large pump-fees event should parse via heap fallback");
1794
1795        match event {
1796            DexEvent::PumpFeesCreateFeeSharingConfig(event) => {
1797                assert_eq!(event.initial_shareholders.len(), 64);
1798                assert_eq!(event.status, crate::core::events::PumpFeesConfigStatus::Active);
1799            }
1800            other => panic!("expected PumpFeesCreateFeeSharingConfig, got {other:?}"),
1801        }
1802    }
1803
1804    #[test]
1805    fn completion_parser_extracts_program_id() {
1806        assert_eq!(
1807            parse_program_complete_info(
1808                "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success"
1809            ),
1810            Some("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj")
1811        );
1812        assert_eq!(
1813            parse_program_complete_info(
1814                "Program CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C failed: custom program error: 0x1"
1815            ),
1816            Some("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C")
1817        );
1818    }
1819}