Skip to main content

sol_parser_sdk/accounts/
orca_whirlpool.rs

1//! Orca Whirlpool account parsing.
2
3use crate::core::events::{
4    EventMetadata, OrcaFeeTierAccount, OrcaFeeTierAccountEvent, OrcaPositionAccount,
5    OrcaPositionAccountEvent, OrcaPositionRewardInfo, OrcaTick, OrcaTickArrayAccount,
6    OrcaTickArrayAccountEvent, OrcaWhirlpoolAccount, OrcaWhirlpoolAccountEvent,
7    OrcaWhirlpoolRewardInfo, OrcaWhirlpoolsConfigAccount, OrcaWhirlpoolsConfigAccountEvent,
8};
9use crate::DexEvent;
10
11use super::token::AccountData;
12use super::utils::*;
13
14pub mod discriminators {
15    pub const WHIRLPOOL: &[u8] = &[63, 149, 209, 12, 225, 128, 99, 9];
16    pub const POSITION: &[u8] = &[170, 188, 143, 228, 122, 64, 247, 208];
17    pub const TICK_ARRAY: &[u8] = &[69, 97, 189, 190, 110, 7, 66, 187];
18    pub const FEE_TIER: &[u8] = &[56, 75, 159, 76, 142, 68, 190, 105];
19    pub const WHIRLPOOLS_CONFIG: &[u8] = &[157, 20, 49, 224, 217, 87, 193, 254];
20}
21
22pub const WHIRLPOOL_SIZE: usize = 645;
23pub const POSITION_SIZE: usize = 208;
24pub const TICK_ARRAY_SIZE: usize = 9980;
25pub const FEE_TIER_SIZE: usize = 36;
26pub const WHIRLPOOLS_CONFIG_SIZE: usize = 98;
27const TICK_ARRAY_LEN: usize = 88;
28
29pub fn parse_account(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
30    if is_whirlpool_account(&account.data) {
31        return parse_whirlpool(account, metadata);
32    }
33    if is_position_account(&account.data) {
34        return parse_position(account, metadata);
35    }
36    if is_tick_array_account(&account.data) {
37        return parse_tick_array(account, metadata);
38    }
39    if is_fee_tier_account(&account.data) {
40        return parse_fee_tier(account, metadata);
41    }
42    if is_whirlpools_config_account(&account.data) {
43        return parse_whirlpools_config(account, metadata);
44    }
45    None
46}
47
48pub fn parse_whirlpool(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
49    if account.data.len() < 8 + WHIRLPOOL_SIZE
50        || !has_discriminator(&account.data, discriminators::WHIRLPOOL)
51    {
52        return None;
53    }
54
55    let data = &account.data[8..];
56    let mut offset = 0;
57    let whirlpool = OrcaWhirlpoolAccount {
58        whirlpools_config: read_pubkey_at(data, &mut offset)?,
59        whirlpool_bump: read_u8_at(data, &mut offset)?,
60        tick_spacing: read_u16_at(data, &mut offset)?,
61        tick_spacing_seed: read_u8_array(data, &mut offset)?,
62        fee_rate: read_u16_at(data, &mut offset)?,
63        protocol_fee_rate: read_u16_at(data, &mut offset)?,
64        liquidity: read_u128_at(data, &mut offset)?,
65        sqrt_price: read_u128_at(data, &mut offset)?,
66        tick_current_index: read_i32_at(data, &mut offset)?,
67        protocol_fee_owed_a: read_u64_at(data, &mut offset)?,
68        protocol_fee_owed_b: read_u64_at(data, &mut offset)?,
69        token_mint_a: read_pubkey_at(data, &mut offset)?,
70        token_vault_a: read_pubkey_at(data, &mut offset)?,
71        fee_growth_global_a: read_u128_at(data, &mut offset)?,
72        token_mint_b: read_pubkey_at(data, &mut offset)?,
73        token_vault_b: read_pubkey_at(data, &mut offset)?,
74        fee_growth_global_b: read_u128_at(data, &mut offset)?,
75        reward_last_updated_timestamp: read_u64_at(data, &mut offset)?,
76        reward_infos: [
77            parse_reward_info(data, &mut offset)?,
78            parse_reward_info(data, &mut offset)?,
79            parse_reward_info(data, &mut offset)?,
80        ],
81    };
82
83    Some(DexEvent::OrcaWhirlpoolAccount(Box::new(OrcaWhirlpoolAccountEvent {
84        metadata,
85        pubkey: account.pubkey,
86        whirlpool,
87    })))
88}
89
90pub fn parse_position(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
91    if account.data.len() < 8 + POSITION_SIZE
92        || !has_discriminator(&account.data, discriminators::POSITION)
93    {
94        return None;
95    }
96
97    let data = &account.data[8..];
98    let mut offset = 0;
99    let position = OrcaPositionAccount {
100        whirlpool: read_pubkey_at(data, &mut offset)?,
101        position_mint: read_pubkey_at(data, &mut offset)?,
102        liquidity: read_u128_at(data, &mut offset)?,
103        tick_lower_index: read_i32_at(data, &mut offset)?,
104        tick_upper_index: read_i32_at(data, &mut offset)?,
105        fee_growth_checkpoint_a: read_u128_at(data, &mut offset)?,
106        fee_owed_a: read_u64_at(data, &mut offset)?,
107        fee_growth_checkpoint_b: read_u128_at(data, &mut offset)?,
108        fee_owed_b: read_u64_at(data, &mut offset)?,
109        reward_infos: [
110            parse_position_reward_info(data, &mut offset)?,
111            parse_position_reward_info(data, &mut offset)?,
112            parse_position_reward_info(data, &mut offset)?,
113        ],
114    };
115
116    Some(DexEvent::OrcaPositionAccount(Box::new(OrcaPositionAccountEvent {
117        metadata,
118        pubkey: account.pubkey,
119        position,
120    })))
121}
122
123pub fn parse_tick_array(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
124    if account.data.len() < 8 + TICK_ARRAY_SIZE
125        || !has_discriminator(&account.data, discriminators::TICK_ARRAY)
126    {
127        return None;
128    }
129
130    let data = &account.data[8..];
131    let mut offset = 0;
132    let start_tick_index = read_i32_at(data, &mut offset)?;
133    let mut ticks = Vec::with_capacity(TICK_ARRAY_LEN);
134    for _ in 0..TICK_ARRAY_LEN {
135        ticks.push(parse_tick(data, &mut offset)?);
136    }
137    let tick_array = OrcaTickArrayAccount {
138        start_tick_index,
139        ticks,
140        whirlpool: read_pubkey_at(data, &mut offset)?,
141    };
142
143    Some(DexEvent::OrcaTickArrayAccount(Box::new(OrcaTickArrayAccountEvent {
144        metadata,
145        pubkey: account.pubkey,
146        tick_array,
147    })))
148}
149
150pub fn parse_fee_tier(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
151    if account.data.len() < 8 + FEE_TIER_SIZE
152        || !has_discriminator(&account.data, discriminators::FEE_TIER)
153    {
154        return None;
155    }
156    let data = &account.data[8..];
157    let mut offset = 0;
158    let fee_tier = OrcaFeeTierAccount {
159        whirlpools_config: read_pubkey_at(data, &mut offset)?,
160        tick_spacing: read_u16_at(data, &mut offset)?,
161        default_fee_rate: read_u16_at(data, &mut offset)?,
162    };
163    Some(DexEvent::OrcaFeeTierAccount(Box::new(OrcaFeeTierAccountEvent {
164        metadata,
165        pubkey: account.pubkey,
166        fee_tier,
167    })))
168}
169
170pub fn parse_whirlpools_config(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
171    if account.data.len() < 8 + WHIRLPOOLS_CONFIG_SIZE
172        || !has_discriminator(&account.data, discriminators::WHIRLPOOLS_CONFIG)
173    {
174        return None;
175    }
176    let data = &account.data[8..];
177    let mut offset = 0;
178    let config = OrcaWhirlpoolsConfigAccount {
179        fee_authority: read_pubkey_at(data, &mut offset)?,
180        collect_protocol_fees_authority: read_pubkey_at(data, &mut offset)?,
181        reward_emissions_super_authority: read_pubkey_at(data, &mut offset)?,
182        default_protocol_fee_rate: read_u16_at(data, &mut offset)?,
183    };
184    Some(DexEvent::OrcaWhirlpoolsConfigAccount(Box::new(OrcaWhirlpoolsConfigAccountEvent {
185        metadata,
186        pubkey: account.pubkey,
187        config,
188    })))
189}
190
191pub fn is_whirlpool_account(data: &[u8]) -> bool {
192    has_discriminator(data, discriminators::WHIRLPOOL)
193}
194
195pub fn is_position_account(data: &[u8]) -> bool {
196    has_discriminator(data, discriminators::POSITION)
197}
198
199pub fn is_tick_array_account(data: &[u8]) -> bool {
200    has_discriminator(data, discriminators::TICK_ARRAY)
201}
202
203pub fn is_fee_tier_account(data: &[u8]) -> bool {
204    has_discriminator(data, discriminators::FEE_TIER)
205}
206
207pub fn is_whirlpools_config_account(data: &[u8]) -> bool {
208    has_discriminator(data, discriminators::WHIRLPOOLS_CONFIG)
209}
210
211fn parse_reward_info(data: &[u8], offset: &mut usize) -> Option<OrcaWhirlpoolRewardInfo> {
212    Some(OrcaWhirlpoolRewardInfo {
213        mint: read_pubkey_at(data, offset)?,
214        vault: read_pubkey_at(data, offset)?,
215        authority: read_pubkey_at(data, offset)?,
216        emissions_per_second_x64: read_u128_at(data, offset)?,
217        growth_global_x64: read_u128_at(data, offset)?,
218    })
219}
220
221fn parse_position_reward_info(data: &[u8], offset: &mut usize) -> Option<OrcaPositionRewardInfo> {
222    Some(OrcaPositionRewardInfo {
223        growth_inside_checkpoint: read_u128_at(data, offset)?,
224        amount_owed: read_u64_at(data, offset)?,
225    })
226}
227
228fn parse_tick(data: &[u8], offset: &mut usize) -> Option<OrcaTick> {
229    Some(OrcaTick {
230        initialized: read_bool_at(data, offset)?,
231        liquidity_net: read_i128_at(data, offset)?,
232        liquidity_gross: read_u128_at(data, offset)?,
233        fee_growth_outside_a: read_u128_at(data, offset)?,
234        fee_growth_outside_b: read_u128_at(data, offset)?,
235        reward_growths_outside: read_u128_array(data, offset)?,
236    })
237}
238
239#[inline]
240fn read_pubkey_at(data: &[u8], offset: &mut usize) -> Option<solana_sdk::pubkey::Pubkey> {
241    let value = read_pubkey(data, *offset)?;
242    *offset += 32;
243    Some(value)
244}
245
246#[inline]
247fn read_bool_at(data: &[u8], offset: &mut usize) -> Option<bool> {
248    Some(read_u8_at(data, offset)? != 0)
249}
250
251#[inline]
252fn read_u8_at(data: &[u8], offset: &mut usize) -> Option<u8> {
253    let value = read_u8(data, *offset)?;
254    *offset += 1;
255    Some(value)
256}
257
258#[inline]
259fn read_u16_at(data: &[u8], offset: &mut usize) -> Option<u16> {
260    let value = read_u16_le(data, *offset)?;
261    *offset += 2;
262    Some(value)
263}
264
265#[inline]
266fn read_i32_at(data: &[u8], offset: &mut usize) -> Option<i32> {
267    let value = i32::from_le_bytes(data.get(*offset..*offset + 4)?.try_into().ok()?);
268    *offset += 4;
269    Some(value)
270}
271
272#[inline]
273fn read_u64_at(data: &[u8], offset: &mut usize) -> Option<u64> {
274    let value = read_u64_le(data, *offset)?;
275    *offset += 8;
276    Some(value)
277}
278
279#[inline]
280fn read_u128_at(data: &[u8], offset: &mut usize) -> Option<u128> {
281    let value = u128::from_le_bytes(data.get(*offset..*offset + 16)?.try_into().ok()?);
282    *offset += 16;
283    Some(value)
284}
285
286#[inline]
287fn read_i128_at(data: &[u8], offset: &mut usize) -> Option<i128> {
288    let value = i128::from_le_bytes(data.get(*offset..*offset + 16)?.try_into().ok()?);
289    *offset += 16;
290    Some(value)
291}
292
293#[inline]
294fn read_u8_array<const N: usize>(data: &[u8], offset: &mut usize) -> Option<[u8; N]> {
295    let value = data.get(*offset..*offset + N)?.try_into().ok()?;
296    *offset += N;
297    Some(value)
298}
299
300#[inline]
301fn read_u128_array<const N: usize>(data: &[u8], offset: &mut usize) -> Option<[u128; N]> {
302    let mut values = [0u128; N];
303    for value in &mut values {
304        *value = read_u128_at(data, offset)?;
305    }
306    Some(values)
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use solana_sdk::pubkey::Pubkey;
313
314    fn account(data: Vec<u8>) -> AccountData {
315        AccountData {
316            pubkey: Pubkey::new_unique(),
317            owner: crate::instr::program_ids::ORCA_WHIRLPOOL_PROGRAM_ID,
318            data,
319            executable: false,
320            lamports: 1,
321            rent_epoch: 0,
322        }
323    }
324
325    fn push_pubkey(data: &mut Vec<u8>, byte: u8) -> Pubkey {
326        let key = Pubkey::new_from_array([byte; 32]);
327        data.extend_from_slice(key.as_ref());
328        key
329    }
330
331    #[test]
332    fn parses_whirlpool_account() {
333        let mut data = Vec::with_capacity(8 + WHIRLPOOL_SIZE);
334        data.extend_from_slice(discriminators::WHIRLPOOL);
335        let config = push_pubkey(&mut data, 1);
336        data.push(9);
337        data.extend_from_slice(&64u16.to_le_bytes());
338        data.extend_from_slice(&64u16.to_le_bytes());
339        data.extend_from_slice(&300u16.to_le_bytes());
340        data.extend_from_slice(&100u16.to_le_bytes());
341        data.extend_from_slice(&123u128.to_le_bytes());
342        data.extend_from_slice(&(1u128 << 64).to_le_bytes());
343        data.extend_from_slice(&(-12i32).to_le_bytes());
344        data.extend_from_slice(&1u64.to_le_bytes());
345        data.extend_from_slice(&2u64.to_le_bytes());
346        let token_mint_a = push_pubkey(&mut data, 2);
347        push_pubkey(&mut data, 3);
348        data.extend_from_slice(&10u128.to_le_bytes());
349        let token_mint_b = push_pubkey(&mut data, 4);
350        push_pubkey(&mut data, 5);
351        data.extend_from_slice(&20u128.to_le_bytes());
352        data.extend_from_slice(&999u64.to_le_bytes());
353        for reward in 0..3u8 {
354            push_pubkey(&mut data, 10 + reward * 3);
355            push_pubkey(&mut data, 11 + reward * 3);
356            push_pubkey(&mut data, 12 + reward * 3);
357            data.extend_from_slice(&(1000u128 + reward as u128).to_le_bytes());
358            data.extend_from_slice(&(2000u128 + reward as u128).to_le_bytes());
359        }
360
361        let event = parse_whirlpool(&account(data), EventMetadata::default()).expect("event");
362        let DexEvent::OrcaWhirlpoolAccount(event) = event else {
363            panic!("wrong event type");
364        };
365        assert_eq!(event.whirlpool.whirlpools_config, config);
366        assert_eq!(event.whirlpool.tick_current_index, -12);
367        assert_eq!(event.whirlpool.token_mint_a, token_mint_a);
368        assert_eq!(event.whirlpool.token_mint_b, token_mint_b);
369        assert_eq!(event.whirlpool.reward_infos[2].growth_global_x64, 2002);
370    }
371
372    #[test]
373    fn parses_position_and_fee_tier_accounts() {
374        let mut position = Vec::with_capacity(8 + POSITION_SIZE);
375        position.extend_from_slice(discriminators::POSITION);
376        let whirlpool = push_pubkey(&mut position, 1);
377        let mint = push_pubkey(&mut position, 2);
378        position.extend_from_slice(&777u128.to_le_bytes());
379        position.extend_from_slice(&(-20i32).to_le_bytes());
380        position.extend_from_slice(&30i32.to_le_bytes());
381        position.extend_from_slice(&10u128.to_le_bytes());
382        position.extend_from_slice(&11u64.to_le_bytes());
383        position.extend_from_slice(&12u128.to_le_bytes());
384        position.extend_from_slice(&13u64.to_le_bytes());
385        for i in 0..3u64 {
386            position.extend_from_slice(&(100u128 + i as u128).to_le_bytes());
387            position.extend_from_slice(&(200u64 + i).to_le_bytes());
388        }
389
390        let event = parse_position(&account(position), EventMetadata::default()).expect("event");
391        let DexEvent::OrcaPositionAccount(event) = event else {
392            panic!("wrong event type");
393        };
394        assert_eq!(event.position.whirlpool, whirlpool);
395        assert_eq!(event.position.position_mint, mint);
396        assert_eq!(event.position.liquidity, 777);
397        assert_eq!(event.position.reward_infos[1].amount_owed, 201);
398
399        let mut fee_tier = Vec::with_capacity(8 + FEE_TIER_SIZE);
400        fee_tier.extend_from_slice(discriminators::FEE_TIER);
401        let cfg = push_pubkey(&mut fee_tier, 9);
402        fee_tier.extend_from_slice(&128u16.to_le_bytes());
403        fee_tier.extend_from_slice(&500u16.to_le_bytes());
404        let event = parse_fee_tier(&account(fee_tier), EventMetadata::default()).expect("event");
405        let DexEvent::OrcaFeeTierAccount(event) = event else {
406            panic!("wrong event type");
407        };
408        assert_eq!(event.fee_tier.whirlpools_config, cfg);
409        assert_eq!(event.fee_tier.tick_spacing, 128);
410        assert_eq!(event.fee_tier.default_fee_rate, 500);
411    }
412
413    #[test]
414    fn parses_tick_array_and_config_accounts() {
415        let mut tick_array = Vec::with_capacity(8 + TICK_ARRAY_SIZE);
416        tick_array.extend_from_slice(discriminators::TICK_ARRAY);
417        tick_array.extend_from_slice(&(-704i32).to_le_bytes());
418        for i in 0..TICK_ARRAY_LEN {
419            tick_array.push((i % 2) as u8);
420            tick_array.extend_from_slice(&(i as i128 - 44).to_le_bytes());
421            tick_array.extend_from_slice(&(1000u128 + i as u128).to_le_bytes());
422            tick_array.extend_from_slice(&(2000u128 + i as u128).to_le_bytes());
423            tick_array.extend_from_slice(&(3000u128 + i as u128).to_le_bytes());
424            for j in 0..3 {
425                tick_array.extend_from_slice(&(4000u128 + i as u128 + j).to_le_bytes());
426            }
427        }
428        let whirlpool = push_pubkey(&mut tick_array, 8);
429        let event =
430            parse_tick_array(&account(tick_array), EventMetadata::default()).expect("event");
431        let DexEvent::OrcaTickArrayAccount(event) = event else {
432            panic!("wrong event type");
433        };
434        assert_eq!(event.tick_array.whirlpool, whirlpool);
435        assert_eq!(event.tick_array.ticks[87].liquidity_gross, 1087);
436
437        let mut config = Vec::with_capacity(8 + WHIRLPOOLS_CONFIG_SIZE);
438        config.extend_from_slice(discriminators::WHIRLPOOLS_CONFIG);
439        let fee_authority = push_pubkey(&mut config, 1);
440        push_pubkey(&mut config, 2);
441        push_pubkey(&mut config, 3);
442        config.extend_from_slice(&250u16.to_le_bytes());
443        let event =
444            parse_whirlpools_config(&account(config), EventMetadata::default()).expect("event");
445        let DexEvent::OrcaWhirlpoolsConfigAccount(event) = event else {
446            panic!("wrong event type");
447        };
448        assert_eq!(event.config.fee_authority, fee_authority);
449        assert_eq!(event.config.default_protocol_fee_rate, 250);
450    }
451}