1pub mod meteora_amm;
6pub mod meteora_damm;
7pub mod meteora_dlmm;
8pub mod orca_whirlpool;
9pub mod program_ids;
10pub mod pump;
11pub mod pump_amm;
12pub mod pump_fees;
13pub mod raydium_amm;
14pub mod raydium_clmm;
15pub mod raydium_cpmm;
16pub mod raydium_launchlab;
17pub mod utils;
18
19pub mod all_inner;
21pub mod inner_common; pub mod pump_amm_inner; pub mod pump_inner; pub mod raydium_clmm_inner; use crate::grpc::types::EventTypeFilter;
26pub use meteora_damm::parse_instruction as parse_meteora_damm_instruction;
28pub use pump::parse_instruction as parse_pumpfun_instruction;
29pub use pump_amm::parse_instruction as parse_pumpswap_instruction;
30pub use raydium_launchlab::parse_instruction as parse_raydium_launchlab_instruction;
31
32pub use utils::*;
34
35use crate::core::events::DexEvent;
36use program_ids::*;
37use solana_sdk::{pubkey::Pubkey, signature::Signature};
38
39#[inline(always)]
40fn disc8(instruction_data: &[u8]) -> Option<[u8; 8]> {
41 instruction_data.get(..8)?.try_into().ok()
42}
43
44#[inline(always)]
45fn supports_pumpfun_instruction(disc: [u8; 8]) -> bool {
46 matches!(
47 disc,
48 pump::discriminators::CREATE
49 | pump::discriminators::CREATE_V2
50 | pump::discriminators::BUY
51 | pump::discriminators::SELL
52 | pump::discriminators::BUY_EXACT_SOL_IN
53 | pump::discriminators::BUY_V2
54 | pump::discriminators::BUY_EXACT_QUOTE_IN_V2
55 | pump::discriminators::SELL_V2
56 )
57}
58
59#[inline(always)]
60fn supports_pumpswap_instruction(disc: [u8; 8]) -> bool {
61 matches!(
62 disc,
63 pump_amm::discriminators::BUY
64 | pump_amm::discriminators::SELL
65 | pump_amm::discriminators::CREATE_POOL
66 | pump_amm::discriminators::BUY_EXACT_QUOTE_IN
67 | pump_amm::discriminators::DEPOSIT
68 | pump_amm::discriminators::WITHDRAW
69 )
70}
71
72#[inline(always)]
73fn supports_pump_fees_instruction(disc: [u8; 8]) -> bool {
74 matches!(
75 disc,
76 pump_fees::CREATE_FEE_SHARING_IX
77 | pump_fees::INITIALIZE_FEE_CONFIG_IX
78 | pump_fees::RESET_FEE_SHARING_IX
79 | pump_fees::RESET_FEE_SHARING_V2_IX
80 | pump_fees::REVOKE_FEE_SHARING_IX
81 | pump_fees::TRANSFER_FEE_SHARING_IX
82 | pump_fees::UPDATE_ADMIN_IX
83 | pump_fees::UPDATE_FEE_CONFIG_IX
84 | pump_fees::UPDATE_FEE_SHARES_IX
85 | pump_fees::UPDATE_FEE_SHARES_V2_IX
86 | pump_fees::UPSERT_FEE_TIERS_IX
87 )
88}
89
90#[inline(always)]
91fn supports_launchlab_instruction(disc: [u8; 8]) -> bool {
92 matches!(
93 disc,
94 raydium_launchlab::discriminators::BUY_EXACT_IN
95 | raydium_launchlab::discriminators::BUY_EXACT_OUT
96 | raydium_launchlab::discriminators::SELL_EXACT_IN
97 | raydium_launchlab::discriminators::SELL_EXACT_OUT
98 | raydium_launchlab::discriminators::INITIALIZE
99 | raydium_launchlab::discriminators::INITIALIZE_V2
100 | raydium_launchlab::discriminators::INITIALIZE_WITH_TOKEN_2022
101 )
102}
103
104#[inline(always)]
105fn supports_cpmm_instruction(disc: [u8; 8]) -> bool {
106 matches!(
107 disc,
108 raydium_cpmm::discriminators::SWAP_BASE_IN
109 | raydium_cpmm::discriminators::SWAP_BASE_OUT
110 | raydium_cpmm::discriminators::INITIALIZE
111 | raydium_cpmm::discriminators::DEPOSIT
112 | raydium_cpmm::discriminators::WITHDRAW
113 )
114}
115
116#[inline(always)]
117fn supports_clmm_instruction(disc: [u8; 8]) -> bool {
118 matches!(
119 disc,
120 raydium_clmm::discriminators::SWAP
121 | raydium_clmm::discriminators::SWAP_V2
122 | raydium_clmm::discriminators::INCREASE_LIQUIDITY_V2
123 | raydium_clmm::discriminators::DECREASE_LIQUIDITY_V2
124 | raydium_clmm::discriminators::CREATE_POOL
125 | raydium_clmm::discriminators::CREATE_CUSTOMIZABLE_POOL
126 | raydium_clmm::discriminators::OPEN_POSITION
127 | raydium_clmm::discriminators::OPEN_POSITION_V2
128 | raydium_clmm::discriminators::OPEN_POSITION_WITH_TOKEN_22_NFT
129 | raydium_clmm::discriminators::CLOSE_POSITION
130 )
131}
132
133#[inline(always)]
134fn supports_raydium_amm_v4_instruction(instruction_data: &[u8]) -> bool {
135 matches!(
136 instruction_data.first().copied(),
137 Some(raydium_amm::discriminators::SWAP_BASE_IN)
138 | Some(raydium_amm::discriminators::SWAP_BASE_OUT)
139 | Some(raydium_amm::discriminators::DEPOSIT)
140 | Some(raydium_amm::discriminators::WITHDRAW)
141 | Some(raydium_amm::discriminators::INITIALIZE2)
142 | Some(raydium_amm::discriminators::WITHDRAW_PNL)
143 )
144}
145
146#[inline(always)]
147fn supports_orca_instruction(disc: [u8; 8]) -> bool {
148 matches!(
149 disc,
150 orca_whirlpool::discriminators::SWAP
151 | orca_whirlpool::discriminators::SWAP_V2
152 | orca_whirlpool::discriminators::INCREASE_LIQUIDITY
153 | orca_whirlpool::discriminators::DECREASE_LIQUIDITY
154 | orca_whirlpool::discriminators::INITIALIZE_POOL
155 )
156}
157
158#[inline(always)]
159fn supports_meteora_pools_instruction(disc: [u8; 8]) -> bool {
160 matches!(
161 disc,
162 meteora_amm::discriminators::SWAP
163 | meteora_amm::discriminators::ADD_LIQUIDITY
164 | meteora_amm::discriminators::REMOVE_LIQUIDITY
165 | meteora_amm::discriminators::CREATE_POOL
166 )
167}
168
169#[inline(always)]
170fn supports_meteora_damm_v2_instruction(instruction_data: &[u8]) -> bool {
171 let Some(disc) = disc8(instruction_data) else {
172 return false;
173 };
174 if disc == meteora_damm::discriminators::INITIALIZE_POOL {
175 return true;
176 }
177 let Some(cpi_disc) = instruction_data.get(8..16).and_then(|bytes| bytes.try_into().ok()) else {
178 return false;
179 };
180 matches!(
181 cpi_disc,
182 meteora_damm::discriminators::SWAP_LOG
183 | meteora_damm::discriminators::SWAP2_LOG
184 | meteora_damm::discriminators::CREATE_POSITION_LOG
185 | meteora_damm::discriminators::CLOSE_POSITION_LOG
186 | meteora_damm::discriminators::ADD_LIQUIDITY_LOG
187 | meteora_damm::discriminators::REMOVE_LIQUIDITY_LOG
188 )
189}
190
191#[inline(always)]
192fn supports_meteora_dlmm_instruction(instruction_data: &[u8]) -> bool {
193 matches!(instruction_data.first().copied(), Some(0 | 1 | 2 | 7 | 8 | 11 | 13 | 14))
194}
195
196#[inline(always)]
197pub(crate) fn instruction_data_may_parse(program_id: &Pubkey, instruction_data: &[u8]) -> bool {
198 if instruction_data.is_empty() {
199 return false;
200 }
201 if *program_id == RAYDIUM_AMM_V4_PROGRAM_ID {
202 return supports_raydium_amm_v4_instruction(instruction_data);
203 }
204 if *program_id == METEORA_DLMM_PROGRAM_ID {
205 return supports_meteora_dlmm_instruction(instruction_data);
206 }
207 if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
208 return supports_meteora_damm_v2_instruction(instruction_data);
209 }
210
211 let Some(disc) = disc8(instruction_data) else {
212 return false;
213 };
214 if *program_id == PUMPFUN_PROGRAM_ID {
215 supports_pumpfun_instruction(disc)
216 } else if *program_id == PUMPSWAP_PROGRAM_ID {
217 supports_pumpswap_instruction(disc)
218 } else if *program_id == PUMP_FEES_PROGRAM_ID {
219 supports_pump_fees_instruction(disc)
220 } else if *program_id == RAYDIUM_LAUNCHLAB_PROGRAM_ID {
221 supports_launchlab_instruction(disc)
222 } else if *program_id == RAYDIUM_CPMM_PROGRAM_ID {
223 supports_cpmm_instruction(disc)
224 } else if *program_id == RAYDIUM_CLMM_PROGRAM_ID {
225 supports_clmm_instruction(disc)
226 } else if *program_id == ORCA_WHIRLPOOL_PROGRAM_ID {
227 supports_orca_instruction(disc)
228 } else if *program_id == METEORA_POOLS_PROGRAM_ID {
229 supports_meteora_pools_instruction(disc)
230 } else {
231 false
232 }
233}
234
235#[inline(always)]
236pub(crate) fn normal_instruction_data_may_parse(
237 program_id: &Pubkey,
238 instruction_data: &[u8],
239) -> bool {
240 if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
241 return disc8(instruction_data)
242 .is_some_and(|disc| disc == meteora_damm::discriminators::INITIALIZE_POOL);
243 }
244 instruction_data_may_parse(program_id, instruction_data)
245}
246
247#[inline(always)]
248fn filter_parsed_event(
249 event: Option<DexEvent>,
250 event_type_filter: Option<&EventTypeFilter>,
251) -> Option<DexEvent> {
252 let event = event?;
253 if event_type_filter.map(|f| f.should_include_dex_event(&event)).unwrap_or(true) {
254 Some(event)
255 } else {
256 None
257 }
258}
259
260#[inline]
262pub fn parse_instruction_unified(
263 instruction_data: &[u8],
264 accounts: &[Pubkey],
265 signature: Signature,
266 slot: u64,
267 tx_index: u64,
268 block_time_us: Option<i64>,
269 grpc_recv_us: i64,
270 event_type_filter: Option<&EventTypeFilter>,
271 program_id: &Pubkey,
272) -> Option<DexEvent> {
273 if instruction_data.is_empty() {
275 return None;
276 }
277
278 if *program_id == PUMPFUN_PROGRAM_ID {
282 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pumpfun() {
283 return None;
284 }
285 return filter_parsed_event(
286 parse_pumpfun_instruction(
287 instruction_data,
288 accounts,
289 signature,
290 slot,
291 tx_index,
292 block_time_us,
293 grpc_recv_us,
294 ),
295 event_type_filter,
296 );
297 }
298 else if *program_id == PUMPSWAP_PROGRAM_ID {
300 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pumpswap() {
301 return None;
302 }
303 return filter_parsed_event(
304 parse_pumpswap_instruction(
305 instruction_data,
306 accounts,
307 signature,
308 slot,
309 tx_index,
310 block_time_us,
311 ),
312 event_type_filter,
313 );
314 }
315 else if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
317 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_damm_v2() {
318 return None;
319 }
320 return filter_parsed_event(
321 parse_meteora_damm_instruction(
322 instruction_data,
323 accounts,
324 signature,
325 slot,
326 tx_index,
327 block_time_us,
328 grpc_recv_us,
329 ),
330 event_type_filter,
331 );
332 }
333 else if *program_id == PUMP_FEES_PROGRAM_ID {
335 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pump_fees() {
336 return None;
337 }
338 return filter_parsed_event(
339 crate::instr::pump_fees::parse_instruction(
340 instruction_data,
341 accounts,
342 signature,
343 slot,
344 tx_index,
345 block_time_us,
346 grpc_recv_us,
347 ),
348 event_type_filter,
349 );
350 }
351 else if *program_id == RAYDIUM_LAUNCHLAB_PROGRAM_ID {
353 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_launchlab() {
354 return None;
355 }
356 return filter_parsed_event(
357 parse_raydium_launchlab_instruction(
358 instruction_data,
359 accounts,
360 signature,
361 slot,
362 tx_index,
363 block_time_us,
364 ),
365 event_type_filter,
366 );
367 }
368 else if *program_id == RAYDIUM_CPMM_PROGRAM_ID {
370 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_cpmm() {
371 return None;
372 }
373 return filter_parsed_event(
374 crate::instr::raydium_cpmm::parse_instruction(
375 instruction_data,
376 accounts,
377 signature,
378 slot,
379 tx_index,
380 block_time_us,
381 ),
382 event_type_filter,
383 );
384 }
385 else if *program_id == RAYDIUM_CLMM_PROGRAM_ID {
387 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_clmm() {
388 return None;
389 }
390 return filter_parsed_event(
391 crate::instr::raydium_clmm::parse_instruction(
392 instruction_data,
393 accounts,
394 signature,
395 slot,
396 tx_index,
397 block_time_us,
398 ),
399 event_type_filter,
400 );
401 }
402 else if *program_id == RAYDIUM_AMM_V4_PROGRAM_ID {
404 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_amm_v4() {
405 return None;
406 }
407 return filter_parsed_event(
408 crate::instr::raydium_amm::parse_instruction(
409 instruction_data,
410 accounts,
411 signature,
412 slot,
413 tx_index,
414 block_time_us,
415 ),
416 event_type_filter,
417 );
418 }
419 else if *program_id == ORCA_WHIRLPOOL_PROGRAM_ID {
421 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_orca_whirlpool() {
422 return None;
423 }
424 return filter_parsed_event(
425 crate::instr::orca_whirlpool::parse_instruction(
426 instruction_data,
427 accounts,
428 signature,
429 slot,
430 tx_index,
431 block_time_us,
432 ),
433 event_type_filter,
434 );
435 }
436 else if *program_id == METEORA_POOLS_PROGRAM_ID {
438 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_pools() {
439 return None;
440 }
441 return filter_parsed_event(
442 crate::instr::meteora_amm::parse_instruction(
443 instruction_data,
444 accounts,
445 signature,
446 slot,
447 tx_index,
448 block_time_us,
449 ),
450 event_type_filter,
451 );
452 }
453 else if *program_id == METEORA_DLMM_PROGRAM_ID {
455 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_dlmm() {
456 return None;
457 }
458 return filter_parsed_event(
459 crate::instr::meteora_dlmm::parse_instruction(
460 instruction_data,
461 accounts,
462 signature,
463 slot,
464 tx_index,
465 block_time_us,
466 ),
467 event_type_filter,
468 );
469 }
470
471 None
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 fn data8(disc: [u8; 8]) -> Vec<u8> {
479 let mut data = Vec::from(disc);
480 data.extend_from_slice(&1u64.to_le_bytes());
481 data.extend_from_slice(&2u64.to_le_bytes());
482 data
483 }
484
485 #[test]
486 fn instruction_data_gate_covers_supported_normal_instruction_protocols() {
487 assert!(instruction_data_may_parse(
488 &PUMPSWAP_PROGRAM_ID,
489 &data8(pump_amm::discriminators::CREATE_POOL)
490 ));
491 assert!(instruction_data_may_parse(
492 &PUMP_FEES_PROGRAM_ID,
493 &data8(pump_fees::UPDATE_FEE_SHARES_IX)
494 ));
495 assert!(instruction_data_may_parse(
496 &RAYDIUM_LAUNCHLAB_PROGRAM_ID,
497 &data8(raydium_launchlab::discriminators::BUY_EXACT_IN)
498 ));
499 assert!(instruction_data_may_parse(
500 &RAYDIUM_CPMM_PROGRAM_ID,
501 &data8(raydium_cpmm::discriminators::SWAP_BASE_IN)
502 ));
503 assert!(instruction_data_may_parse(
504 &RAYDIUM_CLMM_PROGRAM_ID,
505 &data8(raydium_clmm::discriminators::SWAP_V2)
506 ));
507 assert!(instruction_data_may_parse(
508 &RAYDIUM_AMM_V4_PROGRAM_ID,
509 &[raydium_amm::discriminators::SWAP_BASE_IN, 1, 0, 0, 0, 0, 0, 0, 0]
510 ));
511 assert!(instruction_data_may_parse(
512 &ORCA_WHIRLPOOL_PROGRAM_ID,
513 &data8(orca_whirlpool::discriminators::SWAP)
514 ));
515 assert!(instruction_data_may_parse(
516 &METEORA_POOLS_PROGRAM_ID,
517 &data8(meteora_amm::discriminators::CREATE_POOL)
518 ));
519 assert!(instruction_data_may_parse(
520 &METEORA_DAMM_V2_PROGRAM_ID,
521 &data8(meteora_damm::discriminators::INITIALIZE_POOL)
522 ));
523 assert!(instruction_data_may_parse(&METEORA_DLMM_PROGRAM_ID, &[11, 1, 2, 3]));
524 }
525
526 #[test]
527 fn instruction_data_gate_rejects_unknown_program_and_event_cpi_layouts() {
528 assert!(!instruction_data_may_parse(&Pubkey::new_unique(), &data8([1; 8])));
529 assert!(!instruction_data_may_parse(&PUMPSWAP_PROGRAM_ID, &data8([0xff; 8])));
530 assert!(!instruction_data_may_parse(
531 &PUMPFUN_PROGRAM_ID,
532 &data8(pump::discriminators::MIGRATE_BONDING_CURVE_CREATOR)
533 ));
534
535 let mut pumpswap_event_cpi = Vec::from(pump_amm_inner::discriminators::CREATE_POOL);
536 pumpswap_event_cpi.extend_from_slice(&[0; 64]);
537 assert!(!instruction_data_may_parse(&PUMPSWAP_PROGRAM_ID, &pumpswap_event_cpi));
538 }
539
540 #[test]
541 fn normal_instruction_gate_keeps_meteora_damm_event_cpi_on_event_path() {
542 let mut event_cpi = Vec::new();
543 event_cpi.extend_from_slice(&[228, 69, 165, 46, 81, 203, 154, 29]);
544 event_cpi.extend_from_slice(&meteora_damm::discriminators::SWAP_LOG);
545 event_cpi.extend_from_slice(&[0; 64]);
546
547 assert!(instruction_data_may_parse(&METEORA_DAMM_V2_PROGRAM_ID, &event_cpi));
548 assert!(!normal_instruction_data_may_parse(&METEORA_DAMM_V2_PROGRAM_ID, &event_cpi));
549 assert!(normal_instruction_data_may_parse(
550 &METEORA_DAMM_V2_PROGRAM_ID,
551 &data8(meteora_damm::discriminators::INITIALIZE_POOL)
552 ));
553 }
554}