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