1use 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}