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            | 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/// 统一的指令解析入口函数
288#[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    // 快速检查指令数据长度,避免无效解析
301    if instruction_data.is_empty() {
302        return None;
303    }
304
305    // 根据程序 ID 路由到相应的解析器,按使用频率排序
306
307    // Pumpfun
308    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    // PumpSwap (Pump AMM)
326    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    // Meteora DAMM
343    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    // Pump fees (`pfeeUx...`)
361    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    // RaydiumLaunchlab / LaunchLab
379    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    // Raydium CPMM
396    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    // Raydium CLMM
413    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    // Raydium AMM V4
430    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    // Orca Whirlpool
447    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    // Meteora Pools / AMM
464    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    // Meteora DLMM
481    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}