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