Skip to main content

sol_parser_sdk/accounts/
mod.rs

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