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 | raydium_launchlab::discriminators::MIGRATE_TO_AMM
102 | raydium_launchlab::discriminators::MIGRATE_TO_CPSWAP
103 )
104}
105
106#[inline(always)]
107fn supports_cpmm_instruction(disc: [u8; 8]) -> bool {
108 matches!(
109 disc,
110 raydium_cpmm::discriminators::SWAP_BASE_IN
111 | raydium_cpmm::discriminators::SWAP_BASE_OUT
112 | raydium_cpmm::discriminators::INITIALIZE
113 | raydium_cpmm::discriminators::DEPOSIT
114 | raydium_cpmm::discriminators::WITHDRAW
115 )
116}
117
118#[inline(always)]
119fn supports_clmm_instruction(disc: [u8; 8]) -> bool {
120 matches!(
121 disc,
122 raydium_clmm::discriminators::SWAP
123 | raydium_clmm::discriminators::SWAP_V2
124 | raydium_clmm::discriminators::INCREASE_LIQUIDITY_V2
125 | raydium_clmm::discriminators::DECREASE_LIQUIDITY_V2
126 | raydium_clmm::discriminators::CREATE_POOL
127 | raydium_clmm::discriminators::CREATE_CUSTOMIZABLE_POOL
128 | raydium_clmm::discriminators::OPEN_POSITION
129 | raydium_clmm::discriminators::OPEN_POSITION_V2
130 | raydium_clmm::discriminators::OPEN_POSITION_WITH_TOKEN_22_NFT
131 | raydium_clmm::discriminators::CLOSE_POSITION
132 )
133}
134
135#[inline(always)]
136fn supports_raydium_amm_v4_instruction(instruction_data: &[u8]) -> bool {
137 matches!(
138 instruction_data.first().copied(),
139 Some(raydium_amm::discriminators::SWAP_BASE_IN)
140 | Some(raydium_amm::discriminators::SWAP_BASE_OUT)
141 | Some(raydium_amm::discriminators::DEPOSIT)
142 | Some(raydium_amm::discriminators::WITHDRAW)
143 | Some(raydium_amm::discriminators::INITIALIZE2)
144 | Some(raydium_amm::discriminators::WITHDRAW_PNL)
145 )
146}
147
148#[inline(always)]
149fn supports_orca_instruction(disc: [u8; 8]) -> bool {
150 matches!(
151 disc,
152 orca_whirlpool::discriminators::SWAP
153 | orca_whirlpool::discriminators::SWAP_V2
154 | orca_whirlpool::discriminators::INCREASE_LIQUIDITY
155 | orca_whirlpool::discriminators::DECREASE_LIQUIDITY
156 | orca_whirlpool::discriminators::INITIALIZE_POOL
157 )
158}
159
160#[inline(always)]
161fn supports_meteora_pools_instruction(disc: [u8; 8]) -> bool {
162 matches!(
163 disc,
164 meteora_amm::discriminators::SWAP
165 | meteora_amm::discriminators::ADD_LIQUIDITY
166 | meteora_amm::discriminators::REMOVE_LIQUIDITY
167 | meteora_amm::discriminators::CREATE_POOL
168 )
169}
170
171#[inline(always)]
172fn supports_meteora_damm_v2_instruction(instruction_data: &[u8]) -> bool {
173 let Some(disc) = disc8(instruction_data) else {
174 return false;
175 };
176 if disc == meteora_damm::discriminators::INITIALIZE_POOL {
177 return true;
178 }
179 let Some(cpi_disc) = instruction_data.get(8..16).and_then(|bytes| bytes.try_into().ok()) else {
180 return false;
181 };
182 matches!(
183 cpi_disc,
184 meteora_damm::discriminators::SWAP_LOG
185 | meteora_damm::discriminators::SWAP2_LOG
186 | meteora_damm::discriminators::CREATE_POSITION_LOG
187 | meteora_damm::discriminators::CLOSE_POSITION_LOG
188 | meteora_damm::discriminators::ADD_LIQUIDITY_LOG
189 | meteora_damm::discriminators::REMOVE_LIQUIDITY_LOG
190 )
191}
192
193#[inline(always)]
194fn supports_meteora_dlmm_instruction(instruction_data: &[u8]) -> bool {
195 let Some(disc) = disc8(instruction_data) else {
196 return false;
197 };
198 matches!(
199 disc,
200 meteora_dlmm::discriminators::INITIALIZE_LB_PAIR
201 | meteora_dlmm::discriminators::INITIALIZE_LB_PAIR2
202 | meteora_dlmm::discriminators::INITIALIZE_BIN_ARRAY
203 | meteora_dlmm::discriminators::ADD_LIQUIDITY
204 | meteora_dlmm::discriminators::ADD_LIQUIDITY2
205 | meteora_dlmm::discriminators::REMOVE_LIQUIDITY
206 | meteora_dlmm::discriminators::REMOVE_LIQUIDITY2
207 | meteora_dlmm::discriminators::INITIALIZE_POSITION
208 | meteora_dlmm::discriminators::INITIALIZE_POSITION2
209 | meteora_dlmm::discriminators::INITIALIZE_POSITION_PDA
210 | meteora_dlmm::discriminators::SWAP
211 | meteora_dlmm::discriminators::SWAP2
212 | meteora_dlmm::discriminators::SWAP_EXACT_OUT
213 | meteora_dlmm::discriminators::SWAP_EXACT_OUT2
214 | meteora_dlmm::discriminators::SWAP_WITH_PRICE_IMPACT
215 | meteora_dlmm::discriminators::SWAP_WITH_PRICE_IMPACT2
216 | meteora_dlmm::discriminators::CLAIM_FEE
217 | meteora_dlmm::discriminators::CLAIM_FEE2
218 | meteora_dlmm::discriminators::CLOSE_POSITION
219 | meteora_dlmm::discriminators::CLOSE_POSITION2
220 )
221}
222
223#[inline(always)]
224pub(crate) fn instruction_data_may_parse(program_id: &Pubkey, instruction_data: &[u8]) -> bool {
225 if instruction_data.is_empty() {
226 return false;
227 }
228 if *program_id == RAYDIUM_AMM_V4_PROGRAM_ID {
229 return supports_raydium_amm_v4_instruction(instruction_data);
230 }
231 if *program_id == METEORA_DLMM_PROGRAM_ID {
232 return supports_meteora_dlmm_instruction(instruction_data);
233 }
234 if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
235 return supports_meteora_damm_v2_instruction(instruction_data);
236 }
237
238 let Some(disc) = disc8(instruction_data) else {
239 return false;
240 };
241 if *program_id == PUMPFUN_PROGRAM_ID {
242 supports_pumpfun_instruction(disc)
243 } else if *program_id == PUMPSWAP_PROGRAM_ID {
244 supports_pumpswap_instruction(disc)
245 } else if *program_id == PUMP_FEES_PROGRAM_ID {
246 supports_pump_fees_instruction(disc)
247 } else if *program_id == RAYDIUM_LAUNCHLAB_PROGRAM_ID {
248 supports_launchlab_instruction(disc)
249 } else if *program_id == RAYDIUM_CPMM_PROGRAM_ID {
250 supports_cpmm_instruction(disc)
251 } else if *program_id == RAYDIUM_CLMM_PROGRAM_ID {
252 supports_clmm_instruction(disc)
253 } else if *program_id == ORCA_WHIRLPOOL_PROGRAM_ID {
254 supports_orca_instruction(disc)
255 } else if *program_id == METEORA_POOLS_PROGRAM_ID {
256 supports_meteora_pools_instruction(disc)
257 } else {
258 false
259 }
260}
261
262#[inline(always)]
263pub(crate) fn normal_instruction_data_may_parse(
264 program_id: &Pubkey,
265 instruction_data: &[u8],
266) -> bool {
267 if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
268 return disc8(instruction_data)
269 .is_some_and(|disc| disc == meteora_damm::discriminators::INITIALIZE_POOL);
270 }
271 instruction_data_may_parse(program_id, instruction_data)
272}
273
274#[inline(always)]
275fn filter_parsed_event(
276 event: Option<DexEvent>,
277 event_type_filter: Option<&EventTypeFilter>,
278) -> Option<DexEvent> {
279 let event = event?;
280 if event_type_filter.map(|f| f.should_include_dex_event(&event)).unwrap_or(true) {
281 Some(event)
282 } else {
283 None
284 }
285}
286
287#[inline]
289pub fn parse_instruction_unified(
290 instruction_data: &[u8],
291 accounts: &[Pubkey],
292 signature: Signature,
293 slot: u64,
294 tx_index: u64,
295 block_time_us: Option<i64>,
296 grpc_recv_us: i64,
297 event_type_filter: Option<&EventTypeFilter>,
298 program_id: &Pubkey,
299) -> Option<DexEvent> {
300 if instruction_data.is_empty() {
302 return None;
303 }
304
305 if *program_id == PUMPFUN_PROGRAM_ID {
309 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pumpfun() {
310 return None;
311 }
312 return filter_parsed_event(
313 parse_pumpfun_instruction(
314 instruction_data,
315 accounts,
316 signature,
317 slot,
318 tx_index,
319 block_time_us,
320 grpc_recv_us,
321 ),
322 event_type_filter,
323 );
324 }
325 else if *program_id == PUMPSWAP_PROGRAM_ID {
327 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pumpswap() {
328 return None;
329 }
330 return filter_parsed_event(
331 parse_pumpswap_instruction(
332 instruction_data,
333 accounts,
334 signature,
335 slot,
336 tx_index,
337 block_time_us,
338 ),
339 event_type_filter,
340 );
341 }
342 else if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
344 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_damm_v2() {
345 return None;
346 }
347 return filter_parsed_event(
348 parse_meteora_damm_instruction(
349 instruction_data,
350 accounts,
351 signature,
352 slot,
353 tx_index,
354 block_time_us,
355 grpc_recv_us,
356 ),
357 event_type_filter,
358 );
359 }
360 else if *program_id == PUMP_FEES_PROGRAM_ID {
362 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pump_fees() {
363 return None;
364 }
365 return filter_parsed_event(
366 crate::instr::pump_fees::parse_instruction(
367 instruction_data,
368 accounts,
369 signature,
370 slot,
371 tx_index,
372 block_time_us,
373 grpc_recv_us,
374 ),
375 event_type_filter,
376 );
377 }
378 else if *program_id == RAYDIUM_LAUNCHLAB_PROGRAM_ID {
380 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_launchlab() {
381 return None;
382 }
383 return filter_parsed_event(
384 parse_raydium_launchlab_instruction(
385 instruction_data,
386 accounts,
387 signature,
388 slot,
389 tx_index,
390 block_time_us,
391 ),
392 event_type_filter,
393 );
394 }
395 else if *program_id == RAYDIUM_CPMM_PROGRAM_ID {
397 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_cpmm() {
398 return None;
399 }
400 return filter_parsed_event(
401 crate::instr::raydium_cpmm::parse_instruction(
402 instruction_data,
403 accounts,
404 signature,
405 slot,
406 tx_index,
407 block_time_us,
408 ),
409 event_type_filter,
410 );
411 }
412 else if *program_id == RAYDIUM_CLMM_PROGRAM_ID {
414 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_clmm() {
415 return None;
416 }
417 return filter_parsed_event(
418 crate::instr::raydium_clmm::parse_instruction(
419 instruction_data,
420 accounts,
421 signature,
422 slot,
423 tx_index,
424 block_time_us,
425 ),
426 event_type_filter,
427 );
428 }
429 else if *program_id == RAYDIUM_AMM_V4_PROGRAM_ID {
431 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_amm_v4() {
432 return None;
433 }
434 return filter_parsed_event(
435 crate::instr::raydium_amm::parse_instruction(
436 instruction_data,
437 accounts,
438 signature,
439 slot,
440 tx_index,
441 block_time_us,
442 ),
443 event_type_filter,
444 );
445 }
446 else if *program_id == ORCA_WHIRLPOOL_PROGRAM_ID {
448 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_orca_whirlpool() {
449 return None;
450 }
451 return filter_parsed_event(
452 crate::instr::orca_whirlpool::parse_instruction(
453 instruction_data,
454 accounts,
455 signature,
456 slot,
457 tx_index,
458 block_time_us,
459 ),
460 event_type_filter,
461 );
462 }
463 else if *program_id == METEORA_POOLS_PROGRAM_ID {
465 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_pools() {
466 return None;
467 }
468 return filter_parsed_event(
469 crate::instr::meteora_amm::parse_instruction(
470 instruction_data,
471 accounts,
472 signature,
473 slot,
474 tx_index,
475 block_time_us,
476 ),
477 event_type_filter,
478 );
479 }
480 else if *program_id == METEORA_DLMM_PROGRAM_ID {
482 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_dlmm() {
483 return None;
484 }
485 return filter_parsed_event(
486 crate::instr::meteora_dlmm::parse_instruction(
487 instruction_data,
488 accounts,
489 signature,
490 slot,
491 tx_index,
492 block_time_us,
493 ),
494 event_type_filter,
495 );
496 }
497
498 None
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 fn data8(disc: [u8; 8]) -> Vec<u8> {
506 let mut data = Vec::from(disc);
507 data.extend_from_slice(&1u64.to_le_bytes());
508 data.extend_from_slice(&2u64.to_le_bytes());
509 data
510 }
511
512 #[test]
513 fn instruction_data_gate_covers_supported_normal_instruction_protocols() {
514 assert!(instruction_data_may_parse(
515 &PUMPSWAP_PROGRAM_ID,
516 &data8(pump_amm::discriminators::CREATE_POOL)
517 ));
518 assert!(instruction_data_may_parse(
519 &PUMP_FEES_PROGRAM_ID,
520 &data8(pump_fees::UPDATE_FEE_SHARES_IX)
521 ));
522 assert!(instruction_data_may_parse(
523 &RAYDIUM_LAUNCHLAB_PROGRAM_ID,
524 &data8(raydium_launchlab::discriminators::BUY_EXACT_IN)
525 ));
526 assert!(instruction_data_may_parse(
527 &RAYDIUM_CPMM_PROGRAM_ID,
528 &data8(raydium_cpmm::discriminators::SWAP_BASE_IN)
529 ));
530 assert!(instruction_data_may_parse(
531 &RAYDIUM_CLMM_PROGRAM_ID,
532 &data8(raydium_clmm::discriminators::SWAP_V2)
533 ));
534 assert!(instruction_data_may_parse(
535 &RAYDIUM_AMM_V4_PROGRAM_ID,
536 &[raydium_amm::discriminators::SWAP_BASE_IN, 1, 0, 0, 0, 0, 0, 0, 0]
537 ));
538 assert!(instruction_data_may_parse(
539 &ORCA_WHIRLPOOL_PROGRAM_ID,
540 &data8(orca_whirlpool::discriminators::SWAP)
541 ));
542 assert!(instruction_data_may_parse(
543 &METEORA_POOLS_PROGRAM_ID,
544 &data8(meteora_amm::discriminators::CREATE_POOL)
545 ));
546 assert!(instruction_data_may_parse(
547 &METEORA_DAMM_V2_PROGRAM_ID,
548 &data8(meteora_damm::discriminators::INITIALIZE_POOL)
549 ));
550 assert!(instruction_data_may_parse(
551 &METEORA_DLMM_PROGRAM_ID,
552 &data8(meteora_dlmm::discriminators::SWAP2)
553 ));
554 assert!(!instruction_data_may_parse(&METEORA_DLMM_PROGRAM_ID, &[11, 1, 2, 3]));
555 }
556
557 #[test]
558 fn instruction_data_gate_rejects_unknown_program_and_event_cpi_layouts() {
559 assert!(!instruction_data_may_parse(&Pubkey::new_unique(), &data8([1; 8])));
560 assert!(!instruction_data_may_parse(&PUMPSWAP_PROGRAM_ID, &data8([0xff; 8])));
561 assert!(!instruction_data_may_parse(
562 &PUMPFUN_PROGRAM_ID,
563 &data8(pump::discriminators::MIGRATE_BONDING_CURVE_CREATOR)
564 ));
565
566 let mut pumpswap_event_cpi = Vec::from(pump_amm_inner::discriminators::CREATE_POOL);
567 pumpswap_event_cpi.extend_from_slice(&[0; 64]);
568 assert!(!instruction_data_may_parse(&PUMPSWAP_PROGRAM_ID, &pumpswap_event_cpi));
569 }
570
571 #[test]
572 fn normal_instruction_gate_keeps_meteora_damm_event_cpi_on_event_path() {
573 let mut event_cpi = Vec::new();
574 event_cpi.extend_from_slice(&[228, 69, 165, 46, 81, 203, 154, 29]);
575 event_cpi.extend_from_slice(&meteora_damm::discriminators::SWAP_LOG);
576 event_cpi.extend_from_slice(&[0; 64]);
577
578 assert!(instruction_data_may_parse(&METEORA_DAMM_V2_PROGRAM_ID, &event_cpi));
579 assert!(!normal_instruction_data_may_parse(&METEORA_DAMM_V2_PROGRAM_ID, &event_cpi));
580 assert!(normal_instruction_data_may_parse(
581 &METEORA_DAMM_V2_PROGRAM_ID,
582 &data8(meteora_damm::discriminators::INITIALIZE_POOL)
583 ));
584 }
585}