Skip to main content

sol_parser_sdk/logs/
raydium_launchlab.rs

1//! LaunchLab 日志解析器
2//!
3//! 底层按 `idl/raydium_launchpad.json` 的真实 event discriminator 和 Borsh
4//! 布局解析,对外事件名统一为 `RaydiumLaunchlab*`。
5
6use super::utils::*;
7use crate::core::events::*;
8use solana_sdk::{pubkey::Pubkey, signature::Signature};
9
10/// LaunchLab event discriminators from `idl/raydium_launchpad.json`.
11pub mod discriminators {
12    pub const CLAIM_VESTED: [u8; 8] = [21, 194, 114, 87, 120, 211, 226, 32];
13    pub const CREATE_VESTING: [u8; 8] = [150, 152, 11, 179, 52, 210, 191, 125];
14    pub const POOL_CREATE: [u8; 8] = [151, 215, 226, 9, 118, 161, 115, 174];
15    pub const TRADE: [u8; 8] = [189, 219, 127, 211, 78, 230, 97, 238];
16}
17
18/// LaunchLab 程序 ID
19pub const PROGRAM_ID: &str = "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj";
20
21/// 检查日志是否来自 LaunchLab 程序
22pub fn is_raydium_launchlab_log(log: &str) -> bool {
23    log.contains(&format!("Program {} invoke", PROGRAM_ID))
24        || log.contains(&format!("Program {} success", PROGRAM_ID))
25}
26
27/// 主要的 LaunchLab 日志解析函数
28pub fn parse_log(
29    log: &str,
30    signature: Signature,
31    slot: u64,
32    tx_index: u64,
33    block_time_us: Option<i64>,
34    grpc_recv_us: i64,
35) -> Option<DexEvent> {
36    let program_data = extract_program_data(log)?;
37    if program_data.len() < 8 {
38        return None;
39    }
40
41    let discriminator: [u8; 8] = program_data[0..8].try_into().ok()?;
42    let data = &program_data[8..];
43    let metadata = EventMetadata {
44        signature,
45        slot,
46        tx_index,
47        block_time_us: block_time_us.unwrap_or(0),
48        grpc_recv_us,
49        recent_blockhash: None,
50    };
51
52    match discriminator {
53        discriminators::TRADE => parse_trade_from_data(data, metadata),
54        discriminators::POOL_CREATE => parse_pool_create_from_data(data, metadata),
55        _ => None,
56    }
57}
58
59/// Parse LaunchLab TradeEvent from pre-decoded event data.
60#[inline]
61pub fn parse_trade_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
62    const TRADE_EVENT_LEN: usize = 32 + 13 * 8 + 3;
63    if data.len() != TRADE_EVENT_LEN {
64        return None;
65    }
66
67    let pool_state = read_pubkey(data, 0)?;
68    let total_base_sell = read_u64_le(data, 32)?;
69    let virtual_base = read_u64_le(data, 40)?;
70    let virtual_quote = read_u64_le(data, 48)?;
71    let real_base_before = read_u64_le(data, 56)?;
72    let real_quote_before = read_u64_le(data, 64)?;
73    let real_base_after = read_u64_le(data, 72)?;
74    let real_quote_after = read_u64_le(data, 80)?;
75    let amount_in = read_u64_le(data, 88)?;
76    let amount_out = read_u64_le(data, 96)?;
77    let protocol_fee = read_u64_le(data, 104)?;
78    let platform_fee = read_u64_le(data, 112)?;
79    let creator_fee = read_u64_le(data, 120)?;
80    let share_fee = read_u64_le(data, 128)?;
81    let trade_direction = *data.get(136)?;
82    if trade_direction > 1 {
83        return None;
84    }
85    let pool_status = *data.get(137)?;
86    if pool_status > 2 {
87        return None;
88    }
89    let exact_in_raw = *data.get(138)?;
90    if exact_in_raw > 1 {
91        return None;
92    }
93    let exact_in = exact_in_raw == 1;
94    let is_buy = trade_direction == 0;
95    let pool_status = match pool_status {
96        0 => RaydiumLaunchlabPoolStatus::Fund,
97        1 => RaydiumLaunchlabPoolStatus::Migrate,
98        2 => RaydiumLaunchlabPoolStatus::Trade,
99        _ => return None,
100    };
101
102    Some(DexEvent::RaydiumLaunchlabTrade(RaydiumLaunchlabTradeEvent {
103        third_program: None,
104        parse_status: Default::default(),
105        metadata,
106        pool_state,
107        user: Pubkey::default(),
108        amount_in,
109        amount_out,
110        is_buy,
111        total_base_sell,
112        virtual_base,
113        virtual_quote,
114        real_base_before,
115        real_quote_before,
116        real_base_after,
117        real_quote_after,
118        protocol_fee,
119        platform_fee,
120        creator_fee,
121        share_fee,
122        trade_direction: if is_buy { TradeDirection::Buy } else { TradeDirection::Sell },
123        pool_status,
124        exact_in,
125        global_config: Pubkey::default(),
126        platform_config: Pubkey::default(),
127        user_base_token: Pubkey::default(),
128        user_quote_token: Pubkey::default(),
129        base_vault: Pubkey::default(),
130        quote_vault: Pubkey::default(),
131        base_mint: Pubkey::default(),
132        quote_mint: Pubkey::default(),
133        base_token_program: Pubkey::default(),
134        quote_token_program: Pubkey::default(),
135        system_program: Pubkey::default(),
136        platform_associated_account: Pubkey::default(),
137        creator_associated_account: Pubkey::default(),
138    }))
139}
140
141/// Parse LaunchLab PoolCreateEvent from pre-decoded event data.
142#[inline]
143pub fn parse_pool_create_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
144    let mut offset = 0usize;
145    let pool_state = read_pubkey(data, offset)?;
146    offset += 32;
147    let creator = read_pubkey(data, offset)?;
148    offset += 32;
149    let _config = read_pubkey(data, offset)?;
150    offset += 32;
151    let base_mint_param = parse_mint_params(data, &mut offset)?;
152
153    Some(DexEvent::RaydiumLaunchlabPoolCreate(RaydiumLaunchlabPoolCreateEvent {
154        metadata,
155        base_mint_param,
156        pool_state,
157        payer: Pubkey::default(),
158        creator,
159        global_config: Pubkey::default(),
160        platform_config: Pubkey::default(),
161        base_mint: Pubkey::default(),
162        quote_mint: Pubkey::default(),
163        base_vault: Pubkey::default(),
164        quote_vault: Pubkey::default(),
165        base_token_program: Pubkey::default(),
166        quote_token_program: Pubkey::default(),
167    }))
168}
169
170fn parse_mint_params(data: &[u8], offset: &mut usize) -> Option<BaseMintParam> {
171    let decimals = *data.get(*offset)?;
172    *offset += 1;
173    let name = read_borsh_string(data, offset)?;
174    let symbol = read_borsh_string(data, offset)?;
175    let uri = read_borsh_string(data, offset)?;
176    Some(BaseMintParam { symbol, name, uri, decimals })
177}
178
179fn read_borsh_string(data: &[u8], offset: &mut usize) -> Option<String> {
180    let len = read_u32_le(data, *offset)? as usize;
181    *offset += 4;
182    let end = (*offset).checked_add(len)?;
183    let bytes = data.get(*offset..end)?;
184    *offset = end;
185    std::str::from_utf8(bytes).ok().map(str::to_owned)
186}
187
188#[inline]
189fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
190    let bytes = data.get(offset..offset + 4)?;
191    Some(u32::from_le_bytes(bytes.try_into().ok()?))
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn current_trade_layout_preserves_reserves_and_all_fee_legs() {
200        let mut data = Vec::with_capacity(139);
201        data.extend_from_slice(Pubkey::new_unique().as_ref());
202        for value in [
203            793_100_000_000_000_u64,
204            1_073_025_605_597_286,
205            21_191_598_554,
206            284_268_264_059_232,
207            7_637_455_325,
208            284_320_641_177_089,
209            7_639_369_834,
210            1_938_744,
211            52_377_117_857,
212            4_847,
213            19_388,
214            0,
215            0,
216        ] {
217            data.extend_from_slice(&value.to_le_bytes());
218        }
219        data.extend_from_slice(&[0, 0, 1]);
220
221        let DexEvent::RaydiumLaunchlabTrade(event) =
222            parse_trade_from_data(&data, EventMetadata::default()).expect("trade")
223        else {
224            panic!("LaunchLab trade")
225        };
226
227        assert_eq!(event.total_base_sell, 793_100_000_000_000);
228        assert_eq!(event.amount_in, 1_938_744);
229        assert_eq!(event.amount_out, 52_377_117_857);
230        assert_eq!(event.protocol_fee, 4_847);
231        assert_eq!(event.platform_fee, 19_388);
232        assert_eq!(event.creator_fee, 0);
233        assert_eq!(event.share_fee, 0);
234        assert_eq!(event.pool_status, RaydiumLaunchlabPoolStatus::Fund);
235        assert!(event.is_buy);
236        assert!(event.exact_in);
237    }
238}