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