Skip to main content

sol_parser_sdk/logs/
meteora_dbc.rs

1//! Meteora DBC log parser.
2
3use super::utils::*;
4use crate::core::events::*;
5use solana_sdk::signature::Signature;
6
7pub mod discriminators {
8    pub const SWAP2_EVENT: [u8; 8] = [189, 66, 51, 168, 38, 80, 117, 153];
9    pub const SWAP2_TRANSFER_HOOK_EVENT: [u8; 8] = [134, 59, 168, 120, 94, 51, 114, 231];
10    pub const SWAP_EVENT: [u8; 8] = [27, 60, 21, 213, 138, 170, 187, 147];
11    pub const INITIALIZE_POOL_EVENT: [u8; 8] = [228, 50, 246, 85, 203, 66, 134, 37];
12    pub const CURVE_COMPLETE_EVENT: [u8; 8] = [229, 231, 86, 84, 156, 134, 75, 24];
13}
14
15pub fn parse_log(
16    log: &str,
17    signature: Signature,
18    slot: u64,
19    tx_index: u64,
20    block_time_us: Option<i64>,
21    grpc_recv_us: i64,
22) -> Option<DexEvent> {
23    let program_data = extract_program_data(log)?;
24    if program_data.len() < 8 {
25        return None;
26    }
27
28    let discriminator: [u8; 8] = program_data[0..8].try_into().ok()?;
29    let data = &program_data[8..];
30    let pool = read_pubkey(data, 0).unwrap_or_default();
31    let metadata =
32        create_metadata_simple(signature, slot, tx_index, block_time_us, pool, grpc_recv_us);
33
34    match discriminator {
35        discriminators::SWAP_EVENT => parse_swap_from_data(data, metadata),
36        discriminators::SWAP2_EVENT | discriminators::SWAP2_TRANSFER_HOOK_EVENT => {
37            parse_swap2_from_data(
38                data,
39                metadata,
40                discriminator == discriminators::SWAP2_TRANSFER_HOOK_EVENT,
41            )
42        }
43        discriminators::INITIALIZE_POOL_EVENT => parse_initialize_pool_from_data(data, metadata),
44        discriminators::CURVE_COMPLETE_EVENT => parse_curve_complete_from_data(data, metadata),
45        _ => None,
46    }
47}
48
49#[inline(always)]
50pub fn parse_swap_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
51    let mut offset = 0;
52
53    let pool = read_pubkey(data, offset)?;
54    offset += 32;
55    let config = read_pubkey(data, offset)?;
56    offset += 32;
57    let trade_direction = read_u8(data, offset)?;
58    offset += 1;
59    let has_referral = read_bool(data, offset)?;
60    offset += 1;
61    let params_amount_in = read_u64_le(data, offset)?;
62    offset += 8;
63    let minimum_amount_out = read_u64_le(data, offset)?;
64    offset += 8;
65    let actual_input_amount = read_u64_le(data, offset)?;
66    offset += 8;
67    let output_amount = read_u64_le(data, offset)?;
68    offset += 8;
69    let next_sqrt_price = read_u128_le(data, offset)?;
70    offset += 16;
71    let trading_fee = read_u64_le(data, offset)?;
72    offset += 8;
73    let protocol_fee = read_u64_le(data, offset)?;
74    offset += 8;
75    let referral_fee = read_u64_le(data, offset)?;
76    offset += 8;
77    let amount_in = read_u64_le(data, offset).unwrap_or(params_amount_in);
78    offset += 8;
79    let current_timestamp = read_u64_le(data, offset)?;
80
81    Some(DexEvent::MeteoraDbcSwap(MeteoraDbcSwapEvent {
82        metadata,
83        pool,
84        config,
85        trade_direction,
86        has_referral,
87        amount_in,
88        minimum_amount_out,
89        actual_input_amount,
90        output_amount,
91        next_sqrt_price,
92        trading_fee,
93        protocol_fee,
94        referral_fee,
95        current_timestamp,
96        ..Default::default()
97    }))
98}
99
100#[inline(always)]
101pub fn parse_initialize_pool_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
102    let mut offset = 0;
103
104    let pool = read_pubkey(data, offset)?;
105    offset += 32;
106    let config = read_pubkey(data, offset)?;
107    offset += 32;
108    let creator = read_pubkey(data, offset)?;
109    offset += 32;
110    let base_mint = read_pubkey(data, offset)?;
111    offset += 32;
112    let pool_type = read_u8(data, offset)?;
113    offset += 1;
114    let activation_point = read_u64_le(data, offset)?;
115
116    Some(DexEvent::MeteoraDbcInitializePool(MeteoraDbcInitializePoolEvent {
117        metadata,
118        pool,
119        config,
120        creator,
121        base_mint,
122        pool_type,
123        activation_point,
124    }))
125}
126
127#[inline(always)]
128pub fn parse_curve_complete_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
129    let mut offset = 0;
130
131    let pool = read_pubkey(data, offset)?;
132    offset += 32;
133    let config = read_pubkey(data, offset)?;
134    offset += 32;
135    let base_reserve = read_u64_le(data, offset)?;
136    offset += 8;
137    let quote_reserve = read_u64_le(data, offset)?;
138
139    Some(DexEvent::MeteoraDbcCurveComplete(MeteoraDbcCurveCompleteEvent {
140        metadata,
141        pool,
142        config,
143        base_reserve,
144        quote_reserve,
145    }))
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use solana_sdk::pubkey::Pubkey;
152
153    fn push_pubkey(buf: &mut Vec<u8>, byte: u8) -> Pubkey {
154        let key = Pubkey::new_from_array([byte; 32]);
155        buf.extend_from_slice(key.as_ref());
156        key
157    }
158
159    #[test]
160    fn parses_dbc_swap_layout() {
161        let mut data = Vec::new();
162        let pool = push_pubkey(&mut data, 1);
163        let config = push_pubkey(&mut data, 2);
164        data.push(1);
165        data.push(1);
166        data.extend_from_slice(&10_u64.to_le_bytes());
167        data.extend_from_slice(&9_u64.to_le_bytes());
168        data.extend_from_slice(&10_u64.to_le_bytes());
169        data.extend_from_slice(&8_u64.to_le_bytes());
170        data.extend_from_slice(&(1_u128 << 64).to_le_bytes());
171        data.extend_from_slice(&1_u64.to_le_bytes());
172        data.extend_from_slice(&2_u64.to_le_bytes());
173        data.extend_from_slice(&3_u64.to_le_bytes());
174        data.extend_from_slice(&10_u64.to_le_bytes());
175        data.extend_from_slice(&123_u64.to_le_bytes());
176
177        let event = parse_swap_from_data(&data, EventMetadata::default()).unwrap();
178        match event {
179            DexEvent::MeteoraDbcSwap(event) => {
180                assert_eq!(event.pool, pool);
181                assert_eq!(event.config, config);
182                assert_eq!(event.trade_direction, 1);
183                assert!(event.has_referral);
184                assert_eq!(event.minimum_amount_out, 9);
185                assert_eq!(event.output_amount, 8);
186                assert_eq!(event.protocol_fee, 2);
187                assert_eq!(event.current_timestamp, 123);
188            }
189            other => panic!("expected MeteoraDbcSwap, got {other:?}"),
190        }
191    }
192
193    #[test]
194    fn parses_dbc_initialize_pool_layout() {
195        let mut data = Vec::new();
196        let pool = push_pubkey(&mut data, 1);
197        let config = push_pubkey(&mut data, 2);
198        let creator = push_pubkey(&mut data, 3);
199        let base_mint = push_pubkey(&mut data, 4);
200        data.push(2);
201        data.extend_from_slice(&456_u64.to_le_bytes());
202
203        let event = parse_initialize_pool_from_data(&data, EventMetadata::default()).unwrap();
204        match event {
205            DexEvent::MeteoraDbcInitializePool(event) => {
206                assert_eq!(event.pool, pool);
207                assert_eq!(event.config, config);
208                assert_eq!(event.creator, creator);
209                assert_eq!(event.base_mint, base_mint);
210                assert_eq!(event.pool_type, 2);
211                assert_eq!(event.activation_point, 456);
212            }
213            other => panic!("expected MeteoraDbcInitializePool, got {other:?}"),
214        }
215    }
216}
217
218/// Current IDL EvtSwap2 / EvtSwap2WithTransferHook share the same 179-byte body.
219pub fn parse_swap2_from_data(
220    data: &[u8],
221    metadata: EventMetadata,
222    has_transfer_hook: bool,
223) -> Option<DexEvent> {
224    if data.len() < 179 {
225        return None;
226    }
227    let swap_mode = read_u8(data, 82)?;
228    let trade_direction = read_u8(data, 64)?;
229    if swap_mode > 2 || trade_direction > 1 || data[65] > 1 {
230        return None;
231    }
232    let amount_0 = read_u64_le(data, 66)?;
233    let amount_1 = read_u64_le(data, 74)?;
234    let included_fee_input_amount = read_u64_le(data, 83)?;
235    Some(DexEvent::MeteoraDbcSwap(MeteoraDbcSwapEvent {
236        metadata,
237        pool: read_pubkey(data, 0)?,
238        config: read_pubkey(data, 32)?,
239        trade_direction,
240        has_referral: read_bool(data, 65)?,
241        event_version: 2,
242        swap_mode,
243        amount_0,
244        amount_1,
245        has_transfer_hook,
246        amount_in: included_fee_input_amount,
247        minimum_amount_out: if swap_mode == 2 { 0 } else { amount_1 },
248        maximum_amount_in: if swap_mode == 2 { amount_1 } else { 0 },
249        included_fee_input_amount,
250        actual_input_amount: read_u64_le(data, 91)?,
251        amount_left: read_u64_le(data, 99)?,
252        output_amount: read_u64_le(data, 107)?,
253        next_sqrt_price: read_u128_le(data, 115)?,
254        trading_fee: read_u64_le(data, 131)?,
255        protocol_fee: read_u64_le(data, 139)?,
256        referral_fee: read_u64_le(data, 147)?,
257        quote_reserve_amount: read_u64_le(data, 155)?,
258        migration_threshold: read_u64_le(data, 163)?,
259        current_timestamp: read_u64_le(data, 171)?,
260    }))
261}
262
263/// Decode Anchor event CPI with explicit DBC program context at the caller.
264pub fn parse_event_cpi(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
265    if data.get(..8)? != [228, 69, 165, 46, 81, 203, 154, 29] {
266        return None;
267    }
268    let disc: [u8; 8] = data.get(8..16)?.try_into().ok()?;
269    let body = data.get(16..)?;
270    match disc {
271        discriminators::SWAP_EVENT => parse_swap_from_data(body, metadata),
272        discriminators::SWAP2_EVENT | discriminators::SWAP2_TRANSFER_HOOK_EVENT => {
273            parse_swap2_from_data(body, metadata, disc == discriminators::SWAP2_TRANSFER_HOOK_EVENT)
274        }
275        discriminators::INITIALIZE_POOL_EVENT => parse_initialize_pool_from_data(body, metadata),
276        discriminators::CURVE_COMPLETE_EVENT => parse_curve_complete_from_data(body, metadata),
277        _ => None,
278    }
279}
280
281#[cfg(test)]
282mod current_dbc_tests {
283    use super::*;
284    use base64::{engine::general_purpose::STANDARD, Engine};
285    #[test]
286    fn current_official_modes_and_transfer_hook_cpi() {
287        let fixture: serde_json::Value =
288            serde_json::from_str(include_str!("../../tests/fixtures/dbc_swap2.json")).unwrap();
289        for case in fixture["cases"].as_array().unwrap() {
290            let bytes = STANDARD.decode(case["data"].as_str().unwrap()).unwrap();
291            let mut cpi = vec![228, 69, 165, 46, 81, 203, 154, 29];
292            cpi.extend(&bytes);
293            let e = parse_event_cpi(&cpi, EventMetadata::default()).unwrap();
294            let DexEvent::MeteoraDbcSwap(e) = e else { panic!() };
295            let mode = case["mode"].as_u64().unwrap() as u8;
296            assert_eq!(e.event_version, 2);
297            assert_eq!(e.swap_mode, mode);
298            assert_eq!(e.amount_in, if mode == 0 { 100000 } else { 90000 });
299            assert_eq!(e.minimum_amount_out, if mode == 2 { 0 } else { 79000 });
300            assert_eq!(e.maximum_amount_in, if mode == 2 { 100000 } else { 0 });
301            assert_eq!(e.actual_input_amount, e.amount_in - 1000);
302            assert_eq!(e.amount_left, if mode == 1 { 10000 } else { 0 });
303            assert_eq!(e.output_amount, 80000);
304            assert_eq!(e.next_sqrt_price, (1u128 << 100) + 7);
305            assert_eq!(e.quote_reserve_amount, 9007199254740993);
306            assert_eq!(e.migration_threshold, 9007199254740995);
307            assert_eq!(e.has_transfer_hook, case["name"] == "EvtSwap2WithTransferHook");
308            for len in 0..cpi.len() {
309                assert!(parse_event_cpi(&cpi[..len], EventMetadata::default()).is_none());
310            }
311            let mut invalid = bytes[8..].to_vec();
312            invalid[82] = 3;
313            assert!(parse_swap2_from_data(&invalid, EventMetadata::default(), false).is_none());
314        }
315    }
316}