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 let Some(disc) = disc8(instruction_data) else {
194 return false;
195 };
196 matches!(
197 disc,
198 meteora_dlmm::discriminators::INITIALIZE_LB_PAIR
199 | meteora_dlmm::discriminators::INITIALIZE_LB_PAIR2
200 | meteora_dlmm::discriminators::INITIALIZE_BIN_ARRAY
201 | meteora_dlmm::discriminators::ADD_LIQUIDITY
202 | meteora_dlmm::discriminators::ADD_LIQUIDITY2
203 | meteora_dlmm::discriminators::REMOVE_LIQUIDITY
204 | meteora_dlmm::discriminators::REMOVE_LIQUIDITY2
205 | meteora_dlmm::discriminators::INITIALIZE_POSITION
206 | meteora_dlmm::discriminators::INITIALIZE_POSITION2
207 | meteora_dlmm::discriminators::INITIALIZE_POSITION_PDA
208 | meteora_dlmm::discriminators::SWAP
209 | meteora_dlmm::discriminators::SWAP2
210 | meteora_dlmm::discriminators::SWAP_EXACT_OUT
211 | meteora_dlmm::discriminators::SWAP_EXACT_OUT2
212 | meteora_dlmm::discriminators::SWAP_WITH_PRICE_IMPACT
213 | meteora_dlmm::discriminators::SWAP_WITH_PRICE_IMPACT2
214 | meteora_dlmm::discriminators::CLAIM_FEE
215 | meteora_dlmm::discriminators::CLAIM_FEE2
216 | meteora_dlmm::discriminators::CLOSE_POSITION
217 | meteora_dlmm::discriminators::CLOSE_POSITION2
218 )
219}
220
221#[inline(always)]
222pub(crate) fn instruction_data_may_parse(program_id: &Pubkey, instruction_data: &[u8]) -> bool {
223 if instruction_data.is_empty() {
224 return false;
225 }
226 if *program_id == RAYDIUM_AMM_V4_PROGRAM_ID {
227 return supports_raydium_amm_v4_instruction(instruction_data);
228 }
229 if *program_id == METEORA_DLMM_PROGRAM_ID {
230 return supports_meteora_dlmm_instruction(instruction_data);
231 }
232 if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
233 return supports_meteora_damm_v2_instruction(instruction_data);
234 }
235
236 let Some(disc) = disc8(instruction_data) else {
237 return false;
238 };
239 if *program_id == PUMPFUN_PROGRAM_ID {
240 supports_pumpfun_instruction(disc)
241 } else if *program_id == PUMPSWAP_PROGRAM_ID {
242 supports_pumpswap_instruction(disc)
243 } else if *program_id == PUMP_FEES_PROGRAM_ID {
244 supports_pump_fees_instruction(disc)
245 } else if *program_id == RAYDIUM_LAUNCHLAB_PROGRAM_ID {
246 supports_launchlab_instruction(disc)
247 } else if *program_id == RAYDIUM_CPMM_PROGRAM_ID {
248 supports_cpmm_instruction(disc)
249 } else if *program_id == RAYDIUM_CLMM_PROGRAM_ID {
250 supports_clmm_instruction(disc)
251 } else if *program_id == ORCA_WHIRLPOOL_PROGRAM_ID {
252 supports_orca_instruction(disc)
253 } else if *program_id == METEORA_POOLS_PROGRAM_ID {
254 supports_meteora_pools_instruction(disc)
255 } else {
256 false
257 }
258}
259
260#[inline(always)]
261pub(crate) fn normal_instruction_data_may_parse(
262 program_id: &Pubkey,
263 instruction_data: &[u8],
264) -> bool {
265 if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
266 return disc8(instruction_data)
267 .is_some_and(|disc| disc == meteora_damm::discriminators::INITIALIZE_POOL);
268 }
269 instruction_data_may_parse(program_id, instruction_data)
270}
271
272#[inline(always)]
273fn filter_parsed_event(
274 event: Option<DexEvent>,
275 event_type_filter: Option<&EventTypeFilter>,
276) -> Option<DexEvent> {
277 let event = event?;
278 if event_type_filter.map(|f| f.should_include_dex_event(&event)).unwrap_or(true) {
279 Some(event)
280 } else {
281 None
282 }
283}
284
285#[inline]
287pub fn parse_instruction_unified(
288 instruction_data: &[u8],
289 accounts: &[Pubkey],
290 signature: Signature,
291 slot: u64,
292 tx_index: u64,
293 block_time_us: Option<i64>,
294 grpc_recv_us: i64,
295 event_type_filter: Option<&EventTypeFilter>,
296 program_id: &Pubkey,
297) -> Option<DexEvent> {
298 if instruction_data.is_empty() {
300 return None;
301 }
302
303 if *program_id == PUMPFUN_PROGRAM_ID {
307 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pumpfun() {
308 return None;
309 }
310 return filter_parsed_event(
311 parse_pumpfun_instruction(
312 instruction_data,
313 accounts,
314 signature,
315 slot,
316 tx_index,
317 block_time_us,
318 grpc_recv_us,
319 ),
320 event_type_filter,
321 );
322 }
323 else if *program_id == PUMPSWAP_PROGRAM_ID {
325 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pumpswap() {
326 return None;
327 }
328 return filter_parsed_event(
329 parse_pumpswap_instruction(
330 instruction_data,
331 accounts,
332 signature,
333 slot,
334 tx_index,
335 block_time_us,
336 ),
337 event_type_filter,
338 );
339 }
340 else if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
342 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_damm_v2() {
343 return None;
344 }
345 return filter_parsed_event(
346 parse_meteora_damm_instruction(
347 instruction_data,
348 accounts,
349 signature,
350 slot,
351 tx_index,
352 block_time_us,
353 grpc_recv_us,
354 ),
355 event_type_filter,
356 );
357 }
358 else if *program_id == PUMP_FEES_PROGRAM_ID {
360 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pump_fees() {
361 return None;
362 }
363 return filter_parsed_event(
364 crate::instr::pump_fees::parse_instruction(
365 instruction_data,
366 accounts,
367 signature,
368 slot,
369 tx_index,
370 block_time_us,
371 grpc_recv_us,
372 ),
373 event_type_filter,
374 );
375 }
376 else if *program_id == RAYDIUM_LAUNCHLAB_PROGRAM_ID {
378 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_launchlab() {
379 return None;
380 }
381 return filter_parsed_event(
382 parse_raydium_launchlab_instruction(
383 instruction_data,
384 accounts,
385 signature,
386 slot,
387 tx_index,
388 block_time_us,
389 ),
390 event_type_filter,
391 );
392 }
393 else if *program_id == RAYDIUM_CPMM_PROGRAM_ID {
395 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_cpmm() {
396 return None;
397 }
398 return filter_parsed_event(
399 crate::instr::raydium_cpmm::parse_instruction(
400 instruction_data,
401 accounts,
402 signature,
403 slot,
404 tx_index,
405 block_time_us,
406 ),
407 event_type_filter,
408 );
409 }
410 else if *program_id == RAYDIUM_CLMM_PROGRAM_ID {
412 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_clmm() {
413 return None;
414 }
415 return filter_parsed_event(
416 crate::instr::raydium_clmm::parse_instruction(
417 instruction_data,
418 accounts,
419 signature,
420 slot,
421 tx_index,
422 block_time_us,
423 ),
424 event_type_filter,
425 );
426 }
427 else if *program_id == RAYDIUM_AMM_V4_PROGRAM_ID {
429 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_amm_v4() {
430 return None;
431 }
432 return filter_parsed_event(
433 crate::instr::raydium_amm::parse_instruction(
434 instruction_data,
435 accounts,
436 signature,
437 slot,
438 tx_index,
439 block_time_us,
440 ),
441 event_type_filter,
442 );
443 }
444 else if *program_id == ORCA_WHIRLPOOL_PROGRAM_ID {
446 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_orca_whirlpool() {
447 return None;
448 }
449 return filter_parsed_event(
450 crate::instr::orca_whirlpool::parse_instruction(
451 instruction_data,
452 accounts,
453 signature,
454 slot,
455 tx_index,
456 block_time_us,
457 ),
458 event_type_filter,
459 );
460 }
461 else if *program_id == METEORA_POOLS_PROGRAM_ID {
463 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_pools() {
464 return None;
465 }
466 return filter_parsed_event(
467 crate::instr::meteora_amm::parse_instruction(
468 instruction_data,
469 accounts,
470 signature,
471 slot,
472 tx_index,
473 block_time_us,
474 ),
475 event_type_filter,
476 );
477 }
478 else if *program_id == METEORA_DLMM_PROGRAM_ID {
480 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_dlmm() {
481 return None;
482 }
483 return filter_parsed_event(
484 crate::instr::meteora_dlmm::parse_instruction(
485 instruction_data,
486 accounts,
487 signature,
488 slot,
489 tx_index,
490 block_time_us,
491 ),
492 event_type_filter,
493 );
494 }
495
496 None
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 fn data8(disc: [u8; 8]) -> Vec<u8> {
504 let mut data = Vec::from(disc);
505 data.extend_from_slice(&1u64.to_le_bytes());
506 data.extend_from_slice(&2u64.to_le_bytes());
507 data
508 }
509
510 #[test]
511 fn instruction_data_gate_covers_supported_normal_instruction_protocols() {
512 assert!(instruction_data_may_parse(
513 &PUMPSWAP_PROGRAM_ID,
514 &data8(pump_amm::discriminators::CREATE_POOL)
515 ));
516 assert!(instruction_data_may_parse(
517 &PUMP_FEES_PROGRAM_ID,
518 &data8(pump_fees::UPDATE_FEE_SHARES_IX)
519 ));
520 assert!(instruction_data_may_parse(
521 &RAYDIUM_LAUNCHLAB_PROGRAM_ID,
522 &data8(raydium_launchlab::discriminators::BUY_EXACT_IN)
523 ));
524 assert!(instruction_data_may_parse(
525 &RAYDIUM_CPMM_PROGRAM_ID,
526 &data8(raydium_cpmm::discriminators::SWAP_BASE_IN)
527 ));
528 assert!(instruction_data_may_parse(
529 &RAYDIUM_CLMM_PROGRAM_ID,
530 &data8(raydium_clmm::discriminators::SWAP_V2)
531 ));
532 assert!(instruction_data_may_parse(
533 &RAYDIUM_AMM_V4_PROGRAM_ID,
534 &[raydium_amm::discriminators::SWAP_BASE_IN, 1, 0, 0, 0, 0, 0, 0, 0]
535 ));
536 assert!(instruction_data_may_parse(
537 &ORCA_WHIRLPOOL_PROGRAM_ID,
538 &data8(orca_whirlpool::discriminators::SWAP)
539 ));
540 assert!(instruction_data_may_parse(
541 &METEORA_POOLS_PROGRAM_ID,
542 &data8(meteora_amm::discriminators::CREATE_POOL)
543 ));
544 assert!(instruction_data_may_parse(
545 &METEORA_DAMM_V2_PROGRAM_ID,
546 &data8(meteora_damm::discriminators::INITIALIZE_POOL)
547 ));
548 assert!(instruction_data_may_parse(
549 &METEORA_DLMM_PROGRAM_ID,
550 &data8(meteora_dlmm::discriminators::SWAP2)
551 ));
552 assert!(!instruction_data_may_parse(&METEORA_DLMM_PROGRAM_ID, &[11, 1, 2, 3]));
553 }
554
555 #[test]
556 fn instruction_data_gate_rejects_unknown_program_and_event_cpi_layouts() {
557 assert!(!instruction_data_may_parse(&Pubkey::new_unique(), &data8([1; 8])));
558 assert!(!instruction_data_may_parse(&PUMPSWAP_PROGRAM_ID, &data8([0xff; 8])));
559 assert!(!instruction_data_may_parse(
560 &PUMPFUN_PROGRAM_ID,
561 &data8(pump::discriminators::MIGRATE_BONDING_CURVE_CREATOR)
562 ));
563
564 let mut pumpswap_event_cpi = Vec::from(pump_amm_inner::discriminators::CREATE_POOL);
565 pumpswap_event_cpi.extend_from_slice(&[0; 64]);
566 assert!(!instruction_data_may_parse(&PUMPSWAP_PROGRAM_ID, &pumpswap_event_cpi));
567 }
568
569 #[test]
570 fn normal_instruction_gate_keeps_meteora_damm_event_cpi_on_event_path() {
571 let mut event_cpi = Vec::new();
572 event_cpi.extend_from_slice(&[228, 69, 165, 46, 81, 203, 154, 29]);
573 event_cpi.extend_from_slice(&meteora_damm::discriminators::SWAP_LOG);
574 event_cpi.extend_from_slice(&[0; 64]);
575
576 assert!(instruction_data_may_parse(&METEORA_DAMM_V2_PROGRAM_ID, &event_cpi));
577 assert!(!normal_instruction_data_may_parse(&METEORA_DAMM_V2_PROGRAM_ID, &event_cpi));
578 assert!(normal_instruction_data_may_parse(
579 &METEORA_DAMM_V2_PROGRAM_ID,
580 &data8(meteora_damm::discriminators::INITIALIZE_POOL)
581 ));
582 }
583}