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        let mut offset = 0usize;
413        let virtual_token_reserves = read_u64_le(data, offset)?;
414        offset += 8;
415        let virtual_quote_reserves = read_u64_le(data, offset)?;
416        offset += 8;
417        let real_token_reserves = read_u64_le(data, offset)?;
418        offset += 8;
419        let real_quote_reserves = read_u64_le(data, offset)?;
420        offset += 8;
421        let token_total_supply = read_u64_le(data, offset)?;
422        offset += 8;
423        let complete = read_u8(data, offset)? != 0;
424        offset += 1;
425        let creator = read_pubkey(data, offset)?;
426        offset += 32;
427        let is_mayhem_mode = read_u8(data, offset)? != 0;
428        offset += 1;
429        let is_cashback_coin = read_u8(data, offset)? != 0;
430        offset += 1;
431        let quote_mint = read_pubkey(data, offset)?;
432
433        return Some(DexEvent::PumpFunBondingCurveAccount(PumpFunBondingCurveAccountEvent {
434            metadata,
435            pubkey: account.pubkey,
436            bonding_curve: PumpFunBondingCurve {
437                virtual_token_reserves,
438                virtual_quote_reserves,
439                real_token_reserves,
440                real_quote_reserves,
441                token_total_supply,
442                complete,
443                creator,
444                is_mayhem_mode,
445                is_cashback_coin,
446                quote_mint,
447            },
448        }));
449    }
450    if !has_discriminator(&account.data, GLOBAL_DISCRIMINATOR) {
451        return None;
452    }
453
454    let data = &account.data[8..];
455    let mut offset = 0usize;
456    let initialized = read_u8(data, offset)? != 0;
457    offset += 1;
458    let authority = read_pubkey(data, offset)?;
459    offset += 32;
460    let fee_recipient = read_pubkey(data, offset)?;
461    offset += 32;
462    let initial_virtual_token_reserves = read_u64_le(data, offset)?;
463    offset += 8;
464    let initial_virtual_sol_reserves = read_u64_le(data, offset)?;
465    offset += 8;
466    let initial_real_token_reserves = read_u64_le(data, offset)?;
467    offset += 8;
468    let token_total_supply = read_u64_le(data, offset)?;
469    offset += 8;
470    let fee_basis_points = read_u64_le(data, offset)?;
471    offset += 8;
472    let withdraw_authority = read_pubkey(data, offset)?;
473    offset += 32;
474    let enable_migrate = read_u8(data, offset)? != 0;
475    offset += 1;
476    let pool_migration_fee = read_u64_le(data, offset)?;
477    offset += 8;
478    let creator_fee_basis_points = read_u64_le(data, offset)?;
479    offset += 8;
480    let mut fee_recipients = [solana_sdk::pubkey::Pubkey::default(); 7];
481    for fee_recipient in &mut fee_recipients {
482        *fee_recipient = read_pubkey(data, offset)?;
483        offset += 32;
484    }
485    let set_creator_authority = read_pubkey(data, offset)?;
486    offset += 32;
487    let admin_set_creator_authority = read_pubkey(data, offset)?;
488    offset += 32;
489    let create_v2_enabled = read_u8(data, offset)? != 0;
490    offset += 1;
491    let whitelist_pda = read_pubkey(data, offset)?;
492    offset += 32;
493    let reserved_fee_recipient = read_pubkey(data, offset)?;
494    offset += 32;
495    let mayhem_mode_enabled = read_u8(data, offset)? != 0;
496    offset += 1;
497    let mut reserved_fee_recipients = [solana_sdk::pubkey::Pubkey::default(); 7];
498    for reserved_fee_recipient in &mut reserved_fee_recipients {
499        *reserved_fee_recipient = read_pubkey(data, offset)?;
500        offset += 32;
501    }
502    let is_cashback_enabled = read_u8(data, offset)? != 0;
503    offset += 1;
504    let buyback_fee_recipients = {
505        let mut keys = [solana_sdk::pubkey::Pubkey::default(); 8];
506        for key in &mut keys {
507            *key = read_pubkey(data, offset)?;
508            offset += 32;
509        }
510        keys
511    };
512    let buyback_basis_points = read_u64_le(data, offset)?;
513    offset += 8;
514    let initial_virtual_quote_reserves = read_u64_le(data, offset)?;
515    offset += 8;
516    let whitelisted_quote_mints = {
517        let mut keys = [solana_sdk::pubkey::Pubkey::default(); 1];
518        keys[0] = read_pubkey(data, offset)?;
519        keys
520    };
521
522    let global = PumpFunGlobal {
523        initialized,
524        authority,
525        fee_recipient,
526        initial_virtual_token_reserves,
527        initial_virtual_sol_reserves,
528        initial_real_token_reserves,
529        token_total_supply,
530        fee_basis_points,
531        withdraw_authority,
532        enable_migrate,
533        pool_migration_fee,
534        creator_fee_basis_points,
535        fee_recipients,
536        set_creator_authority,
537        admin_set_creator_authority,
538        create_v2_enabled,
539        whitelist_pda,
540        reserved_fee_recipient,
541        mayhem_mode_enabled,
542        reserved_fee_recipients,
543        is_cashback_enabled,
544        buyback_fee_recipients,
545        buyback_basis_points,
546        initial_virtual_quote_reserves,
547        whitelisted_quote_mints,
548    };
549
550    Some(DexEvent::PumpFunGlobalAccount(PumpFunGlobalAccountEvent {
551        metadata,
552        pubkey: account.pubkey,
553        global,
554    }))
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use crate::grpc::{EventType, EventTypeFilter};
561    use solana_sdk::pubkey::Pubkey;
562    use solana_sdk::signature::Signature;
563
564    fn metadata() -> EventMetadata {
565        EventMetadata {
566            signature: Signature::default(),
567            slot: 1,
568            tx_index: 0,
569            block_time_us: 0,
570            grpc_recv_us: 0,
571            recent_blockhash: None,
572        }
573    }
574
575    fn push_pk(out: &mut Vec<u8>, seed: u8) -> Pubkey {
576        let key = Pubkey::new_from_array([seed; 32]);
577        out.extend_from_slice(key.as_ref());
578        key
579    }
580
581    #[test]
582    fn parse_pumpfun_bonding_curve_reads_quote_fields() {
583        let mut data = vec![23, 183, 248, 55, 96, 216, 172, 96];
584        data.extend_from_slice(&100u64.to_le_bytes());
585        data.extend_from_slice(&4_292_000_000u64.to_le_bytes());
586        data.extend_from_slice(&200u64.to_le_bytes());
587        data.extend_from_slice(&3_000_000_000u64.to_le_bytes());
588        data.extend_from_slice(&1_000u64.to_le_bytes());
589        data.push(1);
590        let creator = push_pk(&mut data, 7);
591        data.push(1);
592        data.push(0);
593        let quote_mint = push_pk(&mut data, 8);
594        let account = AccountData {
595            pubkey: Pubkey::new_unique(),
596            executable: false,
597            lamports: 0,
598            owner: crate::grpc::program_ids::PUMPFUN_PROGRAM,
599            rent_epoch: 0,
600            data,
601        };
602        let filter = EventTypeFilter::include_only(vec![EventType::AccountPumpFunBondingCurve]);
603
604        let ev = parse_account_unified(&account, metadata(), Some(&filter)).expect("event");
605
606        match ev {
607            DexEvent::PumpFunBondingCurveAccount(e) => {
608                assert_eq!(e.bonding_curve.virtual_quote_reserves, 4_292_000_000);
609                assert_eq!(e.bonding_curve.real_quote_reserves, 3_000_000_000);
610                assert_eq!(e.bonding_curve.creator, creator);
611                assert_eq!(e.bonding_curve.quote_mint, quote_mint);
612                assert!(e.bonding_curve.complete);
613                assert!(e.bonding_curve.is_mayhem_mode);
614                assert!(!e.bonding_curve.is_cashback_coin);
615            }
616            other => panic!("expected bonding curve account, got {other:?}"),
617        }
618    }
619
620    #[test]
621    fn parse_unified_routes_cpmm_and_orca_account_filters() {
622        let mut cpmm = Vec::with_capacity(8 + raydium_cpmm::AMM_CONFIG_SIZE);
623        cpmm.extend_from_slice(raydium_cpmm::discriminators::AMM_CONFIG);
624        cpmm.push(1);
625        cpmm.push(0);
626        cpmm.extend_from_slice(&42u16.to_le_bytes());
627        for value in [10u64, 20, 30, 40] {
628            cpmm.extend_from_slice(&value.to_le_bytes());
629        }
630        push_pk(&mut cpmm, 1);
631        push_pk(&mut cpmm, 2);
632        cpmm.extend_from_slice(&50u64.to_le_bytes());
633        for value in 0u64..15 {
634            cpmm.extend_from_slice(&value.to_le_bytes());
635        }
636        let cpmm_account = AccountData {
637            pubkey: Pubkey::new_unique(),
638            executable: false,
639            lamports: 0,
640            owner: crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
641            rent_epoch: 0,
642            data: cpmm,
643        };
644        let cpmm_filter =
645            EventTypeFilter::include_only(vec![EventType::AccountRaydiumCpmmAmmConfig]);
646        assert!(matches!(
647            parse_account_unified(&cpmm_account, metadata(), Some(&cpmm_filter)),
648            Some(DexEvent::RaydiumCpmmAmmConfigAccount(_))
649        ));
650        let cpmm_mismatch_filter =
651            EventTypeFilter::include_only(vec![EventType::AccountRaydiumCpmmPoolState]);
652        assert!(
653            parse_account_unified(&cpmm_account, metadata(), Some(&cpmm_mismatch_filter)).is_none()
654        );
655
656        let mut fee_tier = Vec::with_capacity(8 + orca_whirlpool::FEE_TIER_SIZE);
657        fee_tier.extend_from_slice(orca_whirlpool::discriminators::FEE_TIER);
658        push_pk(&mut fee_tier, 3);
659        fee_tier.extend_from_slice(&64u16.to_le_bytes());
660        fee_tier.extend_from_slice(&300u16.to_le_bytes());
661        let orca_account = AccountData {
662            pubkey: Pubkey::new_unique(),
663            executable: false,
664            lamports: 0,
665            owner: crate::instr::program_ids::ORCA_WHIRLPOOL_PROGRAM_ID,
666            rent_epoch: 0,
667            data: fee_tier,
668        };
669        let orca_filter = EventTypeFilter::include_only(vec![EventType::AccountOrcaFeeTier]);
670        assert!(matches!(
671            parse_account_unified(&orca_account, metadata(), Some(&orca_filter)),
672            Some(DexEvent::OrcaFeeTierAccount(_))
673        ));
674        let orca_exclude_filter =
675            EventTypeFilter::exclude_types(vec![EventType::AccountOrcaFeeTier]);
676        assert!(
677            parse_account_unified(&orca_account, metadata(), Some(&orca_exclude_filter)).is_none()
678        );
679    }
680
681    #[test]
682    fn parse_unified_filters_token_info_and_token_account_separately() {
683        let mut data = vec![0u8; 82];
684        data[36..44].copy_from_slice(&1_000_000u64.to_le_bytes());
685        data[44] = 6;
686        let account = AccountData {
687            pubkey: Pubkey::new_unique(),
688            executable: false,
689            lamports: 0,
690            owner: Pubkey::new_from_array(spl_token::ID.to_bytes()),
691            rent_epoch: 0,
692            data,
693        };
694
695        let token_info_filter = EventTypeFilter::include_only(vec![EventType::TokenInfo]);
696        assert!(matches!(
697            parse_account_unified(&account, metadata(), Some(&token_info_filter)),
698            Some(DexEvent::TokenInfo(_))
699        ));
700
701        let token_account_filter = EventTypeFilter::include_only(vec![EventType::TokenAccount]);
702        assert!(parse_account_unified(&account, metadata(), Some(&token_account_filter)).is_none());
703    }
704
705    #[test]
706    fn parse_unified_known_dex_owner_does_not_fall_through_to_token_parser() {
707        let mut data = vec![0u8; 82];
708        data[36..44].copy_from_slice(&1_000_000u64.to_le_bytes());
709        data[44] = 6;
710        let account = AccountData {
711            pubkey: Pubkey::new_unique(),
712            executable: false,
713            lamports: 0,
714            owner: crate::grpc::program_ids::PUMPFUN_PROGRAM,
715            rent_epoch: 0,
716            data,
717        };
718
719        assert!(parse_account_unified(&account, metadata(), None).is_none());
720    }
721}