Skip to main content

pump_rust_client/sdk/
mod.rs

1use anchor_lang::solana_program::pubkey::Pubkey;
2
3use crate::math::amm::{
4    self as amm_math, AmmContext, BuyBaseInputResult, BuyQuoteInputResult, SellBaseInputResult,
5};
6use crate::math::bonding_curve as bc_math;
7use crate::math::utils::{add_slippage, sub_slippage};
8pub use crate::math::QuoteError;
9use crate::math::QuoteResult;
10use crate::pda;
11use crate::state::pump_amm::{GlobalConfig, Pool};
12use crate::state::{BondingCurve, FeeConfig, Global};
13
14mod pump_amm_ix;
15mod pump_legacy;
16mod pump_v2;
17mod trade_tx;
18
19/// Quote output: `amount` is tokens-out or lamports-out; `min_out` / `max_input` carry slippage where relevant.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct Quote {
22    pub amount: u64,
23    pub min_out: u64,
24    pub input_amount_used: u64,
25    pub max_input: u64,
26}
27
28/// Force bonding-curve vs AMM routing for [`PumpSdk::trade_tx_instructions_with_venue`].
29#[derive(Clone, Copy, Debug)]
30pub enum TradeVenue<'a> {
31    BondingCurve {
32        global: &'a Global,
33        bonding_curve: &'a BondingCurve,
34    },
35    Amm {
36        pool: Pubkey,
37        amm_global: &'a GlobalConfig,
38        pool_state: &'a Pool,
39    },
40}
41
42/// AMM quote inputs: live pool reserves, or estimated reserves from a completed curve (not on-chain AMM state).
43#[derive(Clone, Copy, Debug)]
44pub enum AmmQuoteSource<'a> {
45    Pool {
46        pool: &'a Pool,
47        base_reserve: u64,
48        quote_reserve: u64,
49        base_mint_supply: u64,
50    },
51    BondingCurveComplete {
52        global: &'a Global,
53        bonding_curve: &'a BondingCurve,
54        base_mint: &'a Pubkey,
55        base_mint_supply: u64,
56    },
57}
58
59/// [`PumpSdk::trade_tx_instructions_with_venue`] parameters. wSOL uses wrap/unwrap;
60/// amounts come from the `*_quote_*` helpers.
61#[derive(Clone, Copy, Debug)]
62pub struct TradeTxWithVenueParams<'a> {
63    pub mint: Pubkey,
64    pub base_token_program: Pubkey,
65    pub quote_token_program: Pubkey,
66    pub user: Pubkey,
67    pub is_buy: bool,
68    pub venue: TradeVenue<'a>,
69    pub base_amount: u64,
70    pub sol_amount_threshold: u64,
71}
72
73/// `pump_amm` pool after migration (`BondingCurve::complete`).
74#[derive(Clone, Copy, Debug)]
75pub struct PumpPoolCtx<'a> {
76    pub pool: Pubkey,
77    pub amm_global: &'a GlobalConfig,
78    pub pool_state: &'a Pool,
79}
80
81/// [`PumpSdk::trade_tx_instructions`] parameters: routes from [`BondingCurve::complete`].
82/// `pump_pool` is required when the curve is complete.
83#[derive(Clone, Copy, Debug)]
84pub struct TradeTxParams<'a> {
85    pub mint: Pubkey,
86    pub base_token_program: Pubkey,
87    pub quote_token_program: Pubkey,
88    pub user: Pubkey,
89    pub is_buy: bool,
90    pub base_amount: u64,
91    pub sol_amount_threshold: u64,
92    pub pump_global: &'a Global,
93    pub bonding_curve: &'a BondingCurve,
94    pub pump_pool: Option<PumpPoolCtx<'a>>,
95}
96
97/// `pump_amm` pool quote inputs — like [`PumpPoolCtx`] but with the reserves and
98/// fee config the AMM math needs.
99#[derive(Clone, Copy, Debug)]
100pub struct PumpPoolQuoteCtx<'a> {
101    pub amm_global: &'a GlobalConfig,
102    pub amm_fee_config: Option<&'a FeeConfig>,
103    pub pool_state: &'a Pool,
104    pub base_reserve: u64,
105    pub quote_reserve: u64,
106}
107
108/// [`PumpSdk::quote_trade`] parameters: routes from [`BondingCurve::complete`], mirroring
109/// [`TradeTxParams`]. `pump_pool` is required when the curve is complete.
110#[derive(Clone, Copy, Debug)]
111pub struct TradeQuoteParams<'a> {
112    pub is_buy: bool,
113    pub base_amount: u64,
114    pub slippage_bps: u16,
115    pub base_mint_supply: u64,
116    pub pump_global: &'a Global,
117    pub pump_fee_config: Option<&'a FeeConfig>,
118    pub bonding_curve: &'a BondingCurve,
119    pub pump_pool: Option<PumpPoolQuoteCtx<'a>>,
120}
121
122/// [`PumpSdk::create_coin_instructions`]. New mint keypair must co-sign. The
123/// initial buy is in the chosen `quote_mint` (`Pubkey::default()` → wSOL).
124///
125/// `tokenized_agent_buyback_bps`: when `Some(bps)`, an `agent_initialize`
126/// instruction (pump_agent_payments) is appended that registers `creator` as
127/// the agent payment authority with the given buyback rate (basis points;
128/// must be ≤ 10000). `None` skips it.
129#[derive(Clone, Debug)]
130pub struct CreateCoinParams<'a> {
131    pub mint: Pubkey,
132    pub user: Pubkey,
133    pub creator: Pubkey,
134    pub name: String,
135    pub symbol: String,
136    pub uri: String,
137    pub mayhem_mode: bool,
138    pub cashback: bool,
139    pub quote_mint: Pubkey,
140    pub global: &'a Global,
141    pub token_amount: u64,
142    pub max_quote_tokens: u64,
143    pub tokenized_agent_buyback_bps: Option<u16>,
144}
145
146/// Instruction builders (no RPC).
147#[derive(Clone, Copy, Debug, Default)]
148pub struct PumpSdk;
149
150impl PumpSdk {
151    pub const fn new() -> Self {
152        Self
153    }
154
155    /// Random protocol fee recipient from `Global` (deduped).
156    pub fn fee_recipient_from_pump_global(global: &Global, is_mayhem: bool) -> Option<Pubkey> {
157        let mut candidates = Vec::new();
158        let (primary, pool) = if is_mayhem {
159            (
160                global.reserved_fee_recipient,
161                global.reserved_fee_recipients.as_slice(),
162            )
163        } else {
164            (global.fee_recipient, global.fee_recipients.as_slice())
165        };
166        if primary != Pubkey::default() {
167            candidates.push(primary);
168        }
169        candidates.extend(pool.iter().copied().filter(|p| *p != Pubkey::default()));
170        Self::dedupe_sorted(&mut candidates);
171        Self::pick_random_pubkey(&candidates)
172    }
173
174    /// Random buyback fee recipient from `Global`.
175    pub fn buyback_fee_recipient_from_pump_global(global: &Global) -> Option<Pubkey> {
176        let mut candidates: Vec<Pubkey> = global
177            .buyback_fee_recipients
178            .iter()
179            .copied()
180            .filter(|p| *p != Pubkey::default())
181            .collect();
182        Self::dedupe_sorted(&mut candidates);
183        Self::pick_random_pubkey(&candidates)
184    }
185
186    /// Random protocol fee recipient from AMM `GlobalConfig`.
187    pub fn protocol_fee_recipient_from_amm_global(global_config: &GlobalConfig) -> Option<Pubkey> {
188        let mut candidates: Vec<Pubkey> = global_config
189            .protocol_fee_recipients
190            .iter()
191            .copied()
192            .filter(|p| *p != Pubkey::default())
193            .collect();
194        Self::dedupe_sorted(&mut candidates);
195        Self::pick_random_pubkey(&candidates)
196    }
197
198    /// Random buyback fee recipient from AMM `GlobalConfig`.
199    pub fn buyback_fee_recipient_from_amm_global(global_config: &GlobalConfig) -> Option<Pubkey> {
200        let mut candidates: Vec<Pubkey> = global_config
201            .buyback_fee_recipients
202            .iter()
203            .copied()
204            .filter(|p| *p != Pubkey::default())
205            .collect();
206        Self::dedupe_sorted(&mut candidates);
207        Self::pick_random_pubkey(&candidates)
208    }
209
210    /// `(fee_recipient, buyback_fee_recipient)` from `Global`. `None` when either pool is empty.
211    pub fn pump_fee_recipients_pair(global: &Global, is_mayhem: bool) -> Option<(Pubkey, Pubkey)> {
212        Some((
213            Self::fee_recipient_from_pump_global(global, is_mayhem)?,
214            Self::buyback_fee_recipient_from_pump_global(global)?,
215        ))
216    }
217
218    /// `(protocol_fee_recipient, buyback_fee_recipient)` from AMM `GlobalConfig`. `None` when either pool is empty.
219    pub fn amm_fee_recipients_pair(global_config: &GlobalConfig) -> Option<(Pubkey, Pubkey)> {
220        Some((
221            Self::protocol_fee_recipient_from_amm_global(global_config)?,
222            Self::buyback_fee_recipient_from_amm_global(global_config)?,
223        ))
224    }
225
226    fn dedupe_sorted(keys: &mut Vec<Pubkey>) {
227        keys.sort();
228        keys.dedup();
229    }
230
231    fn pick_random_pubkey(candidates: &[Pubkey]) -> Option<Pubkey> {
232        if candidates.is_empty() {
233            return None;
234        }
235        Some(candidates[fastrand::usize(..candidates.len())])
236    }
237
238    fn map_amm_quote_source<R>(
239        global_config: &GlobalConfig,
240        fee_config: Option<&FeeConfig>,
241        source: AmmQuoteSource<'_>,
242        with: impl FnOnce(AmmContext<'_>) -> QuoteResult<R>,
243    ) -> QuoteResult<R> {
244        match source {
245            AmmQuoteSource::Pool {
246                pool,
247                base_reserve,
248                quote_reserve,
249                base_mint_supply,
250            } => {
251                let ctx = AmmContext {
252                    global_config,
253                    fee_config,
254                    base_mint: &pool.base_mint,
255                    pool_creator: &pool.creator,
256                    coin_creator: &pool.coin_creator,
257                    base_reserve,
258                    quote_reserve,
259                    base_mint_supply,
260                };
261                with(ctx)
262            }
263            AmmQuoteSource::BondingCurveComplete {
264                global,
265                bonding_curve,
266                base_mint,
267                base_mint_supply,
268            } => {
269                let (base_reserve, quote_reserve) = estimated_reserves(global, bonding_curve)?;
270                let pool_creator_pk = pda::pump::pool_authority(base_mint).0;
271                let ctx = AmmContext {
272                    global_config,
273                    fee_config,
274                    base_mint,
275                    pool_creator: &pool_creator_pk,
276                    coin_creator: &bonding_curve.creator,
277                    base_reserve,
278                    quote_reserve,
279                    base_mint_supply,
280                };
281                with(ctx)
282            }
283        }
284    }
285
286    /// Bonding curve buy: SOL in → tokens out.
287    pub fn buy_quote_bonding_curve_sol_in(
288        &self,
289        global: &Global,
290        fee_config: Option<&FeeConfig>,
291        bonding_curve: &BondingCurve,
292        mint_supply: u64,
293        sol_amount: u64,
294        slippage_bps: u16,
295    ) -> QuoteResult<Quote> {
296        let tokens_out = bc_math::buy_token_amount_from_sol_amount(
297            global,
298            fee_config,
299            bonding_curve,
300            mint_supply,
301            sol_amount,
302        )?;
303        Ok(Quote {
304            amount: tokens_out,
305            min_out: sub_slippage(tokens_out, slippage_bps),
306            input_amount_used: sol_amount,
307            max_input: sol_amount,
308        })
309    }
310
311    /// Bonding curve buy: fixed token out → SOL cost.
312    pub fn buy_quote_bonding_curve_token_out(
313        &self,
314        global: &Global,
315        fee_config: Option<&FeeConfig>,
316        bonding_curve: &BondingCurve,
317        mint_supply: u64,
318        token_amount: u64,
319        slippage_bps: u16,
320    ) -> QuoteResult<Quote> {
321        let sol_cost = bc_math::buy_sol_amount_from_token_amount(
322            global,
323            fee_config,
324            bonding_curve,
325            mint_supply,
326            token_amount,
327        )?;
328        Ok(Quote {
329            amount: sol_cost,
330            min_out: token_amount,
331            input_amount_used: token_amount,
332            max_input: add_slippage(sol_cost, slippage_bps),
333        })
334    }
335
336    /// Bonding curve sell.
337    pub fn sell_quote_bonding_curve(
338        &self,
339        global: &Global,
340        fee_config: Option<&FeeConfig>,
341        bonding_curve: &BondingCurve,
342        mint_supply: u64,
343        token_amount: u64,
344        slippage_bps: u16,
345    ) -> QuoteResult<Quote> {
346        let sol_out = bc_math::sell_sol_amount_from_token_amount(
347            global,
348            fee_config,
349            bonding_curve,
350            mint_supply,
351            token_amount,
352        )?;
353        Ok(Quote {
354            amount: sol_out,
355            min_out: sub_slippage(sol_out, slippage_bps),
356            input_amount_used: token_amount,
357            max_input: token_amount,
358        })
359    }
360
361    /// AMM buy: quote in.
362    pub fn buy_quote_amm_sol_in(
363        &self,
364        global_config: &GlobalConfig,
365        fee_config: Option<&FeeConfig>,
366        source: AmmQuoteSource<'_>,
367        sol_amount: u64,
368        slippage_bps: u16,
369    ) -> QuoteResult<Quote> {
370        Self::map_amm_quote_source(global_config, fee_config, source, |ctx| {
371            let BuyQuoteInputResult {
372                base_amount_out, ..
373            } = amm_math::buy_quote_input(&ctx, sol_amount)?;
374            Ok(Quote {
375                amount: base_amount_out,
376                min_out: sub_slippage(base_amount_out, slippage_bps),
377                input_amount_used: sol_amount,
378                max_input: add_slippage(sol_amount, slippage_bps),
379            })
380        })
381    }
382
383    /// AMM buy: token amount out.
384    pub fn buy_quote_amm_token_out(
385        &self,
386        global_config: &GlobalConfig,
387        fee_config: Option<&FeeConfig>,
388        source: AmmQuoteSource<'_>,
389        token_amount: u64,
390        slippage_bps: u16,
391    ) -> QuoteResult<Quote> {
392        Self::map_amm_quote_source(global_config, fee_config, source, |ctx| {
393            let BuyBaseInputResult { total_quote_in, .. } =
394                amm_math::buy_base_input(&ctx, token_amount)?;
395            Ok(Quote {
396                amount: total_quote_in,
397                min_out: token_amount,
398                input_amount_used: token_amount,
399                max_input: add_slippage(total_quote_in, slippage_bps),
400            })
401        })
402    }
403
404    /// AMM sell.
405    pub fn sell_quote_amm(
406        &self,
407        global_config: &GlobalConfig,
408        fee_config: Option<&FeeConfig>,
409        source: AmmQuoteSource<'_>,
410        token_amount: u64,
411        slippage_bps: u16,
412    ) -> QuoteResult<Quote> {
413        Self::map_amm_quote_source(global_config, fee_config, source, |ctx| {
414            let SellBaseInputResult {
415                final_quote_out, ..
416            } = amm_math::sell_base_input(&ctx, token_amount)?;
417            Ok(Quote {
418                amount: final_quote_out,
419                min_out: sub_slippage(final_quote_out, slippage_bps),
420                input_amount_used: token_amount,
421                max_input: token_amount,
422            })
423        })
424    }
425}
426
427/// Heuristic AMM reserves from a completed curve (not live pool state).
428fn estimated_reserves(global: &Global, bonding_curve: &BondingCurve) -> QuoteResult<(u64, u64)> {
429    let base_reserve = global
430        .token_total_supply
431        .checked_sub(global.initial_real_token_reserves)
432        .ok_or(QuoteError::EmptyReserves)?;
433    let quote_reserve = bonding_curve
434        .real_quote_reserves
435        .checked_sub(global.pool_migration_fee)
436        .ok_or(QuoteError::EmptyReserves)?;
437    if base_reserve == 0 || quote_reserve == 0 {
438        return Err(QuoteError::EmptyReserves);
439    }
440    Ok((base_reserve, quote_reserve))
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use anchor_lang::Discriminator;
447    use anchor_lang::solana_program::{pubkey::Pubkey, system_program};
448
449    use crate::constants;
450    use crate::math::amm::AmmContext;
451    use crate::pda;
452    use crate::pump::client;
453    use crate::pump_agent_payments::client as agent_client;
454    use crate::pump_amm::client as amm_client;
455    use crate::state::pump_amm::{GlobalConfig, GlobalConfigFromIdl, Pool, PoolFromIdl};
456    use crate::state::{BondingCurve, BondingCurveFromIdl, Global, GlobalFromIdl};
457
458    fn fake_pubkey(seed: u8) -> Pubkey {
459        Pubkey::new_from_array([seed; 32])
460    }
461
462    fn pump_global_with_fees(fee_recipient: Pubkey, buyback_fee_recipient: Pubkey) -> Global {
463        let mut g = Global::default();
464        g.fee_recipient = fee_recipient;
465        g.buyback_fee_recipients[0] = buyback_fee_recipient;
466        g
467    }
468
469    fn amm_global_with_fees(protocol: Pubkey, buyback: Pubkey) -> GlobalConfig {
470        let mut g = GlobalConfig::default();
471        g.protocol_fee_recipients[0] = protocol;
472        g.buyback_fee_recipients[0] = buyback;
473        g
474    }
475
476    #[test]
477    #[allow(deprecated)]
478    fn create_instruction_wires_metadata_pdas() {
479        let sdk = PumpSdk::new();
480        let mint = fake_pubkey(51);
481        let user = fake_pubkey(52);
482        let creator = fake_pubkey(53);
483
484        let ix = sdk.create_instruction(
485            mint,
486            user,
487            "Test",
488            "TST",
489            "https://example.com/metadata.json",
490            creator,
491        );
492
493        assert_eq!(ix.program_id, crate::pump::ID);
494        assert_eq!(
495            &ix.data[..8],
496            <client::args::Create as Discriminator>::DISCRIMINATOR,
497        );
498        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
499        assert!(metas.contains(&pda::pump::metadata(&mint).0));
500        assert!(metas.contains(&constants::MPL_TOKEN_METADATA_PROGRAM_ID));
501        assert!(metas.contains(&constants::SPL_TOKEN_PROGRAM_ID));
502        assert!(metas.contains(&pda::pump::bonding_curve(&mint).0));
503        // Rent sysvar id (path differs across anchor/solana majors, so use the literal).
504        assert!(metas.contains(&Pubkey::from_str_const(
505            "SysvarRent111111111111111111111111111111111"
506        )));
507    }
508
509    #[test]
510    fn create_v2_instruction_wires_mayhem_pdas() {
511        let sdk = PumpSdk::new();
512        let mint = fake_pubkey(21);
513        let user = fake_pubkey(22);
514        let creator = fake_pubkey(23);
515
516        let ix = sdk.create_v2_instruction(
517            mint,
518            user,
519            "Test",
520            "TST",
521            "https://example.com/metadata.json",
522            creator,
523            Pubkey::default(),
524            false,
525            false,
526        );
527
528        assert_eq!(ix.program_id, crate::pump::ID);
529        assert_eq!(
530            &ix.data[..8],
531            <client::args::CreateV2 as Discriminator>::DISCRIMINATOR,
532        );
533        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
534        assert!(metas.contains(&pda::mayhem::global_params().0));
535        assert!(metas.contains(&pda::mayhem::sol_vault().0));
536        assert!(metas.contains(&pda::mayhem::mayhem_state(&mint).0));
537        assert!(metas.contains(&pda::mayhem::mayhem_token_vault(&mint).0));
538        assert!(metas.contains(&constants::MAYHEM_PROGRAM_ID));
539        assert!(metas.contains(&constants::SPL_TOKEN_2022_PROGRAM_ID));
540    }
541
542    #[test]
543    fn create_v2_instruction_appends_remaining_accounts_for_non_sol_quote() {
544        let sdk = PumpSdk::new();
545        let mint = fake_pubkey(0xA1);
546        let user = fake_pubkey(0xA2);
547        let creator = fake_pubkey(0xA3);
548        let quote_mint = fake_pubkey(0xC1);
549
550        let baseline = sdk.create_v2_instruction(
551            mint,
552            user,
553            "Test",
554            "TST",
555            "https://example.com/metadata.json",
556            creator,
557            Pubkey::default(),
558            false,
559            false,
560        );
561        let ix = sdk.create_v2_instruction(
562            mint,
563            user,
564            "Test",
565            "TST",
566            "https://example.com/metadata.json",
567            creator,
568            quote_mint,
569            false,
570            false,
571        );
572
573        assert_eq!(ix.accounts.len(), baseline.accounts.len() + 3);
574
575        let n = ix.accounts.len();
576        let quote_meta = &ix.accounts[n - 3];
577        let ata_meta = &ix.accounts[n - 2];
578        let token_program_meta = &ix.accounts[n - 1];
579
580        assert_eq!(quote_meta.pubkey, quote_mint);
581        assert!(!quote_meta.is_writable);
582        assert!(!quote_meta.is_signer);
583
584        let bonding_curve = pda::pump::bonding_curve(&mint).0;
585        let expected_ata = pda::associated_token(
586            &bonding_curve,
587            &constants::SPL_TOKEN_PROGRAM_ID,
588            &quote_mint,
589        )
590        .0;
591        assert_eq!(ata_meta.pubkey, expected_ata);
592        assert!(ata_meta.is_writable);
593        assert!(!ata_meta.is_signer);
594
595        assert_eq!(token_program_meta.pubkey, constants::SPL_TOKEN_PROGRAM_ID);
596        assert!(!token_program_meta.is_writable);
597        assert!(!token_program_meta.is_signer);
598    }
599
600    #[test]
601    fn create_v2_instruction_omits_remaining_accounts_for_sol_quote() {
602        let sdk = PumpSdk::new();
603        let mint = fake_pubkey(0xB1);
604        let user = fake_pubkey(0xB2);
605        let creator = fake_pubkey(0xB3);
606
607        let with_default = sdk.create_v2_instruction(
608            mint,
609            user,
610            "Test",
611            "TST",
612            "https://example.com/metadata.json",
613            creator,
614            Pubkey::default(),
615            false,
616            false,
617        );
618        let with_native = sdk.create_v2_instruction(
619            mint,
620            user,
621            "Test",
622            "TST",
623            "https://example.com/metadata.json",
624            creator,
625            constants::NATIVE_MINT,
626            false,
627            false,
628        );
629
630        assert_eq!(with_default.accounts.len(), with_native.accounts.len());
631        for (a, b) in with_default
632            .accounts
633            .iter()
634            .zip(with_native.accounts.iter())
635        {
636            assert_eq!(a.pubkey, b.pubkey);
637            assert_eq!(a.is_writable, b.is_writable);
638            assert_eq!(a.is_signer, b.is_signer);
639        }
640        assert!(!with_default
641            .accounts
642            .iter()
643            .any(|m| m.pubkey == constants::SPL_TOKEN_PROGRAM_ID));
644    }
645
646    #[test]
647    #[allow(deprecated)]
648    fn buy_instruction_appends_bonding_curve_v2_then_buyback_fee_recipient() {
649        let sdk = PumpSdk::new();
650        let mint = fake_pubkey(61);
651        let user = fake_pubkey(62);
652        let creator = fake_pubkey(63);
653        let fee_recipient = fake_pubkey(64);
654        let buyback_fee_recipient = fake_pubkey(65);
655        let token_program = constants::SPL_TOKEN_PROGRAM_ID;
656
657        let ix = sdk.buy_instruction(
658            mint,
659            user,
660            creator,
661            fee_recipient,
662            buyback_fee_recipient,
663            100,
664            5_000_000,
665            token_program,
666        );
667
668        assert_eq!(ix.program_id, crate::pump::ID);
669        assert_eq!(
670            &ix.data[..8],
671            <client::args::Buy as Discriminator>::DISCRIMINATOR,
672        );
673
674        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
675        assert!(metas.contains(&pda::pump::global().0));
676        assert!(metas.contains(&pda::pump::bonding_curve(&mint).0));
677        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
678        assert!(metas.contains(&pda::pump::user_volume_accumulator(&user).0));
679        assert!(metas.contains(&pda::pump::fee_config().0));
680        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
681        assert!(metas.contains(&fee_recipient));
682
683        let n = ix.accounts.len();
684        let bcv2 = &ix.accounts[n - 2];
685        let buyback = &ix.accounts[n - 1];
686        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&mint).0);
687        assert!(!bcv2.is_writable);
688        assert!(!bcv2.is_signer);
689        assert_eq!(buyback.pubkey, buyback_fee_recipient);
690        assert!(buyback.is_writable);
691        assert!(!buyback.is_signer);
692    }
693
694    #[test]
695    fn create_v2_and_buy_instruction_returns_create_then_buy_v2_prefix_in_order() {
696        let sdk = PumpSdk::new();
697        let mint = fake_pubkey(71);
698        let user = fake_pubkey(72);
699        let creator = fake_pubkey(73);
700        let fee_recipient = fake_pubkey(74);
701        let buyback_fee_recipient = fake_pubkey(75);
702        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
703
704        let ixs = sdk
705            .create_v2_and_buy_instruction(
706                mint,
707                user,
708                "Test",
709                "TST",
710                "https://example.com/metadata.json",
711                creator,
712                Pubkey::default(),
713                false,
714                false,
715                None,
716                &global,
717                1_000,
718                500_000,
719            )
720            .expect("create_v2_and_buy_instruction");
721
722        assert_eq!(ixs.len(), 6);
723        assert_eq!(
724            &ixs[0].data[..8],
725            <client::args::CreateV2 as Discriminator>::DISCRIMINATOR,
726        );
727        for i in 1..5 {
728            assert_eq!(ixs[i].data, vec![1], "expected SPL ATA idempotent ix");
729        }
730        assert_eq!(
731            &ixs[5].data[..8],
732            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
733        );
734
735        let buy_metas: Vec<Pubkey> = ixs[5].accounts.iter().map(|m| m.pubkey).collect();
736        assert!(buy_metas.contains(&constants::SPL_TOKEN_2022_PROGRAM_ID));
737        assert!(buy_metas.contains(&constants::SPL_TOKEN_PROGRAM_ID));
738        assert!(buy_metas.contains(&constants::NATIVE_MINT));
739        assert!(buy_metas.contains(&pda::pump::sharing_config(&mint).0));
740    }
741
742    #[test]
743    fn buy_v2_instruction_wires_quote_and_sharing_config_pdas() {
744        let sdk = PumpSdk::new();
745        let base_mint = fake_pubkey(81);
746        let quote_mint = fake_pubkey(82);
747        let user = fake_pubkey(83);
748        let creator = fake_pubkey(84);
749        let fee_recipient = fake_pubkey(85);
750        let buyback_fee_recipient = fake_pubkey(86);
751        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
752        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
753        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
754        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
755            creator,
756            quote_mint,
757            ..Default::default()
758        });
759
760        let ix = sdk
761            .buy_v2_instruction(
762                &global,
763                &bonding_curve,
764                base_mint,
765                quote_token_program,
766                user,
767                1_000,
768                5_000_000,
769            )
770            .expect("buy_v2_instruction");
771
772        assert_eq!(ix.program_id, crate::pump::ID);
773        assert_eq!(
774            &ix.data[..8],
775            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
776        );
777
778        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
779        assert!(metas.contains(&pda::pump::global().0));
780        assert!(metas.contains(&base_mint));
781        assert!(metas.contains(&quote_mint));
782        assert!(metas.contains(&fee_recipient));
783        assert!(metas.contains(&buyback_fee_recipient));
784        assert!(metas.contains(&pda::pump::bonding_curve(&base_mint).0));
785        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
786        assert!(metas.contains(&pda::pump::sharing_config(&base_mint).0));
787        assert!(metas.contains(&pda::pump::user_volume_accumulator(&user).0));
788        assert!(metas.contains(&pda::pump::fee_config().0));
789        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
790
791        assert!(metas
792            .contains(&pda::associated_token(&fee_recipient, &quote_token_program, &quote_mint).0));
793        assert!(metas.contains(
794            &pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0
795        ));
796
797        let signers: Vec<Pubkey> = ix
798            .accounts
799            .iter()
800            .filter(|m| m.is_signer)
801            .map(|m| m.pubkey)
802            .collect();
803        assert_eq!(signers, vec![user]);
804    }
805
806    #[test]
807    fn buy_v2_instructions_prepends_four_ata_creates() {
808        let sdk = PumpSdk::new();
809        let base_mint = fake_pubkey(91);
810        let quote_mint = fake_pubkey(92);
811        let user = fake_pubkey(93);
812        let creator = fake_pubkey(94);
813        let fee_recipient = fake_pubkey(95);
814        let buyback_fee_recipient = fake_pubkey(96);
815        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
816        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
817        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
818        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
819            creator,
820            quote_mint,
821            ..Default::default()
822        });
823
824        let ixs = sdk
825            .buy_v2_instructions(
826                &global,
827                &bonding_curve,
828                base_mint,
829                quote_token_program,
830                user,
831                1_000,
832                5_000_000,
833            )
834            .expect("buy_v2_instructions");
835
836        assert_eq!(ixs.len(), 5);
837        for ata_ix in &ixs[..4] {
838            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
839            assert_eq!(ata_ix.data, vec![1]);
840        }
841        assert_eq!(
842            &ixs[4].data[..8],
843            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
844        );
845
846        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
847        let expected_atas: [Pubkey; 4] = [
848            pda::associated_token(&user, &base_token_program, &base_mint).0,
849            pda::associated_token(&bonding_curve, &quote_token_program, &quote_mint).0,
850            pda::associated_token(&user, &quote_token_program, &quote_mint).0,
851            pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0,
852        ];
853        for (i, expected) in expected_atas.iter().enumerate() {
854            let metas: Vec<Pubkey> = ixs[i].accounts.iter().map(|m| m.pubkey).collect();
855            assert!(metas.contains(expected), "ix {i} missing expected ATA");
856        }
857    }
858
859    #[test]
860    #[allow(deprecated)]
861    fn sell_instruction_cashback_appends_uva_then_bonding_curve_v2() {
862        let sdk = PumpSdk::new();
863        let mint = fake_pubkey(101);
864        let user = fake_pubkey(102);
865        let creator = fake_pubkey(103);
866        let fee_recipient = fake_pubkey(104);
867        let buyback_fee_recipient = fake_pubkey(105);
868        let token_program = constants::SPL_TOKEN_PROGRAM_ID;
869
870        let ix = sdk.sell_instruction(
871            mint,
872            user,
873            creator,
874            fee_recipient,
875            buyback_fee_recipient,
876            100,
877            1_000_000,
878            token_program,
879            true,
880        );
881
882        assert_eq!(ix.program_id, crate::pump::ID);
883        assert_eq!(
884            &ix.data[..8],
885            <client::args::Sell as Discriminator>::DISCRIMINATOR,
886        );
887
888        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
889        assert!(metas.contains(&pda::pump::global().0));
890        assert!(metas.contains(&pda::pump::bonding_curve(&mint).0));
891        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
892        assert!(metas.contains(&pda::pump::fee_config().0));
893        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
894        assert!(metas.contains(&fee_recipient));
895        assert!(metas.contains(&pda::associated_token(&user, &token_program, &mint).0));
896
897        let n = ix.accounts.len();
898        let uva = &ix.accounts[n - 2];
899        let bcv2 = &ix.accounts[n - 1];
900        assert_eq!(uva.pubkey, pda::pump::user_volume_accumulator(&user).0);
901        assert!(uva.is_writable);
902        assert!(!uva.is_signer);
903        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&mint).0);
904        assert!(!bcv2.is_writable);
905        assert!(!bcv2.is_signer);
906    }
907
908    #[test]
909    #[allow(deprecated)]
910    fn sell_instruction_non_cashback_skips_uva() {
911        let sdk = PumpSdk::new();
912        let mint = fake_pubkey(101);
913        let user = fake_pubkey(102);
914        let creator = fake_pubkey(103);
915        let fee_recipient = fake_pubkey(104);
916        let buyback_fee_recipient = fake_pubkey(105);
917        let token_program = constants::SPL_TOKEN_PROGRAM_ID;
918
919        let ix = sdk.sell_instruction(
920            mint,
921            user,
922            creator,
923            fee_recipient,
924            buyback_fee_recipient,
925            100,
926            1_000_000,
927            token_program,
928            false,
929        );
930
931        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
932        assert!(!metas.contains(&pda::pump::user_volume_accumulator(&user).0));
933
934        let bcv2 = &ix.accounts[ix.accounts.len() - 1];
935        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&mint).0);
936        assert!(!bcv2.is_writable);
937        assert!(!bcv2.is_signer);
938    }
939
940    #[test]
941    fn sell_v2_instruction_wires_quote_and_sharing_config_pdas() {
942        let sdk = PumpSdk::new();
943        let base_mint = fake_pubkey(111);
944        let quote_mint = fake_pubkey(112);
945        let user = fake_pubkey(113);
946        let creator = fake_pubkey(114);
947        let fee_recipient = fake_pubkey(115);
948        let buyback_fee_recipient = fake_pubkey(116);
949        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
950        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
951        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
952            creator,
953            quote_mint,
954            ..Default::default()
955        });
956
957        let ix = sdk
958            .sell_v2_instruction(
959                &global,
960                &bonding_curve,
961                base_mint,
962                quote_token_program,
963                user,
964                1_000,
965                1_000_000,
966            )
967            .expect("sell_v2_instruction");
968
969        assert_eq!(ix.program_id, crate::pump::ID);
970        assert_eq!(
971            &ix.data[..8],
972            <client::args::SellV2 as Discriminator>::DISCRIMINATOR,
973        );
974
975        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
976        assert!(metas.contains(&pda::pump::global().0));
977        assert!(metas.contains(&base_mint));
978        assert!(metas.contains(&quote_mint));
979        assert!(metas.contains(&fee_recipient));
980        assert!(metas.contains(&buyback_fee_recipient));
981        assert!(metas.contains(&pda::pump::bonding_curve(&base_mint).0));
982        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
983        assert!(metas.contains(&pda::pump::sharing_config(&base_mint).0));
984        assert!(metas.contains(&pda::pump::user_volume_accumulator(&user).0));
985        assert!(metas.contains(&pda::pump::fee_config().0));
986        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
987
988        assert!(!metas.contains(&pda::pump::global_volume_accumulator().0));
989
990        assert!(metas
991            .contains(&pda::associated_token(&fee_recipient, &quote_token_program, &quote_mint).0));
992        assert!(metas.contains(
993            &pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0
994        ));
995
996        let signers: Vec<Pubkey> = ix
997            .accounts
998            .iter()
999            .filter(|m| m.is_signer)
1000            .map(|m| m.pubkey)
1001            .collect();
1002        assert_eq!(signers, vec![user]);
1003    }
1004
1005    #[test]
1006    fn sell_v2_instructions_prepends_four_ata_creates() {
1007        let sdk = PumpSdk::new();
1008        let base_mint = fake_pubkey(121);
1009        let quote_mint = fake_pubkey(122);
1010        let user = fake_pubkey(123);
1011        let creator = fake_pubkey(124);
1012        let fee_recipient = fake_pubkey(125);
1013        let buyback_fee_recipient = fake_pubkey(126);
1014        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1015        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1016        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1017        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1018            creator,
1019            quote_mint,
1020            ..Default::default()
1021        });
1022
1023        let ixs = sdk
1024            .sell_v2_instructions(
1025                &global,
1026                &bonding_curve,
1027                base_mint,
1028                quote_token_program,
1029                user,
1030                1_000,
1031                1_000_000,
1032            )
1033            .expect("sell_v2_instructions");
1034
1035        assert_eq!(ixs.len(), 5);
1036        for ata_ix in &ixs[..4] {
1037            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
1038            assert_eq!(ata_ix.data, vec![1]);
1039        }
1040        assert_eq!(
1041            &ixs[4].data[..8],
1042            <client::args::SellV2 as Discriminator>::DISCRIMINATOR,
1043        );
1044
1045        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
1046        let expected_atas: [Pubkey; 4] = [
1047            pda::associated_token(&user, &base_token_program, &base_mint).0,
1048            pda::associated_token(&bonding_curve, &quote_token_program, &quote_mint).0,
1049            pda::associated_token(&user, &quote_token_program, &quote_mint).0,
1050            pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0,
1051        ];
1052        for (i, expected) in expected_atas.iter().enumerate() {
1053            let metas: Vec<Pubkey> = ixs[i].accounts.iter().map(|m| m.pubkey).collect();
1054            assert!(metas.contains(expected), "ix {i} missing expected ATA");
1055        }
1056    }
1057
1058    fn fake_amm_inputs() -> (
1059        Pubkey,
1060        Pubkey,
1061        Pubkey,
1062        Pubkey,
1063        Pubkey,
1064        Pubkey,
1065        Pubkey,
1066        Pubkey,
1067    ) {
1068        (
1069            fake_pubkey(0xA1),
1070            fake_pubkey(0xA2),
1071            fake_pubkey(0xA3),
1072            fake_pubkey(0xA4),
1073            fake_pubkey(0xA5),
1074            fake_pubkey(0xA6),
1075            fake_pubkey(0xA7),
1076            constants::SPL_TOKEN_PROGRAM_ID,
1077        )
1078    }
1079
1080    #[test]
1081    fn buy_amm_instruction_wires_pump_amm_pdas() {
1082        let sdk = PumpSdk::new();
1083        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
1084            fake_amm_inputs();
1085        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1086        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1087        let amm_global = amm_global_with_fees(fee_rec, buyback);
1088        let pool_state = Pool::new(PoolFromIdl {
1089            base_mint,
1090            quote_mint,
1091            coin_creator,
1092            is_cashback_coin: true,
1093            ..Default::default()
1094        });
1095
1096        let ix = sdk
1097            .buy_amm_instruction(
1098                pool,
1099                &amm_global,
1100                &pool_state,
1101                base_token_program,
1102                quote_token_program,
1103                user,
1104                1_000,
1105                500_000,
1106            )
1107            .expect("buy_amm_instruction");
1108
1109        assert_eq!(ix.program_id, crate::pump_amm::ID);
1110        assert_eq!(
1111            &ix.data[..8],
1112            <amm_client::args::Buy as Discriminator>::DISCRIMINATOR,
1113        );
1114        assert_eq!(ix.accounts.len(), 27);
1115
1116        let pubkeys: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1117        assert!(pubkeys.contains(&pda::pump_amm::global_config().0));
1118        assert!(pubkeys.contains(&pda::pump_amm::event_authority().0));
1119        assert!(pubkeys.contains(&pda::pump_amm::global_volume_accumulator().0));
1120        assert!(pubkeys.contains(&pda::pump_amm::user_volume_accumulator(&user).0));
1121        assert!(pubkeys.contains(&pda::pump_amm::fee_config().0));
1122        assert!(pubkeys.contains(&pda::pump_amm::coin_creator_vault_authority(&coin_creator).0));
1123
1124        assert_eq!(ix.accounts[0].pubkey, pool);
1125        assert!(ix.accounts[0].is_writable);
1126        assert!(!ix.accounts[0].is_signer);
1127        assert_eq!(ix.accounts[1].pubkey, user);
1128        assert!(ix.accounts[1].is_signer);
1129
1130        let n = ix.accounts.len();
1131        let cashback = &ix.accounts[n - 4];
1132        let bcv2 = &ix.accounts[n - 3];
1133        let buyback_meta = &ix.accounts[n - 2];
1134        let buyback_ata = &ix.accounts[n - 1];
1135
1136        let user_vol_accum = pda::pump_amm::user_volume_accumulator(&user).0;
1137        assert_eq!(
1138            cashback.pubkey,
1139            pda::associated_token(
1140                &user_vol_accum,
1141                &quote_token_program,
1142                &constants::NATIVE_MINT,
1143            )
1144            .0
1145        );
1146        assert!(cashback.is_writable);
1147
1148        assert_eq!(bcv2.pubkey, pda::pump_amm::pool_v2(&base_mint).0);
1149        assert!(!bcv2.is_writable);
1150
1151        assert_eq!(buyback_meta.pubkey, buyback);
1152        assert!(buyback_meta.is_writable);
1153
1154        assert_eq!(
1155            buyback_ata.pubkey,
1156            pda::associated_token(&buyback, &quote_token_program, &quote_mint).0
1157        );
1158        assert!(buyback_ata.is_writable);
1159    }
1160
1161    #[test]
1162    fn buy_amm_instruction_skips_remaining_when_no_cashback_no_creator() {
1163        let sdk = PumpSdk::new();
1164        let (pool, base_mint, quote_mint, user, _, fee_rec, buyback, _) = fake_amm_inputs();
1165        let amm_global = amm_global_with_fees(fee_rec, buyback);
1166        let pool_state = Pool::new(PoolFromIdl {
1167            base_mint,
1168            quote_mint,
1169            coin_creator: Pubkey::default(),
1170            is_cashback_coin: false,
1171            ..Default::default()
1172        });
1173
1174        let ix = sdk
1175            .buy_amm_instruction(
1176                pool,
1177                &amm_global,
1178                &pool_state,
1179                constants::SPL_TOKEN_2022_PROGRAM_ID,
1180                constants::SPL_TOKEN_PROGRAM_ID,
1181                user,
1182                1_000,
1183                500_000,
1184            )
1185            .expect("buy_amm_instruction");
1186
1187        assert_eq!(ix.accounts.len(), 25);
1188        let n = ix.accounts.len();
1189        assert_eq!(ix.accounts[n - 2].pubkey, buyback);
1190        assert!(ix.accounts[n - 2].is_writable);
1191    }
1192
1193    #[test]
1194    fn sell_amm_instruction_wires_pump_amm_pdas() {
1195        let sdk = PumpSdk::new();
1196        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
1197            fake_amm_inputs();
1198        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1199        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1200        let amm_global = amm_global_with_fees(fee_rec, buyback);
1201        let pool_state = Pool::new(PoolFromIdl {
1202            base_mint,
1203            quote_mint,
1204            coin_creator,
1205            is_cashback_coin: true,
1206            ..Default::default()
1207        });
1208
1209        let ix = sdk
1210            .sell_amm_instruction(
1211                pool,
1212                &amm_global,
1213                &pool_state,
1214                base_token_program,
1215                quote_token_program,
1216                user,
1217                1_000_000,
1218                500,
1219            )
1220            .expect("sell_amm_instruction");
1221
1222        assert_eq!(ix.program_id, crate::pump_amm::ID);
1223        assert_eq!(
1224            &ix.data[..8],
1225            <amm_client::args::Sell as Discriminator>::DISCRIMINATOR,
1226        );
1227        assert_eq!(ix.accounts.len(), 26);
1228
1229        let pubkeys: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1230        assert!(!pubkeys.contains(&pda::pump_amm::global_volume_accumulator().0));
1231
1232        let user_vol_accum = pda::pump_amm::user_volume_accumulator(&user).0;
1233        let n = ix.accounts.len();
1234        let cashback_ata = &ix.accounts[n - 5];
1235        let cashback_uva = &ix.accounts[n - 4];
1236        let bcv2 = &ix.accounts[n - 3];
1237        let buyback_meta = &ix.accounts[n - 2];
1238        let buyback_ata = &ix.accounts[n - 1];
1239
1240        assert_eq!(
1241            cashback_ata.pubkey,
1242            pda::associated_token(&user_vol_accum, &quote_token_program, &quote_mint).0
1243        );
1244        assert!(cashback_ata.is_writable);
1245
1246        assert_eq!(cashback_uva.pubkey, user_vol_accum);
1247        assert!(cashback_uva.is_writable);
1248
1249        assert_eq!(bcv2.pubkey, pda::pump_amm::pool_v2(&base_mint).0);
1250        assert!(!bcv2.is_writable);
1251
1252        assert_eq!(buyback_meta.pubkey, buyback);
1253        assert!(!buyback_meta.is_writable);
1254
1255        assert_eq!(
1256            buyback_ata.pubkey,
1257            pda::associated_token(&buyback, &quote_token_program, &quote_mint).0
1258        );
1259        assert!(buyback_ata.is_writable);
1260    }
1261
1262    #[test]
1263    fn sell_amm_instruction_skips_remaining_when_no_cashback_no_creator() {
1264        let sdk = PumpSdk::new();
1265        let (pool, base_mint, quote_mint, user, _, fee_rec, buyback, _) = fake_amm_inputs();
1266        let amm_global = amm_global_with_fees(fee_rec, buyback);
1267        let pool_state = Pool::new(PoolFromIdl {
1268            base_mint,
1269            quote_mint,
1270            coin_creator: Pubkey::default(),
1271            is_cashback_coin: false,
1272            ..Default::default()
1273        });
1274
1275        let ix = sdk
1276            .sell_amm_instruction(
1277                pool,
1278                &amm_global,
1279                &pool_state,
1280                constants::SPL_TOKEN_2022_PROGRAM_ID,
1281                constants::SPL_TOKEN_PROGRAM_ID,
1282                user,
1283                1_000_000,
1284                500,
1285            )
1286            .expect("sell_amm_instruction");
1287
1288        assert_eq!(ix.accounts.len(), 23);
1289        let n = ix.accounts.len();
1290        assert_eq!(ix.accounts[n - 2].pubkey, buyback);
1291        assert!(!ix.accounts[n - 2].is_writable);
1292    }
1293
1294    #[test]
1295    fn buy_amm_instructions_prepends_one_ata_create() {
1296        let sdk = PumpSdk::new();
1297        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
1298            fake_amm_inputs();
1299        let amm_global = amm_global_with_fees(fee_rec, buyback);
1300        let pool_state = Pool::new(PoolFromIdl {
1301            base_mint,
1302            quote_mint,
1303            coin_creator,
1304            is_cashback_coin: false,
1305            ..Default::default()
1306        });
1307
1308        let ixs = sdk
1309            .buy_amm_instructions(
1310                pool,
1311                &amm_global,
1312                &pool_state,
1313                constants::SPL_TOKEN_2022_PROGRAM_ID,
1314                constants::SPL_TOKEN_PROGRAM_ID,
1315                user,
1316                1_000,
1317                500_000,
1318            )
1319            .expect("buy_amm_instructions");
1320
1321        assert_eq!(ixs.len(), 2);
1322        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1323        assert_eq!(ixs[0].data, vec![1]);
1324        assert_eq!(ixs[1].program_id, crate::pump_amm::ID);
1325        assert_eq!(
1326            &ixs[1].data[..8],
1327            <amm_client::args::Buy as Discriminator>::DISCRIMINATOR,
1328        );
1329    }
1330
1331    #[test]
1332    fn sell_amm_instructions_prepends_one_ata_create() {
1333        let sdk = PumpSdk::new();
1334        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
1335            fake_amm_inputs();
1336        let amm_global = amm_global_with_fees(fee_rec, buyback);
1337        let pool_state = Pool::new(PoolFromIdl {
1338            base_mint,
1339            quote_mint,
1340            coin_creator,
1341            is_cashback_coin: false,
1342            ..Default::default()
1343        });
1344
1345        let ixs = sdk
1346            .sell_amm_instructions(
1347                pool,
1348                &amm_global,
1349                &pool_state,
1350                constants::SPL_TOKEN_2022_PROGRAM_ID,
1351                constants::SPL_TOKEN_PROGRAM_ID,
1352                user,
1353                1_000_000,
1354                500,
1355            )
1356            .expect("sell_amm_instructions");
1357
1358        assert_eq!(ixs.len(), 2);
1359        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1360        assert_eq!(ixs[0].data, vec![1]);
1361        assert_eq!(ixs[1].program_id, crate::pump_amm::ID);
1362        assert_eq!(
1363            &ixs[1].data[..8],
1364            <amm_client::args::Sell as Discriminator>::DISCRIMINATOR,
1365        );
1366    }
1367
1368    fn user_wsol_ata(user: &Pubkey) -> Pubkey {
1369        pda::associated_token(
1370            user,
1371            &constants::SPL_TOKEN_PROGRAM_ID,
1372            &constants::NATIVE_MINT,
1373        )
1374        .0
1375    }
1376
1377    /// Parses lamports from a system `transfer` instruction (tests).
1378    fn parse_system_transfer_lamports(ix: &anchor_lang::solana_program::instruction::Instruction) -> u64 {
1379        assert_eq!(ix.program_id, system_program::ID);
1380        assert_eq!(ix.data.len(), 12);
1381        assert_eq!(&ix.data[..4], &2u32.to_le_bytes());
1382        u64::from_le_bytes(ix.data[4..12].try_into().unwrap())
1383    }
1384
1385    #[test]
1386    fn trade_tx_buy_bc_delegates_to_buy_v2_instructions() {
1387        let sdk = PumpSdk::new();
1388        let mint = fake_pubkey(0xB1);
1389        let user = fake_pubkey(0xB2);
1390        let creator = fake_pubkey(0xB3);
1391        let fee_recipient = fake_pubkey(0xB4);
1392        let buyback_fee_recipient = fake_pubkey(0xB5);
1393        let max_sol = 7_500_000u64;
1394        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1395        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1396            creator,
1397            quote_mint: constants::NATIVE_MINT,
1398            ..Default::default()
1399        });
1400
1401        let ixs = sdk
1402            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1403                mint,
1404                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1405                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1406                user,
1407                is_buy: true,
1408                venue: TradeVenue::BondingCurve {
1409                    global: &global,
1410                    bonding_curve: &bonding_curve,
1411                },
1412                base_amount: 1_000,
1413                sol_amount_threshold: max_sol,
1414            })
1415            .expect("trade_tx_instructions");
1416
1417        assert_eq!(ixs.len(), 3);
1418        for ata_ix in &ixs[..2] {
1419            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
1420            assert_eq!(ata_ix.data, vec![1]);
1421        }
1422        assert_eq!(ixs[2].program_id, crate::pump::ID);
1423        assert_eq!(
1424            &ixs[2].data[..8],
1425            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
1426        );
1427
1428        let expected_atas: [Pubkey; 2] = [
1429            pda::associated_token(&user, &constants::SPL_TOKEN_2022_PROGRAM_ID, &mint).0,
1430            user_wsol_ata(&user),
1431        ];
1432        for (i, expected) in expected_atas.iter().enumerate() {
1433            let metas: Vec<Pubkey> = ixs[i].accounts.iter().map(|m| m.pubkey).collect();
1434            assert!(metas.contains(expected), "ata ix {i} missing expected ATA");
1435        }
1436    }
1437
1438    #[test]
1439    fn trade_tx_sell_bc_delegates_to_sell_v2_instructions() {
1440        let sdk = PumpSdk::new();
1441        let mint = fake_pubkey(0xC1);
1442        let user = fake_pubkey(0xC2);
1443        let creator = fake_pubkey(0xC3);
1444        let fee_recipient = fake_pubkey(0xC4);
1445        let buyback_fee_recipient = fake_pubkey(0xC5);
1446        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1447        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1448            creator,
1449            quote_mint: constants::NATIVE_MINT,
1450            ..Default::default()
1451        });
1452
1453        let ixs = sdk
1454            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1455                mint,
1456                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1457                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1458                user,
1459                is_buy: false,
1460                venue: TradeVenue::BondingCurve {
1461                    global: &global,
1462                    bonding_curve: &bonding_curve,
1463                },
1464                base_amount: 1_000,
1465                sol_amount_threshold: 1_000_000,
1466            })
1467            .expect("trade_tx_instructions");
1468
1469        assert_eq!(ixs.len(), 2);
1470        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1471        assert_eq!(ixs[0].data, vec![1]);
1472        assert_eq!(ixs[1].program_id, crate::pump::ID);
1473        assert_eq!(
1474            &ixs[1].data[..8],
1475            <client::args::SellV2 as Discriminator>::DISCRIMINATOR,
1476        );
1477        let metas: Vec<Pubkey> = ixs[0].accounts.iter().map(|m| m.pubkey).collect();
1478        assert!(metas.contains(&user_wsol_ata(&user)));
1479    }
1480
1481    #[test]
1482    fn trade_tx_auto_matches_explicit_bc_when_not_complete() {
1483        let sdk = PumpSdk::new();
1484        let mint = fake_pubkey(0x70);
1485        let user = fake_pubkey(0x71);
1486        let creator = fake_pubkey(0x72);
1487        let fee_recipient = fake_pubkey(0x73);
1488        let buyback_fee_recipient = fake_pubkey(0x74);
1489        let max_sol = 7_500_000u64;
1490        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1491        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1492            creator,
1493            quote_mint: constants::NATIVE_MINT,
1494            complete: false,
1495            ..Default::default()
1496        });
1497
1498        let explicit = sdk
1499            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1500                mint,
1501                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1502                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1503                user,
1504                is_buy: true,
1505                venue: TradeVenue::BondingCurve {
1506                    global: &global,
1507                    bonding_curve: &bonding_curve,
1508                },
1509                base_amount: 1_000,
1510                sol_amount_threshold: max_sol,
1511            })
1512            .expect("explicit bc");
1513
1514        let pool = fake_pubkey(0x75);
1515        let amm_g = amm_global_with_fees(fake_pubkey(0x76), fake_pubkey(0x77));
1516        let ps = Pool::new(PoolFromIdl {
1517            base_mint: mint,
1518            quote_mint: constants::NATIVE_MINT,
1519            ..Default::default()
1520        });
1521        let auto = sdk
1522            .trade_tx_instructions(TradeTxParams {
1523                mint,
1524                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1525                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1526                user,
1527                is_buy: true,
1528                base_amount: 1_000,
1529                sol_amount_threshold: max_sol,
1530                pump_global: &global,
1531                bonding_curve: &bonding_curve,
1532                pump_pool: Some(PumpPoolCtx {
1533                    pool,
1534                    amm_global: &amm_g,
1535                    pool_state: &ps,
1536                }),
1537            })
1538            .expect("auto bc");
1539
1540        assert_eq!(explicit.len(), auto.len());
1541        for (a, b) in explicit.iter().zip(auto.iter()) {
1542            assert_eq!(a.program_id, b.program_id);
1543            assert_eq!(a.data, b.data);
1544        }
1545    }
1546
1547    #[test]
1548    fn trade_tx_auto_returns_none_when_complete_without_graduated() {
1549        let sdk = PumpSdk::new();
1550        let global = pump_global_with_fees(fake_pubkey(0x80), fake_pubkey(0x81));
1551        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1552            complete: true,
1553            ..Default::default()
1554        });
1555        assert!(sdk
1556            .trade_tx_instructions(TradeTxParams {
1557                mint: fake_pubkey(0x82),
1558                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1559                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1560                user: fake_pubkey(0x83),
1561                is_buy: true,
1562                base_amount: 1,
1563                sol_amount_threshold: 1,
1564                pump_global: &global,
1565                bonding_curve: &bonding_curve,
1566                pump_pool: None,
1567            })
1568            .is_none());
1569    }
1570
1571    #[test]
1572    fn trade_tx_auto_matches_explicit_amm_when_complete() {
1573        let sdk = PumpSdk::new();
1574        let mint = fake_pubkey(0x90);
1575        let user = fake_pubkey(0x91);
1576        let coin_creator = fake_pubkey(0x92);
1577        let fee_recipient = fake_pubkey(0x93);
1578        let buyback_fee_recipient = fake_pubkey(0x94);
1579        let pool = fake_pubkey(0x95);
1580        let max_sol = 2_500_000u64;
1581        let amm_global = amm_global_with_fees(fee_recipient, buyback_fee_recipient);
1582        let pool_state = Pool::new(PoolFromIdl {
1583            base_mint: mint,
1584            quote_mint: constants::NATIVE_MINT,
1585            coin_creator,
1586            is_cashback_coin: true,
1587            ..Default::default()
1588        });
1589        let pump_global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1590        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1591            creator: coin_creator,
1592            quote_mint: constants::NATIVE_MINT,
1593            complete: true,
1594            ..Default::default()
1595        });
1596
1597        let explicit = sdk
1598            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1599                mint,
1600                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1601                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1602                user,
1603                is_buy: true,
1604                venue: TradeVenue::Amm {
1605                    pool,
1606                    amm_global: &amm_global,
1607                    pool_state: &pool_state,
1608                },
1609                base_amount: 1_000,
1610                sol_amount_threshold: max_sol,
1611            })
1612            .expect("explicit amm");
1613
1614        let auto = sdk
1615            .trade_tx_instructions(TradeTxParams {
1616                mint,
1617                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1618                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1619                user,
1620                is_buy: true,
1621                base_amount: 1_000,
1622                sol_amount_threshold: max_sol,
1623                pump_global: &pump_global,
1624                bonding_curve: &bonding_curve,
1625                pump_pool: Some(PumpPoolCtx {
1626                    pool,
1627                    amm_global: &amm_global,
1628                    pool_state: &pool_state,
1629                }),
1630            })
1631            .expect("auto amm");
1632
1633        assert_eq!(explicit.len(), auto.len());
1634        for (a, b) in explicit.iter().zip(auto.iter()) {
1635            assert_eq!(a.program_id, b.program_id);
1636            assert_eq!(a.data, b.data);
1637        }
1638    }
1639
1640    #[test]
1641    fn quote_trade_bc_matches_direct_buy_and_sell_quotes() {
1642        let sdk = PumpSdk::new();
1643        let g = fixture_global();
1644        let bc = fixture_bonding_curve(fake_pubkey(7));
1645        let supply = g.token_total_supply;
1646        let target = 100_000_000_000_000u64;
1647        let slip = 200u16;
1648
1649        let buy_direct = sdk
1650            .buy_quote_bonding_curve_token_out(&g, None, &bc, supply, target, slip)
1651            .expect("buy direct");
1652        let buy_auto = sdk
1653            .quote_trade(TradeQuoteParams {
1654                is_buy: true,
1655                base_amount: target,
1656                slippage_bps: slip,
1657                base_mint_supply: supply,
1658                pump_global: &g,
1659                pump_fee_config: None,
1660                bonding_curve: &bc,
1661                pump_pool: None,
1662            })
1663            .expect("auto routed")
1664            .expect("buy auto");
1665        assert_eq!(buy_direct, buy_auto);
1666
1667        let sell_direct = sdk
1668            .sell_quote_bonding_curve(&g, None, &bc, supply, target, slip)
1669            .expect("sell direct");
1670        let sell_auto = sdk
1671            .quote_trade(TradeQuoteParams {
1672                is_buy: false,
1673                base_amount: target,
1674                slippage_bps: slip,
1675                base_mint_supply: supply,
1676                pump_global: &g,
1677                pump_fee_config: None,
1678                bonding_curve: &bc,
1679                pump_pool: None,
1680            })
1681            .expect("auto routed")
1682            .expect("sell auto");
1683        assert_eq!(sell_direct, sell_auto);
1684    }
1685
1686    #[test]
1687    fn quote_trade_amm_matches_direct_buy_and_sell_quotes() {
1688        let sdk = PumpSdk::new();
1689        let g = fixture_global();
1690        let gc = fixture_amm_global_config();
1691        let pool_state = fixture_pool(fake_pubkey(0xCC));
1692        let mut bc = fixture_bonding_curve(fake_pubkey(7));
1693        bc.complete = true;
1694
1695        let base_reserve = 200_000_000_000_000u64;
1696        let quote_reserve = 30_000_000_000u64;
1697        let base_mint_supply = 1_000_000_000_000_000u64;
1698        let target = 1_000_000_000_000u64;
1699        let slip = 100u16;
1700
1701        let source = AmmQuoteSource::Pool {
1702            pool: &pool_state,
1703            base_reserve,
1704            quote_reserve,
1705            base_mint_supply,
1706        };
1707        let buy_direct = sdk
1708            .buy_quote_amm_token_out(&gc, None, source, target, slip)
1709            .expect("buy direct");
1710        let buy_auto = sdk
1711            .quote_trade(TradeQuoteParams {
1712                is_buy: true,
1713                base_amount: target,
1714                slippage_bps: slip,
1715                base_mint_supply,
1716                pump_global: &g,
1717                pump_fee_config: None,
1718                bonding_curve: &bc,
1719                pump_pool: Some(PumpPoolQuoteCtx {
1720                    amm_global: &gc,
1721                    amm_fee_config: None,
1722                    pool_state: &pool_state,
1723                    base_reserve,
1724                    quote_reserve,
1725                }),
1726            })
1727            .expect("auto routed")
1728            .expect("buy auto");
1729        assert_eq!(buy_direct, buy_auto);
1730
1731        let sell_source = AmmQuoteSource::Pool {
1732            pool: &pool_state,
1733            base_reserve,
1734            quote_reserve,
1735            base_mint_supply,
1736        };
1737        let sell_direct = sdk
1738            .sell_quote_amm(&gc, None, sell_source, target, slip)
1739            .expect("sell direct");
1740        let sell_auto = sdk
1741            .quote_trade(TradeQuoteParams {
1742                is_buy: false,
1743                base_amount: target,
1744                slippage_bps: slip,
1745                base_mint_supply,
1746                pump_global: &g,
1747                pump_fee_config: None,
1748                bonding_curve: &bc,
1749                pump_pool: Some(PumpPoolQuoteCtx {
1750                    amm_global: &gc,
1751                    amm_fee_config: None,
1752                    pool_state: &pool_state,
1753                    base_reserve,
1754                    quote_reserve,
1755                }),
1756            })
1757            .expect("auto routed")
1758            .expect("sell auto");
1759        assert_eq!(sell_direct, sell_auto);
1760    }
1761
1762    #[test]
1763    fn quote_trade_returns_none_when_complete_without_pump_pool() {
1764        let sdk = PumpSdk::new();
1765        let g = fixture_global();
1766        let mut bc = fixture_bonding_curve(fake_pubkey(7));
1767        bc.complete = true;
1768        assert!(sdk
1769            .quote_trade(TradeQuoteParams {
1770                is_buy: true,
1771                base_amount: 1,
1772                slippage_bps: 0,
1773                base_mint_supply: g.token_total_supply,
1774                pump_global: &g,
1775                pump_fee_config: None,
1776                bonding_curve: &bc,
1777                pump_pool: None,
1778            })
1779            .is_none());
1780    }
1781
1782    #[test]
1783    fn trade_tx_buy_amm_wraps_and_unwraps_with_cashback() {
1784        let sdk = PumpSdk::new();
1785        let mint = fake_pubkey(0xD1);
1786        let user = fake_pubkey(0xD2);
1787        let coin_creator = fake_pubkey(0xD3);
1788        let fee_recipient = fake_pubkey(0xD4);
1789        let buyback_fee_recipient = fake_pubkey(0xD5);
1790        let pool = fake_pubkey(0xD6);
1791        let max_sol = 2_500_000u64;
1792        let amm_global = amm_global_with_fees(fee_recipient, buyback_fee_recipient);
1793        let pool_state = Pool::new(PoolFromIdl {
1794            base_mint: mint,
1795            quote_mint: constants::NATIVE_MINT,
1796            coin_creator,
1797            is_cashback_coin: true,
1798            ..Default::default()
1799        });
1800
1801        let ixs = sdk
1802            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1803                mint,
1804                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1805                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1806                user,
1807                is_buy: true,
1808                venue: TradeVenue::Amm {
1809                    pool,
1810                    amm_global: &amm_global,
1811                    pool_state: &pool_state,
1812                },
1813                base_amount: 1_000,
1814                sol_amount_threshold: max_sol,
1815            })
1816            .expect("trade_tx_instructions");
1817
1818        assert_eq!(ixs.len(), 6);
1819        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1820        assert_eq!(ixs[1].program_id, constants::SPL_ATA_PROGRAM_ID);
1821        assert_eq!(parse_system_transfer_lamports(&ixs[2]), max_sol);
1822        assert_eq!(ixs[3].program_id, constants::SPL_TOKEN_PROGRAM_ID);
1823        assert_eq!(ixs[4].program_id, crate::pump_amm::ID);
1824        assert_eq!(
1825            &ixs[4].data[..8],
1826            <amm_client::args::Buy as Discriminator>::DISCRIMINATOR,
1827        );
1828        assert_eq!(ixs[5].program_id, constants::SPL_TOKEN_PROGRAM_ID);
1829
1830        let pubkeys: Vec<Pubkey> = ixs[4].accounts.iter().map(|m| m.pubkey).collect();
1831        let user_vol_accum = pda::pump_amm::user_volume_accumulator(&user).0;
1832        assert!(pubkeys.contains(
1833            &pda::associated_token(
1834                &user_vol_accum,
1835                &constants::SPL_TOKEN_PROGRAM_ID,
1836                &constants::NATIVE_MINT,
1837            )
1838            .0
1839        ));
1840        assert!(pubkeys.contains(&pda::pump_amm::pool_v2(&mint).0));
1841    }
1842
1843    #[test]
1844    fn trade_tx_sell_amm_unwraps_proceeds_no_wrap() {
1845        let sdk = PumpSdk::new();
1846        let mint = fake_pubkey(0xE1);
1847        let user = fake_pubkey(0xE2);
1848        let coin_creator = fake_pubkey(0xE3);
1849        let fee_recipient = fake_pubkey(0xE4);
1850        let buyback_fee_recipient = fake_pubkey(0xE5);
1851        let pool = fake_pubkey(0xE6);
1852        let amm_global = amm_global_with_fees(fee_recipient, buyback_fee_recipient);
1853        let pool_state = Pool::new(PoolFromIdl {
1854            base_mint: mint,
1855            quote_mint: constants::NATIVE_MINT,
1856            coin_creator,
1857            is_cashback_coin: false,
1858            ..Default::default()
1859        });
1860
1861        let ixs = sdk
1862            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1863                mint,
1864                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1865                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1866                user,
1867                is_buy: false,
1868                venue: TradeVenue::Amm {
1869                    pool,
1870                    amm_global: &amm_global,
1871                    pool_state: &pool_state,
1872                },
1873                base_amount: 5_000,
1874                sol_amount_threshold: 100,
1875            })
1876            .expect("trade_tx_instructions");
1877
1878        assert_eq!(ixs.len(), 3);
1879        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1880        let wsol_ata = user_wsol_ata(&user);
1881        assert!(ixs[0].accounts.iter().any(|m| m.pubkey == wsol_ata));
1882        assert_eq!(ixs[1].program_id, crate::pump_amm::ID);
1883        assert_eq!(
1884            &ixs[1].data[..8],
1885            <amm_client::args::Sell as Discriminator>::DISCRIMINATOR,
1886        );
1887        assert_eq!(ixs[2].program_id, constants::SPL_TOKEN_PROGRAM_ID);
1888        assert!(ixs[2].accounts.iter().any(|m| m.pubkey == wsol_ata));
1889    }
1890
1891    #[test]
1892    fn create_coin_instructions_emits_create_then_buy_v2() {
1893        let sdk = PumpSdk::new();
1894        let mint = fake_pubkey(0xF1);
1895        let user = fake_pubkey(0xF2);
1896        let creator = fake_pubkey(0xF3);
1897        let fee_recipient = fake_pubkey(0xF4);
1898        let buyback_fee_recipient = fake_pubkey(0xF5);
1899        let max_sol = 1_234_567u64;
1900        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1901
1902        let ixs = sdk
1903            .create_coin_instructions(CreateCoinParams {
1904                mint,
1905                user,
1906                creator,
1907                name: "Test".into(),
1908                symbol: "TST".into(),
1909                uri: "https://example.com/metadata.json".into(),
1910                mayhem_mode: false,
1911                cashback: false,
1912                quote_mint: Pubkey::default(),
1913                global: &global,
1914                token_amount: 1_000,
1915                max_quote_tokens: max_sol,
1916                tokenized_agent_buyback_bps: None,
1917            })
1918            .expect("create_coin_instructions");
1919
1920        assert_eq!(ixs.len(), 4);
1921        assert_eq!(
1922            &ixs[0].data[..8],
1923            <client::args::CreateV2 as Discriminator>::DISCRIMINATOR,
1924        );
1925        for ata_ix in &ixs[1..3] {
1926            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
1927            assert_eq!(ata_ix.data, vec![1]);
1928        }
1929        assert_eq!(ixs[3].program_id, crate::pump::ID);
1930        assert_eq!(
1931            &ixs[3].data[..8],
1932            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
1933        );
1934        let buy_metas: Vec<Pubkey> = ixs[3].accounts.iter().map(|m| m.pubkey).collect();
1935        assert!(buy_metas.contains(&constants::SPL_TOKEN_2022_PROGRAM_ID));
1936        assert!(buy_metas.contains(&constants::NATIVE_MINT));
1937        assert!(buy_metas.contains(&pda::pump::sharing_config(&mint).0));
1938    }
1939
1940    #[test]
1941    fn create_coin_instructions_appends_agent_initialize_when_some() {
1942        let sdk = PumpSdk::new();
1943        let mint = fake_pubkey(0xA1);
1944        let user = fake_pubkey(0xA2);
1945        let creator = fake_pubkey(0xA3);
1946        let fee_recipient = fake_pubkey(0xA4);
1947        let buyback_fee_recipient = fake_pubkey(0xA5);
1948        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1949
1950        let buyback_bps = 5000u16;
1951        let with_agent = sdk
1952            .create_coin_instructions(CreateCoinParams {
1953                mint,
1954                user,
1955                creator,
1956                name: "Test".into(),
1957                symbol: "TST".into(),
1958                uri: "https://example.com/metadata.json".into(),
1959                mayhem_mode: false,
1960                cashback: false,
1961                quote_mint: Pubkey::default(),
1962                global: &global,
1963                token_amount: 1_000,
1964                max_quote_tokens: 500_000,
1965                tokenized_agent_buyback_bps: Some(buyback_bps),
1966            })
1967            .expect("create_coin_instructions Some");
1968        let without_agent = sdk
1969            .create_coin_instructions(CreateCoinParams {
1970                mint,
1971                user,
1972                creator,
1973                name: "Test".into(),
1974                symbol: "TST".into(),
1975                uri: "https://example.com/metadata.json".into(),
1976                mayhem_mode: false,
1977                cashback: false,
1978                quote_mint: Pubkey::default(),
1979                global: &global,
1980                token_amount: 1_000,
1981                max_quote_tokens: 500_000,
1982                tokenized_agent_buyback_bps: None,
1983            })
1984            .expect("create_coin_instructions None");
1985
1986        assert_eq!(
1987            with_agent.len(),
1988            without_agent.len() + 1,
1989            "Some(bps) appends exactly one extra ix"
1990        );
1991        let agent_ix = with_agent.last().expect("agent ix present");
1992        assert_eq!(agent_ix.program_id, crate::pump_agent_payments::ID);
1993        assert_eq!(
1994            &agent_ix.data[..8],
1995            <agent_client::args::AgentInitialize as Discriminator>::DISCRIMINATOR,
1996        );
1997        // args.authority (creator) + args.buyback_bps trail the discriminator.
1998        assert_eq!(&agent_ix.data[8..40], creator.as_ref());
1999        assert_eq!(
2000            u16::from_le_bytes([agent_ix.data[40], agent_ix.data[41]]),
2001            buyback_bps,
2002        );
2003        let agent_metas: Vec<Pubkey> = agent_ix.accounts.iter().map(|m| m.pubkey).collect();
2004        assert!(agent_metas.contains(&user), "user is the signer");
2005        assert!(agent_metas.contains(&pda::pump::bonding_curve(&mint).0));
2006        assert!(agent_metas.contains(&pda::pump_agent_payments::global_config().0));
2007        assert!(agent_metas.contains(&mint));
2008        assert!(agent_metas.contains(&pda::pump_agent_payments::token_agent_payments(&mint).0));
2009        assert!(agent_metas.contains(&pda::pump_agent_payments::event_authority().0));
2010        assert!(agent_metas.contains(&crate::pump_agent_payments::ID));
2011        assert!(agent_metas.contains(&pda::pump::sharing_config(&mint).0));
2012    }
2013
2014    #[test]
2015    fn program_ids_match_idl() {
2016        assert_eq!(
2017            crate::pump::ID,
2018            Pubkey::from_str_const("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
2019        );
2020        assert_eq!(
2021            crate::pump_amm::ID,
2022            Pubkey::from_str_const("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
2023        );
2024    }
2025
2026    fn fixture_global() -> Global {
2027        Global::new(GlobalFromIdl {
2028            fee_basis_points: 100,
2029            creator_fee_basis_points: 50,
2030            token_total_supply: 1_000_000_000_000_000,
2031            initial_real_token_reserves: 793_100_000_000_000,
2032            pool_migration_fee: 6_900_000_000,
2033            ..Default::default()
2034        })
2035    }
2036
2037    fn fixture_bonding_curve(creator: Pubkey) -> BondingCurve {
2038        BondingCurve::new(BondingCurveFromIdl {
2039            virtual_token_reserves: 1_073_000_000_000_000,
2040            virtual_quote_reserves: 30_000_000_000,
2041            real_token_reserves: 793_100_000_000_000,
2042            real_quote_reserves: 0,
2043            token_total_supply: 1_000_000_000_000_000,
2044            complete: false,
2045            creator,
2046            is_mayhem_mode: false,
2047            is_cashback_coin: false,
2048            quote_mint: constants::NATIVE_MINT,
2049        })
2050    }
2051
2052    fn fixture_amm_global_config() -> GlobalConfig {
2053        GlobalConfig::new(GlobalConfigFromIdl {
2054            lp_fee_basis_points: 20,
2055            protocol_fee_basis_points: 5,
2056            coin_creator_fee_basis_points: 5,
2057            ..Default::default()
2058        })
2059    }
2060
2061    fn fixture_pool(coin_creator: Pubkey) -> Pool {
2062        Pool::new(PoolFromIdl {
2063            base_mint: fake_pubkey(0xAA),
2064            creator: fake_pubkey(0xBB),
2065            coin_creator,
2066            quote_mint: constants::NATIVE_MINT,
2067            ..Default::default()
2068        })
2069    }
2070
2071    #[test]
2072    fn bc_buy_sol_in_matches_ts() {
2073        let sdk = PumpSdk::new();
2074        let g = fixture_global();
2075        let bc = fixture_bonding_curve(fake_pubkey(7));
2076        let q = sdk
2077            .buy_quote_bonding_curve_sol_in(
2078                &g,
2079                None,
2080                &bc,
2081                g.token_total_supply,
2082                1_000_000_000, // 1 SOL
2083                100,           // 1% slippage
2084            )
2085            .unwrap();
2086        assert_eq!(q.amount, 34_117_646_995_895);
2087        assert_eq!(q.min_out, 33_776_470_525_936);
2088        assert_eq!(q.input_amount_used, 1_000_000_000);
2089        assert_eq!(q.max_input, 1_000_000_000);
2090    }
2091
2092    #[test]
2093    fn bc_buy_token_out_matches_ts() {
2094        let sdk = PumpSdk::new();
2095        let g = fixture_global();
2096        let bc = fixture_bonding_curve(fake_pubkey(7));
2097        let q = sdk
2098            .buy_quote_bonding_curve_token_out(
2099                &g,
2100                None,
2101                &bc,
2102                g.token_total_supply,
2103                100_000_000_000_000,
2104                200, // 2% slippage
2105            )
2106            .unwrap();
2107        assert_eq!(q.amount, 3_129_496_404);
2108        assert_eq!(q.max_input, 3_192_086_332);
2109        assert_eq!(q.min_out, 100_000_000_000_000);
2110        assert_eq!(q.input_amount_used, 100_000_000_000_000);
2111    }
2112
2113    #[test]
2114    fn bc_sell_with_creator_matches_ts() {
2115        let sdk = PumpSdk::new();
2116        let g = fixture_global();
2117        let bc = fixture_bonding_curve(fake_pubkey(7));
2118        let q = sdk
2119            .sell_quote_bonding_curve(
2120                &g,
2121                None,
2122                &bc,
2123                g.token_total_supply,
2124                50_000_000_000_000,
2125                50, // 0.5% slippage
2126            )
2127            .unwrap();
2128        assert_eq!(q.amount, 1_315_672_305);
2129        assert_eq!(q.min_out, 1_309_093_943);
2130    }
2131
2132    #[test]
2133    fn bc_sell_default_creator_skips_creator_fee() {
2134        let sdk = PumpSdk::new();
2135        let g = fixture_global();
2136        let bc = fixture_bonding_curve(Pubkey::default());
2137        let q = sdk
2138            .sell_quote_bonding_curve(&g, None, &bc, g.token_total_supply, 50_000_000_000_000, 0)
2139            .unwrap();
2140        assert_eq!(q.amount, 1_322_350_845);
2141    }
2142
2143    #[test]
2144    fn amm_buy_sol_in_matches_ts() {
2145        let sdk = PumpSdk::new();
2146        let gc = fixture_amm_global_config();
2147        let pool = fixture_pool(fake_pubkey(0xCC));
2148        let q = sdk
2149            .buy_quote_amm_sol_in(
2150                &gc,
2151                None,
2152                AmmQuoteSource::Pool {
2153                    pool: &pool,
2154                    base_reserve: 200_000_000_000_000,
2155                    quote_reserve: 30_000_000_000,
2156                    base_mint_supply: 1_000_000_000_000_000,
2157                },
2158                1_000_000_000,
2159                100,
2160            )
2161            .unwrap();
2162        assert_eq!(q.amount, 6_432_936_635_069);
2163        assert_eq!(q.min_out, 6_368_607_268_718);
2164        assert_eq!(q.max_input, 1_010_000_000);
2165    }
2166
2167    #[test]
2168    fn amm_buy_token_out_matches_ts() {
2169        let sdk = PumpSdk::new();
2170        let gc = fixture_amm_global_config();
2171        let pool = fixture_pool(fake_pubkey(0xCC));
2172        let q = sdk
2173            .buy_quote_amm_token_out(
2174                &gc,
2175                None,
2176                AmmQuoteSource::Pool {
2177                    pool: &pool,
2178                    base_reserve: 200_000_000_000_000,
2179                    quote_reserve: 30_000_000_000,
2180                    base_mint_supply: 1_000_000_000_000_000,
2181                },
2182                1_000_000_000_000,
2183                100,
2184            )
2185            .unwrap();
2186        assert_eq!(q.amount, 151_206_031);
2187        assert_eq!(q.max_input, 152_718_091);
2188    }
2189
2190    #[test]
2191    fn amm_sell_with_coin_creator_matches_ts() {
2192        let sdk = PumpSdk::new();
2193        let gc = fixture_amm_global_config();
2194        let pool = fixture_pool(fake_pubkey(0xCC));
2195        let q = sdk
2196            .sell_quote_amm(
2197                &gc,
2198                None,
2199                AmmQuoteSource::Pool {
2200                    pool: &pool,
2201                    base_reserve: 200_000_000_000_000,
2202                    quote_reserve: 30_000_000_000,
2203                    base_mint_supply: 1_000_000_000_000_000,
2204                },
2205                1_000_000_000_000,
2206                100,
2207            )
2208            .unwrap();
2209        assert_eq!(q.amount, 148_805_969);
2210        assert_eq!(q.min_out, 147_317_909);
2211    }
2212
2213    #[test]
2214    fn amm_sell_default_coin_creator_skips_creator_fee() {
2215        let sdk = PumpSdk::new();
2216        let gc = fixture_amm_global_config();
2217        let pool = fixture_pool(Pubkey::default());
2218        let q = sdk
2219            .sell_quote_amm(
2220                &gc,
2221                None,
2222                AmmQuoteSource::Pool {
2223                    pool: &pool,
2224                    base_reserve: 200_000_000_000_000,
2225                    quote_reserve: 30_000_000_000,
2226                    base_mint_supply: 1_000_000_000_000_000,
2227                },
2228                1_000_000_000_000,
2229                0,
2230            )
2231            .unwrap();
2232        assert_eq!(q.amount, 148_880_596);
2233    }
2234
2235    #[test]
2236    fn zero_input_returns_zero() {
2237        let sdk = PumpSdk::new();
2238        let g = fixture_global();
2239        let bc = fixture_bonding_curve(fake_pubkey(7));
2240        let q = sdk
2241            .buy_quote_bonding_curve_sol_in(&g, None, &bc, g.token_total_supply, 0, 100)
2242            .unwrap();
2243        assert_eq!(q.amount, 0);
2244        let q = sdk
2245            .sell_quote_bonding_curve(&g, None, &bc, g.token_total_supply, 0, 100)
2246            .unwrap();
2247        assert_eq!(q.amount, 0);
2248    }
2249
2250    #[test]
2251    fn migrated_curve_returns_zero() {
2252        let sdk = PumpSdk::new();
2253        let g = fixture_global();
2254        let mut bc = fixture_bonding_curve(fake_pubkey(7));
2255        bc.virtual_token_reserves = 0;
2256        let q = sdk
2257            .buy_quote_bonding_curve_sol_in(&g, None, &bc, g.token_total_supply, 1_000_000_000, 0)
2258            .unwrap();
2259        assert_eq!(q.amount, 0);
2260    }
2261
2262    #[test]
2263    fn amm_empty_reserves_errors() {
2264        let sdk = PumpSdk::new();
2265        let gc = fixture_amm_global_config();
2266        let pool = fixture_pool(fake_pubkey(0xCC));
2267        let err = sdk
2268            .buy_quote_amm_sol_in(
2269                &gc,
2270                None,
2271                AmmQuoteSource::Pool {
2272                    pool: &pool,
2273                    base_reserve: 0,
2274                    quote_reserve: 1_000,
2275                    base_mint_supply: 1_000,
2276                },
2277                1,
2278                0,
2279            )
2280            .unwrap_err();
2281        assert_eq!(err, QuoteError::EmptyReserves);
2282        let err = sdk
2283            .sell_quote_amm(
2284                &gc,
2285                None,
2286                AmmQuoteSource::Pool {
2287                    pool: &pool,
2288                    base_reserve: 1_000,
2289                    quote_reserve: 0,
2290                    base_mint_supply: 1_000,
2291                },
2292                1,
2293                0,
2294            )
2295            .unwrap_err();
2296        assert_eq!(err, QuoteError::EmptyReserves);
2297    }
2298
2299    #[test]
2300    fn amm_buy_token_out_oversize_errors() {
2301        let sdk = PumpSdk::new();
2302        let gc = fixture_amm_global_config();
2303        let pool = fixture_pool(fake_pubkey(0xCC));
2304        let err = sdk
2305            .buy_quote_amm_token_out(
2306                &gc,
2307                None,
2308                AmmQuoteSource::Pool {
2309                    pool: &pool,
2310                    base_reserve: 1_000,
2311                    quote_reserve: 1_000,
2312                    base_mint_supply: 1_000_000,
2313                },
2314                1_000,
2315                0,
2316            )
2317            .unwrap_err();
2318        assert_eq!(err, QuoteError::BaseOutExceedsReserve);
2319        let err = sdk
2320            .buy_quote_amm_token_out(
2321                &gc,
2322                None,
2323                AmmQuoteSource::Pool {
2324                    pool: &pool,
2325                    base_reserve: 1_000,
2326                    quote_reserve: 1_000,
2327                    base_mint_supply: 1_000_000,
2328                },
2329                5_000,
2330                0,
2331            )
2332            .unwrap_err();
2333        assert_eq!(err, QuoteError::BaseOutExceedsReserve);
2334    }
2335
2336    #[test]
2337    fn bc_token_out_buy_at_real_reserves_errors() {
2338        let sdk = PumpSdk::new();
2339        let g = fixture_global();
2340        let mut bc = fixture_bonding_curve(fake_pubkey(7));
2341        bc.real_token_reserves = bc.virtual_token_reserves;
2342        let err = sdk
2343            .buy_quote_bonding_curve_token_out(
2344                &g,
2345                None,
2346                &bc,
2347                g.token_total_supply,
2348                bc.virtual_token_reserves,
2349                0,
2350            )
2351            .unwrap_err();
2352        assert_eq!(err, QuoteError::DepletedBondingCurve);
2353    }
2354
2355    #[test]
2356    fn amm_quote_bonding_curve_complete_underflow_errors() {
2357        let sdk = PumpSdk::new();
2358        let g = fixture_global();
2359        let gc = fixture_amm_global_config();
2360        let bc = fixture_bonding_curve(fake_pubkey(7)); // real_quote_reserves = 0
2361        let base_mint = fake_pubkey(0xAA);
2362        let err = sdk
2363            .buy_quote_amm_sol_in(
2364                &gc,
2365                None,
2366                AmmQuoteSource::BondingCurveComplete {
2367                    global: &g,
2368                    bonding_curve: &bc,
2369                    base_mint: &base_mint,
2370                    base_mint_supply: g.token_total_supply,
2371                },
2372                1_000_000_000,
2373                0,
2374            )
2375            .unwrap_err();
2376        assert_eq!(err, QuoteError::EmptyReserves);
2377    }
2378
2379    #[test]
2380    fn amm_quote_bonding_curve_complete_matches_formula() {
2381        let sdk = PumpSdk::new();
2382        let g = fixture_global();
2383        let gc = fixture_amm_global_config();
2384        let mut bc = fixture_bonding_curve(fake_pubkey(7));
2385        bc.real_quote_reserves = 100_000_000_000;
2386        let base_mint = fake_pubkey(0xAA);
2387
2388        let q = sdk
2389            .buy_quote_amm_sol_in(
2390                &gc,
2391                None,
2392                AmmQuoteSource::BondingCurveComplete {
2393                    global: &g,
2394                    bonding_curve: &bc,
2395                    base_mint: &base_mint,
2396                    base_mint_supply: g.token_total_supply,
2397                },
2398                1_000_000_000,
2399                0,
2400            )
2401            .unwrap();
2402
2403        let base_reserve = g.token_total_supply - g.initial_real_token_reserves;
2404        let quote_reserve = bc.real_quote_reserves - g.pool_migration_fee;
2405        let pool_creator = pda::pump::pool_authority(&base_mint).0;
2406        let ctx = AmmContext {
2407            global_config: &gc,
2408            fee_config: None,
2409            base_mint: &base_mint,
2410            pool_creator: &pool_creator,
2411            coin_creator: &bc.creator,
2412            base_reserve,
2413            quote_reserve,
2414            base_mint_supply: g.token_total_supply,
2415        };
2416        let expected = amm_math::buy_quote_input(&ctx, 1_000_000_000)
2417            .unwrap()
2418            .base_amount_out;
2419        assert_eq!(q.amount, expected);
2420    }
2421}