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    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/// 统一的指令解析入口函数
261#[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    // 快速检查指令数据长度,避免无效解析
274    if instruction_data.is_empty() {
275        return None;
276    }
277
278    // 根据程序 ID 路由到相应的解析器,按使用频率排序
279
280    // Pumpfun
281    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    // PumpSwap (Pump AMM)
299    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    // Meteora DAMM
316    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    // Pump fees (`pfeeUx...`)
334    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    // RaydiumLaunchlab / Raydium LaunchLab
352    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    // Raydium CPMM
369    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    // Raydium CLMM
386    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    // Raydium AMM V4
403    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    // Orca Whirlpool
420    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    // Meteora Pools / AMM
437    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    // Meteora DLMM
454    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}