Skip to main content

sol_parser_sdk/accounts/
mod.rs

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    // Early filtering based on event type filter
47    if let Some(filter) = event_type_filter {
48        if let Some(ref include_only) = filter.include_only {
49            // Check if any of the account event types are in the include list
50            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        // Check filter for NonceAccount specifically
187        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    // Parse token account (includes both TokenAccount and TokenInfo)
195    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    // 检查 discriminator 以确定账户类型
207    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}