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 | pump::discriminators::MIGRATE_BONDING_CURVE_CREATOR
57 )
58}
59
60#[inline(always)]
61fn supports_pumpswap_instruction(disc: [u8; 8]) -> bool {
62 matches!(
63 disc,
64 pump_amm::discriminators::BUY
65 | pump_amm::discriminators::SELL
66 | pump_amm::discriminators::CREATE_POOL
67 | pump_amm::discriminators::BUY_EXACT_QUOTE_IN
68 | pump_amm::discriminators::DEPOSIT
69 | pump_amm::discriminators::WITHDRAW
70 )
71}
72
73#[inline(always)]
74fn supports_pump_fees_instruction(disc: [u8; 8]) -> bool {
75 matches!(
76 disc,
77 pump_fees::CREATE_FEE_SHARING_IX
78 | pump_fees::INITIALIZE_FEE_CONFIG_IX
79 | pump_fees::RESET_FEE_SHARING_IX
80 | pump_fees::RESET_FEE_SHARING_V2_IX
81 | pump_fees::REVOKE_FEE_SHARING_IX
82 | pump_fees::TRANSFER_FEE_SHARING_IX
83 | pump_fees::UPDATE_ADMIN_IX
84 | pump_fees::UPDATE_FEE_CONFIG_IX
85 | pump_fees::UPDATE_FEE_SHARES_IX
86 | pump_fees::UPDATE_FEE_SHARES_V2_IX
87 | pump_fees::UPSERT_FEE_TIERS_IX
88 )
89}
90
91#[inline(always)]
92fn supports_launchlab_instruction(disc: [u8; 8]) -> bool {
93 matches!(
94 disc,
95 raydium_launchlab::discriminators::BUY_EXACT_IN
96 | raydium_launchlab::discriminators::BUY_EXACT_OUT
97 | raydium_launchlab::discriminators::SELL_EXACT_IN
98 | raydium_launchlab::discriminators::SELL_EXACT_OUT
99 | raydium_launchlab::discriminators::INITIALIZE
100 | raydium_launchlab::discriminators::INITIALIZE_V2
101 | raydium_launchlab::discriminators::INITIALIZE_WITH_TOKEN_2022
102 | raydium_launchlab::discriminators::MIGRATE_TO_AMM
103 | raydium_launchlab::discriminators::MIGRATE_TO_CPSWAP
104 )
105}
106
107#[inline(always)]
108fn supports_cpmm_instruction(disc: [u8; 8]) -> bool {
109 matches!(
110 disc,
111 raydium_cpmm::discriminators::SWAP_BASE_IN
112 | raydium_cpmm::discriminators::SWAP_BASE_OUT
113 | raydium_cpmm::discriminators::INITIALIZE
114 | raydium_cpmm::discriminators::DEPOSIT
115 | raydium_cpmm::discriminators::WITHDRAW
116 | raydium_cpmm::discriminators::COLLECT_CREATOR_FEE
117 | raydium_cpmm::discriminators::COLLECT_CREATOR_FEE_PERMISSIONLESS
118 )
119}
120
121#[inline(always)]
122fn supports_clmm_instruction(disc: [u8; 8]) -> bool {
123 matches!(
124 disc,
125 raydium_clmm::discriminators::SWAP
126 | raydium_clmm::discriminators::SWAP_V2
127 | raydium_clmm::discriminators::INCREASE_LIQUIDITY_V2
128 | raydium_clmm::discriminators::DECREASE_LIQUIDITY_V2
129 | raydium_clmm::discriminators::CREATE_POOL
130 | raydium_clmm::discriminators::CREATE_CUSTOMIZABLE_POOL
131 | raydium_clmm::discriminators::OPEN_POSITION
132 | raydium_clmm::discriminators::OPEN_POSITION_V2
133 | raydium_clmm::discriminators::OPEN_POSITION_WITH_TOKEN_22_NFT
134 | raydium_clmm::discriminators::CLOSE_POSITION
135 )
136}
137
138#[inline(always)]
139fn supports_raydium_amm_v4_instruction(instruction_data: &[u8]) -> bool {
140 matches!(
141 instruction_data.first().copied(),
142 Some(raydium_amm::discriminators::SWAP_BASE_IN)
143 | Some(raydium_amm::discriminators::SWAP_BASE_OUT)
144 | Some(raydium_amm::discriminators::DEPOSIT)
145 | Some(raydium_amm::discriminators::WITHDRAW)
146 | Some(raydium_amm::discriminators::INITIALIZE2)
147 | Some(raydium_amm::discriminators::WITHDRAW_PNL)
148 | Some(raydium_amm::discriminators::SWAP_BASE_IN_V2)
149 | Some(raydium_amm::discriminators::SWAP_BASE_OUT_V2)
150 )
151}
152
153#[inline(always)]
154fn supports_orca_instruction(disc: [u8; 8]) -> bool {
155 matches!(
156 disc,
157 orca_whirlpool::discriminators::SWAP
158 | orca_whirlpool::discriminators::SWAP_V2
159 | orca_whirlpool::discriminators::INCREASE_LIQUIDITY
160 | orca_whirlpool::discriminators::DECREASE_LIQUIDITY
161 | orca_whirlpool::discriminators::INITIALIZE_POOL
162 | orca_whirlpool::discriminators::INITIALIZE_POOL_V2
163 | orca_whirlpool::discriminators::INCREASE_LIQUIDITY_V2
164 | orca_whirlpool::discriminators::DECREASE_LIQUIDITY_V2
165 )
166}
167
168#[inline(always)]
169fn supports_meteora_pools_instruction(disc: [u8; 8]) -> bool {
170 matches!(
171 disc,
172 meteora_amm::discriminators::SWAP
173 | meteora_amm::discriminators::ADD_LIQUIDITY
174 | meteora_amm::discriminators::ADD_IMBALANCE_LIQUIDITY
175 | meteora_amm::discriminators::REMOVE_LIQUIDITY
176 | meteora_amm::discriminators::REMOVE_LIQUIDITY_SINGLE_SIDE
177 | meteora_amm::discriminators::BOOTSTRAP_LIQUIDITY
178 | meteora_amm::discriminators::INITIALIZE_PERMISSIONED_POOL
179 | meteora_amm::discriminators::INITIALIZE_PERMISSIONLESS_POOL
180 | meteora_amm::discriminators::INITIALIZE_PERMISSIONLESS_POOL_WITH_FEE_TIER
181 | meteora_amm::discriminators::INITIALIZE_CUSTOMIZABLE_POOL
182 | meteora_amm::discriminators::CREATE_POOL
183 | meteora_amm::discriminators::CREATE_POOL_WITH_CONFIG2
184 | meteora_amm::discriminators::SET_POOL_FEES
185 )
186}
187
188#[inline(always)]
189fn supports_meteora_damm_v2_instruction(instruction_data: &[u8]) -> bool {
190 let Some(disc) = disc8(instruction_data) else {
191 return false;
192 };
193 if disc == meteora_damm::discriminators::INITIALIZE_POOL {
194 return true;
195 }
196 let Some(cpi_disc) = instruction_data.get(8..16).and_then(|bytes| bytes.try_into().ok()) else {
197 return false;
198 };
199 matches!(
200 cpi_disc,
201 meteora_damm::discriminators::SWAP_LOG
202 | meteora_damm::discriminators::SWAP2_LOG
203 | meteora_damm::discriminators::CREATE_POSITION_LOG
204 | meteora_damm::discriminators::CLOSE_POSITION_LOG
205 | meteora_damm::discriminators::ADD_LIQUIDITY_LOG
206 | meteora_damm::discriminators::REMOVE_LIQUIDITY_LOG
207 )
208}
209
210#[inline(always)]
211fn supports_meteora_dlmm_instruction(instruction_data: &[u8]) -> bool {
212 let Some(disc) = disc8(instruction_data) else {
213 return false;
214 };
215 matches!(
216 disc,
217 meteora_dlmm::discriminators::INITIALIZE_LB_PAIR
218 | meteora_dlmm::discriminators::INITIALIZE_LB_PAIR2
219 | meteora_dlmm::discriminators::INITIALIZE_BIN_ARRAY
220 | meteora_dlmm::discriminators::ADD_LIQUIDITY
221 | meteora_dlmm::discriminators::ADD_LIQUIDITY2
222 | meteora_dlmm::discriminators::REMOVE_LIQUIDITY
223 | meteora_dlmm::discriminators::REMOVE_LIQUIDITY2
224 | meteora_dlmm::discriminators::INITIALIZE_POSITION
225 | meteora_dlmm::discriminators::INITIALIZE_POSITION2
226 | meteora_dlmm::discriminators::INITIALIZE_POSITION_PDA
227 | meteora_dlmm::discriminators::SWAP
228 | meteora_dlmm::discriminators::SWAP2
229 | meteora_dlmm::discriminators::SWAP_EXACT_OUT
230 | meteora_dlmm::discriminators::SWAP_EXACT_OUT2
231 | meteora_dlmm::discriminators::SWAP_WITH_PRICE_IMPACT
232 | meteora_dlmm::discriminators::SWAP_WITH_PRICE_IMPACT2
233 | meteora_dlmm::discriminators::CLAIM_FEE
234 | meteora_dlmm::discriminators::CLAIM_FEE2
235 | meteora_dlmm::discriminators::CLOSE_POSITION
236 | meteora_dlmm::discriminators::CLOSE_POSITION2
237 )
238}
239
240#[inline(always)]
241pub(crate) fn instruction_data_may_parse(program_id: &Pubkey, instruction_data: &[u8]) -> bool {
242 if instruction_data.is_empty() {
243 return false;
244 }
245 if *program_id == RAYDIUM_AMM_V4_PROGRAM_ID {
246 return supports_raydium_amm_v4_instruction(instruction_data);
247 }
248 if *program_id == METEORA_DLMM_PROGRAM_ID {
249 return supports_meteora_dlmm_instruction(instruction_data);
250 }
251 if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
252 return supports_meteora_damm_v2_instruction(instruction_data);
253 }
254
255 let Some(disc) = disc8(instruction_data) else {
256 return false;
257 };
258 if *program_id == PUMPFUN_PROGRAM_ID {
259 supports_pumpfun_instruction(disc)
260 } else if *program_id == PUMPSWAP_PROGRAM_ID {
261 supports_pumpswap_instruction(disc)
262 } else if *program_id == PUMP_FEES_PROGRAM_ID {
263 supports_pump_fees_instruction(disc)
264 } else if *program_id == RAYDIUM_LAUNCHLAB_PROGRAM_ID {
265 supports_launchlab_instruction(disc)
266 } else if *program_id == RAYDIUM_CPMM_PROGRAM_ID {
267 supports_cpmm_instruction(disc)
268 } else if *program_id == RAYDIUM_CLMM_PROGRAM_ID {
269 supports_clmm_instruction(disc)
270 } else if *program_id == ORCA_WHIRLPOOL_PROGRAM_ID {
271 supports_orca_instruction(disc)
272 } else if *program_id == METEORA_POOLS_PROGRAM_ID {
273 supports_meteora_pools_instruction(disc)
274 } else {
275 false
276 }
277}
278
279#[inline(always)]
280pub(crate) fn normal_instruction_data_may_parse(
281 program_id: &Pubkey,
282 instruction_data: &[u8],
283) -> bool {
284 if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
285 return disc8(instruction_data)
286 .is_some_and(|disc| disc == meteora_damm::discriminators::INITIALIZE_POOL);
287 }
288 instruction_data_may_parse(program_id, instruction_data)
289}
290
291#[inline(always)]
292fn filter_parsed_event(
293 event: Option<DexEvent>,
294 event_type_filter: Option<&EventTypeFilter>,
295) -> Option<DexEvent> {
296 let event = event?;
297 if event_type_filter.map(|f| f.should_include_dex_event(&event)).unwrap_or(true) {
298 Some(event)
299 } else {
300 None
301 }
302}
303
304#[inline]
306pub fn parse_instruction_unified(
307 instruction_data: &[u8],
308 accounts: &[Pubkey],
309 signature: Signature,
310 slot: u64,
311 tx_index: u64,
312 block_time_us: Option<i64>,
313 grpc_recv_us: i64,
314 event_type_filter: Option<&EventTypeFilter>,
315 program_id: &Pubkey,
316) -> Option<DexEvent> {
317 if instruction_data.is_empty() {
319 return None;
320 }
321
322 if *program_id == PUMPFUN_PROGRAM_ID {
326 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pumpfun() {
327 return None;
328 }
329 return filter_parsed_event(
330 parse_pumpfun_instruction(
331 instruction_data,
332 accounts,
333 signature,
334 slot,
335 tx_index,
336 block_time_us,
337 grpc_recv_us,
338 ),
339 event_type_filter,
340 );
341 }
342 else if *program_id == PUMPSWAP_PROGRAM_ID {
344 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pumpswap() {
345 return None;
346 }
347 return filter_parsed_event(
348 parse_pumpswap_instruction(
349 instruction_data,
350 accounts,
351 signature,
352 slot,
353 tx_index,
354 block_time_us,
355 ),
356 event_type_filter,
357 );
358 }
359 else if *program_id == METEORA_DAMM_V2_PROGRAM_ID {
361 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_damm_v2() {
362 return None;
363 }
364 return filter_parsed_event(
365 parse_meteora_damm_instruction(
366 instruction_data,
367 accounts,
368 signature,
369 slot,
370 tx_index,
371 block_time_us,
372 grpc_recv_us,
373 ),
374 event_type_filter,
375 );
376 }
377 else if *program_id == PUMP_FEES_PROGRAM_ID {
379 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_pump_fees() {
380 return None;
381 }
382 return filter_parsed_event(
383 crate::instr::pump_fees::parse_instruction(
384 instruction_data,
385 accounts,
386 signature,
387 slot,
388 tx_index,
389 block_time_us,
390 grpc_recv_us,
391 ),
392 event_type_filter,
393 );
394 }
395 else if *program_id == RAYDIUM_LAUNCHLAB_PROGRAM_ID {
397 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_launchlab() {
398 return None;
399 }
400 return filter_parsed_event(
401 parse_raydium_launchlab_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_CPMM_PROGRAM_ID {
414 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_cpmm() {
415 return None;
416 }
417 return filter_parsed_event(
418 crate::instr::raydium_cpmm::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_CLMM_PROGRAM_ID {
431 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_clmm() {
432 return None;
433 }
434 return filter_parsed_event(
435 crate::instr::raydium_clmm::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 == RAYDIUM_AMM_V4_PROGRAM_ID {
448 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_raydium_amm_v4() {
449 return None;
450 }
451 return filter_parsed_event(
452 crate::instr::raydium_amm::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 == ORCA_WHIRLPOOL_PROGRAM_ID {
465 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_orca_whirlpool() {
466 return None;
467 }
468 return filter_parsed_event(
469 crate::instr::orca_whirlpool::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_POOLS_PROGRAM_ID {
482 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_pools() {
483 return None;
484 }
485 return filter_parsed_event(
486 crate::instr::meteora_amm::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 else if *program_id == METEORA_DLMM_PROGRAM_ID {
499 if event_type_filter.is_some() && !event_type_filter.unwrap().includes_meteora_dlmm() {
500 return None;
501 }
502 return filter_parsed_event(
503 crate::instr::meteora_dlmm::parse_instruction(
504 instruction_data,
505 accounts,
506 signature,
507 slot,
508 tx_index,
509 block_time_us,
510 ),
511 event_type_filter,
512 );
513 }
514
515 None
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 fn data8(disc: [u8; 8]) -> Vec<u8> {
523 let mut data = Vec::from(disc);
524 data.extend_from_slice(&1u64.to_le_bytes());
525 data.extend_from_slice(&2u64.to_le_bytes());
526 data
527 }
528
529 #[test]
530 fn instruction_data_gate_covers_supported_normal_instruction_protocols() {
531 assert!(instruction_data_may_parse(
532 &PUMPSWAP_PROGRAM_ID,
533 &data8(pump_amm::discriminators::CREATE_POOL)
534 ));
535 assert!(instruction_data_may_parse(
536 &PUMP_FEES_PROGRAM_ID,
537 &data8(pump_fees::UPDATE_FEE_SHARES_IX)
538 ));
539 assert!(instruction_data_may_parse(
540 &RAYDIUM_LAUNCHLAB_PROGRAM_ID,
541 &data8(raydium_launchlab::discriminators::BUY_EXACT_IN)
542 ));
543 assert!(instruction_data_may_parse(
544 &RAYDIUM_CPMM_PROGRAM_ID,
545 &data8(raydium_cpmm::discriminators::SWAP_BASE_IN)
546 ));
547 assert!(instruction_data_may_parse(
548 &RAYDIUM_CLMM_PROGRAM_ID,
549 &data8(raydium_clmm::discriminators::SWAP_V2)
550 ));
551 assert!(instruction_data_may_parse(
552 &RAYDIUM_AMM_V4_PROGRAM_ID,
553 &[raydium_amm::discriminators::SWAP_BASE_IN, 1, 0, 0, 0, 0, 0, 0, 0]
554 ));
555 assert!(instruction_data_may_parse(
556 &ORCA_WHIRLPOOL_PROGRAM_ID,
557 &data8(orca_whirlpool::discriminators::SWAP)
558 ));
559 assert!(instruction_data_may_parse(
560 &METEORA_POOLS_PROGRAM_ID,
561 &data8(meteora_amm::discriminators::CREATE_POOL)
562 ));
563 assert!(instruction_data_may_parse(
564 &METEORA_DAMM_V2_PROGRAM_ID,
565 &data8(meteora_damm::discriminators::INITIALIZE_POOL)
566 ));
567 assert!(instruction_data_may_parse(
568 &METEORA_DLMM_PROGRAM_ID,
569 &data8(meteora_dlmm::discriminators::SWAP2)
570 ));
571 assert!(!instruction_data_may_parse(&METEORA_DLMM_PROGRAM_ID, &[11, 1, 2, 3]));
572 }
573
574 #[test]
575 fn instruction_data_gate_rejects_unknown_program_and_event_cpi_layouts() {
576 assert!(!instruction_data_may_parse(&Pubkey::new_unique(), &data8([1; 8])));
577 assert!(!instruction_data_may_parse(&PUMPSWAP_PROGRAM_ID, &data8([0xff; 8])));
578 assert!(instruction_data_may_parse(
579 &PUMPFUN_PROGRAM_ID,
580 &data8(pump::discriminators::MIGRATE_BONDING_CURVE_CREATOR)
581 ));
582
583 let mut pumpswap_event_cpi = Vec::from(pump_amm_inner::discriminators::CREATE_POOL);
584 pumpswap_event_cpi.extend_from_slice(&[0; 64]);
585 assert!(!instruction_data_may_parse(&PUMPSWAP_PROGRAM_ID, &pumpswap_event_cpi));
586 }
587
588 #[test]
589 fn normal_instruction_gate_keeps_meteora_damm_event_cpi_on_event_path() {
590 let mut event_cpi = Vec::new();
591 event_cpi.extend_from_slice(&[228, 69, 165, 46, 81, 203, 154, 29]);
592 event_cpi.extend_from_slice(&meteora_damm::discriminators::SWAP_LOG);
593 event_cpi.extend_from_slice(&[0; 64]);
594
595 assert!(instruction_data_may_parse(&METEORA_DAMM_V2_PROGRAM_ID, &event_cpi));
596 assert!(!normal_instruction_data_may_parse(&METEORA_DAMM_V2_PROGRAM_ID, &event_cpi));
597 assert!(normal_instruction_data_may_parse(
598 &METEORA_DAMM_V2_PROGRAM_ID,
599 &data8(meteora_damm::discriminators::INITIALIZE_POOL)
600 ));
601 }
602}