Skip to main content

sol_parser_sdk/logs/
optimized_matcher.rs

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