1pub mod liquidity_snapshot;
2pub mod nonce;
3pub mod orca_whirlpool;
4pub mod program_ids;
5pub mod pumpswap;
6pub mod raydium_clmm;
7pub mod raydium_cpmm;
8pub mod rpc_wallet;
9pub mod token;
10pub mod utils;
11use crate::core::events::EventMetadata;
12use crate::grpc::EventTypeFilter;
13use crate::DexEvent;
14pub use nonce::parse_nonce_account;
15use program_ids::*;
16pub use pumpswap::{
17 parse_global_config as parse_pumpswap_global_config, parse_pool as parse_pumpswap_pool,
18};
19pub use rpc_wallet::rpc_resolve_user_wallet_pubkey;
20pub use token::parse_token_account;
21pub use token::AccountData;
22pub use utils::*;
23
24#[inline(always)]
25fn filter_parsed_event(
26 event: Option<DexEvent>,
27 event_type_filter: Option<&EventTypeFilter>,
28) -> Option<DexEvent> {
29 let event = event?;
30 if event_type_filter.map(|f| f.should_include_dex_event(&event)).unwrap_or(true) {
31 Some(event)
32 } else {
33 None
34 }
35}
36
37pub fn parse_account_unified(
38 account: &AccountData,
39 metadata: EventMetadata,
40 event_type_filter: Option<&EventTypeFilter>,
41) -> Option<DexEvent> {
42 if account.data.is_empty() {
43 return None;
44 }
45
46 if let Some(filter) = event_type_filter {
48 if let Some(ref include_only) = filter.include_only {
49 let should_parse = include_only.iter().any(|t| {
51 use crate::grpc::EventType;
52 matches!(
53 t,
54 EventType::AccountLiquiditySnapshot
55 | EventType::TokenAccount
56 | EventType::TokenInfo
57 | EventType::NonceAccount
58 | EventType::AccountPumpFunGlobal
59 | EventType::AccountPumpFunBondingCurve
60 | EventType::AccountPumpFunFeeConfig
61 | EventType::AccountPumpFunSharingConfig
62 | EventType::AccountPumpFunGlobalVolumeAccumulator
63 | EventType::AccountPumpFunUserVolumeAccumulator
64 | EventType::AccountPumpSwapGlobalConfig
65 | EventType::AccountPumpSwapPool
66 | EventType::AccountRaydiumClmmAmmConfig
67 | EventType::AccountRaydiumClmmPoolState
68 | EventType::AccountRaydiumClmmTickArrayState
69 | EventType::AccountRaydiumCpmmAmmConfig
70 | EventType::AccountRaydiumCpmmPoolState
71 | EventType::AccountRaydiumCpmmCreatorFeeShare
72 | EventType::AccountOrcaWhirlpool
73 | EventType::AccountOrcaPosition
74 | EventType::AccountOrcaTickArray
75 | EventType::AccountOrcaFeeTier
76 | EventType::AccountOrcaWhirlpoolsConfig
77 )
78 });
79 if !should_parse {
80 return None;
81 }
82 }
83 }
84
85 if event_type_filter
86 .is_none_or(|f| f.should_include(crate::grpc::EventType::AccountLiquiditySnapshot))
87 {
88 if let Some(event) = liquidity_snapshot::parse_account(account, metadata.clone()) {
89 return Some(event);
90 }
91 }
92 if account.owner == PUMPSWAP_PROGRAM_ID {
93 let should_parse = event_type_filter.is_none_or(|filter| {
94 filter.should_include(crate::grpc::EventType::AccountPumpSwapGlobalConfig)
95 || filter.should_include(crate::grpc::EventType::AccountPumpSwapPool)
96 });
97 if should_parse {
98 let event = filter_parsed_event(
99 parse_pumpswap_account(account, metadata.clone()),
100 event_type_filter,
101 );
102 if event.is_some() {
103 return event;
104 }
105 }
106 return None;
107 }
108 if account.owner == crate::instr::program_ids::RAYDIUM_CLMM_PROGRAM_ID {
109 let should_parse = event_type_filter.is_none_or(|filter| {
110 filter.should_include(crate::grpc::EventType::AccountRaydiumClmmAmmConfig)
111 || filter.should_include(crate::grpc::EventType::AccountRaydiumClmmPoolState)
112 || filter.should_include(crate::grpc::EventType::AccountRaydiumClmmTickArrayState)
113 });
114 if should_parse {
115 let event = filter_parsed_event(
116 raydium_clmm::parse_account(account, metadata.clone()),
117 event_type_filter,
118 );
119 if event.is_some() {
120 return event;
121 }
122 }
123 return None;
124 }
125 if account.owner == crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID {
126 let should_parse = event_type_filter.is_none_or(|filter| {
127 filter.should_include(crate::grpc::EventType::AccountRaydiumCpmmAmmConfig)
128 || filter.should_include(crate::grpc::EventType::AccountRaydiumCpmmPoolState)
129 || filter.should_include(crate::grpc::EventType::AccountRaydiumCpmmCreatorFeeShare)
130 });
131 if should_parse {
132 let event = filter_parsed_event(
133 raydium_cpmm::parse_account(account, metadata.clone()),
134 event_type_filter,
135 );
136 if event.is_some() {
137 return event;
138 }
139 }
140 return None;
141 }
142 if account.owner == crate::instr::program_ids::ORCA_WHIRLPOOL_PROGRAM_ID {
143 let should_parse = event_type_filter.is_none_or(|filter| {
144 filter.should_include(crate::grpc::EventType::AccountOrcaWhirlpool)
145 || filter.should_include(crate::grpc::EventType::AccountOrcaPosition)
146 || filter.should_include(crate::grpc::EventType::AccountOrcaTickArray)
147 || filter.should_include(crate::grpc::EventType::AccountOrcaFeeTier)
148 || filter.should_include(crate::grpc::EventType::AccountOrcaWhirlpoolsConfig)
149 });
150 if should_parse {
151 let event = filter_parsed_event(
152 orca_whirlpool::parse_account(account, metadata.clone()),
153 event_type_filter,
154 );
155 if event.is_some() {
156 return event;
157 }
158 }
159 return None;
160 }
161 if account.owner == crate::grpc::program_ids::PUMPFUN_PROGRAM
162 || account.owner == crate::instr::program_ids::PUMP_FEES_PROGRAM_ID
163 {
164 let should_parse = event_type_filter.is_none_or(|filter| {
165 filter.should_include(crate::grpc::EventType::AccountPumpFunGlobal)
166 || filter.should_include(crate::grpc::EventType::AccountPumpFunBondingCurve)
167 || filter.should_include(crate::grpc::EventType::AccountPumpFunFeeConfig)
168 || filter.should_include(crate::grpc::EventType::AccountPumpFunSharingConfig)
169 || filter
170 .should_include(crate::grpc::EventType::AccountPumpFunGlobalVolumeAccumulator)
171 || filter
172 .should_include(crate::grpc::EventType::AccountPumpFunUserVolumeAccumulator)
173 });
174 if should_parse {
175 let event = filter_parsed_event(
176 parse_pumpfun_account(account, metadata.clone()),
177 event_type_filter,
178 );
179 if event.is_some() {
180 return event;
181 }
182 }
183 return None;
184 }
185 if nonce::is_nonce_account(&account.data) {
186 if let Some(filter) = event_type_filter {
188 if !filter.should_include(crate::grpc::EventType::NonceAccount) {
189 return None;
190 }
191 }
192 return parse_nonce_account(account, metadata);
193 }
194 if let Some(filter) = event_type_filter {
196 let includes_token = filter.should_include(crate::grpc::EventType::TokenAccount)
197 || filter.should_include(crate::grpc::EventType::TokenInfo);
198 if !includes_token {
199 return None;
200 }
201 }
202 filter_parsed_event(parse_token_account(account, metadata), event_type_filter)
203}
204
205fn parse_pumpswap_account(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
206 if pumpswap::is_global_config_account(&account.data) {
208 return pumpswap::parse_global_config(account, metadata);
209 }
210 if pumpswap::is_pool_account(&account.data) {
211 return pumpswap::parse_pool(account, metadata);
212 }
213 None
214}
215
216fn parse_pumpfun_account(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
217 use crate::core::events::{
218 PumpFeesConfigStatus, PumpFeesFeeTier, PumpFeesFees, PumpFeesShareholder,
219 PumpFunBondingCurve, PumpFunBondingCurveAccountEvent, PumpFunFeeConfig,
220 PumpFunFeeConfigAccountEvent, PumpFunGlobal, PumpFunGlobalAccountEvent,
221 PumpFunGlobalVolumeAccumulator, PumpFunGlobalVolumeAccumulatorAccountEvent,
222 PumpFunSharingConfig, PumpFunSharingConfigAccountEvent, PumpFunUserVolumeAccumulator,
223 PumpFunUserVolumeAccumulatorAccountEvent,
224 };
225
226 const GLOBAL_DISCRIMINATOR: &[u8; 8] = &[167, 232, 232, 177, 200, 108, 114, 127];
227 const BONDING_CURVE_DISCRIMINATOR: &[u8; 8] = &[23, 183, 248, 55, 96, 216, 172, 96];
228 const FEE_CONFIG_DISCRIMINATOR: &[u8; 8] = &[143, 52, 146, 187, 219, 123, 76, 155];
229 const GLOBAL_VOLUME_ACCUMULATOR_DISCRIMINATOR: &[u8; 8] =
230 &[202, 42, 246, 43, 142, 190, 30, 255];
231 const SHARING_CONFIG_DISCRIMINATOR: &[u8; 8] = &[216, 74, 9, 0, 56, 140, 93, 75];
232 const USER_VOLUME_ACCUMULATOR_DISCRIMINATOR: &[u8; 8] = &[86, 255, 112, 14, 102, 53, 154, 250];
233 const MAX_FEE_TIERS: usize = 64;
234 const MAX_SHAREHOLDERS: usize = 64;
235
236 fn read_i64(data: &[u8], offset: &mut usize) -> Option<i64> {
237 let value = i64::from_le_bytes(data.get(*offset..*offset + 8)?.try_into().ok()?);
238 *offset += 8;
239 Some(value)
240 }
241 fn read_u32(data: &[u8], offset: &mut usize) -> Option<u32> {
242 let value = u32::from_le_bytes(data.get(*offset..*offset + 4)?.try_into().ok()?);
243 *offset += 4;
244 Some(value)
245 }
246 fn read_u64(data: &[u8], offset: &mut usize) -> Option<u64> {
247 let value = read_u64_le(data, *offset)?;
248 *offset += 8;
249 Some(value)
250 }
251 fn read_u128(data: &[u8], offset: &mut usize) -> Option<u128> {
252 let value = u128::from_le_bytes(data.get(*offset..*offset + 16)?.try_into().ok()?);
253 *offset += 16;
254 Some(value)
255 }
256 fn read_pk(data: &[u8], offset: &mut usize) -> Option<solana_sdk::pubkey::Pubkey> {
257 let value = read_pubkey(data, *offset)?;
258 *offset += 32;
259 Some(value)
260 }
261 fn read_fees(data: &[u8], offset: &mut usize) -> Option<PumpFeesFees> {
262 Some(PumpFeesFees {
263 lp_fee_bps: read_u64(data, offset)?,
264 protocol_fee_bps: read_u64(data, offset)?,
265 creator_fee_bps: read_u64(data, offset)?,
266 })
267 }
268 fn read_fee_tiers(data: &[u8], offset: &mut usize) -> Option<Vec<PumpFeesFeeTier>> {
269 let len = read_u32(data, offset)? as usize;
270 if len > MAX_FEE_TIERS {
271 return None;
272 }
273 let mut out = Vec::with_capacity(len);
274 for _ in 0..len {
275 out.push(PumpFeesFeeTier {
276 market_cap_lamports_threshold: read_u128(data, offset)?,
277 fees: read_fees(data, offset)?,
278 });
279 }
280 Some(out)
281 }
282 fn read_shareholders(data: &[u8], offset: &mut usize) -> Option<Vec<PumpFeesShareholder>> {
283 let len = read_u32(data, offset)? as usize;
284 if len > MAX_SHAREHOLDERS {
285 return None;
286 }
287 let mut out = Vec::with_capacity(len);
288 for _ in 0..len {
289 let address = read_pk(data, offset)?;
290 let share_bps = u16::from_le_bytes(data.get(*offset..*offset + 2)?.try_into().ok()?);
291 *offset += 2;
292 out.push(PumpFeesShareholder { address, share_bps });
293 }
294 Some(out)
295 }
296 fn read_status(data: &[u8], offset: &mut usize) -> Option<PumpFeesConfigStatus> {
297 let value = *data.get(*offset)?;
298 *offset += 1;
299 match value {
300 0 => Some(PumpFeesConfigStatus::Paused),
301 1 => Some(PumpFeesConfigStatus::Active),
302 _ => None,
303 }
304 }
305
306 if has_discriminator(&account.data, FEE_CONFIG_DISCRIMINATOR) {
307 let data = &account.data[8..];
308 let mut offset = 0usize;
309 let fee_config = PumpFunFeeConfig {
310 bump: *data.get(offset)?,
311 admin: {
312 offset += 1;
313 read_pk(data, &mut offset)?
314 },
315 flat_fees: read_fees(data, &mut offset)?,
316 fee_tiers: read_fee_tiers(data, &mut offset)?,
317 stable_fee_tiers: read_fee_tiers(data, &mut offset)?,
318 exotic_flat_fees: if data.len() == offset {
319 PumpFeesFees::default()
320 } else {
321 read_fees(data, &mut offset)?
322 },
323 };
324 return Some(DexEvent::PumpFunFeeConfigAccount(PumpFunFeeConfigAccountEvent {
325 metadata,
326 pubkey: account.pubkey,
327 fee_config,
328 }));
329 }
330
331 if has_discriminator(&account.data, SHARING_CONFIG_DISCRIMINATOR) {
332 let data = &account.data[8..];
333 let mut offset = 0usize;
334 let bump = *data.get(offset)?;
335 offset += 1;
336 let version = *data.get(offset)?;
337 offset += 1;
338 let status = read_status(data, &mut offset)?;
339 let mint = read_pk(data, &mut offset)?;
340 let admin = read_pk(data, &mut offset)?;
341 let admin_revoked = *data.get(offset)? != 0;
342 offset += 1;
343 let shareholders = read_shareholders(data, &mut offset)?;
344 return Some(DexEvent::PumpFunSharingConfigAccount(PumpFunSharingConfigAccountEvent {
345 metadata,
346 pubkey: account.pubkey,
347 sharing_config: PumpFunSharingConfig {
348 bump,
349 version,
350 status,
351 mint,
352 admin,
353 admin_revoked,
354 shareholders,
355 },
356 }));
357 }
358
359 if has_discriminator(&account.data, GLOBAL_VOLUME_ACCUMULATOR_DISCRIMINATOR) {
360 let data = &account.data[8..];
361 let mut offset = 0usize;
362 let start_time = read_i64(data, &mut offset)?;
363 let end_time = read_i64(data, &mut offset)?;
364 let seconds_in_a_day = read_i64(data, &mut offset)?;
365 let mint = read_pk(data, &mut offset)?;
366 let mut total_token_supply = [0u64; 30];
367 for value in &mut total_token_supply {
368 *value = read_u64(data, &mut offset)?;
369 }
370 let mut sol_volumes = [0u64; 30];
371 for value in &mut sol_volumes {
372 *value = read_u64(data, &mut offset)?;
373 }
374 return Some(DexEvent::PumpFunGlobalVolumeAccumulatorAccount(
375 PumpFunGlobalVolumeAccumulatorAccountEvent {
376 metadata,
377 pubkey: account.pubkey,
378 global_volume_accumulator: PumpFunGlobalVolumeAccumulator {
379 start_time,
380 end_time,
381 seconds_in_a_day,
382 mint,
383 total_token_supply,
384 sol_volumes,
385 },
386 },
387 ));
388 }
389
390 if has_discriminator(&account.data, USER_VOLUME_ACCUMULATOR_DISCRIMINATOR) {
391 let data = &account.data[8..];
392 let mut offset = 0usize;
393 let user = read_pk(data, &mut offset)?;
394 let needs_claim = *data.get(offset)? != 0;
395 offset += 1;
396 let total_unclaimed_tokens = read_u64(data, &mut offset)?;
397 let total_claimed_tokens = read_u64(data, &mut offset)?;
398 let current_sol_volume = read_u64(data, &mut offset)?;
399 let last_update_timestamp = read_i64(data, &mut offset)?;
400 let has_total_claimed_tokens = *data.get(offset)? != 0;
401 offset += 1;
402 let cashback_earned = read_u64(data, &mut offset)?;
403 let total_cashback_claimed = read_u64(data, &mut offset)?;
404 let stable_cashback_earned = read_u64(data, &mut offset)?;
405 let total_stable_cashback_claimed = read_u64(data, &mut offset)?;
406 return Some(DexEvent::PumpFunUserVolumeAccumulatorAccount(
407 PumpFunUserVolumeAccumulatorAccountEvent {
408 metadata,
409 pubkey: account.pubkey,
410 user_volume_accumulator: PumpFunUserVolumeAccumulator {
411 user,
412 needs_claim,
413 total_unclaimed_tokens,
414 total_claimed_tokens,
415 current_sol_volume,
416 last_update_timestamp,
417 has_total_claimed_tokens,
418 cashback_earned,
419 total_cashback_claimed,
420 stable_cashback_earned,
421 total_stable_cashback_claimed,
422 },
423 },
424 ));
425 }
426 if has_discriminator(&account.data, BONDING_CURVE_DISCRIMINATOR) {
427 let data = &account.data[8..];
428 const LEGACY_BODY_LEN: usize = 107;
429 const CREATOR_FEE_BODY_LEN: usize = 116;
430 const HOLDER_REWARD_BODY_LEN: usize = 117;
431 if data.len() != LEGACY_BODY_LEN
432 && data.len() != CREATOR_FEE_BODY_LEN
433 && data.len() < HOLDER_REWARD_BODY_LEN
434 {
435 return None;
436 }
437 let mut offset = 0usize;
438 let virtual_token_reserves = read_u64_le(data, offset)?;
439 offset += 8;
440 let virtual_quote_reserves = read_u64_le(data, offset)?;
441 offset += 8;
442 let real_token_reserves = read_u64_le(data, offset)?;
443 offset += 8;
444 let real_quote_reserves = read_u64_le(data, offset)?;
445 offset += 8;
446 let token_total_supply = read_u64_le(data, offset)?;
447 offset += 8;
448 let complete = read_u8(data, offset)? != 0;
449 offset += 1;
450 let creator = read_pubkey(data, offset)?;
451 offset += 32;
452 let is_mayhem_mode = read_u8(data, offset)? != 0;
453 offset += 1;
454 let is_cashback_coin = read_u8(data, offset)? != 0;
455 offset += 1;
456 let quote_mint = read_pubkey(data, offset)?;
457 offset += 32;
458 let creator_fee_bps = read_u64_le(data, offset).unwrap_or_default();
459 offset += 8;
460 let can_edit_creator_fee = read_u8(data, offset).unwrap_or_default() != 0;
461 offset += 1;
462 let is_holder_reward = read_u8(data, offset).unwrap_or_default() != 0;
463
464 return Some(DexEvent::PumpFunBondingCurveAccount(PumpFunBondingCurveAccountEvent {
465 metadata,
466 pubkey: account.pubkey,
467 bonding_curve: PumpFunBondingCurve {
468 creator_fee: read_u64_le(data, 117).unwrap_or_default(),
469 protocol_fees: read_u64_le(data, 125).unwrap_or_default(),
470 depth: read_u8(data, 133).unwrap_or_default(),
471 initial_virtual_quote_reserves: read_u64_le(data, 134).unwrap_or_default(),
472 post_complete_base_out: read_u64_le(data, 142).unwrap_or_default(),
473 post_complete_quote_in: read_u64_le(data, 150).unwrap_or_default(),
474 virtual_token_reserves,
475 virtual_quote_reserves,
476 real_token_reserves,
477 real_quote_reserves,
478 token_total_supply,
479 complete,
480 creator,
481 is_mayhem_mode,
482 is_cashback_coin,
483 quote_mint,
484 creator_fee_bps,
485 can_edit_creator_fee,
486 is_holder_reward,
487 },
488 }));
489 }
490 if !has_discriminator(&account.data, GLOBAL_DISCRIMINATOR) {
491 return None;
492 }
493
494 let data = &account.data[8..];
495 let mut offset = 0usize;
496 let initialized = read_u8(data, offset)? != 0;
497 offset += 1;
498 let authority = read_pubkey(data, offset)?;
499 offset += 32;
500 let fee_recipient = read_pubkey(data, offset)?;
501 offset += 32;
502 let initial_virtual_token_reserves = read_u64_le(data, offset)?;
503 offset += 8;
504 let initial_virtual_sol_reserves = read_u64_le(data, offset)?;
505 offset += 8;
506 let initial_real_token_reserves = read_u64_le(data, offset)?;
507 offset += 8;
508 let token_total_supply = read_u64_le(data, offset)?;
509 offset += 8;
510 let fee_basis_points = read_u64_le(data, offset)?;
511 offset += 8;
512 let withdraw_authority = read_pubkey(data, offset)?;
513 offset += 32;
514 let enable_migrate = read_u8(data, offset)? != 0;
515 offset += 1;
516 let pool_migration_fee = read_u64_le(data, offset)?;
517 offset += 8;
518 let creator_fee_basis_points = read_u64_le(data, offset)?;
519 offset += 8;
520 let mut fee_recipients = [solana_sdk::pubkey::Pubkey::default(); 7];
521 for fee_recipient in &mut fee_recipients {
522 *fee_recipient = read_pubkey(data, offset)?;
523 offset += 32;
524 }
525 let set_creator_authority = read_pubkey(data, offset)?;
526 offset += 32;
527 let admin_set_creator_authority = read_pubkey(data, offset)?;
528 offset += 32;
529 let create_v2_enabled = read_u8(data, offset)? != 0;
530 offset += 1;
531 let whitelist_pda = read_pubkey(data, offset)?;
532 offset += 32;
533 let reserved_fee_recipient = read_pubkey(data, offset)?;
534 offset += 32;
535 let mayhem_mode_enabled = read_u8(data, offset)? != 0;
536 offset += 1;
537 let mut reserved_fee_recipients = [solana_sdk::pubkey::Pubkey::default(); 7];
538 for reserved_fee_recipient in &mut reserved_fee_recipients {
539 *reserved_fee_recipient = read_pubkey(data, offset)?;
540 offset += 32;
541 }
542 let is_cashback_enabled = read_u8(data, offset)? != 0;
543 offset += 1;
544 let buyback_fee_recipients = {
545 let mut keys = [solana_sdk::pubkey::Pubkey::default(); 8];
546 for key in &mut keys {
547 *key = read_pubkey(data, offset)?;
548 offset += 32;
549 }
550 keys
551 };
552 let buyback_basis_points = read_u64_le(data, offset)?;
553 offset += 8;
554 let initial_virtual_quote_reserves = read_u64_le(data, offset)?;
555 offset += 8;
556 let whitelisted_quote_mints = {
557 let mut keys = [solana_sdk::pubkey::Pubkey::default(); 1];
558 keys[0] = read_pubkey(data, offset)?;
559 keys
560 };
561
562 let global = PumpFunGlobal {
563 creator_fee_configurable: data.get(1037).is_some_and(|b| *b == 1),
564 max_configurable_creator_fee_bps: data
565 .get(1038..1046)
566 .map(|b| u64::from_le_bytes(b.try_into().unwrap()))
567 .unwrap_or(0),
568 holder_reward_claim_authority: data
569 .get(1046..1078)
570 .map(|b| solana_sdk::pubkey::Pubkey::new_from_array(b.try_into().unwrap()))
571 .unwrap_or_default(),
572 is_holder_reward_enabled: data.get(1078).is_some_and(|b| *b == 1),
573 max_curve_depth: data.get(1079).copied().unwrap_or(0),
574
575 initialized,
576 authority,
577 fee_recipient,
578 initial_virtual_token_reserves,
579 initial_virtual_sol_reserves,
580 initial_real_token_reserves,
581 token_total_supply,
582 fee_basis_points,
583 withdraw_authority,
584 enable_migrate,
585 pool_migration_fee,
586 creator_fee_basis_points,
587 fee_recipients,
588 set_creator_authority,
589 admin_set_creator_authority,
590 create_v2_enabled,
591 whitelist_pda,
592 reserved_fee_recipient,
593 mayhem_mode_enabled,
594 reserved_fee_recipients,
595 is_cashback_enabled,
596 buyback_fee_recipients,
597 buyback_basis_points,
598 initial_virtual_quote_reserves,
599 whitelisted_quote_mints,
600 };
601
602 Some(DexEvent::PumpFunGlobalAccount(PumpFunGlobalAccountEvent {
603 metadata,
604 pubkey: account.pubkey,
605 global,
606 }))
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612 use crate::grpc::{EventType, EventTypeFilter};
613 use solana_sdk::pubkey::Pubkey;
614 use solana_sdk::signature::Signature;
615
616 fn metadata() -> EventMetadata {
617 EventMetadata {
618 signature: Signature::default(),
619 slot: 1,
620 tx_index: 0,
621 block_time_us: 0,
622 grpc_recv_us: 0,
623 recent_blockhash: None,
624 }
625 }
626
627 fn push_pk(out: &mut Vec<u8>, seed: u8) -> Pubkey {
628 let key = Pubkey::new_from_array([seed; 32]);
629 out.extend_from_slice(key.as_ref());
630 key
631 }
632
633 #[test]
634 fn parse_pumpfun_bonding_curve_reads_quote_fields() {
635 let mut data = vec![23, 183, 248, 55, 96, 216, 172, 96];
636 data.extend_from_slice(&100u64.to_le_bytes());
637 data.extend_from_slice(&4_292_000_000u64.to_le_bytes());
638 data.extend_from_slice(&200u64.to_le_bytes());
639 data.extend_from_slice(&3_000_000_000u64.to_le_bytes());
640 data.extend_from_slice(&1_000u64.to_le_bytes());
641 data.push(1);
642 let creator = push_pk(&mut data, 7);
643 data.push(1);
644 data.push(0);
645 let quote_mint = push_pk(&mut data, 8);
646 data.extend_from_slice(&250u64.to_le_bytes());
647 data.push(1);
648 data.push(1);
649 let account = AccountData {
650 pubkey: Pubkey::new_unique(),
651 executable: false,
652 lamports: 0,
653 owner: crate::grpc::program_ids::PUMPFUN_PROGRAM,
654 rent_epoch: 0,
655 data,
656 };
657 let filter = EventTypeFilter::include_only(vec![EventType::AccountPumpFunBondingCurve]);
658
659 let ev = parse_account_unified(&account, metadata(), Some(&filter)).expect("event");
660
661 match ev {
662 DexEvent::PumpFunBondingCurveAccount(e) => {
663 assert_eq!(e.bonding_curve.virtual_quote_reserves, 4_292_000_000);
664 assert_eq!(e.bonding_curve.real_quote_reserves, 3_000_000_000);
665 assert_eq!(e.bonding_curve.creator, creator);
666 assert_eq!(e.bonding_curve.quote_mint, quote_mint);
667 assert!(e.bonding_curve.complete);
668 assert!(e.bonding_curve.is_mayhem_mode);
669 assert!(!e.bonding_curve.is_cashback_coin);
670 assert_eq!(e.bonding_curve.creator_fee_bps, 250);
671 assert!(e.bonding_curve.can_edit_creator_fee);
672 assert!(e.bonding_curve.is_holder_reward);
673 }
674 other => panic!("expected bonding curve account, got {other:?}"),
675 }
676
677 for body_len in 108..116 {
678 let mut partial = account.clone();
679 partial.data.truncate(8 + body_len);
680 assert!(parse_account_unified(&partial, metadata(), Some(&filter)).is_none());
681 }
682
683 let mut creator_fee_layout = account.clone();
684 creator_fee_layout.data.truncate(8 + 116);
685 assert!(parse_account_unified(&creator_fee_layout, metadata(), Some(&filter)).is_some());
686 }
687
688 #[test]
689 fn parse_unified_routes_cpmm_and_orca_account_filters() {
690 let mut cpmm = Vec::with_capacity(8 + raydium_cpmm::AMM_CONFIG_SIZE);
691 cpmm.extend_from_slice(raydium_cpmm::discriminators::AMM_CONFIG);
692 cpmm.push(1);
693 cpmm.push(0);
694 cpmm.extend_from_slice(&42u16.to_le_bytes());
695 for value in [10u64, 20, 30, 40] {
696 cpmm.extend_from_slice(&value.to_le_bytes());
697 }
698 push_pk(&mut cpmm, 1);
699 push_pk(&mut cpmm, 2);
700 cpmm.extend_from_slice(&50u64.to_le_bytes());
701 for value in 0u64..15 {
702 cpmm.extend_from_slice(&value.to_le_bytes());
703 }
704 let cpmm_account = AccountData {
705 pubkey: Pubkey::new_unique(),
706 executable: false,
707 lamports: 0,
708 owner: crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
709 rent_epoch: 0,
710 data: cpmm,
711 };
712 let cpmm_filter =
713 EventTypeFilter::include_only(vec![EventType::AccountRaydiumCpmmAmmConfig]);
714 assert!(matches!(
715 parse_account_unified(&cpmm_account, metadata(), Some(&cpmm_filter)),
716 Some(DexEvent::RaydiumCpmmAmmConfigAccount(_))
717 ));
718 let cpmm_mismatch_filter =
719 EventTypeFilter::include_only(vec![EventType::AccountRaydiumCpmmPoolState]);
720 assert!(
721 parse_account_unified(&cpmm_account, metadata(), Some(&cpmm_mismatch_filter)).is_none()
722 );
723
724 let mut fee_tier = Vec::with_capacity(8 + orca_whirlpool::FEE_TIER_SIZE);
725 fee_tier.extend_from_slice(orca_whirlpool::discriminators::FEE_TIER);
726 push_pk(&mut fee_tier, 3);
727 fee_tier.extend_from_slice(&64u16.to_le_bytes());
728 fee_tier.extend_from_slice(&300u16.to_le_bytes());
729 let orca_account = AccountData {
730 pubkey: Pubkey::new_unique(),
731 executable: false,
732 lamports: 0,
733 owner: crate::instr::program_ids::ORCA_WHIRLPOOL_PROGRAM_ID,
734 rent_epoch: 0,
735 data: fee_tier,
736 };
737 let orca_filter = EventTypeFilter::include_only(vec![EventType::AccountOrcaFeeTier]);
738 assert!(matches!(
739 parse_account_unified(&orca_account, metadata(), Some(&orca_filter)),
740 Some(DexEvent::OrcaFeeTierAccount(_))
741 ));
742 let orca_exclude_filter =
743 EventTypeFilter::exclude_types(vec![EventType::AccountOrcaFeeTier]);
744 assert!(
745 parse_account_unified(&orca_account, metadata(), Some(&orca_exclude_filter)).is_none()
746 );
747 }
748
749 #[test]
750 fn parse_unified_filters_token_info_and_token_account_separately() {
751 let mut data = vec![0u8; 82];
752 data[36..44].copy_from_slice(&1_000_000u64.to_le_bytes());
753 data[44] = 6;
754 let account = AccountData {
755 pubkey: Pubkey::new_unique(),
756 executable: false,
757 lamports: 0,
758 owner: Pubkey::new_from_array(spl_token::ID.to_bytes()),
759 rent_epoch: 0,
760 data,
761 };
762
763 let token_info_filter = EventTypeFilter::include_only(vec![EventType::TokenInfo]);
764 assert!(matches!(
765 parse_account_unified(&account, metadata(), Some(&token_info_filter)),
766 Some(DexEvent::TokenInfo(_))
767 ));
768
769 let token_account_filter = EventTypeFilter::include_only(vec![EventType::TokenAccount]);
770 assert!(parse_account_unified(&account, metadata(), Some(&token_account_filter)).is_none());
771 }
772
773 #[test]
774 fn parse_unified_known_dex_owner_does_not_fall_through_to_token_parser() {
775 let mut data = vec![0u8; 82];
776 data[36..44].copy_from_slice(&1_000_000u64.to_le_bytes());
777 data[44] = 6;
778 let account = AccountData {
779 pubkey: Pubkey::new_unique(),
780 executable: false,
781 lamports: 0,
782 owner: crate::grpc::program_ids::PUMPFUN_PROGRAM,
783 rent_epoch: 0,
784 data,
785 };
786
787 assert!(parse_account_unified(&account, metadata(), None).is_none());
788 }
789}