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 RAYDIUM_LAUNCHLAB_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 RAYDIUM_LAUNCHLAB_INVOKE: &str =
49        "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke";
50    pub const RAYDIUM_LAUNCHLAB_SUCCESS: &str =
51        "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success";
52    pub const RAYDIUM_LAUNCHLAB_ID: &str = "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj";
53
54    pub const RAYDIUM_CLMM_INVOKE: &str =
55        "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK invoke";
56    pub const RAYDIUM_CLMM_SUCCESS: &str =
57        "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK success";
58    pub const RAYDIUM_CLMM_ID: &str = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
59
60    pub const RAYDIUM_CPMM_INVOKE: &str =
61        "Program CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C invoke";
62    pub const RAYDIUM_CPMM_SUCCESS: &str =
63        "Program CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C success";
64    pub const RAYDIUM_CPMM_ID: &str = "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C";
65
66    pub const RAYDIUM_AMM_V4_ID: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
67
68    // 常用的日志模式
69    pub const PROGRAM_DATA: &str = "Program data: ";
70    pub const PROGRAM_LOG: &str = "Program log: ";
71
72    // PumpFun 事件 discriminator (base64)
73    pub const PUMPFUN_CREATE_DISCRIMINATOR: &str = "GB7IKAUcB3c"; // [24, 30, 200, 40, 5, 28, 7, 119]
74}
75
76/// 快速日志类型枚举
77#[derive(Debug, Copy, Clone, PartialEq)]
78pub enum LogType {
79    PumpFun,
80    RaydiumLaunchlab,
81    PumpAmm,
82    RaydiumClmm,
83    RaydiumCpmm,
84    RaydiumAmm,
85    OrcaWhirlpool,
86    MeteoraAmm,
87    MeteoraDamm,
88    MeteoraDlmm,
89    Unknown,
90}
91
92/// SIMD 优化的日志类型检测器 - 激进早期退出
93#[inline(always)]
94pub fn detect_log_type(log: &str) -> LogType {
95    let log_bytes = log.as_bytes();
96
97    // 第一步:快速长度检查 - 太短的日志直接跳过
98    if log_bytes.len() < 20 {
99        return LogType::Unknown;
100    }
101
102    // 第二步:检查是否有 "Program data:" - 这是事件日志的标志
103    let has_program_data = PROGRAM_DATA_FINDER.find(log_bytes).is_some();
104
105    // 只有 "Program data:" 日志才可能是交易事件
106    if unlikely(!has_program_data) {
107        return LogType::Unknown;
108    }
109
110    // 第三步:使用 SIMD 快速检测具体协议
111    // Raydium AMM - 高频,有明确程序ID(最常见)
112    if likely(RAYDIUM_AMM_FINDER.find(log_bytes).is_some()) {
113        return LogType::RaydiumAmm;
114    }
115
116    // Raydium CLMM
117    if RAYDIUM_CLMM_FINDER.find(log_bytes).is_some() {
118        return LogType::RaydiumClmm;
119    }
120
121    // Raydium CPMM
122    if RAYDIUM_CPMM_FINDER.find(log_bytes).is_some() {
123        return LogType::RaydiumCpmm;
124    }
125
126    // Raydium LaunchLab (RaydiumLaunchlab)
127    if RAYDIUM_LAUNCHLAB_FINDER.find(log_bytes).is_some() {
128        return LogType::RaydiumLaunchlab;
129    }
130
131    // Orca Whirlpool
132    if WHIRL_FINDER.find(log_bytes).is_some() {
133        return LogType::OrcaWhirlpool;
134    }
135
136    // Meteora - SIMD 优化
137    if let Some(pos) = METEORA_FINDER.find(log_bytes) {
138        let rest = &log_bytes[pos..];
139        if METEORA_LB_FINDER.find(rest).is_some() {
140            return LogType::MeteoraDamm;
141        } else if METEORA_DLMM_FINDER.find(rest).is_some() {
142            return LogType::MeteoraDlmm;
143        } else {
144            return LogType::MeteoraAmm;
145        }
146    }
147
148    // Pump AMM
149    if PUMPSWAP_LOWER_FINDER.find(log_bytes).is_some()
150        || PUMPSWAP_UPPER_FINDER.find(log_bytes).is_some()
151    {
152        return LogType::PumpAmm;
153    }
154
155    // PumpFun - 特殊处理:可能有程序ID,也可能直接是base64数据
156    // 1. 先检查是否包含程序ID(高频事件)
157    if likely(PUMPFUN_FINDER.find(log_bytes).is_some()) {
158        return LogType::PumpFun;
159    }
160
161    // 2. 兜底:有 "Program data:" 但无法识别协议的,尝试作为 PumpFun 解析
162    // PumpFun的日志格式:Program data: <base64>
163    // 只要日志够长且包含Program data,就认为可能是PumpFun
164    if log.len() > 30 {
165        return LogType::PumpFun;
166    }
167
168    LogType::Unknown
169}
170
171// ============================================================================
172// Discriminator constants (compile-time computed) - All protocols
173// ============================================================================
174mod discriminators {
175    // PumpFun discriminators
176    pub const PUMPFUN_CREATE: u64 = u64::from_le_bytes([27, 114, 169, 77, 222, 235, 99, 118]);
177    pub const PUMPFUN_TRADE: u64 = u64::from_le_bytes([189, 219, 127, 211, 78, 230, 97, 238]);
178    pub const PUMPFUN_MIGRATE: u64 = u64::from_le_bytes([189, 233, 93, 185, 92, 148, 234, 148]);
179    pub const PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR: u64 =
180        u64::from_le_bytes([155, 167, 104, 220, 213, 108, 243, 3]);
181    // Raydium LaunchLab event discriminators. `TRADE` intentionally equals
182    // PumpFun's TradeEvent discriminator, so gRPC must route logs with program
183    // context instead of discriminator alone.
184    pub const RAYDIUM_LAUNCHLAB_POOL_CREATE: u64 =
185        u64::from_le_bytes([151, 215, 226, 9, 118, 161, 115, 174]);
186    pub const RAYDIUM_LAUNCHLAB_TRADE: u64 =
187        u64::from_le_bytes([189, 219, 127, 211, 78, 230, 97, 238]);
188    // Pump fees (`idls/pump_fees.json` event discriminators)
189    pub const PUMP_FEES_CREATE_FEE_SHARING_CONFIG: u64 =
190        u64::from_le_bytes([133, 105, 170, 200, 184, 116, 251, 88]);
191    pub const PUMP_FEES_INITIALIZE_FEE_CONFIG: u64 =
192        u64::from_le_bytes([89, 138, 244, 230, 10, 56, 226, 126]);
193    pub const PUMP_FEES_RESET_FEE_SHARING_CONFIG: u64 =
194        u64::from_le_bytes([203, 204, 151, 226, 120, 55, 214, 243]);
195    pub const PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY: u64 =
196        u64::from_le_bytes([114, 23, 101, 60, 14, 190, 153, 62]);
197    pub const PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY: u64 =
198        u64::from_le_bytes([124, 143, 198, 245, 77, 184, 8, 236]);
199    pub const PUMP_FEES_UPDATE_ADMIN: u64 =
200        u64::from_le_bytes([225, 152, 171, 87, 246, 63, 66, 234]);
201    pub const PUMP_FEES_UPDATE_FEE_CONFIG: u64 =
202        u64::from_le_bytes([90, 23, 65, 35, 62, 244, 188, 208]);
203    pub const PUMP_FEES_UPDATE_FEE_SHARES: u64 =
204        u64::from_le_bytes([21, 186, 196, 184, 91, 228, 225, 203]);
205    pub const PUMP_FEES_UPSERT_FEE_TIERS: u64 =
206        u64::from_le_bytes([171, 89, 169, 187, 122, 186, 33, 204]);
207
208    // PumpSwap discriminators
209    pub const PUMPSWAP_BUY: u64 = u64::from_le_bytes([103, 244, 82, 31, 44, 245, 119, 119]);
210    pub const PUMPSWAP_SELL: u64 = u64::from_le_bytes([62, 47, 55, 10, 165, 3, 220, 42]);
211    pub const PUMPSWAP_CREATE_POOL: u64 =
212        u64::from_le_bytes([177, 49, 12, 210, 160, 118, 167, 116]);
213    pub const PUMPSWAP_ADD_LIQUIDITY: u64 =
214        u64::from_le_bytes([120, 248, 61, 83, 31, 142, 107, 144]);
215    pub const PUMPSWAP_REMOVE_LIQUIDITY: u64 =
216        u64::from_le_bytes([22, 9, 133, 26, 160, 44, 71, 192]);
217
218    // Raydium CLMM discriminators
219    pub const RAYDIUM_CLMM_SWAP: u64 = u64::from_le_bytes([64, 198, 205, 232, 38, 8, 113, 226]);
220    pub const RAYDIUM_CLMM_INCREASE_LIQUIDITY: u64 =
221        u64::from_le_bytes([49, 79, 105, 212, 32, 34, 30, 84]);
222    pub const RAYDIUM_CLMM_DECREASE_LIQUIDITY: u64 =
223        u64::from_le_bytes([58, 222, 86, 58, 68, 50, 85, 56]);
224    pub const RAYDIUM_CLMM_LIQUIDITY_CHANGE: u64 =
225        u64::from_le_bytes([126, 240, 175, 206, 158, 88, 153, 107]);
226    pub const RAYDIUM_CLMM_CONFIG_CHANGE: u64 =
227        u64::from_le_bytes([247, 189, 7, 119, 106, 112, 95, 151]);
228    pub const RAYDIUM_CLMM_CREATE_PERSONAL_POSITION: u64 =
229        u64::from_le_bytes([100, 30, 87, 249, 196, 223, 154, 206]);
230    pub const RAYDIUM_CLMM_LIQUIDITY_CALCULATE: u64 =
231        u64::from_le_bytes([237, 112, 148, 230, 57, 84, 180, 162]);
232    pub const RAYDIUM_CLMM_OPEN_LIMIT_ORDER: u64 =
233        u64::from_le_bytes([106, 24, 71, 85, 57, 169, 158, 216]);
234    pub const RAYDIUM_CLMM_INCREASE_LIMIT_ORDER: u64 =
235        u64::from_le_bytes([11, 120, 13, 204, 199, 87, 19, 200]);
236    pub const RAYDIUM_CLMM_DECREASE_LIMIT_ORDER: u64 =
237        u64::from_le_bytes([70, 48, 40, 221, 219, 237, 212, 163]);
238    pub const RAYDIUM_CLMM_SETTLE_LIMIT_ORDER: u64 =
239        u64::from_le_bytes([88, 119, 77, 164, 125, 124, 10, 194]);
240    pub const RAYDIUM_CLMM_UPDATE_REWARD_INFOS: u64 =
241        u64::from_le_bytes([109, 127, 186, 78, 114, 65, 37, 236]);
242    pub const RAYDIUM_CLMM_CREATE_POOL: u64 = u64::from_le_bytes([25, 94, 75, 47, 112, 99, 53, 63]);
243    pub const RAYDIUM_CLMM_COLLECT_PERSONAL_FEE: u64 =
244        u64::from_le_bytes([166, 174, 105, 192, 81, 161, 83, 105]);
245    pub const RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE: u64 =
246        u64::from_le_bytes([206, 87, 17, 79, 45, 41, 213, 61]);
247
248    // Raydium CPMM discriminators
249    pub const RAYDIUM_CPMM_SWAP_BASE_IN: u64 =
250        u64::from_le_bytes([143, 190, 90, 218, 196, 30, 51, 222]);
251    pub const RAYDIUM_CPMM_SWAP_EVENT: u64 =
252        u64::from_le_bytes([64, 198, 205, 232, 38, 8, 113, 226]);
253    pub const RAYDIUM_CPMM_SWAP_BASE_OUT: u64 =
254        u64::from_le_bytes([55, 217, 98, 86, 163, 74, 180, 173]);
255    pub const RAYDIUM_CPMM_CREATE_POOL: u64 =
256        u64::from_le_bytes([233, 146, 209, 142, 207, 104, 64, 188]);
257    pub const RAYDIUM_CPMM_DEPOSIT: u64 =
258        u64::from_le_bytes([242, 35, 198, 137, 82, 225, 242, 182]);
259    pub const RAYDIUM_CPMM_WITHDRAW: u64 =
260        u64::from_le_bytes([183, 18, 70, 156, 148, 109, 161, 34]);
261
262    // Raydium AMM V4 discriminators
263    pub const RAYDIUM_AMM_SWAP_BASE_IN: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 9]);
264    pub const RAYDIUM_AMM_SWAP_BASE_OUT: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 11]);
265    pub const RAYDIUM_AMM_DEPOSIT: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 3]);
266    pub const RAYDIUM_AMM_WITHDRAW: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 4]);
267    pub const RAYDIUM_AMM_INITIALIZE2: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 1]);
268    pub const RAYDIUM_AMM_WITHDRAW_PNL: u64 = u64::from_le_bytes([0, 0, 0, 0, 0, 0, 0, 7]);
269
270    // Orca Whirlpool discriminators
271    pub const ORCA_TRADED: u64 = u64::from_le_bytes([225, 202, 73, 175, 147, 43, 160, 150]);
272    pub const ORCA_LIQUIDITY_INCREASED: u64 =
273        u64::from_le_bytes([30, 7, 144, 181, 102, 254, 155, 161]);
274    pub const ORCA_LIQUIDITY_DECREASED: u64 =
275        u64::from_le_bytes([166, 1, 36, 71, 112, 202, 181, 171]);
276    pub const ORCA_POOL_INITIALIZED: u64 =
277        u64::from_le_bytes([100, 118, 173, 87, 12, 198, 254, 229]);
278
279    // Meteora AMM discriminators
280    pub const METEORA_AMM_SWAP: u64 = u64::from_le_bytes([81, 108, 227, 190, 205, 208, 10, 196]);
281    pub const METEORA_AMM_ADD_LIQUIDITY: u64 =
282        u64::from_le_bytes([31, 94, 125, 90, 227, 52, 61, 186]);
283    pub const METEORA_AMM_REMOVE_LIQUIDITY: u64 =
284        u64::from_le_bytes([116, 244, 97, 232, 103, 31, 152, 58]);
285    pub const METEORA_AMM_BOOTSTRAP_LIQUIDITY: u64 =
286        u64::from_le_bytes([121, 127, 38, 136, 92, 55, 14, 247]);
287    pub const METEORA_AMM_POOL_CREATED: u64 =
288        u64::from_le_bytes([202, 44, 41, 88, 104, 220, 157, 82]);
289    pub const METEORA_AMM_SET_POOL_FEES: u64 =
290        u64::from_le_bytes([245, 26, 198, 164, 88, 18, 75, 9]);
291
292    // Meteora DAMM V2 discriminators
293    pub const METEORA_DAMM_SWAP: u64 = u64::from_le_bytes([27, 60, 21, 213, 138, 170, 187, 147]);
294    pub const METEORA_DAMM_SWAP2: u64 = u64::from_le_bytes([189, 66, 51, 168, 38, 80, 117, 153]);
295    pub const METEORA_DAMM_ADD_LIQUIDITY: u64 =
296        u64::from_le_bytes([175, 242, 8, 157, 30, 247, 185, 169]);
297    pub const METEORA_DAMM_REMOVE_LIQUIDITY: u64 =
298        u64::from_le_bytes([87, 46, 88, 98, 175, 96, 34, 91]);
299    pub const METEORA_DAMM_INITIALIZE_POOL: u64 =
300        u64::from_le_bytes([228, 50, 246, 85, 203, 66, 134, 37]);
301    pub const METEORA_DAMM_CREATE_POSITION: u64 =
302        u64::from_le_bytes([156, 15, 119, 198, 29, 181, 221, 55]);
303    pub const METEORA_DAMM_CLOSE_POSITION: u64 =
304        u64::from_le_bytes([20, 145, 144, 68, 143, 142, 214, 178]);
305
306    // Meteora DBC discriminators. Some values intentionally overlap DAMM V2,
307    // so they must be routed with program context.
308    pub const METEORA_DBC_SWAP: u64 = u64::from_le_bytes([27, 60, 21, 213, 138, 170, 187, 147]);
309    pub const METEORA_DBC_INITIALIZE_POOL: u64 =
310        u64::from_le_bytes([228, 50, 246, 85, 203, 66, 134, 37]);
311    pub const METEORA_DBC_CURVE_COMPLETE: u64 =
312        u64::from_le_bytes([229, 231, 86, 84, 156, 134, 75, 24]);
313
314    // Meteora DLMM event discriminators. Current DLMM uses Anchor event-CPI
315    // discriminators that overlap with Meteora Pools, so scoped routing is required.
316    pub const METEORA_DLMM_SWAP: u64 = u64::from_le_bytes([81, 108, 227, 190, 205, 208, 10, 196]);
317    pub const METEORA_DLMM_SWAP2: u64 = u64::from_le_bytes([46, 116, 82, 215, 148, 27, 84, 77]);
318    pub const METEORA_DLMM_ADD_LIQUIDITY: u64 =
319        u64::from_le_bytes([31, 94, 125, 90, 227, 52, 61, 186]);
320    pub const METEORA_DLMM_REMOVE_LIQUIDITY: u64 =
321        u64::from_le_bytes([116, 244, 97, 232, 103, 31, 152, 58]);
322    pub const METEORA_DLMM_INITIALIZE_POOL: u64 =
323        u64::from_le_bytes([185, 74, 252, 125, 27, 215, 188, 111]);
324    pub const METEORA_DLMM_INITIALIZE_BIN_ARRAY: u64 =
325        u64::from_le_bytes([11, 18, 155, 194, 33, 115, 238, 119]);
326    pub const METEORA_DLMM_CREATE_POSITION: u64 =
327        u64::from_le_bytes([144, 142, 252, 84, 157, 53, 37, 121]);
328    pub const METEORA_DLMM_CLOSE_POSITION: u64 =
329        u64::from_le_bytes([255, 196, 16, 107, 28, 202, 53, 128]);
330    pub const METEORA_DLMM_CLAIM_FEE: u64 =
331        u64::from_le_bytes([75, 122, 154, 48, 140, 74, 123, 163]);
332    pub const METEORA_DLMM_CLAIM_FEE2: u64 =
333        u64::from_le_bytes([232, 171, 242, 97, 58, 77, 35, 45]);
334}
335
336/// Optimized unified log parser with **discriminator predecode, decode-on-match** strategy
337///
338/// **Performance Strategy**:
339/// 1. Decode only the first 8 event bytes to read the discriminator
340/// 2. Check filter BEFORE decoding the full event payload
341/// 3. Decode matching payloads once to a stack buffer; rare large events use heap
342/// 4. Parse only the specific event type requested
343///
344/// **Key optimization**: narrow filters skip full base64 decoding entirely.
345/// Old: decode full payload -> check filter -> parse
346/// New: decode discriminator prefix -> check filter -> decode matching payload once
347#[inline(always)]
348/// `recent_blockhash`: pass as `Option<&[u8]>`; only cloned when an event is built (low latency).
349pub fn parse_log_optimized(
350    log: &str,
351    signature: Signature,
352    slot: u64,
353    tx_index: u64,
354    block_time_us: Option<i64>,
355    grpc_recv_us: i64,
356    event_type_filter: Option<&EventTypeFilter>,
357    is_created_buy: bool,
358    recent_blockhash: Option<&[u8]>,
359) -> Option<DexEvent> {
360    parse_log_optimized_inner(
361        log,
362        signature,
363        slot,
364        tx_index,
365        block_time_us,
366        grpc_recv_us,
367        event_type_filter,
368        is_created_buy,
369        recent_blockhash,
370        None,
371    )
372}
373
374/// Program-aware log parser for gRPC/RPC transaction logs.
375///
376/// `Program data:` lines do not carry the emitting program id. The caller should
377/// pass the current invoke stack's program id so discriminators shared by
378/// multiple Anchor programs are routed correctly.
379#[inline(always)]
380pub fn parse_log_optimized_with_program_id(
381    log: &str,
382    signature: Signature,
383    slot: u64,
384    tx_index: u64,
385    block_time_us: Option<i64>,
386    grpc_recv_us: i64,
387    event_type_filter: Option<&EventTypeFilter>,
388    is_created_buy: bool,
389    recent_blockhash: Option<&[u8]>,
390    program_id: Option<&Pubkey>,
391) -> Option<DexEvent> {
392    parse_log_optimized_inner(
393        log,
394        signature,
395        slot,
396        tx_index,
397        block_time_us,
398        grpc_recv_us,
399        event_type_filter,
400        is_created_buy,
401        recent_blockhash,
402        program_id,
403    )
404}
405
406#[inline(always)]
407fn decode_base64_discriminator(trimmed: &str) -> Option<u64> {
408    let bytes = trimmed.as_bytes();
409    if bytes.len() < 12 {
410        return None;
411    }
412
413    let mut discriminator_buf = [0u8; 9];
414    let decoded_len = {
415        use base64_simd::AsOut;
416        let decoded =
417            base64_simd::STANDARD.decode(&bytes[..12], discriminator_buf.as_mut().as_out()).ok()?;
418        decoded.len()
419    };
420    if decoded_len < 8 {
421        return None;
422    }
423
424    Some(unsafe { (discriminator_buf.as_ptr() as *const u64).read_unaligned() })
425}
426
427#[inline(always)]
428fn filter_includes_known_program(program_id: &Pubkey, filter: &EventTypeFilter) -> bool {
429    match *program_id {
430        program_ids::PUMPFUN_PROGRAM_ID => filter.includes_pumpfun(),
431        program_ids::PUMP_FEES_PROGRAM_ID => filter.includes_pump_fees(),
432        program_ids::PUMPSWAP_PROGRAM_ID => filter.includes_pumpswap(),
433        program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID => filter.includes_raydium_launchlab(),
434        program_ids::RAYDIUM_CLMM_PROGRAM_ID => filter.includes_raydium_clmm(),
435        program_ids::RAYDIUM_CPMM_PROGRAM_ID => filter.includes_raydium_cpmm(),
436        program_ids::RAYDIUM_AMM_V4_PROGRAM_ID => filter.includes_raydium_amm_v4(),
437        program_ids::ORCA_WHIRLPOOL_PROGRAM_ID => filter.includes_orca_whirlpool(),
438        program_ids::METEORA_POOLS_PROGRAM_ID => filter.includes_meteora_pools(),
439        program_ids::METEORA_DAMM_V2_PROGRAM_ID => filter.includes_meteora_damm_v2(),
440        program_ids::METEORA_DLMM_PROGRAM_ID => filter.includes_meteora_dlmm(),
441        program_ids::METEORA_DBC_PROGRAM_ID => filter.includes_meteora_dbc(),
442        _ => true,
443    }
444}
445
446#[inline(always)]
447fn filter_wants_supported_logs(filter: &EventTypeFilter) -> bool {
448    filter.includes_pumpfun()
449        || filter.includes_pump_fees()
450        || filter.includes_pumpswap()
451        || filter.includes_raydium_launchlab()
452        || filter.includes_raydium_clmm()
453        || filter.includes_raydium_cpmm()
454        || filter.includes_raydium_amm_v4()
455        || filter.includes_orca_whirlpool()
456        || filter.includes_meteora_pools()
457        || filter.includes_meteora_damm_v2()
458        || filter.includes_meteora_dlmm()
459        || filter.includes_meteora_dbc()
460}
461
462#[inline(always)]
463fn filter_wants_pumpfun_trade_event(filter: &EventTypeFilter) -> bool {
464    filter.should_include(EventType::PumpFunTrade)
465        || filter.should_include(EventType::PumpFunBuy)
466        || filter.should_include(EventType::PumpFunSell)
467        || filter.should_include(EventType::PumpFunBuyExactSolIn)
468}
469
470#[inline(always)]
471fn filter_wants_launchlab_trade_event(filter: &EventTypeFilter) -> bool {
472    filter.should_include(EventType::RaydiumLaunchlabTrade)
473}
474
475#[inline(always)]
476fn unscoped_filter_allows_discriminator(discriminator: u64, filter: &EventTypeFilter) -> bool {
477    match discriminator {
478        // Shared by Pump.fun trade and Raydium LaunchLab/RaydiumLaunchlab trade.
479        discriminators::PUMPFUN_TRADE => {
480            filter_wants_pumpfun_trade_event(filter)
481                || filter.should_include(EventType::RaydiumLaunchlabTrade)
482        }
483        // Legacy DLMM swap overlapped Raydium CPMM. Current DLMM swap overlaps
484        // Meteora Pools swap and is resolved by scoped routing.
485        discriminators::RAYDIUM_CPMM_SWAP_BASE_IN => {
486            filter.should_include(EventType::RaydiumCpmmSwap)
487                || filter.should_include(EventType::MeteoraDlmmSwap)
488        }
489        _ => discriminator_to_event_type(discriminator)
490            .map(|event_type| filter.should_include(event_type))
491            .unwrap_or_else(|| filter_wants_supported_logs(filter)),
492    }
493}
494
495#[inline(always)]
496fn filter_allows_discriminator(
497    program_id: Option<&Pubkey>,
498    discriminator: u64,
499    event_type_filter: Option<&EventTypeFilter>,
500) -> bool {
501    let Some(filter) = event_type_filter else {
502        return true;
503    };
504
505    if let Some(program_id) = program_id {
506        if *program_id == program_ids::PUMPFUN_PROGRAM_ID
507            && discriminator == discriminators::PUMPFUN_TRADE
508        {
509            return filter_wants_pumpfun_trade_event(filter);
510        }
511        if let Some(event_type) =
512            program_scoped_discriminator_to_event_type(program_id, discriminator)
513        {
514            return filter.should_include(event_type);
515        }
516        return filter_includes_known_program(program_id, filter);
517    }
518
519    unscoped_filter_allows_discriminator(discriminator, filter)
520}
521
522#[inline(always)]
523fn apply_event_type_filter(
524    event: DexEvent,
525    event_type_filter: Option<&EventTypeFilter>,
526) -> Option<DexEvent> {
527    if let Some(filter) = event_type_filter {
528        if !filter.should_include_dex_event(&event) {
529            return None;
530        }
531    }
532    Some(event)
533}
534
535#[inline(always)]
536fn parse_unscoped_pumpfun_launchlab_trade(
537    data: &[u8],
538    metadata: EventMetadata,
539    event_type_filter: Option<&EventTypeFilter>,
540    is_created_buy: bool,
541) -> Option<DexEvent> {
542    let wants_pumpfun = event_type_filter.map(filter_wants_pumpfun_trade_event).unwrap_or(true);
543    let wants_launchlab = event_type_filter.map(filter_wants_launchlab_trade_event).unwrap_or(true);
544
545    if wants_pumpfun {
546        if let Some(event) =
547            crate::logs::pump::parse_trade_from_data(data, metadata.clone(), is_created_buy)
548                .and_then(|event| apply_event_type_filter(event, event_type_filter))
549        {
550            return Some(event);
551        }
552    }
553
554    if wants_launchlab {
555        return crate::logs::raydium_launchlab::parse_trade_from_data(data, metadata)
556            .and_then(|event| apply_event_type_filter(event, event_type_filter));
557    }
558
559    None
560}
561
562#[inline(always)]
563fn parse_log_optimized_inner(
564    log: &str,
565    signature: Signature,
566    slot: u64,
567    tx_index: u64,
568    block_time_us: Option<i64>,
569    grpc_recv_us: i64,
570    event_type_filter: Option<&EventTypeFilter>,
571    is_created_buy: bool,
572    recent_blockhash: Option<&[u8]>,
573    program_id: Option<&Pubkey>,
574) -> Option<DexEvent> {
575    if program_id == Some(&program_ids::RAYDIUM_AMM_V4_PROGRAM_ID) && log.contains("ray_log: ") {
576        if event_type_filter
577            .map(|filter| !filter.should_include(EventType::RaydiumAmmV4Swap))
578            .unwrap_or(false)
579        {
580            return None;
581        }
582        let metadata = EventMetadata {
583            signature,
584            slot,
585            tx_index,
586            block_time_us: block_time_us.unwrap_or(0),
587            grpc_recv_us,
588            recent_blockhash: recent_blockhash.map(|s| bs58::encode(s).into_string()),
589        };
590        return crate::logs::raydium_amm::parse_ray_log_swap(log, metadata);
591    }
592    // Step 1: Find "Program data: " prefix using SIMD
593    let log_bytes = log.as_bytes();
594    let pos = PROGRAM_DATA_FINDER.find(log_bytes)?;
595    let data_start = pos + 14; // "Program data: " length
596
597    if log_bytes.len() <= data_start {
598        return None;
599    }
600
601    // Step 2: Decode base64 ONCE. Normal swap logs stay on the stack; rare large
602    // IDL events (for example pump-fees vectors) fall back to heap instead of being dropped.
603    const STACK_DECODE_CAP: usize = 2048;
604    let data_part = &log[data_start..];
605    let trimmed = data_part.trim();
606
607    // Decode the discriminator prefix before touching the full payload. This is
608    // the fastest reject path for users subscribing to a narrow event set.
609    let discriminator = decode_base64_discriminator(trimmed)?;
610    if !filter_allows_discriminator(program_id, discriminator, event_type_filter) {
611        return None;
612    }
613
614    // SIMD-accelerated base64 decoding (AVX2/SSE4/NEON)
615    use base64_simd::AsOut;
616    let max_decoded_len = (trimmed.len() / 4).saturating_mul(3).saturating_add(3);
617    let mut stack_buf = [0u8; STACK_DECODE_CAP];
618    let heap_buf: Vec<u8>;
619    let program_data: &[u8] = if max_decoded_len <= STACK_DECODE_CAP {
620        let decoded_len = {
621            let decoded_slice = base64_simd::STANDARD
622                .decode(trimmed.as_bytes(), stack_buf.as_mut().as_out())
623                .ok()?;
624            decoded_slice.len()
625        };
626        &stack_buf[..decoded_len]
627    } else {
628        heap_buf = base64_simd::STANDARD.decode_to_vec(trimmed.as_bytes()).ok()?;
629        heap_buf.as_slice()
630    };
631
632    if program_data.len() < 8 {
633        return None;
634    }
635
636    debug_assert_eq!(discriminator, unsafe {
637        (program_data.as_ptr() as *const u64).read_unaligned()
638    });
639
640    // Step 6: Parse the specific event type (data already decoded)
641    let data = &program_data[8..]; // Skip discriminator
642
643    use crate::core::events::*;
644
645    let metadata = EventMetadata {
646        signature,
647        slot,
648        tx_index,
649        block_time_us: block_time_us.unwrap_or(0),
650        grpc_recv_us,
651        recent_blockhash: recent_blockhash.map(|s| bs58::encode(s).into_string()),
652    };
653
654    if let Some(program_id) = program_id {
655        return parse_program_scoped_event(
656            program_id,
657            discriminator,
658            data,
659            metadata,
660            log,
661            signature,
662            slot,
663            tx_index,
664            block_time_us,
665            grpc_recv_us,
666            event_type_filter,
667            is_created_buy,
668        );
669    }
670
671    // ========================================================================
672    // Hot-path optimization: Fast check for top 5 most common discriminators
673    // This avoids the large match statement for ~80% of events
674    // Expected savings: 5-20ns per hot event
675    // ========================================================================
676
677    // Check hot-path discriminators first (ordered by frequency)
678    if likely(discriminator == discriminators::PUMPFUN_TRADE) {
679        // Shared by PumpFun and Raydium LaunchLab. Without program context,
680        // avoid parsing protocols the filter does not request.
681        return parse_unscoped_pumpfun_launchlab_trade(
682            data,
683            metadata,
684            event_type_filter,
685            is_created_buy,
686        );
687    }
688
689    if likely(discriminator == discriminators::RAYDIUM_CLMM_SWAP) {
690        // Raydium CLMM Swap - High frequency (~20% of events)
691        return apply_event_type_filter(
692            crate::logs::raydium_clmm::parse_swap_from_data(data, metadata)?,
693            event_type_filter,
694        );
695    }
696
697    if likely(discriminator == discriminators::RAYDIUM_AMM_SWAP_BASE_IN) {
698        // Raydium AMM Swap Base In - High frequency (~15% of events)
699        return apply_event_type_filter(
700            crate::logs::raydium_amm::parse_swap_base_in_from_data(data, metadata)?,
701            event_type_filter,
702        );
703    }
704
705    if likely(discriminator == discriminators::PUMPSWAP_BUY) {
706        // PumpSwap Buy - Medium frequency (~10% of events)
707        return apply_event_type_filter(
708            crate::logs::pump_amm::parse_buy_from_data(data, metadata)?,
709            event_type_filter,
710        );
711    }
712
713    if discriminator == discriminators::PUMPSWAP_SELL {
714        // PumpSwap Sell - Medium frequency (~5% of events)
715        return apply_event_type_filter(
716            crate::logs::pump_amm::parse_sell_from_data(data, metadata)?,
717            event_type_filter,
718        );
719    }
720
721    // ========================================================================
722    // Cold path: Handle remaining ~10% of events via match statement
723    // ========================================================================
724
725    let event = match discriminator {
726        // Note: Hot-path discriminators (PUMPFUN_TRADE, RAYDIUM_CLMM_SWAP, RAYDIUM_AMM_SWAP_BASE_IN,
727        // PUMPSWAP_BUY, PUMPSWAP_SELL) are handled above and never reach this match statement
728
729        // PumpFun events (cold path)
730        discriminators::PUMPFUN_CREATE => crate::logs::pump::parse_create_from_data(data, metadata),
731        discriminators::PUMPFUN_MIGRATE => {
732            crate::logs::pump::parse_migrate_from_data(data, metadata)
733        }
734        discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG => {
735            crate::logs::pump_fees::parse_create_fee_sharing_config_from_data(data, metadata)
736        }
737        discriminators::PUMP_FEES_INITIALIZE_FEE_CONFIG => {
738            crate::logs::pump_fees::parse_initialize_fee_config_from_data(data, metadata)
739        }
740        discriminators::PUMP_FEES_RESET_FEE_SHARING_CONFIG => {
741            crate::logs::pump_fees::parse_reset_fee_sharing_config_from_data(data, metadata)
742        }
743        discriminators::PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY => {
744            crate::logs::pump_fees::parse_revoke_fee_sharing_authority_from_data(data, metadata)
745        }
746        discriminators::PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY => {
747            crate::logs::pump_fees::parse_transfer_fee_sharing_authority_from_data(data, metadata)
748        }
749        discriminators::PUMP_FEES_UPDATE_ADMIN => {
750            crate::logs::pump_fees::parse_update_admin_from_data(data, metadata)
751        }
752        discriminators::PUMP_FEES_UPDATE_FEE_CONFIG => {
753            crate::logs::pump_fees::parse_update_fee_config_from_data(data, metadata)
754        }
755        discriminators::PUMP_FEES_UPDATE_FEE_SHARES => {
756            crate::logs::pump_fees::parse_update_fee_shares_from_data(data, metadata)
757        }
758        discriminators::PUMP_FEES_UPSERT_FEE_TIERS => {
759            crate::logs::pump_fees::parse_upsert_fee_tiers_from_data(data, metadata)
760        }
761        discriminators::PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR => {
762            crate::logs::pump::parse_migrate_bonding_curve_creator_from_data(data, metadata)
763        }
764        discriminators::PUMPSWAP_CREATE_POOL => {
765            crate::logs::pump_amm::parse_create_pool_from_data(data, metadata)
766        }
767        discriminators::PUMPSWAP_ADD_LIQUIDITY => {
768            crate::logs::pump_amm::parse_add_liquidity_from_data(data, metadata)
769        }
770        discriminators::PUMPSWAP_REMOVE_LIQUIDITY => {
771            crate::logs::pump_amm::parse_remove_liquidity_from_data(data, metadata)
772        }
773
774        // ========== Other protocols - route by discriminator ==========
775        // Raydium CLMM - use from_data functions (cold path)
776        discriminators::RAYDIUM_CLMM_INCREASE_LIQUIDITY => {
777            crate::logs::raydium_clmm::parse_increase_liquidity_from_data(data, metadata)
778        }
779        discriminators::RAYDIUM_CLMM_DECREASE_LIQUIDITY => {
780            crate::logs::raydium_clmm::parse_decrease_liquidity_from_data(data, metadata)
781        }
782        discriminators::RAYDIUM_CLMM_LIQUIDITY_CHANGE => {
783            crate::logs::raydium_clmm::parse_liquidity_change_from_data(data, metadata)
784        }
785        discriminators::RAYDIUM_CLMM_CONFIG_CHANGE => {
786            crate::logs::raydium_clmm::parse_config_change_from_data(data, metadata)
787        }
788        discriminators::RAYDIUM_CLMM_CREATE_PERSONAL_POSITION => {
789            crate::logs::raydium_clmm::parse_create_personal_position_from_data(data, metadata)
790        }
791        discriminators::RAYDIUM_CLMM_LIQUIDITY_CALCULATE => {
792            crate::logs::raydium_clmm::parse_liquidity_calculate_from_data(data, metadata)
793        }
794        discriminators::RAYDIUM_CLMM_OPEN_LIMIT_ORDER => {
795            crate::logs::raydium_clmm::parse_open_limit_order_from_data(data, metadata)
796        }
797        discriminators::RAYDIUM_CLMM_INCREASE_LIMIT_ORDER => {
798            crate::logs::raydium_clmm::parse_increase_limit_order_from_data(data, metadata)
799        }
800        discriminators::RAYDIUM_CLMM_DECREASE_LIMIT_ORDER => {
801            crate::logs::raydium_clmm::parse_decrease_limit_order_from_data(data, metadata)
802        }
803        discriminators::RAYDIUM_CLMM_SETTLE_LIMIT_ORDER => {
804            crate::logs::raydium_clmm::parse_settle_limit_order_from_data(data, metadata)
805        }
806        discriminators::RAYDIUM_CLMM_UPDATE_REWARD_INFOS => {
807            crate::logs::raydium_clmm::parse_update_reward_infos_from_data(data, metadata)
808        }
809        discriminators::RAYDIUM_CLMM_CREATE_POOL => {
810            crate::logs::raydium_clmm::parse_create_pool_from_data(data, metadata)
811        }
812        discriminators::RAYDIUM_CLMM_COLLECT_PERSONAL_FEE => {
813            crate::logs::raydium_clmm::parse_collect_personal_fee_from_data(data, metadata)
814        }
815        discriminators::RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE => {
816            crate::logs::raydium_clmm::parse_collect_protocol_fee_from_data(data, metadata)
817        }
818
819        // Raydium CPMM - use from_data functions (single decode)
820        discriminators::RAYDIUM_CPMM_SWAP_BASE_IN => {
821            crate::logs::raydium_cpmm::parse_swap_base_in_from_data(data, metadata)
822        }
823        discriminators::RAYDIUM_CPMM_SWAP_BASE_OUT => {
824            crate::logs::raydium_cpmm::parse_swap_base_out_from_data(data, metadata)
825        }
826        // Note: RAYDIUM_CPMM_CREATE_POOL discriminator conflicts with RAYDIUM_CLMM_CREATE_POOL
827        // CPMM create pool is rare, handled via log content detection if needed
828        discriminators::RAYDIUM_CPMM_DEPOSIT => {
829            crate::logs::raydium_cpmm::parse_deposit_from_data(data, metadata)
830        }
831        discriminators::RAYDIUM_CPMM_WITHDRAW => {
832            crate::logs::raydium_cpmm::parse_withdraw_from_data(data, metadata)
833        }
834
835        // Raydium AMM V4 - use from_data functions (single decode)
836        discriminators::RAYDIUM_AMM_SWAP_BASE_IN => {
837            crate::logs::raydium_amm::parse_swap_base_in_from_data(data, metadata)
838        }
839        discriminators::RAYDIUM_AMM_SWAP_BASE_OUT => {
840            crate::logs::raydium_amm::parse_swap_base_out_from_data(data, metadata)
841        }
842        discriminators::RAYDIUM_AMM_DEPOSIT => {
843            crate::logs::raydium_amm::parse_deposit_from_data(data, metadata)
844        }
845        discriminators::RAYDIUM_AMM_WITHDRAW => {
846            crate::logs::raydium_amm::parse_withdraw_from_data(data, metadata)
847        }
848        discriminators::RAYDIUM_AMM_INITIALIZE2 => {
849            crate::logs::raydium_amm::parse_initialize2_from_data(data, metadata)
850        }
851        discriminators::RAYDIUM_AMM_WITHDRAW_PNL => {
852            crate::logs::raydium_amm::parse_withdraw_pnl_from_data(data, metadata)
853        }
854
855        // Orca Whirlpool - use from_data functions (single decode)
856        discriminators::ORCA_TRADED => {
857            crate::logs::orca_whirlpool::parse_traded_from_data(data, metadata)
858        }
859        discriminators::ORCA_LIQUIDITY_INCREASED => {
860            crate::logs::orca_whirlpool::parse_liquidity_increased_from_data(data, metadata)
861        }
862        discriminators::ORCA_LIQUIDITY_DECREASED => {
863            crate::logs::orca_whirlpool::parse_liquidity_decreased_from_data(data, metadata)
864        }
865        discriminators::ORCA_POOL_INITIALIZED => {
866            crate::logs::orca_whirlpool::parse_pool_initialized_from_data(data, metadata)
867        }
868
869        // Meteora AMM - use from_data functions (single decode)
870        discriminators::METEORA_AMM_SWAP => {
871            crate::logs::meteora_amm::parse_swap_from_data(data, metadata)
872        }
873        discriminators::METEORA_AMM_ADD_LIQUIDITY => {
874            crate::logs::meteora_amm::parse_add_liquidity_from_data(data, metadata)
875        }
876        discriminators::METEORA_AMM_REMOVE_LIQUIDITY => {
877            crate::logs::meteora_amm::parse_remove_liquidity_from_data(data, metadata)
878        }
879        discriminators::METEORA_AMM_BOOTSTRAP_LIQUIDITY => {
880            crate::logs::meteora_amm::parse_bootstrap_liquidity_from_data(data, metadata)
881        }
882        discriminators::METEORA_AMM_POOL_CREATED => {
883            crate::logs::meteora_amm::parse_pool_created_from_data(data, metadata)
884        }
885        discriminators::METEORA_AMM_SET_POOL_FEES => {
886            crate::logs::meteora_amm::parse_set_pool_fees_from_data(data, metadata)
887        }
888
889        // Meteora DAMM V2
890        discriminators::METEORA_DAMM_SWAP => {
891            crate::logs::meteora_damm::parse_swap_from_data(data, metadata)
892        }
893        discriminators::METEORA_DAMM_SWAP2 => {
894            crate::logs::meteora_damm::parse_swap2_from_data(data, metadata)
895        }
896        discriminators::METEORA_DAMM_ADD_LIQUIDITY => {
897            crate::logs::meteora_damm::parse_add_liquidity_from_data(data, metadata)
898        }
899        discriminators::METEORA_DAMM_REMOVE_LIQUIDITY => {
900            crate::logs::meteora_damm::parse_remove_liquidity_from_data(data, metadata)
901        }
902        discriminators::METEORA_DAMM_INITIALIZE_POOL => {
903            crate::logs::meteora_damm::parse_initialize_pool_from_data(data, metadata)
904        }
905        discriminators::METEORA_DAMM_CREATE_POSITION => {
906            crate::logs::meteora_damm::parse_create_position_from_data(data, metadata)
907        }
908        discriminators::METEORA_DAMM_CLOSE_POSITION => {
909            crate::logs::meteora_damm::parse_close_position_from_data(data, metadata)
910        }
911
912        // NOTE: current Meteora DLMM discriminators overlap other Meteora
913        // programs, so DLMM is routed in the program-scoped path.
914
915        // Unknown discriminator - try fallback protocols
916        _ => {
917            // Try Meteora DLMM (has discriminator conflict with Raydium CPMM)
918            if let Some(event) = crate::logs::parse_meteora_dlmm_log(
919                log,
920                signature,
921                slot,
922                tx_index,
923                block_time_us,
924                grpc_recv_us,
925            ) {
926                return apply_event_type_filter(event, event_type_filter);
927            }
928            None
929        }
930    }?;
931    apply_event_type_filter(event, event_type_filter)
932}
933
934#[inline(always)]
935fn program_scoped_discriminator_to_event_type(
936    program_id: &Pubkey,
937    discriminator: u64,
938) -> Option<EventType> {
939    match *program_id {
940        program_ids::PUMPFUN_PROGRAM_ID => match discriminator {
941            discriminators::PUMPFUN_CREATE => Some(EventType::PumpFunCreate),
942            discriminators::PUMPFUN_TRADE => Some(EventType::PumpFunTrade),
943            discriminators::PUMPFUN_MIGRATE => Some(EventType::PumpFunMigrate),
944            discriminators::PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR => {
945                Some(EventType::PumpFunMigrateBondingCurveCreator)
946            }
947            _ => None,
948        },
949        program_ids::PUMP_FEES_PROGRAM_ID => match discriminator {
950            discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG => {
951                Some(EventType::PumpFeesCreateFeeSharingConfig)
952            }
953            discriminators::PUMP_FEES_INITIALIZE_FEE_CONFIG => {
954                Some(EventType::PumpFeesInitializeFeeConfig)
955            }
956            discriminators::PUMP_FEES_RESET_FEE_SHARING_CONFIG => {
957                Some(EventType::PumpFeesResetFeeSharingConfig)
958            }
959            discriminators::PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY => {
960                Some(EventType::PumpFeesRevokeFeeSharingAuthority)
961            }
962            discriminators::PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY => {
963                Some(EventType::PumpFeesTransferFeeSharingAuthority)
964            }
965            discriminators::PUMP_FEES_UPDATE_ADMIN => Some(EventType::PumpFeesUpdateAdmin),
966            discriminators::PUMP_FEES_UPDATE_FEE_CONFIG => Some(EventType::PumpFeesUpdateFeeConfig),
967            discriminators::PUMP_FEES_UPDATE_FEE_SHARES => Some(EventType::PumpFeesUpdateFeeShares),
968            discriminators::PUMP_FEES_UPSERT_FEE_TIERS => Some(EventType::PumpFeesUpsertFeeTiers),
969            _ => None,
970        },
971        program_ids::PUMPSWAP_PROGRAM_ID => match discriminator {
972            discriminators::PUMPSWAP_BUY => Some(EventType::PumpSwapBuy),
973            discriminators::PUMPSWAP_SELL => Some(EventType::PumpSwapSell),
974            discriminators::PUMPSWAP_CREATE_POOL => Some(EventType::PumpSwapCreatePool),
975            discriminators::PUMPSWAP_ADD_LIQUIDITY => Some(EventType::PumpSwapLiquidityAdded),
976            discriminators::PUMPSWAP_REMOVE_LIQUIDITY => Some(EventType::PumpSwapLiquidityRemoved),
977            _ => None,
978        },
979        program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID => match discriminator {
980            discriminators::RAYDIUM_LAUNCHLAB_TRADE => Some(EventType::RaydiumLaunchlabTrade),
981            discriminators::RAYDIUM_LAUNCHLAB_POOL_CREATE => {
982                Some(EventType::RaydiumLaunchlabPoolCreate)
983            }
984            _ => None,
985        },
986        program_ids::RAYDIUM_CLMM_PROGRAM_ID => match discriminator {
987            discriminators::RAYDIUM_CLMM_SWAP => Some(EventType::RaydiumClmmSwap),
988            discriminators::RAYDIUM_CLMM_INCREASE_LIQUIDITY => {
989                Some(EventType::RaydiumClmmIncreaseLiquidity)
990            }
991            discriminators::RAYDIUM_CLMM_DECREASE_LIQUIDITY => {
992                Some(EventType::RaydiumClmmDecreaseLiquidity)
993            }
994            discriminators::RAYDIUM_CLMM_LIQUIDITY_CHANGE => {
995                Some(EventType::RaydiumClmmLiquidityChange)
996            }
997            discriminators::RAYDIUM_CLMM_CONFIG_CHANGE => Some(EventType::RaydiumClmmConfigChange),
998            discriminators::RAYDIUM_CLMM_CREATE_PERSONAL_POSITION => {
999                Some(EventType::RaydiumClmmCreatePersonalPosition)
1000            }
1001            discriminators::RAYDIUM_CLMM_LIQUIDITY_CALCULATE => {
1002                Some(EventType::RaydiumClmmLiquidityCalculate)
1003            }
1004            discriminators::RAYDIUM_CLMM_OPEN_LIMIT_ORDER => {
1005                Some(EventType::RaydiumClmmOpenLimitOrder)
1006            }
1007            discriminators::RAYDIUM_CLMM_INCREASE_LIMIT_ORDER => {
1008                Some(EventType::RaydiumClmmIncreaseLimitOrder)
1009            }
1010            discriminators::RAYDIUM_CLMM_DECREASE_LIMIT_ORDER => {
1011                Some(EventType::RaydiumClmmDecreaseLimitOrder)
1012            }
1013            discriminators::RAYDIUM_CLMM_SETTLE_LIMIT_ORDER => {
1014                Some(EventType::RaydiumClmmSettleLimitOrder)
1015            }
1016            discriminators::RAYDIUM_CLMM_UPDATE_REWARD_INFOS => {
1017                Some(EventType::RaydiumClmmUpdateRewardInfos)
1018            }
1019            discriminators::RAYDIUM_CLMM_CREATE_POOL => Some(EventType::RaydiumClmmCreatePool),
1020            discriminators::RAYDIUM_CLMM_COLLECT_PERSONAL_FEE
1021            | discriminators::RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE => {
1022                Some(EventType::RaydiumClmmCollectFee)
1023            }
1024            _ => None,
1025        },
1026        program_ids::RAYDIUM_CPMM_PROGRAM_ID => match discriminator {
1027            discriminators::RAYDIUM_CPMM_SWAP_EVENT
1028            | discriminators::RAYDIUM_CPMM_SWAP_BASE_IN
1029            | discriminators::RAYDIUM_CPMM_SWAP_BASE_OUT => Some(EventType::RaydiumCpmmSwap),
1030            discriminators::RAYDIUM_CPMM_CREATE_POOL => Some(EventType::RaydiumCpmmInitialize),
1031            discriminators::RAYDIUM_CPMM_DEPOSIT => Some(EventType::RaydiumCpmmDeposit),
1032            discriminators::RAYDIUM_CPMM_WITHDRAW => Some(EventType::RaydiumCpmmWithdraw),
1033            _ => None,
1034        },
1035        program_ids::RAYDIUM_AMM_V4_PROGRAM_ID => match discriminator {
1036            discriminators::RAYDIUM_AMM_SWAP_BASE_IN
1037            | discriminators::RAYDIUM_AMM_SWAP_BASE_OUT => Some(EventType::RaydiumAmmV4Swap),
1038            discriminators::RAYDIUM_AMM_DEPOSIT => Some(EventType::RaydiumAmmV4Deposit),
1039            discriminators::RAYDIUM_AMM_WITHDRAW => Some(EventType::RaydiumAmmV4Withdraw),
1040            discriminators::RAYDIUM_AMM_INITIALIZE2 => Some(EventType::RaydiumAmmV4Initialize2),
1041            discriminators::RAYDIUM_AMM_WITHDRAW_PNL => Some(EventType::RaydiumAmmV4WithdrawPnl),
1042            _ => None,
1043        },
1044        program_ids::ORCA_WHIRLPOOL_PROGRAM_ID => match discriminator {
1045            discriminators::ORCA_TRADED => Some(EventType::OrcaWhirlpoolSwap),
1046            discriminators::ORCA_LIQUIDITY_INCREASED => {
1047                Some(EventType::OrcaWhirlpoolLiquidityIncreased)
1048            }
1049            discriminators::ORCA_LIQUIDITY_DECREASED => {
1050                Some(EventType::OrcaWhirlpoolLiquidityDecreased)
1051            }
1052            discriminators::ORCA_POOL_INITIALIZED => Some(EventType::OrcaWhirlpoolPoolInitialized),
1053            _ => None,
1054        },
1055        program_ids::METEORA_POOLS_PROGRAM_ID => match discriminator {
1056            discriminators::METEORA_AMM_SWAP => Some(EventType::MeteoraPoolsSwap),
1057            discriminators::METEORA_AMM_ADD_LIQUIDITY => Some(EventType::MeteoraPoolsAddLiquidity),
1058            discriminators::METEORA_AMM_REMOVE_LIQUIDITY => {
1059                Some(EventType::MeteoraPoolsRemoveLiquidity)
1060            }
1061            discriminators::METEORA_AMM_BOOTSTRAP_LIQUIDITY => {
1062                Some(EventType::MeteoraPoolsBootstrapLiquidity)
1063            }
1064            discriminators::METEORA_AMM_POOL_CREATED => Some(EventType::MeteoraPoolsPoolCreated),
1065            discriminators::METEORA_AMM_SET_POOL_FEES => Some(EventType::MeteoraPoolsSetPoolFees),
1066            _ => None,
1067        },
1068        program_ids::METEORA_DAMM_V2_PROGRAM_ID => match discriminator {
1069            discriminators::METEORA_DAMM_SWAP | discriminators::METEORA_DAMM_SWAP2 => {
1070                Some(EventType::MeteoraDammV2Swap)
1071            }
1072            discriminators::METEORA_DAMM_ADD_LIQUIDITY => {
1073                Some(EventType::MeteoraDammV2AddLiquidity)
1074            }
1075            discriminators::METEORA_DAMM_REMOVE_LIQUIDITY => {
1076                Some(EventType::MeteoraDammV2RemoveLiquidity)
1077            }
1078            discriminators::METEORA_DAMM_INITIALIZE_POOL => {
1079                Some(EventType::MeteoraDammV2InitializePool)
1080            }
1081            discriminators::METEORA_DAMM_CREATE_POSITION => {
1082                Some(EventType::MeteoraDammV2CreatePosition)
1083            }
1084            discriminators::METEORA_DAMM_CLOSE_POSITION => {
1085                Some(EventType::MeteoraDammV2ClosePosition)
1086            }
1087            _ => None,
1088        },
1089        program_ids::METEORA_DBC_PROGRAM_ID => match discriminator {
1090            discriminators::METEORA_DBC_SWAP => Some(EventType::MeteoraDbcSwap),
1091            discriminators::METEORA_DBC_INITIALIZE_POOL => {
1092                Some(EventType::MeteoraDbcInitializePool)
1093            }
1094            discriminators::METEORA_DBC_CURVE_COMPLETE => Some(EventType::MeteoraDbcCurveComplete),
1095            _ => None,
1096        },
1097        program_ids::METEORA_DLMM_PROGRAM_ID => match discriminator {
1098            discriminators::METEORA_DLMM_SWAP | discriminators::METEORA_DLMM_SWAP2 => {
1099                Some(EventType::MeteoraDlmmSwap)
1100            }
1101            discriminators::METEORA_DLMM_ADD_LIQUIDITY => Some(EventType::MeteoraDlmmAddLiquidity),
1102            discriminators::METEORA_DLMM_REMOVE_LIQUIDITY => {
1103                Some(EventType::MeteoraDlmmRemoveLiquidity)
1104            }
1105            discriminators::METEORA_DLMM_INITIALIZE_POOL => {
1106                Some(EventType::MeteoraDlmmInitializePool)
1107            }
1108            discriminators::METEORA_DLMM_INITIALIZE_BIN_ARRAY => {
1109                Some(EventType::MeteoraDlmmInitializeBinArray)
1110            }
1111            discriminators::METEORA_DLMM_CREATE_POSITION => {
1112                Some(EventType::MeteoraDlmmCreatePosition)
1113            }
1114            discriminators::METEORA_DLMM_CLOSE_POSITION => {
1115                Some(EventType::MeteoraDlmmClosePosition)
1116            }
1117            discriminators::METEORA_DLMM_CLAIM_FEE | discriminators::METEORA_DLMM_CLAIM_FEE2 => {
1118                Some(EventType::MeteoraDlmmClaimFee)
1119            }
1120            _ => None,
1121        },
1122        _ => None,
1123    }
1124}
1125
1126#[inline(always)]
1127fn parse_program_scoped_event(
1128    program_id: &Pubkey,
1129    discriminator: u64,
1130    data: &[u8],
1131    metadata: EventMetadata,
1132    log: &str,
1133    signature: Signature,
1134    slot: u64,
1135    tx_index: u64,
1136    block_time_us: Option<i64>,
1137    grpc_recv_us: i64,
1138    event_type_filter: Option<&EventTypeFilter>,
1139    is_created_buy: bool,
1140) -> Option<DexEvent> {
1141    if let Some(filter) = event_type_filter {
1142        if *program_id == program_ids::PUMPFUN_PROGRAM_ID
1143            && discriminator == discriminators::PUMPFUN_TRADE
1144        {
1145            if !filter_wants_pumpfun_trade_event(filter) {
1146                return None;
1147            }
1148        } else if let Some(event_type) =
1149            program_scoped_discriminator_to_event_type(program_id, discriminator)
1150        {
1151            if !filter.should_include(event_type) {
1152                return None;
1153            }
1154        }
1155    }
1156
1157    match *program_id {
1158        program_ids::PUMPFUN_PROGRAM_ID => {
1159            if let Some(filter) = event_type_filter {
1160                if !filter.includes_pumpfun() {
1161                    return None;
1162                }
1163            }
1164            match discriminator {
1165                discriminators::PUMPFUN_TRADE => {
1166                    let event =
1167                        crate::logs::pump::parse_trade_from_data(data, metadata, is_created_buy)?;
1168                    filter_pumpfun_trade_variant(event, event_type_filter)
1169                }
1170                discriminators::PUMPFUN_CREATE => {
1171                    crate::logs::pump::parse_create_from_data(data, metadata)
1172                }
1173                discriminators::PUMPFUN_MIGRATE => {
1174                    crate::logs::pump::parse_migrate_from_data(data, metadata)
1175                }
1176                discriminators::PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR => {
1177                    crate::logs::pump::parse_migrate_bonding_curve_creator_from_data(data, metadata)
1178                }
1179                _ => None,
1180            }
1181        }
1182        program_ids::PUMP_FEES_PROGRAM_ID => {
1183            if let Some(filter) = event_type_filter {
1184                if !filter.includes_pump_fees() {
1185                    return None;
1186                }
1187            }
1188            match discriminator {
1189                discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG => {
1190                    crate::logs::pump_fees::parse_create_fee_sharing_config_from_data(
1191                        data, metadata,
1192                    )
1193                }
1194                discriminators::PUMP_FEES_INITIALIZE_FEE_CONFIG => {
1195                    crate::logs::pump_fees::parse_initialize_fee_config_from_data(data, metadata)
1196                }
1197                discriminators::PUMP_FEES_RESET_FEE_SHARING_CONFIG => {
1198                    crate::logs::pump_fees::parse_reset_fee_sharing_config_from_data(data, metadata)
1199                }
1200                discriminators::PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY => {
1201                    crate::logs::pump_fees::parse_revoke_fee_sharing_authority_from_data(
1202                        data, metadata,
1203                    )
1204                }
1205                discriminators::PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY => {
1206                    crate::logs::pump_fees::parse_transfer_fee_sharing_authority_from_data(
1207                        data, metadata,
1208                    )
1209                }
1210                discriminators::PUMP_FEES_UPDATE_ADMIN => {
1211                    crate::logs::pump_fees::parse_update_admin_from_data(data, metadata)
1212                }
1213                discriminators::PUMP_FEES_UPDATE_FEE_CONFIG => {
1214                    crate::logs::pump_fees::parse_update_fee_config_from_data(data, metadata)
1215                }
1216                discriminators::PUMP_FEES_UPDATE_FEE_SHARES => {
1217                    crate::logs::pump_fees::parse_update_fee_shares_from_data(data, metadata)
1218                }
1219                discriminators::PUMP_FEES_UPSERT_FEE_TIERS => {
1220                    crate::logs::pump_fees::parse_upsert_fee_tiers_from_data(data, metadata)
1221                }
1222                _ => None,
1223            }
1224        }
1225        program_ids::PUMPSWAP_PROGRAM_ID => {
1226            if let Some(filter) = event_type_filter {
1227                if !filter.includes_pumpswap() {
1228                    return None;
1229                }
1230            }
1231            match discriminator {
1232                discriminators::PUMPSWAP_BUY => {
1233                    crate::logs::pump_amm::parse_buy_from_data(data, metadata)
1234                }
1235                discriminators::PUMPSWAP_SELL => {
1236                    crate::logs::pump_amm::parse_sell_from_data(data, metadata)
1237                }
1238                discriminators::PUMPSWAP_CREATE_POOL => {
1239                    crate::logs::pump_amm::parse_create_pool_from_data(data, metadata)
1240                }
1241                discriminators::PUMPSWAP_ADD_LIQUIDITY => {
1242                    crate::logs::pump_amm::parse_add_liquidity_from_data(data, metadata)
1243                }
1244                discriminators::PUMPSWAP_REMOVE_LIQUIDITY => {
1245                    crate::logs::pump_amm::parse_remove_liquidity_from_data(data, metadata)
1246                }
1247                _ => None,
1248            }
1249        }
1250        program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID => {
1251            if let Some(filter) = event_type_filter {
1252                if !filter.includes_raydium_launchlab() {
1253                    return None;
1254                }
1255            }
1256            match discriminator {
1257                discriminators::RAYDIUM_LAUNCHLAB_TRADE => {
1258                    crate::logs::raydium_launchlab::parse_trade_from_data(data, metadata)
1259                }
1260                discriminators::RAYDIUM_LAUNCHLAB_POOL_CREATE => {
1261                    crate::logs::raydium_launchlab::parse_pool_create_from_data(data, metadata)
1262                }
1263                _ => None,
1264            }
1265        }
1266        program_ids::RAYDIUM_CLMM_PROGRAM_ID => {
1267            if let Some(filter) = event_type_filter {
1268                if !filter.includes_raydium_clmm() {
1269                    return None;
1270                }
1271            }
1272            match discriminator {
1273                discriminators::RAYDIUM_CLMM_SWAP => {
1274                    crate::logs::raydium_clmm::parse_swap_from_data(data, metadata)
1275                }
1276                discriminators::RAYDIUM_CLMM_INCREASE_LIQUIDITY => {
1277                    crate::logs::raydium_clmm::parse_increase_liquidity_from_data(data, metadata)
1278                }
1279                discriminators::RAYDIUM_CLMM_DECREASE_LIQUIDITY => {
1280                    crate::logs::raydium_clmm::parse_decrease_liquidity_from_data(data, metadata)
1281                }
1282                discriminators::RAYDIUM_CLMM_LIQUIDITY_CHANGE => {
1283                    crate::logs::raydium_clmm::parse_liquidity_change_from_data(data, metadata)
1284                }
1285                discriminators::RAYDIUM_CLMM_CONFIG_CHANGE => {
1286                    crate::logs::raydium_clmm::parse_config_change_from_data(data, metadata)
1287                }
1288                discriminators::RAYDIUM_CLMM_CREATE_PERSONAL_POSITION => {
1289                    crate::logs::raydium_clmm::parse_create_personal_position_from_data(
1290                        data, metadata,
1291                    )
1292                }
1293                discriminators::RAYDIUM_CLMM_LIQUIDITY_CALCULATE => {
1294                    crate::logs::raydium_clmm::parse_liquidity_calculate_from_data(data, metadata)
1295                }
1296                discriminators::RAYDIUM_CLMM_OPEN_LIMIT_ORDER => {
1297                    crate::logs::raydium_clmm::parse_open_limit_order_from_data(data, metadata)
1298                }
1299                discriminators::RAYDIUM_CLMM_INCREASE_LIMIT_ORDER => {
1300                    crate::logs::raydium_clmm::parse_increase_limit_order_from_data(data, metadata)
1301                }
1302                discriminators::RAYDIUM_CLMM_DECREASE_LIMIT_ORDER => {
1303                    crate::logs::raydium_clmm::parse_decrease_limit_order_from_data(data, metadata)
1304                }
1305                discriminators::RAYDIUM_CLMM_SETTLE_LIMIT_ORDER => {
1306                    crate::logs::raydium_clmm::parse_settle_limit_order_from_data(data, metadata)
1307                }
1308                discriminators::RAYDIUM_CLMM_UPDATE_REWARD_INFOS => {
1309                    crate::logs::raydium_clmm::parse_update_reward_infos_from_data(data, metadata)
1310                }
1311                discriminators::RAYDIUM_CLMM_CREATE_POOL => {
1312                    crate::logs::raydium_clmm::parse_create_pool_from_data(data, metadata)
1313                }
1314                discriminators::RAYDIUM_CLMM_COLLECT_PERSONAL_FEE => {
1315                    crate::logs::raydium_clmm::parse_collect_personal_fee_from_data(data, metadata)
1316                }
1317                discriminators::RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE => {
1318                    crate::logs::raydium_clmm::parse_collect_protocol_fee_from_data(data, metadata)
1319                }
1320                _ => None,
1321            }
1322        }
1323        program_ids::RAYDIUM_CPMM_PROGRAM_ID => {
1324            if let Some(filter) = event_type_filter {
1325                if !filter.includes_raydium_cpmm() {
1326                    return None;
1327                }
1328            }
1329            match discriminator {
1330                discriminators::RAYDIUM_CPMM_SWAP_EVENT => {
1331                    crate::logs::raydium_cpmm::parse_swap_event_from_data(data, metadata)
1332                }
1333                discriminators::RAYDIUM_CPMM_SWAP_BASE_IN => {
1334                    crate::logs::raydium_cpmm::parse_swap_base_in_from_data(data, metadata)
1335                }
1336                discriminators::RAYDIUM_CPMM_SWAP_BASE_OUT => {
1337                    crate::logs::raydium_cpmm::parse_swap_base_out_from_data(data, metadata)
1338                }
1339                discriminators::RAYDIUM_CPMM_CREATE_POOL => {
1340                    crate::logs::raydium_cpmm::parse_create_pool_from_data(data, metadata)
1341                }
1342                discriminators::RAYDIUM_CPMM_DEPOSIT => {
1343                    crate::logs::raydium_cpmm::parse_deposit_from_data(data, metadata)
1344                }
1345                discriminators::RAYDIUM_CPMM_WITHDRAW => {
1346                    crate::logs::raydium_cpmm::parse_withdraw_from_data(data, metadata)
1347                }
1348                _ => None,
1349            }
1350        }
1351        program_ids::RAYDIUM_AMM_V4_PROGRAM_ID => {
1352            if let Some(filter) = event_type_filter {
1353                if !filter.includes_raydium_amm_v4() {
1354                    return None;
1355                }
1356            }
1357            match discriminator {
1358                discriminators::RAYDIUM_AMM_SWAP_BASE_IN => {
1359                    crate::logs::raydium_amm::parse_swap_base_in_from_data(data, metadata)
1360                }
1361                discriminators::RAYDIUM_AMM_SWAP_BASE_OUT => {
1362                    crate::logs::raydium_amm::parse_swap_base_out_from_data(data, metadata)
1363                }
1364                discriminators::RAYDIUM_AMM_DEPOSIT => {
1365                    crate::logs::raydium_amm::parse_deposit_from_data(data, metadata)
1366                }
1367                discriminators::RAYDIUM_AMM_WITHDRAW => {
1368                    crate::logs::raydium_amm::parse_withdraw_from_data(data, metadata)
1369                }
1370                discriminators::RAYDIUM_AMM_INITIALIZE2 => {
1371                    crate::logs::raydium_amm::parse_initialize2_from_data(data, metadata)
1372                }
1373                discriminators::RAYDIUM_AMM_WITHDRAW_PNL => {
1374                    crate::logs::raydium_amm::parse_withdraw_pnl_from_data(data, metadata)
1375                }
1376                _ => None,
1377            }
1378        }
1379        program_ids::ORCA_WHIRLPOOL_PROGRAM_ID => {
1380            if let Some(filter) = event_type_filter {
1381                if !filter.includes_orca_whirlpool() {
1382                    return None;
1383                }
1384            }
1385            match discriminator {
1386                discriminators::ORCA_TRADED => {
1387                    crate::logs::orca_whirlpool::parse_traded_from_data(data, metadata)
1388                }
1389                discriminators::ORCA_LIQUIDITY_INCREASED => {
1390                    crate::logs::orca_whirlpool::parse_liquidity_increased_from_data(data, metadata)
1391                }
1392                discriminators::ORCA_LIQUIDITY_DECREASED => {
1393                    crate::logs::orca_whirlpool::parse_liquidity_decreased_from_data(data, metadata)
1394                }
1395                discriminators::ORCA_POOL_INITIALIZED => {
1396                    crate::logs::orca_whirlpool::parse_pool_initialized_from_data(data, metadata)
1397                }
1398                _ => None,
1399            }
1400        }
1401        program_ids::METEORA_POOLS_PROGRAM_ID => {
1402            if let Some(filter) = event_type_filter {
1403                if !filter.includes_meteora_pools() {
1404                    return None;
1405                }
1406            }
1407            match discriminator {
1408                discriminators::METEORA_AMM_SWAP => {
1409                    crate::logs::meteora_amm::parse_swap_from_data(data, metadata)
1410                }
1411                discriminators::METEORA_AMM_ADD_LIQUIDITY => {
1412                    crate::logs::meteora_amm::parse_add_liquidity_from_data(data, metadata)
1413                }
1414                discriminators::METEORA_AMM_REMOVE_LIQUIDITY => {
1415                    crate::logs::meteora_amm::parse_remove_liquidity_from_data(data, metadata)
1416                }
1417                discriminators::METEORA_AMM_BOOTSTRAP_LIQUIDITY => {
1418                    crate::logs::meteora_amm::parse_bootstrap_liquidity_from_data(data, metadata)
1419                }
1420                discriminators::METEORA_AMM_POOL_CREATED => {
1421                    crate::logs::meteora_amm::parse_pool_created_from_data(data, metadata)
1422                }
1423                discriminators::METEORA_AMM_SET_POOL_FEES => {
1424                    crate::logs::meteora_amm::parse_set_pool_fees_from_data(data, metadata)
1425                }
1426                _ => None,
1427            }
1428        }
1429        program_ids::METEORA_DAMM_V2_PROGRAM_ID => {
1430            if let Some(filter) = event_type_filter {
1431                if !filter.includes_meteora_damm_v2() {
1432                    return None;
1433                }
1434            }
1435            match discriminator {
1436                discriminators::METEORA_DAMM_SWAP => {
1437                    crate::logs::meteora_damm::parse_swap_from_data(data, metadata)
1438                }
1439                discriminators::METEORA_DAMM_SWAP2 => {
1440                    crate::logs::meteora_damm::parse_swap2_from_data(data, metadata)
1441                }
1442                discriminators::METEORA_DAMM_ADD_LIQUIDITY => {
1443                    crate::logs::meteora_damm::parse_add_liquidity_from_data(data, metadata)
1444                }
1445                discriminators::METEORA_DAMM_REMOVE_LIQUIDITY => {
1446                    crate::logs::meteora_damm::parse_remove_liquidity_from_data(data, metadata)
1447                }
1448                discriminators::METEORA_DAMM_INITIALIZE_POOL => {
1449                    crate::logs::meteora_damm::parse_initialize_pool_from_data(data, metadata)
1450                }
1451                discriminators::METEORA_DAMM_CREATE_POSITION => {
1452                    crate::logs::meteora_damm::parse_create_position_from_data(data, metadata)
1453                }
1454                discriminators::METEORA_DAMM_CLOSE_POSITION => {
1455                    crate::logs::meteora_damm::parse_close_position_from_data(data, metadata)
1456                }
1457                _ => None,
1458            }
1459        }
1460        program_ids::METEORA_DBC_PROGRAM_ID => {
1461            if let Some(filter) = event_type_filter {
1462                if !filter.includes_meteora_dbc() {
1463                    return None;
1464                }
1465            }
1466            match discriminator {
1467                discriminators::METEORA_DBC_SWAP => {
1468                    crate::logs::meteora_dbc::parse_swap_from_data(data, metadata)
1469                }
1470                discriminators::METEORA_DBC_INITIALIZE_POOL => {
1471                    crate::logs::meteora_dbc::parse_initialize_pool_from_data(data, metadata)
1472                }
1473                discriminators::METEORA_DBC_CURVE_COMPLETE => {
1474                    crate::logs::meteora_dbc::parse_curve_complete_from_data(data, metadata)
1475                }
1476                _ => None,
1477            }
1478        }
1479        program_ids::METEORA_DLMM_PROGRAM_ID => {
1480            if let Some(filter) = event_type_filter {
1481                if !filter.includes_meteora_dlmm() {
1482                    return None;
1483                }
1484            }
1485            match discriminator {
1486                discriminators::METEORA_DLMM_SWAP => {
1487                    crate::logs::meteora_dlmm::parse_swap_from_data(data, metadata)
1488                }
1489                discriminators::METEORA_DLMM_SWAP2 => {
1490                    crate::logs::meteora_dlmm::parse_swap2_from_data(data, metadata)
1491                }
1492                discriminators::METEORA_DLMM_ADD_LIQUIDITY => {
1493                    crate::logs::meteora_dlmm::parse_add_liquidity_from_data(data, metadata)
1494                }
1495                discriminators::METEORA_DLMM_REMOVE_LIQUIDITY => {
1496                    crate::logs::meteora_dlmm::parse_remove_liquidity_from_data(data, metadata)
1497                }
1498                discriminators::METEORA_DLMM_INITIALIZE_POOL => {
1499                    crate::logs::meteora_dlmm::parse_lb_pair_create_from_data(data, metadata)
1500                }
1501                discriminators::METEORA_DLMM_INITIALIZE_BIN_ARRAY => {
1502                    crate::logs::meteora_dlmm::parse_initialize_bin_array_from_data(data, metadata)
1503                }
1504                discriminators::METEORA_DLMM_CREATE_POSITION => {
1505                    crate::logs::meteora_dlmm::parse_position_create_from_data(data, metadata)
1506                }
1507                discriminators::METEORA_DLMM_CLOSE_POSITION => {
1508                    crate::logs::meteora_dlmm::parse_position_close_from_data(data, metadata)
1509                }
1510                discriminators::METEORA_DLMM_CLAIM_FEE => {
1511                    crate::logs::meteora_dlmm::parse_claim_fee_from_data(data, metadata)
1512                }
1513                discriminators::METEORA_DLMM_CLAIM_FEE2 => {
1514                    crate::logs::meteora_dlmm::parse_claim_fee2_from_data(data, metadata)
1515                }
1516                _ => None,
1517            }
1518        }
1519        _ => None,
1520    }
1521}
1522
1523#[inline(always)]
1524fn filter_pumpfun_trade_variant(
1525    event: DexEvent,
1526    event_type_filter: Option<&EventTypeFilter>,
1527) -> Option<DexEvent> {
1528    if let Some(filter) = event_type_filter {
1529        if let Some(ref include_only) = filter.include_only {
1530            let has_specific_filter = !include_only.contains(&EventType::PumpFunTrade)
1531                && include_only.iter().any(|t| {
1532                    matches!(
1533                        t,
1534                        EventType::PumpFunBuy
1535                            | EventType::PumpFunSell
1536                            | EventType::PumpFunBuyExactSolIn
1537                            | EventType::PumpFunCreate
1538                            | EventType::PumpFunCreateV2
1539                    )
1540                });
1541            if has_specific_filter {
1542                let event_type_matches = match &event {
1543                    DexEvent::PumpFunBuy(_) => include_only.iter().any(|t| {
1544                        matches!(t, EventType::PumpFunBuy | EventType::PumpFunBuyExactSolIn)
1545                    }),
1546                    DexEvent::PumpFunSell(_) => include_only.contains(&EventType::PumpFunSell),
1547                    DexEvent::PumpFunBuyExactSolIn(_) => include_only.iter().any(|t| {
1548                        matches!(t, EventType::PumpFunBuy | EventType::PumpFunBuyExactSolIn)
1549                    }),
1550                    DexEvent::PumpFunTrade(_) => include_only.contains(&EventType::PumpFunTrade),
1551                    DexEvent::PumpFunCreate(_) | DexEvent::PumpFunCreateV2(_) => {
1552                        include_only.iter().any(|t| {
1553                            matches!(t, EventType::PumpFunCreate | EventType::PumpFunCreateV2)
1554                        })
1555                    }
1556                    _ => false,
1557                };
1558                if !event_type_matches {
1559                    return None;
1560                }
1561            }
1562        }
1563        if filter.exclude_types.is_some() && !filter.should_include_dex_event(&event) {
1564            return None;
1565        }
1566    }
1567    Some(event)
1568}
1569
1570/// Map discriminator to EventType (compile-time optimized match)
1571#[inline(always)]
1572fn discriminator_to_event_type(discriminator: u64) -> Option<EventType> {
1573    match discriminator {
1574        discriminators::PUMPFUN_CREATE => Some(EventType::PumpFunCreate),
1575        discriminators::PUMPFUN_TRADE => Some(EventType::PumpFunTrade),
1576        discriminators::PUMPFUN_MIGRATE => Some(EventType::PumpFunMigrate),
1577        discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG => {
1578            Some(EventType::PumpFeesCreateFeeSharingConfig)
1579        }
1580        discriminators::PUMP_FEES_INITIALIZE_FEE_CONFIG => {
1581            Some(EventType::PumpFeesInitializeFeeConfig)
1582        }
1583        discriminators::PUMP_FEES_RESET_FEE_SHARING_CONFIG => {
1584            Some(EventType::PumpFeesResetFeeSharingConfig)
1585        }
1586        discriminators::PUMP_FEES_REVOKE_FEE_SHARING_AUTHORITY => {
1587            Some(EventType::PumpFeesRevokeFeeSharingAuthority)
1588        }
1589        discriminators::PUMP_FEES_TRANSFER_FEE_SHARING_AUTHORITY => {
1590            Some(EventType::PumpFeesTransferFeeSharingAuthority)
1591        }
1592        discriminators::PUMP_FEES_UPDATE_ADMIN => Some(EventType::PumpFeesUpdateAdmin),
1593        discriminators::PUMP_FEES_UPDATE_FEE_CONFIG => Some(EventType::PumpFeesUpdateFeeConfig),
1594        discriminators::PUMP_FEES_UPDATE_FEE_SHARES => Some(EventType::PumpFeesUpdateFeeShares),
1595        discriminators::PUMP_FEES_UPSERT_FEE_TIERS => Some(EventType::PumpFeesUpsertFeeTiers),
1596        discriminators::PUMPFUN_MIGRATE_BONDING_CURVE_CREATOR => {
1597            Some(EventType::PumpFunMigrateBondingCurveCreator)
1598        }
1599        discriminators::PUMPSWAP_BUY => Some(EventType::PumpSwapBuy),
1600        discriminators::PUMPSWAP_SELL => Some(EventType::PumpSwapSell),
1601        discriminators::PUMPSWAP_CREATE_POOL => Some(EventType::PumpSwapCreatePool),
1602        discriminators::PUMPSWAP_ADD_LIQUIDITY => Some(EventType::PumpSwapLiquidityAdded),
1603        discriminators::PUMPSWAP_REMOVE_LIQUIDITY => Some(EventType::PumpSwapLiquidityRemoved),
1604        discriminators::RAYDIUM_LAUNCHLAB_POOL_CREATE => {
1605            Some(EventType::RaydiumLaunchlabPoolCreate)
1606        }
1607        discriminators::RAYDIUM_CLMM_SWAP => Some(EventType::RaydiumClmmSwap),
1608        discriminators::RAYDIUM_CLMM_INCREASE_LIQUIDITY => {
1609            Some(EventType::RaydiumClmmIncreaseLiquidity)
1610        }
1611        discriminators::RAYDIUM_CLMM_DECREASE_LIQUIDITY => {
1612            Some(EventType::RaydiumClmmDecreaseLiquidity)
1613        }
1614        discriminators::RAYDIUM_CLMM_LIQUIDITY_CHANGE => {
1615            Some(EventType::RaydiumClmmLiquidityChange)
1616        }
1617        discriminators::RAYDIUM_CLMM_CONFIG_CHANGE => Some(EventType::RaydiumClmmConfigChange),
1618        discriminators::RAYDIUM_CLMM_CREATE_PERSONAL_POSITION => {
1619            Some(EventType::RaydiumClmmCreatePersonalPosition)
1620        }
1621        discriminators::RAYDIUM_CLMM_LIQUIDITY_CALCULATE => {
1622            Some(EventType::RaydiumClmmLiquidityCalculate)
1623        }
1624        discriminators::RAYDIUM_CLMM_OPEN_LIMIT_ORDER => Some(EventType::RaydiumClmmOpenLimitOrder),
1625        discriminators::RAYDIUM_CLMM_INCREASE_LIMIT_ORDER => {
1626            Some(EventType::RaydiumClmmIncreaseLimitOrder)
1627        }
1628        discriminators::RAYDIUM_CLMM_DECREASE_LIMIT_ORDER => {
1629            Some(EventType::RaydiumClmmDecreaseLimitOrder)
1630        }
1631        discriminators::RAYDIUM_CLMM_SETTLE_LIMIT_ORDER => {
1632            Some(EventType::RaydiumClmmSettleLimitOrder)
1633        }
1634        discriminators::RAYDIUM_CLMM_UPDATE_REWARD_INFOS => {
1635            Some(EventType::RaydiumClmmUpdateRewardInfos)
1636        }
1637        discriminators::RAYDIUM_CLMM_CREATE_POOL => Some(EventType::RaydiumClmmCreatePool),
1638        discriminators::RAYDIUM_CLMM_COLLECT_PERSONAL_FEE
1639        | discriminators::RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE => {
1640            Some(EventType::RaydiumClmmCollectFee)
1641        }
1642        discriminators::RAYDIUM_CPMM_SWAP_BASE_IN | discriminators::RAYDIUM_CPMM_SWAP_BASE_OUT => {
1643            Some(EventType::RaydiumCpmmSwap)
1644        }
1645        discriminators::RAYDIUM_CPMM_DEPOSIT => Some(EventType::RaydiumCpmmDeposit),
1646        discriminators::RAYDIUM_CPMM_WITHDRAW => Some(EventType::RaydiumCpmmWithdraw),
1647        discriminators::RAYDIUM_AMM_SWAP_BASE_IN | discriminators::RAYDIUM_AMM_SWAP_BASE_OUT => {
1648            Some(EventType::RaydiumAmmV4Swap)
1649        }
1650        discriminators::RAYDIUM_AMM_DEPOSIT => Some(EventType::RaydiumAmmV4Deposit),
1651        discriminators::RAYDIUM_AMM_WITHDRAW => Some(EventType::RaydiumAmmV4Withdraw),
1652        discriminators::RAYDIUM_AMM_INITIALIZE2 => Some(EventType::RaydiumAmmV4Initialize2),
1653        discriminators::RAYDIUM_AMM_WITHDRAW_PNL => Some(EventType::RaydiumAmmV4WithdrawPnl),
1654        discriminators::ORCA_TRADED => Some(EventType::OrcaWhirlpoolSwap),
1655        discriminators::ORCA_LIQUIDITY_INCREASED => {
1656            Some(EventType::OrcaWhirlpoolLiquidityIncreased)
1657        }
1658        discriminators::ORCA_LIQUIDITY_DECREASED => {
1659            Some(EventType::OrcaWhirlpoolLiquidityDecreased)
1660        }
1661        discriminators::ORCA_POOL_INITIALIZED => Some(EventType::OrcaWhirlpoolPoolInitialized),
1662        discriminators::METEORA_AMM_SWAP => Some(EventType::MeteoraPoolsSwap),
1663        discriminators::METEORA_AMM_ADD_LIQUIDITY => Some(EventType::MeteoraPoolsAddLiquidity),
1664        discriminators::METEORA_AMM_REMOVE_LIQUIDITY => {
1665            Some(EventType::MeteoraPoolsRemoveLiquidity)
1666        }
1667        discriminators::METEORA_AMM_BOOTSTRAP_LIQUIDITY => {
1668            Some(EventType::MeteoraPoolsBootstrapLiquidity)
1669        }
1670        discriminators::METEORA_AMM_POOL_CREATED => Some(EventType::MeteoraPoolsPoolCreated),
1671        discriminators::METEORA_AMM_SET_POOL_FEES => Some(EventType::MeteoraPoolsSetPoolFees),
1672        discriminators::METEORA_DAMM_SWAP | discriminators::METEORA_DAMM_SWAP2 => {
1673            Some(EventType::MeteoraDammV2Swap)
1674        }
1675        discriminators::METEORA_DAMM_ADD_LIQUIDITY => Some(EventType::MeteoraDammV2AddLiquidity),
1676        discriminators::METEORA_DAMM_REMOVE_LIQUIDITY => {
1677            Some(EventType::MeteoraDammV2RemoveLiquidity)
1678        }
1679        discriminators::METEORA_DAMM_INITIALIZE_POOL => {
1680            Some(EventType::MeteoraDammV2InitializePool)
1681        }
1682        discriminators::METEORA_DAMM_CREATE_POSITION => {
1683            Some(EventType::MeteoraDammV2CreatePosition)
1684        }
1685        discriminators::METEORA_DAMM_CLOSE_POSITION => Some(EventType::MeteoraDammV2ClosePosition),
1686        _ => None,
1687    }
1688}
1689
1690// ============================================================================
1691// SIMD utilities for log detection
1692// ============================================================================
1693#[inline]
1694pub fn detect_pumpfun_create(logs: &[String]) -> bool {
1695    logs.iter().any(|log| PUMPFUN_CREATE_FINDER.find(log.as_bytes()).is_some())
1696}
1697
1698/// SIMD 优化的 "invoke [" 查找器
1699static INVOKE_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"invoke ["));
1700
1701/// 从日志中解析指令调用信息 (SIMD 优化版本)
1702/// 返回 (program_id, depth)
1703#[inline]
1704pub fn parse_invoke_info(log: &str) -> Option<(&str, usize)> {
1705    let log_bytes = log.as_bytes();
1706
1707    // SIMD 快速查找 "invoke ["
1708    let invoke_start = INVOKE_FINDER.find(log_bytes)?;
1709    let bracket_start = invoke_start + 8; // "invoke [" 长度
1710
1711    // 边界检查
1712    if bracket_start >= log_bytes.len() {
1713        return None;
1714    }
1715
1716    // 解析深度数字,直到遇到 ']'
1717    let mut depth = 0usize;
1718    for &byte in &log_bytes[bracket_start..] {
1719        match byte {
1720            b'0'..=b'9' => {
1721                depth = depth * 10 + (byte - b'0') as usize;
1722            }
1723            b']' => break,
1724            _ => return None, // 遇到非数字非']'字符,解析失败
1725        }
1726    }
1727
1728    // 提取程序ID:从 "Program " 开始到 " invoke" 结束
1729    if invoke_start < 8 {
1730        return None; // 没有足够空间放 "Program "
1731    }
1732
1733    let program_start = 8; // "Program " 的长度
1734    let program_end = invoke_start - 1; // " invoke" 前面的空格位置
1735
1736    if program_end <= program_start {
1737        return None;
1738    }
1739
1740    let program_id = std::str::from_utf8(&log_bytes[program_start..program_end]).ok()?;
1741
1742    Some((program_id, depth))
1743}
1744
1745/// Parse `Program <id> success` or `Program <id> failed: ...` completion lines.
1746#[inline]
1747pub fn parse_program_complete_info(log: &str) -> Option<&str> {
1748    let rest = log.strip_prefix("Program ")?;
1749    if let Some(pos) = rest.find(" success") {
1750        return Some(&rest[..pos]);
1751    }
1752    if let Some(pos) = rest.find(" failed:") {
1753        return Some(&rest[..pos]);
1754    }
1755    None
1756}
1757
1758#[cfg(test)]
1759mod tests {
1760    use super::*;
1761    use crate::core::events::PumpFunTradeEvent;
1762    use base64::{engine::general_purpose::STANDARD, Engine as _};
1763    use solana_sdk::{pubkey::Pubkey, signature::Signature};
1764
1765    #[test]
1766    fn program_scoped_launchlab_trade_is_not_parsed_as_pumpfun() {
1767        let pool = Pubkey::new_unique();
1768        let mut raw = Vec::new();
1769        raw.extend_from_slice(&discriminators::RAYDIUM_LAUNCHLAB_TRADE.to_le_bytes());
1770        raw.extend_from_slice(pool.as_ref());
1771        for value in 0u64..13 {
1772            raw.extend_from_slice(&(100 + value).to_le_bytes());
1773        }
1774        raw.push(1); // TradeDirection::Sell
1775        raw.push(2); // PoolStatus::Trade
1776        raw.push(1); // exact_in
1777
1778        let log = format!("Program data: {}", STANDARD.encode(raw));
1779        let filter = EventTypeFilter::include_only(vec![EventType::RaydiumLaunchlabTrade]);
1780        let event = parse_log_optimized_with_program_id(
1781            &log,
1782            Signature::default(),
1783            1,
1784            2,
1785            Some(3),
1786            4,
1787            Some(&filter),
1788            false,
1789            None,
1790            Some(&program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID),
1791        )
1792        .expect("launchlab trade should parse");
1793
1794        match event {
1795            DexEvent::RaydiumLaunchlabTrade(trade) => {
1796                assert_eq!(trade.pool_state, pool);
1797                assert_eq!(trade.amount_in, 107);
1798                assert_eq!(trade.amount_out, 108);
1799                assert!(!trade.is_buy);
1800                assert!(trade.exact_in);
1801            }
1802            other => panic!("expected RaydiumLaunchlabTrade, got {other:?}"),
1803        }
1804    }
1805
1806    fn launchlab_trade_log() -> (String, Pubkey) {
1807        let pool = Pubkey::new_unique();
1808        let mut raw = Vec::new();
1809        raw.extend_from_slice(&discriminators::RAYDIUM_LAUNCHLAB_TRADE.to_le_bytes());
1810        raw.extend_from_slice(pool.as_ref());
1811        for value in 0u64..13 {
1812            raw.extend_from_slice(&(100 + value).to_le_bytes());
1813        }
1814        raw.push(1); // TradeDirection::Sell
1815        raw.push(2); // PoolStatus::Trade
1816        raw.push(1); // exact_in
1817
1818        (format!("Program data: {}", STANDARD.encode(raw)), pool)
1819    }
1820
1821    #[test]
1822    fn unscoped_launchlab_trade_filter_parses_shared_discriminator() {
1823        let (log, pool) = launchlab_trade_log();
1824        let filter = EventTypeFilter::include_only(vec![EventType::RaydiumLaunchlabTrade]);
1825        let event = parse_log_optimized(
1826            &log,
1827            Signature::default(),
1828            1,
1829            2,
1830            Some(3),
1831            4,
1832            Some(&filter),
1833            false,
1834            None,
1835        )
1836        .expect("unscoped LaunchLab trade should parse when requested");
1837
1838        match event {
1839            DexEvent::RaydiumLaunchlabTrade(trade) => {
1840                assert_eq!(trade.pool_state, pool);
1841                assert_eq!(trade.amount_in, 107);
1842                assert_eq!(trade.amount_out, 108);
1843                assert!(!trade.is_buy);
1844                assert!(trade.exact_in);
1845            }
1846            other => panic!("expected RaydiumLaunchlabTrade, got {other:?}"),
1847        }
1848    }
1849
1850    #[test]
1851    fn unscoped_pumpfun_only_filter_does_not_parse_launchlab_trade() {
1852        let (log, _pool) = launchlab_trade_log();
1853        let filter = EventTypeFilter::include_only(vec![EventType::PumpFunBuy]);
1854        assert!(parse_log_optimized(
1855            &log,
1856            Signature::default(),
1857            1,
1858            2,
1859            Some(3),
1860            4,
1861            Some(&filter),
1862            false,
1863            None,
1864        )
1865        .is_none());
1866    }
1867
1868    #[test]
1869    fn program_scoped_dlmm_initialize_bin_array_parses_and_filters() {
1870        let pool = Pubkey::new_unique();
1871        let bin_array = Pubkey::new_unique();
1872        let mut raw = Vec::new();
1873        raw.extend_from_slice(&discriminators::METEORA_DLMM_INITIALIZE_BIN_ARRAY.to_le_bytes());
1874        raw.extend_from_slice(pool.as_ref());
1875        raw.extend_from_slice(bin_array.as_ref());
1876        raw.extend_from_slice(&(-12i64).to_le_bytes());
1877
1878        let log = format!("Program data: {}", STANDARD.encode(raw));
1879        let matching_filter =
1880            EventTypeFilter::include_only(vec![EventType::MeteoraDlmmInitializeBinArray]);
1881        let event = parse_log_optimized_with_program_id(
1882            &log,
1883            Signature::default(),
1884            1,
1885            2,
1886            Some(3),
1887            4,
1888            Some(&matching_filter),
1889            false,
1890            None,
1891            Some(&program_ids::METEORA_DLMM_PROGRAM_ID),
1892        )
1893        .expect("DLMM initialize bin array should parse");
1894
1895        match event {
1896            DexEvent::MeteoraDlmmInitializeBinArray(event) => {
1897                assert_eq!(event.pool, pool);
1898                assert_eq!(event.bin_array, bin_array);
1899                assert_eq!(event.index, -12);
1900            }
1901            other => panic!("expected MeteoraDlmmInitializeBinArray, got {other:?}"),
1902        }
1903
1904        let non_matching_filter = EventTypeFilter::include_only(vec![EventType::MeteoraDlmmSwap]);
1905        assert!(parse_log_optimized_with_program_id(
1906            &log,
1907            Signature::default(),
1908            1,
1909            2,
1910            Some(3),
1911            4,
1912            Some(&non_matching_filter),
1913            false,
1914            None,
1915            Some(&program_ids::METEORA_DLMM_PROGRAM_ID),
1916        )
1917        .is_none());
1918    }
1919
1920    #[test]
1921    fn pumpfun_trade_filter_remains_generic_when_combined_with_specific_type() {
1922        let filter =
1923            EventTypeFilter::include_only(vec![EventType::PumpFunTrade, EventType::PumpFunBuy]);
1924        let event = DexEvent::PumpFunSell(PumpFunTradeEvent {
1925            metadata: EventMetadata::default(),
1926            is_buy: false,
1927            ix_name: "sell".to_string(),
1928            ..Default::default()
1929        });
1930
1931        assert!(filter_pumpfun_trade_variant(event, Some(&filter)).is_some());
1932    }
1933
1934    #[test]
1935    fn pumpfun_buy_family_filter_matches_both_buy_variants() {
1936        let buy = DexEvent::PumpFunBuy(PumpFunTradeEvent {
1937            metadata: EventMetadata::default(),
1938            is_buy: true,
1939            ix_name: "buy_exact_quote_in_v2".to_string(),
1940            ..Default::default()
1941        });
1942        let exact_sol = DexEvent::PumpFunBuyExactSolIn(PumpFunTradeEvent {
1943            metadata: EventMetadata::default(),
1944            is_buy: true,
1945            ix_name: "buy_exact_sol_in".to_string(),
1946            ..Default::default()
1947        });
1948
1949        let buy_filter = EventTypeFilter::include_only(vec![EventType::PumpFunBuy]);
1950        assert!(filter_pumpfun_trade_variant(buy.clone(), Some(&buy_filter)).is_some());
1951        assert!(filter_pumpfun_trade_variant(exact_sol.clone(), Some(&buy_filter)).is_some());
1952
1953        let exact_filter = EventTypeFilter::include_only(vec![EventType::PumpFunBuyExactSolIn]);
1954        assert!(filter_pumpfun_trade_variant(buy, Some(&exact_filter)).is_some());
1955        assert!(filter_pumpfun_trade_variant(exact_sol, Some(&exact_filter)).is_some());
1956    }
1957
1958    #[test]
1959    fn discriminator_prefix_filter_handles_program_scoped_collisions() {
1960        let dlmm_filter = EventTypeFilter::include_only(vec![EventType::MeteoraDlmmSwap]);
1961        assert!(filter_allows_discriminator(
1962            Some(&program_ids::METEORA_DLMM_PROGRAM_ID),
1963            discriminators::METEORA_DLMM_SWAP,
1964            Some(&dlmm_filter),
1965        ));
1966        assert!(!filter_allows_discriminator(
1967            Some(&program_ids::RAYDIUM_CPMM_PROGRAM_ID),
1968            discriminators::METEORA_DLMM_SWAP,
1969            Some(&dlmm_filter),
1970        ));
1971
1972        let raydium_launchlab_filter =
1973            EventTypeFilter::include_only(vec![EventType::RaydiumLaunchlabTrade]);
1974        assert!(filter_allows_discriminator(
1975            Some(&program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID),
1976            discriminators::RAYDIUM_LAUNCHLAB_TRADE,
1977            Some(&raydium_launchlab_filter),
1978        ));
1979        assert!(!filter_allows_discriminator(
1980            Some(&program_ids::PUMPFUN_PROGRAM_ID),
1981            discriminators::PUMPFUN_TRADE,
1982            Some(&raydium_launchlab_filter),
1983        ));
1984
1985        let pumpfun_buy_filter = EventTypeFilter::include_only(vec![EventType::PumpFunBuy]);
1986        assert!(filter_allows_discriminator(
1987            Some(&program_ids::PUMPFUN_PROGRAM_ID),
1988            discriminators::PUMPFUN_TRADE,
1989            Some(&pumpfun_buy_filter),
1990        ));
1991
1992        let dbc_filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
1993        assert!(filter_allows_discriminator(
1994            Some(&program_ids::METEORA_DBC_PROGRAM_ID),
1995            discriminators::METEORA_DBC_SWAP,
1996            Some(&dbc_filter),
1997        ));
1998        assert!(!filter_allows_discriminator(
1999            Some(&program_ids::METEORA_DAMM_V2_PROGRAM_ID),
2000            discriminators::METEORA_DAMM_SWAP,
2001            Some(&dbc_filter),
2002        ));
2003        assert!(!filter_allows_discriminator(
2004            Some(&program_ids::METEORA_DBC_PROGRAM_ID),
2005            discriminators::METEORA_DLMM_CLOSE_POSITION,
2006            Some(&raydium_launchlab_filter),
2007        ));
2008    }
2009
2010    #[test]
2011    fn program_scoped_pumpfun_buy_filter_parses_trade_log_variant() {
2012        let log = "Program data: vdt/007mYe5StuUGXKtQJzSLsEK5h79gIdGUQz7vyn59ApMQyeYlr3cK4wUAAAAA7dnMPhkDAAAB5uPeR/hOJigYiGhz2PiTzeNML3vtbrwijyhrJHoTgitivC1qAAAAALjcux8HAAAAt2E0T9e8AwC4MJgjAAAAALfJIQNGvgIA4ATIfOuY+lzkf4A4Bv0seUXSlSSVmuwA3tl4FPOPeEZfAAAAAAAAACBRDgAAAAAAbf5L76S20PsQ+d4EfYrWKDprZOVyf9lJPbA04mYiiiweAAAAAAAAAGmFBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAGJ1eQAAAAAAAAAAAAAAAAAAAAAAiBMAAAAAAACQKAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHcK4wUAAAAAuNy7HwcAAAC4MJgjAAAAAA==";
2013        let filter = EventTypeFilter::include_only(vec![EventType::PumpFunBuy]);
2014        let event = parse_log_optimized_with_program_id(
2015            log,
2016            Signature::default(),
2017            426270756,
2018            268,
2019            Some(1781382243600841),
2020            1781382243601307,
2021            Some(&filter),
2022            false,
2023            None,
2024            Some(&program_ids::PUMPFUN_PROGRAM_ID),
2025        )
2026        .expect("PumpFun TradeEvent log should parse under PumpFunBuy filter");
2027
2028        match event {
2029            DexEvent::PumpFunBuy(trade) => {
2030                assert_eq!(trade.ix_name, "buy");
2031                assert_eq!(trade.sol_amount, 98_765_431);
2032                assert_eq!(trade.token_amount, 3_406_962_678_253);
2033                assert_eq!(trade.virtual_sol_reserves, 30_597_176_504);
2034                assert_eq!(trade.virtual_token_reserves, 1_052_057_862_955_447);
2035                assert_eq!(trade.real_sol_reserves, 597_176_504);
2036                assert_eq!(trade.real_token_reserves, 772_157_862_955_447);
2037            }
2038            other => panic!("expected PumpFunBuy, got {other:?}"),
2039        }
2040    }
2041
2042    #[test]
2043    fn unscoped_pumpfun_buy_filter_parses_trade_log_variant() {
2044        let log = "Program data: vdt/007mYe5StuUGXKtQJzSLsEK5h79gIdGUQz7vyn59ApMQyeYlr3cK4wUAAAAA7dnMPhkDAAAB5uPeR/hOJigYiGhz2PiTzeNML3vtbrwijyhrJHoTgitivC1qAAAAALjcux8HAAAAt2E0T9e8AwC4MJgjAAAAALfJIQNGvgIA4ATIfOuY+lzkf4A4Bv0seUXSlSSVmuwA3tl4FPOPeEZfAAAAAAAAACBRDgAAAAAAbf5L76S20PsQ+d4EfYrWKDprZOVyf9lJPbA04mYiiiweAAAAAAAAAGmFBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAGJ1eQAAAAAAAAAAAAAAAAAAAAAAiBMAAAAAAACQKAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHcK4wUAAAAAuNy7HwcAAAC4MJgjAAAAAA==";
2045        let filter = EventTypeFilter::include_only(vec![EventType::PumpFunBuy]);
2046        let event = parse_log_optimized(
2047            log,
2048            Signature::default(),
2049            426270756,
2050            268,
2051            Some(1781382243600841),
2052            1781382243601307,
2053            Some(&filter),
2054            false,
2055            None,
2056        )
2057        .expect("Unscoped PumpFun TradeEvent log should parse under PumpFunBuy filter");
2058
2059        match event {
2060            DexEvent::PumpFunBuy(trade) => {
2061                assert_eq!(trade.ix_name, "buy");
2062                assert_eq!(trade.sol_amount, 98_765_431);
2063                assert_eq!(trade.token_amount, 3_406_962_678_253);
2064                assert_eq!(trade.virtual_sol_reserves, 30_597_176_504);
2065                assert_eq!(trade.virtual_token_reserves, 1_052_057_862_955_447);
2066                assert_eq!(trade.real_sol_reserves, 597_176_504);
2067                assert_eq!(trade.real_token_reserves, 772_157_862_955_447);
2068            }
2069            other => panic!("expected PumpFunBuy, got {other:?}"),
2070        }
2071    }
2072
2073    #[test]
2074    fn discriminator_prefix_filter_keeps_unscoped_collision_candidates() {
2075        let dlmm_filter = EventTypeFilter::include_only(vec![EventType::MeteoraDlmmSwap]);
2076        assert!(filter_allows_discriminator(
2077            None,
2078            discriminators::METEORA_DLMM_SWAP2,
2079            Some(&dlmm_filter),
2080        ));
2081
2082        let cpmm_filter = EventTypeFilter::include_only(vec![EventType::RaydiumCpmmInitialize]);
2083        assert!(filter_allows_discriminator(
2084            None,
2085            discriminators::RAYDIUM_CPMM_CREATE_POOL,
2086            Some(&cpmm_filter),
2087        ));
2088
2089        let pumpfun_buy_filter = EventTypeFilter::include_only(vec![EventType::PumpFunBuy]);
2090        assert!(filter_allows_discriminator(
2091            None,
2092            discriminators::PUMPFUN_TRADE,
2093            Some(&pumpfun_buy_filter),
2094        ));
2095    }
2096
2097    #[test]
2098    fn unscoped_collision_does_not_emit_wrong_protocol_event_after_filter() {
2099        let mut raw = Vec::new();
2100        raw.extend_from_slice(&discriminators::METEORA_DLMM_SWAP.to_le_bytes());
2101        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // CPMM pool_state shape
2102        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // CPMM user shape
2103        raw.extend_from_slice(&1u64.to_le_bytes());
2104        raw.extend_from_slice(&2u64.to_le_bytes());
2105        raw.extend_from_slice(&3u64.to_le_bytes());
2106        raw.push(1);
2107
2108        let log = format!("Program data: {}", STANDARD.encode(raw));
2109        let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDlmmSwap]);
2110        assert!(parse_log_optimized(
2111            &log,
2112            Signature::default(),
2113            1,
2114            2,
2115            Some(3),
2116            4,
2117            Some(&filter),
2118            false,
2119            None,
2120        )
2121        .is_none());
2122    }
2123
2124    #[test]
2125    fn unscoped_pumpfun_launchlab_collision_does_not_emit_wrong_protocol_event() {
2126        let mut raw = Vec::new();
2127        raw.extend_from_slice(&discriminators::PUMPFUN_TRADE.to_le_bytes());
2128        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // mint
2129        raw.extend_from_slice(&1u64.to_le_bytes()); // sol_amount
2130        raw.extend_from_slice(&2u64.to_le_bytes()); // token_amount
2131        raw.push(1); // is_buy
2132        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // user
2133        raw.extend_from_slice(&3i64.to_le_bytes()); // timestamp
2134        for value in 4u64..=9 {
2135            raw.extend_from_slice(&value.to_le_bytes());
2136        }
2137        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // fee_recipient
2138        raw.extend_from_slice(&10u64.to_le_bytes()); // fee_basis_points
2139        raw.extend_from_slice(&11u64.to_le_bytes()); // fee
2140        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // creator
2141        raw.extend_from_slice(&12u64.to_le_bytes()); // creator_fee_basis_points
2142        raw.extend_from_slice(&13u64.to_le_bytes()); // creator_fee
2143
2144        let log = format!("Program data: {}", STANDARD.encode(raw));
2145        let filter = EventTypeFilter::include_only(vec![EventType::RaydiumLaunchlabTrade]);
2146        assert!(parse_log_optimized(
2147            &log,
2148            Signature::default(),
2149            1,
2150            2,
2151            Some(3),
2152            4,
2153            Some(&filter),
2154            false,
2155            None,
2156        )
2157        .is_none());
2158    }
2159
2160    #[test]
2161    fn discriminator_prefix_decode_reads_first_event_bytes() {
2162        let mut raw = Vec::new();
2163        raw.extend_from_slice(&discriminators::PUMP_FEES_UPDATE_ADMIN.to_le_bytes());
2164        raw.extend_from_slice(Pubkey::new_unique().as_ref());
2165        raw.extend_from_slice(Pubkey::new_unique().as_ref());
2166
2167        let encoded = STANDARD.encode(raw);
2168        assert_eq!(
2169            decode_base64_discriminator(&encoded),
2170            Some(discriminators::PUMP_FEES_UPDATE_ADMIN)
2171        );
2172    }
2173
2174    #[test]
2175    fn program_scoped_damm_add_liquidity_parses_from_decoded_data() {
2176        let pool = Pubkey::new_unique();
2177        let position = Pubkey::new_unique();
2178        let owner = Pubkey::new_unique();
2179        let mut raw = Vec::new();
2180        raw.extend_from_slice(&discriminators::METEORA_DAMM_ADD_LIQUIDITY.to_le_bytes());
2181        raw.extend_from_slice(pool.as_ref());
2182        raw.extend_from_slice(position.as_ref());
2183        raw.extend_from_slice(owner.as_ref());
2184        raw.extend_from_slice(&123u128.to_le_bytes());
2185        raw.extend_from_slice(&10u64.to_le_bytes());
2186        raw.extend_from_slice(&20u64.to_le_bytes());
2187        raw.extend_from_slice(&30u64.to_le_bytes());
2188        raw.extend_from_slice(&40u64.to_le_bytes());
2189        raw.extend_from_slice(&50u64.to_le_bytes());
2190        raw.extend_from_slice(&60u64.to_le_bytes());
2191
2192        let log = format!("Program data: {}", STANDARD.encode(raw));
2193        let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDammV2AddLiquidity]);
2194        let event = parse_log_optimized_with_program_id(
2195            &log,
2196            Signature::default(),
2197            1,
2198            2,
2199            Some(3),
2200            4,
2201            Some(&filter),
2202            false,
2203            None,
2204            Some(&program_ids::METEORA_DAMM_V2_PROGRAM_ID),
2205        )
2206        .expect("DAMM V2 add-liquidity should parse");
2207
2208        match event {
2209            DexEvent::MeteoraDammV2AddLiquidity(event) => {
2210                assert_eq!(event.pool, pool);
2211                assert_eq!(event.position, position);
2212                assert_eq!(event.owner, owner);
2213                assert_eq!(event.liquidity_delta, 123);
2214                assert_eq!(event.token_a_amount, 30);
2215                assert_eq!(event.token_b_amount, 40);
2216                assert_eq!(event.total_amount_a, 50);
2217                assert_eq!(event.total_amount_b, 60);
2218            }
2219            other => panic!("expected MeteoraDammV2AddLiquidity, got {other:?}"),
2220        }
2221    }
2222
2223    #[test]
2224    fn program_scoped_dbc_swap_parses_and_does_not_emit_as_damm() {
2225        let pool = Pubkey::new_unique();
2226        let config = Pubkey::new_unique();
2227        let mut raw = Vec::new();
2228        raw.extend_from_slice(&discriminators::METEORA_DBC_SWAP.to_le_bytes());
2229        raw.extend_from_slice(pool.as_ref());
2230        raw.extend_from_slice(config.as_ref());
2231        raw.push(1);
2232        raw.push(0);
2233        raw.extend_from_slice(&100u64.to_le_bytes());
2234        raw.extend_from_slice(&90u64.to_le_bytes());
2235        raw.extend_from_slice(&100u64.to_le_bytes());
2236        raw.extend_from_slice(&95u64.to_le_bytes());
2237        raw.extend_from_slice(&(1u128 << 64).to_le_bytes());
2238        raw.extend_from_slice(&2u64.to_le_bytes());
2239        raw.extend_from_slice(&3u64.to_le_bytes());
2240        raw.extend_from_slice(&0u64.to_le_bytes());
2241        raw.extend_from_slice(&100u64.to_le_bytes());
2242        raw.extend_from_slice(&1_777_920_719u64.to_le_bytes());
2243
2244        let log = format!("Program data: {}", STANDARD.encode(raw));
2245        let dbc_filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
2246        let event = parse_log_optimized_with_program_id(
2247            &log,
2248            Signature::default(),
2249            1,
2250            2,
2251            Some(3),
2252            4,
2253            Some(&dbc_filter),
2254            false,
2255            None,
2256            Some(&program_ids::METEORA_DBC_PROGRAM_ID),
2257        )
2258        .expect("DBC swap should parse with DBC program context");
2259
2260        match event {
2261            DexEvent::MeteoraDbcSwap(event) => {
2262                assert_eq!(event.pool, pool);
2263                assert_eq!(event.config, config);
2264                assert_eq!(event.amount_in, 100);
2265                assert_eq!(event.output_amount, 95);
2266            }
2267            other => panic!("expected MeteoraDbcSwap, got {other:?}"),
2268        }
2269
2270        let damm_filter = EventTypeFilter::include_only(vec![EventType::MeteoraDammV2Swap]);
2271        assert!(parse_log_optimized_with_program_id(
2272            &log,
2273            Signature::default(),
2274            1,
2275            2,
2276            Some(3),
2277            4,
2278            Some(&damm_filter),
2279            false,
2280            None,
2281            Some(&program_ids::METEORA_DBC_PROGRAM_ID),
2282        )
2283        .is_none());
2284    }
2285
2286    #[test]
2287    fn large_program_data_uses_heap_fallback_without_dropping_event() {
2288        let mut raw = Vec::new();
2289        raw.extend_from_slice(&discriminators::PUMP_FEES_CREATE_FEE_SHARING_CONFIG.to_le_bytes());
2290        raw.extend_from_slice(&1_777_920_719i64.to_le_bytes());
2291        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // mint
2292        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // bonding_curve
2293        raw.push(0); // pool: None
2294        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // sharing_config
2295        raw.extend_from_slice(Pubkey::new_unique().as_ref()); // admin
2296        raw.extend_from_slice(&64u32.to_le_bytes());
2297        for i in 0..64u16 {
2298            raw.extend_from_slice(Pubkey::new_unique().as_ref());
2299            raw.extend_from_slice(&i.to_le_bytes());
2300        }
2301        raw.push(1); // PumpFeesConfigStatus::Active
2302
2303        let encoded = STANDARD.encode(&raw);
2304        assert!(encoded.len() > 2700, "test must exceed the old fixed stack buffer limit");
2305        let log = format!("Program data: {encoded}");
2306
2307        let event = parse_log_optimized_with_program_id(
2308            &log,
2309            Signature::default(),
2310            1,
2311            2,
2312            Some(3),
2313            4,
2314            None,
2315            false,
2316            None,
2317            Some(&program_ids::PUMP_FEES_PROGRAM_ID),
2318        )
2319        .expect("large pump-fees event should parse via heap fallback");
2320
2321        match event {
2322            DexEvent::PumpFeesCreateFeeSharingConfig(event) => {
2323                assert_eq!(event.initial_shareholders.len(), 64);
2324                assert_eq!(event.status, crate::core::events::PumpFeesConfigStatus::Active);
2325            }
2326            other => panic!("expected PumpFeesCreateFeeSharingConfig, got {other:?}"),
2327        }
2328    }
2329
2330    #[test]
2331    fn completion_parser_extracts_program_id() {
2332        assert_eq!(
2333            parse_program_complete_info(
2334                "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success"
2335            ),
2336            Some("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj")
2337        );
2338        assert_eq!(
2339            parse_program_complete_info(
2340                "Program CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C failed: custom program error: 0x1"
2341            ),
2342            Some("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C")
2343        );
2344    }
2345}