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