Skip to main content

sol_parser_sdk/instr/
mod.rs

1//! 指令解析器模块
2//!
3//! 包含所有 DEX 协议的指令解析器实现
4
5pub 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
19// Inner instruction 解析器(16字节 discriminator)
20pub mod all_inner;
21pub mod inner_common; // 通用零拷贝读取函数
22pub mod pump_amm_inner; // PumpSwap inner instruction
23pub mod pump_inner; // PumpFun inner instruction
24pub mod raydium_clmm_inner; // Raydium CLMM inner instruction // 其他所有协议的 inner instruction(统一文件)
25use crate::grpc::types::EventTypeFilter;
26// 重新导出主要解析函数
27pub 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
32// 重新导出工具函数
33pub 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/// 统一的指令解析入口函数
286#[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    // 快速检查指令数据长度,避免无效解析
299    if instruction_data.is_empty() {
300        return None;
301    }
302
303    // 根据程序 ID 路由到相应的解析器,按使用频率排序
304
305    // Pumpfun
306    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    // PumpSwap (Pump AMM)
324    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    // Meteora DAMM
341    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    // Pump fees (`pfeeUx...`)
359    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    // RaydiumLaunchlab / Raydium LaunchLab
377    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    // Raydium CPMM
394    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    // Raydium CLMM
411    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    // Raydium AMM V4
428    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    // Orca Whirlpool
445    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    // Meteora Pools / AMM
462    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    // Meteora DLMM
479    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}