1pub mod nonce;
2pub mod program_ids;
3pub mod pumpswap;
4pub mod raydium_clmm;
5pub mod rpc_wallet;
6pub mod token;
7pub mod utils;
8use crate::core::events::EventMetadata;
9use crate::grpc::EventTypeFilter;
10use crate::DexEvent;
11pub use nonce::parse_nonce_account;
12use program_ids::*;
13pub use pumpswap::{
14 parse_global_config as parse_pumpswap_global_config, parse_pool as parse_pumpswap_pool,
15};
16pub use rpc_wallet::rpc_resolve_user_wallet_pubkey;
17pub use token::parse_token_account;
18pub use token::AccountData;
19pub use utils::*;
20
21pub fn parse_account_unified(
22 account: &AccountData,
23 metadata: EventMetadata,
24 event_type_filter: Option<&EventTypeFilter>,
25) -> Option<DexEvent> {
26 if account.data.is_empty() {
27 return None;
28 }
29
30 if let Some(filter) = event_type_filter {
32 if let Some(ref include_only) = filter.include_only {
33 let should_parse = include_only.iter().any(|t| {
35 use crate::grpc::EventType;
36 matches!(
37 t,
38 EventType::TokenAccount
39 | EventType::NonceAccount
40 | EventType::AccountPumpFunGlobal
41 | EventType::AccountPumpFunBondingCurve
42 | EventType::AccountPumpFunFeeConfig
43 | EventType::AccountPumpFunSharingConfig
44 | EventType::AccountPumpFunGlobalVolumeAccumulator
45 | EventType::AccountPumpFunUserVolumeAccumulator
46 | EventType::AccountPumpSwapGlobalConfig
47 | EventType::AccountPumpSwapPool
48 | EventType::AccountRaydiumClmmAmmConfig
49 | EventType::AccountRaydiumClmmPoolState
50 | EventType::AccountRaydiumClmmTickArrayState
51 )
52 });
53 if !should_parse {
54 return None;
55 }
56 }
57 }
58
59 if account.owner == PUMPSWAP_PROGRAM_ID {
60 let should_parse = event_type_filter.map_or(true, |filter| {
61 filter.should_include(crate::grpc::EventType::AccountPumpSwapGlobalConfig)
62 || filter.should_include(crate::grpc::EventType::AccountPumpSwapPool)
63 });
64 if should_parse {
65 let event = parse_pumpswap_account(account, metadata.clone());
66 if event.is_some() {
67 return event;
68 }
69 }
70 }
71 if account.owner == crate::instr::program_ids::RAYDIUM_CLMM_PROGRAM_ID {
72 let should_parse = event_type_filter.map_or(true, |filter| {
73 filter.should_include(crate::grpc::EventType::AccountRaydiumClmmAmmConfig)
74 || filter.should_include(crate::grpc::EventType::AccountRaydiumClmmPoolState)
75 || filter.should_include(crate::grpc::EventType::AccountRaydiumClmmTickArrayState)
76 });
77 if should_parse {
78 let event = raydium_clmm::parse_account(account, metadata.clone());
79 if event.is_some() {
80 return event;
81 }
82 }
83 }
84 if account.owner == crate::grpc::program_ids::PUMPFUN_PROGRAM
85 || account.owner == crate::instr::program_ids::PUMP_FEES_PROGRAM_ID
86 {
87 let should_parse = event_type_filter.map_or(true, |filter| {
88 filter.should_include(crate::grpc::EventType::AccountPumpFunGlobal)
89 || filter.should_include(crate::grpc::EventType::AccountPumpFunBondingCurve)
90 || filter.should_include(crate::grpc::EventType::AccountPumpFunFeeConfig)
91 || filter.should_include(crate::grpc::EventType::AccountPumpFunSharingConfig)
92 || filter
93 .should_include(crate::grpc::EventType::AccountPumpFunGlobalVolumeAccumulator)
94 || filter
95 .should_include(crate::grpc::EventType::AccountPumpFunUserVolumeAccumulator)
96 });
97 if should_parse {
98 let event = parse_pumpfun_account(account, metadata.clone());
99 if event.is_some() {
100 return event;
101 }
102 }
103 }
104 if nonce::is_nonce_account(&account.data) {
105 if let Some(filter) = event_type_filter {
107 if !filter.should_include(crate::grpc::EventType::NonceAccount) {
108 return None;
109 }
110 }
111 return parse_nonce_account(account, metadata);
112 }
113 if let Some(filter) = event_type_filter {
115 let includes_token = filter.should_include(crate::grpc::EventType::TokenAccount);
116 if !includes_token {
117 return None;
118 }
119 }
120 return parse_token_account(account, metadata);
121}
122
123fn parse_pumpswap_account(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
124 if pumpswap::is_global_config_account(&account.data) {
126 return pumpswap::parse_global_config(account, metadata);
127 }
128 if pumpswap::is_pool_account(&account.data) {
129 return pumpswap::parse_pool(account, metadata);
130 }
131 None
132}
133
134fn parse_pumpfun_account(account: &AccountData, metadata: EventMetadata) -> Option<DexEvent> {
135 use crate::core::events::{
136 PumpFeesConfigStatus, PumpFeesFeeTier, PumpFeesFees, PumpFeesShareholder,
137 PumpFunBondingCurve, PumpFunBondingCurveAccountEvent, PumpFunFeeConfig,
138 PumpFunFeeConfigAccountEvent, PumpFunGlobal, PumpFunGlobalAccountEvent,
139 PumpFunGlobalVolumeAccumulator, PumpFunGlobalVolumeAccumulatorAccountEvent,
140 PumpFunSharingConfig, PumpFunSharingConfigAccountEvent, PumpFunUserVolumeAccumulator,
141 PumpFunUserVolumeAccumulatorAccountEvent,
142 };
143
144 const GLOBAL_DISCRIMINATOR: &[u8; 8] = &[167, 232, 232, 177, 200, 108, 114, 127];
145 const BONDING_CURVE_DISCRIMINATOR: &[u8; 8] = &[23, 183, 248, 55, 96, 216, 172, 96];
146 const FEE_CONFIG_DISCRIMINATOR: &[u8; 8] = &[143, 52, 146, 187, 219, 123, 76, 155];
147 const GLOBAL_VOLUME_ACCUMULATOR_DISCRIMINATOR: &[u8; 8] =
148 &[202, 42, 246, 43, 142, 190, 30, 255];
149 const SHARING_CONFIG_DISCRIMINATOR: &[u8; 8] = &[216, 74, 9, 0, 56, 140, 93, 75];
150 const USER_VOLUME_ACCUMULATOR_DISCRIMINATOR: &[u8; 8] = &[86, 255, 112, 14, 102, 53, 154, 250];
151 const MAX_FEE_TIERS: usize = 64;
152 const MAX_SHAREHOLDERS: usize = 64;
153
154 fn read_i64(data: &[u8], offset: &mut usize) -> Option<i64> {
155 let value = i64::from_le_bytes(data.get(*offset..*offset + 8)?.try_into().ok()?);
156 *offset += 8;
157 Some(value)
158 }
159 fn read_u32(data: &[u8], offset: &mut usize) -> Option<u32> {
160 let value = u32::from_le_bytes(data.get(*offset..*offset + 4)?.try_into().ok()?);
161 *offset += 4;
162 Some(value)
163 }
164 fn read_u64(data: &[u8], offset: &mut usize) -> Option<u64> {
165 let value = read_u64_le(data, *offset)?;
166 *offset += 8;
167 Some(value)
168 }
169 fn read_u128(data: &[u8], offset: &mut usize) -> Option<u128> {
170 let value = u128::from_le_bytes(data.get(*offset..*offset + 16)?.try_into().ok()?);
171 *offset += 16;
172 Some(value)
173 }
174 fn read_pk(data: &[u8], offset: &mut usize) -> Option<solana_sdk::pubkey::Pubkey> {
175 let value = read_pubkey(data, *offset)?;
176 *offset += 32;
177 Some(value)
178 }
179 fn read_fees(data: &[u8], offset: &mut usize) -> Option<PumpFeesFees> {
180 Some(PumpFeesFees {
181 lp_fee_bps: read_u64(data, offset)?,
182 protocol_fee_bps: read_u64(data, offset)?,
183 creator_fee_bps: read_u64(data, offset)?,
184 })
185 }
186 fn read_fee_tiers(data: &[u8], offset: &mut usize) -> Option<Vec<PumpFeesFeeTier>> {
187 let len = read_u32(data, offset)? as usize;
188 if len > MAX_FEE_TIERS {
189 return None;
190 }
191 let mut out = Vec::with_capacity(len);
192 for _ in 0..len {
193 out.push(PumpFeesFeeTier {
194 market_cap_lamports_threshold: read_u128(data, offset)?,
195 fees: read_fees(data, offset)?,
196 });
197 }
198 Some(out)
199 }
200 fn read_shareholders(data: &[u8], offset: &mut usize) -> Option<Vec<PumpFeesShareholder>> {
201 let len = read_u32(data, offset)? as usize;
202 if len > MAX_SHAREHOLDERS {
203 return None;
204 }
205 let mut out = Vec::with_capacity(len);
206 for _ in 0..len {
207 let address = read_pk(data, offset)?;
208 let share_bps = u16::from_le_bytes(data.get(*offset..*offset + 2)?.try_into().ok()?);
209 *offset += 2;
210 out.push(PumpFeesShareholder { address, share_bps });
211 }
212 Some(out)
213 }
214 fn read_status(data: &[u8], offset: &mut usize) -> Option<PumpFeesConfigStatus> {
215 let value = *data.get(*offset)?;
216 *offset += 1;
217 match value {
218 0 => Some(PumpFeesConfigStatus::Paused),
219 1 => Some(PumpFeesConfigStatus::Active),
220 _ => None,
221 }
222 }
223
224 if has_discriminator(&account.data, FEE_CONFIG_DISCRIMINATOR) {
225 let data = &account.data[8..];
226 let mut offset = 0usize;
227 let fee_config = PumpFunFeeConfig {
228 bump: *data.get(offset)?,
229 admin: {
230 offset += 1;
231 read_pk(data, &mut offset)?
232 },
233 flat_fees: read_fees(data, &mut offset)?,
234 fee_tiers: read_fee_tiers(data, &mut offset)?,
235 stable_fee_tiers: read_fee_tiers(data, &mut offset)?,
236 };
237 return Some(DexEvent::PumpFunFeeConfigAccount(PumpFunFeeConfigAccountEvent {
238 metadata,
239 pubkey: account.pubkey,
240 fee_config,
241 }));
242 }
243
244 if has_discriminator(&account.data, SHARING_CONFIG_DISCRIMINATOR) {
245 let data = &account.data[8..];
246 let mut offset = 0usize;
247 let bump = *data.get(offset)?;
248 offset += 1;
249 let version = *data.get(offset)?;
250 offset += 1;
251 let status = read_status(data, &mut offset)?;
252 let mint = read_pk(data, &mut offset)?;
253 let admin = read_pk(data, &mut offset)?;
254 let admin_revoked = *data.get(offset)? != 0;
255 offset += 1;
256 let shareholders = read_shareholders(data, &mut offset)?;
257 return Some(DexEvent::PumpFunSharingConfigAccount(PumpFunSharingConfigAccountEvent {
258 metadata,
259 pubkey: account.pubkey,
260 sharing_config: PumpFunSharingConfig {
261 bump,
262 version,
263 status,
264 mint,
265 admin,
266 admin_revoked,
267 shareholders,
268 },
269 }));
270 }
271
272 if has_discriminator(&account.data, GLOBAL_VOLUME_ACCUMULATOR_DISCRIMINATOR) {
273 let data = &account.data[8..];
274 let mut offset = 0usize;
275 let start_time = read_i64(data, &mut offset)?;
276 let end_time = read_i64(data, &mut offset)?;
277 let seconds_in_a_day = read_i64(data, &mut offset)?;
278 let mint = read_pk(data, &mut offset)?;
279 let mut total_token_supply = [0u64; 30];
280 for value in &mut total_token_supply {
281 *value = read_u64(data, &mut offset)?;
282 }
283 let mut sol_volumes = [0u64; 30];
284 for value in &mut sol_volumes {
285 *value = read_u64(data, &mut offset)?;
286 }
287 return Some(DexEvent::PumpFunGlobalVolumeAccumulatorAccount(
288 PumpFunGlobalVolumeAccumulatorAccountEvent {
289 metadata,
290 pubkey: account.pubkey,
291 global_volume_accumulator: PumpFunGlobalVolumeAccumulator {
292 start_time,
293 end_time,
294 seconds_in_a_day,
295 mint,
296 total_token_supply,
297 sol_volumes,
298 },
299 },
300 ));
301 }
302
303 if has_discriminator(&account.data, USER_VOLUME_ACCUMULATOR_DISCRIMINATOR) {
304 let data = &account.data[8..];
305 let mut offset = 0usize;
306 let user = read_pk(data, &mut offset)?;
307 let needs_claim = *data.get(offset)? != 0;
308 offset += 1;
309 let total_unclaimed_tokens = read_u64(data, &mut offset)?;
310 let total_claimed_tokens = read_u64(data, &mut offset)?;
311 let current_sol_volume = read_u64(data, &mut offset)?;
312 let last_update_timestamp = read_i64(data, &mut offset)?;
313 let has_total_claimed_tokens = *data.get(offset)? != 0;
314 offset += 1;
315 let cashback_earned = read_u64(data, &mut offset)?;
316 let total_cashback_claimed = read_u64(data, &mut offset)?;
317 let stable_cashback_earned = read_u64(data, &mut offset)?;
318 let total_stable_cashback_claimed = read_u64(data, &mut offset)?;
319 return Some(DexEvent::PumpFunUserVolumeAccumulatorAccount(
320 PumpFunUserVolumeAccumulatorAccountEvent {
321 metadata,
322 pubkey: account.pubkey,
323 user_volume_accumulator: PumpFunUserVolumeAccumulator {
324 user,
325 needs_claim,
326 total_unclaimed_tokens,
327 total_claimed_tokens,
328 current_sol_volume,
329 last_update_timestamp,
330 has_total_claimed_tokens,
331 cashback_earned,
332 total_cashback_claimed,
333 stable_cashback_earned,
334 total_stable_cashback_claimed,
335 },
336 },
337 ));
338 }
339 if has_discriminator(&account.data, BONDING_CURVE_DISCRIMINATOR) {
340 let data = &account.data[8..];
341 let mut offset = 0usize;
342 let virtual_token_reserves = read_u64_le(data, offset)?;
343 offset += 8;
344 let virtual_quote_reserves = read_u64_le(data, offset)?;
345 offset += 8;
346 let real_token_reserves = read_u64_le(data, offset)?;
347 offset += 8;
348 let real_quote_reserves = read_u64_le(data, offset)?;
349 offset += 8;
350 let token_total_supply = read_u64_le(data, offset)?;
351 offset += 8;
352 let complete = read_u8(data, offset)? != 0;
353 offset += 1;
354 let creator = read_pubkey(data, offset)?;
355 offset += 32;
356 let is_mayhem_mode = read_u8(data, offset)? != 0;
357 offset += 1;
358 let is_cashback_coin = read_u8(data, offset)? != 0;
359 offset += 1;
360 let quote_mint = read_pubkey(data, offset)?;
361
362 return Some(DexEvent::PumpFunBondingCurveAccount(PumpFunBondingCurveAccountEvent {
363 metadata,
364 pubkey: account.pubkey,
365 bonding_curve: PumpFunBondingCurve {
366 virtual_token_reserves,
367 virtual_quote_reserves,
368 real_token_reserves,
369 real_quote_reserves,
370 token_total_supply,
371 complete,
372 creator,
373 is_mayhem_mode,
374 is_cashback_coin,
375 quote_mint,
376 },
377 }));
378 }
379 if !has_discriminator(&account.data, GLOBAL_DISCRIMINATOR) {
380 return None;
381 }
382
383 let data = &account.data[8..];
384 let mut offset = 0usize;
385 let initialized = read_u8(data, offset)? != 0;
386 offset += 1;
387 let authority = read_pubkey(data, offset)?;
388 offset += 32;
389 let fee_recipient = read_pubkey(data, offset)?;
390 offset += 32;
391 let initial_virtual_token_reserves = read_u64_le(data, offset)?;
392 offset += 8;
393 let initial_virtual_sol_reserves = read_u64_le(data, offset)?;
394 offset += 8;
395 let initial_real_token_reserves = read_u64_le(data, offset)?;
396 offset += 8;
397 let token_total_supply = read_u64_le(data, offset)?;
398 offset += 8;
399 let fee_basis_points = read_u64_le(data, offset)?;
400 offset += 8;
401 let withdraw_authority = read_pubkey(data, offset)?;
402 offset += 32;
403 let enable_migrate = read_u8(data, offset)? != 0;
404 offset += 1;
405 let pool_migration_fee = read_u64_le(data, offset)?;
406 offset += 8;
407 let creator_fee_basis_points = read_u64_le(data, offset)?;
408 offset += 8;
409 let mut fee_recipients = [solana_sdk::pubkey::Pubkey::default(); 7];
410 for i in 0..7 {
411 fee_recipients[i] = read_pubkey(data, offset)?;
412 offset += 32;
413 }
414 let set_creator_authority = read_pubkey(data, offset)?;
415 offset += 32;
416 let admin_set_creator_authority = read_pubkey(data, offset)?;
417 offset += 32;
418 let create_v2_enabled = read_u8(data, offset)? != 0;
419 offset += 1;
420 let whitelist_pda = read_pubkey(data, offset)?;
421 offset += 32;
422 let reserved_fee_recipient = read_pubkey(data, offset)?;
423 offset += 32;
424 let mayhem_mode_enabled = read_u8(data, offset)? != 0;
425 offset += 1;
426 let mut reserved_fee_recipients = [solana_sdk::pubkey::Pubkey::default(); 7];
427 for i in 0..7 {
428 reserved_fee_recipients[i] = read_pubkey(data, offset)?;
429 offset += 32;
430 }
431 let is_cashback_enabled = read_u8(data, offset)? != 0;
432 offset += 1;
433 let buyback_fee_recipients = {
434 let mut keys = [solana_sdk::pubkey::Pubkey::default(); 8];
435 for i in 0..8 {
436 keys[i] = read_pubkey(data, offset)?;
437 offset += 32;
438 }
439 keys
440 };
441 let buyback_basis_points = read_u64_le(data, offset)?;
442 offset += 8;
443 let initial_virtual_quote_reserves = read_u64_le(data, offset)?;
444 offset += 8;
445 let whitelisted_quote_mints = {
446 let mut keys = [solana_sdk::pubkey::Pubkey::default(); 1];
447 keys[0] = read_pubkey(data, offset)?;
448 keys
449 };
450
451 let global = PumpFunGlobal {
452 initialized,
453 authority,
454 fee_recipient,
455 initial_virtual_token_reserves,
456 initial_virtual_sol_reserves,
457 initial_real_token_reserves,
458 token_total_supply,
459 fee_basis_points,
460 withdraw_authority,
461 enable_migrate,
462 pool_migration_fee,
463 creator_fee_basis_points,
464 fee_recipients,
465 set_creator_authority,
466 admin_set_creator_authority,
467 create_v2_enabled,
468 whitelist_pda,
469 reserved_fee_recipient,
470 mayhem_mode_enabled,
471 reserved_fee_recipients,
472 is_cashback_enabled,
473 buyback_fee_recipients,
474 buyback_basis_points,
475 initial_virtual_quote_reserves,
476 whitelisted_quote_mints,
477 };
478
479 Some(DexEvent::PumpFunGlobalAccount(PumpFunGlobalAccountEvent {
480 metadata,
481 pubkey: account.pubkey,
482 global,
483 }))
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::grpc::{EventType, EventTypeFilter};
490 use solana_sdk::pubkey::Pubkey;
491 use solana_sdk::signature::Signature;
492
493 fn metadata() -> EventMetadata {
494 EventMetadata {
495 signature: Signature::default(),
496 slot: 1,
497 tx_index: 0,
498 block_time_us: 0,
499 grpc_recv_us: 0,
500 recent_blockhash: None,
501 }
502 }
503
504 fn push_pk(out: &mut Vec<u8>, seed: u8) -> Pubkey {
505 let key = Pubkey::new_from_array([seed; 32]);
506 out.extend_from_slice(key.as_ref());
507 key
508 }
509
510 #[test]
511 fn parse_pumpfun_bonding_curve_reads_quote_fields() {
512 let mut data = vec![23, 183, 248, 55, 96, 216, 172, 96];
513 data.extend_from_slice(&100u64.to_le_bytes());
514 data.extend_from_slice(&4_292_000_000u64.to_le_bytes());
515 data.extend_from_slice(&200u64.to_le_bytes());
516 data.extend_from_slice(&3_000_000_000u64.to_le_bytes());
517 data.extend_from_slice(&1_000u64.to_le_bytes());
518 data.push(1);
519 let creator = push_pk(&mut data, 7);
520 data.push(1);
521 data.push(0);
522 let quote_mint = push_pk(&mut data, 8);
523 let account = AccountData {
524 pubkey: Pubkey::new_unique(),
525 executable: false,
526 lamports: 0,
527 owner: crate::grpc::program_ids::PUMPFUN_PROGRAM,
528 rent_epoch: 0,
529 data,
530 };
531 let filter = EventTypeFilter::include_only(vec![EventType::AccountPumpFunBondingCurve]);
532
533 let ev = parse_account_unified(&account, metadata(), Some(&filter)).expect("event");
534
535 match ev {
536 DexEvent::PumpFunBondingCurveAccount(e) => {
537 assert_eq!(e.bonding_curve.virtual_quote_reserves, 4_292_000_000);
538 assert_eq!(e.bonding_curve.real_quote_reserves, 3_000_000_000);
539 assert_eq!(e.bonding_curve.creator, creator);
540 assert_eq!(e.bonding_curve.quote_mint, quote_mint);
541 assert!(e.bonding_curve.complete);
542 assert!(e.bonding_curve.is_mayhem_mode);
543 assert!(!e.bonding_curve.is_cashback_coin);
544 }
545 other => panic!("expected bonding curve account, got {other:?}"),
546 }
547 }
548}