Skip to main content

pump_rust_client/sdk/
mod.rs

1use 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 solana_program::{pubkey, 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        assert!(metas.contains(&solana_program::sysvar::rent::ID));
504    }
505
506    #[test]
507    fn create_v2_instruction_wires_mayhem_pdas() {
508        let sdk = PumpSdk::new();
509        let mint = fake_pubkey(21);
510        let user = fake_pubkey(22);
511        let creator = fake_pubkey(23);
512
513        let ix = sdk.create_v2_instruction(
514            mint,
515            user,
516            "Test",
517            "TST",
518            "https://example.com/metadata.json",
519            creator,
520            Pubkey::default(),
521            false,
522            false,
523        );
524
525        assert_eq!(ix.program_id, crate::pump::ID);
526        assert_eq!(
527            &ix.data[..8],
528            <client::args::CreateV2 as Discriminator>::DISCRIMINATOR,
529        );
530        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
531        assert!(metas.contains(&pda::mayhem::global_params().0));
532        assert!(metas.contains(&pda::mayhem::sol_vault().0));
533        assert!(metas.contains(&pda::mayhem::mayhem_state(&mint).0));
534        assert!(metas.contains(&pda::mayhem::mayhem_token_vault(&mint).0));
535        assert!(metas.contains(&constants::MAYHEM_PROGRAM_ID));
536        assert!(metas.contains(&constants::SPL_TOKEN_2022_PROGRAM_ID));
537    }
538
539    #[test]
540    fn create_v2_instruction_appends_remaining_accounts_for_non_sol_quote() {
541        let sdk = PumpSdk::new();
542        let mint = fake_pubkey(0xA1);
543        let user = fake_pubkey(0xA2);
544        let creator = fake_pubkey(0xA3);
545        let quote_mint = fake_pubkey(0xC1);
546
547        let baseline = sdk.create_v2_instruction(
548            mint,
549            user,
550            "Test",
551            "TST",
552            "https://example.com/metadata.json",
553            creator,
554            Pubkey::default(),
555            false,
556            false,
557        );
558        let ix = sdk.create_v2_instruction(
559            mint,
560            user,
561            "Test",
562            "TST",
563            "https://example.com/metadata.json",
564            creator,
565            quote_mint,
566            false,
567            false,
568        );
569
570        assert_eq!(ix.accounts.len(), baseline.accounts.len() + 3);
571
572        let n = ix.accounts.len();
573        let quote_meta = &ix.accounts[n - 3];
574        let ata_meta = &ix.accounts[n - 2];
575        let token_program_meta = &ix.accounts[n - 1];
576
577        assert_eq!(quote_meta.pubkey, quote_mint);
578        assert!(!quote_meta.is_writable);
579        assert!(!quote_meta.is_signer);
580
581        let bonding_curve = pda::pump::bonding_curve(&mint).0;
582        let expected_ata = pda::associated_token(
583            &bonding_curve,
584            &constants::SPL_TOKEN_PROGRAM_ID,
585            &quote_mint,
586        )
587        .0;
588        assert_eq!(ata_meta.pubkey, expected_ata);
589        assert!(ata_meta.is_writable);
590        assert!(!ata_meta.is_signer);
591
592        assert_eq!(token_program_meta.pubkey, constants::SPL_TOKEN_PROGRAM_ID);
593        assert!(!token_program_meta.is_writable);
594        assert!(!token_program_meta.is_signer);
595    }
596
597    #[test]
598    fn create_v2_instruction_omits_remaining_accounts_for_sol_quote() {
599        let sdk = PumpSdk::new();
600        let mint = fake_pubkey(0xB1);
601        let user = fake_pubkey(0xB2);
602        let creator = fake_pubkey(0xB3);
603
604        let with_default = sdk.create_v2_instruction(
605            mint,
606            user,
607            "Test",
608            "TST",
609            "https://example.com/metadata.json",
610            creator,
611            Pubkey::default(),
612            false,
613            false,
614        );
615        let with_native = sdk.create_v2_instruction(
616            mint,
617            user,
618            "Test",
619            "TST",
620            "https://example.com/metadata.json",
621            creator,
622            constants::NATIVE_MINT,
623            false,
624            false,
625        );
626
627        assert_eq!(with_default.accounts.len(), with_native.accounts.len());
628        for (a, b) in with_default
629            .accounts
630            .iter()
631            .zip(with_native.accounts.iter())
632        {
633            assert_eq!(a.pubkey, b.pubkey);
634            assert_eq!(a.is_writable, b.is_writable);
635            assert_eq!(a.is_signer, b.is_signer);
636        }
637        assert!(!with_default
638            .accounts
639            .iter()
640            .any(|m| m.pubkey == constants::SPL_TOKEN_PROGRAM_ID));
641    }
642
643    #[test]
644    #[allow(deprecated)]
645    fn buy_instruction_appends_bonding_curve_v2_then_buyback_fee_recipient() {
646        let sdk = PumpSdk::new();
647        let mint = fake_pubkey(61);
648        let user = fake_pubkey(62);
649        let creator = fake_pubkey(63);
650        let fee_recipient = fake_pubkey(64);
651        let buyback_fee_recipient = fake_pubkey(65);
652        let token_program = constants::SPL_TOKEN_PROGRAM_ID;
653
654        let ix = sdk.buy_instruction(
655            mint,
656            user,
657            creator,
658            fee_recipient,
659            buyback_fee_recipient,
660            100,
661            5_000_000,
662            token_program,
663        );
664
665        assert_eq!(ix.program_id, crate::pump::ID);
666        assert_eq!(
667            &ix.data[..8],
668            <client::args::Buy as Discriminator>::DISCRIMINATOR,
669        );
670
671        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
672        assert!(metas.contains(&pda::pump::global().0));
673        assert!(metas.contains(&pda::pump::bonding_curve(&mint).0));
674        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
675        assert!(metas.contains(&pda::pump::user_volume_accumulator(&user).0));
676        assert!(metas.contains(&pda::pump::fee_config().0));
677        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
678        assert!(metas.contains(&fee_recipient));
679
680        let n = ix.accounts.len();
681        let bcv2 = &ix.accounts[n - 2];
682        let buyback = &ix.accounts[n - 1];
683        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&mint).0);
684        assert!(!bcv2.is_writable);
685        assert!(!bcv2.is_signer);
686        assert_eq!(buyback.pubkey, buyback_fee_recipient);
687        assert!(buyback.is_writable);
688        assert!(!buyback.is_signer);
689    }
690
691    #[test]
692    fn create_v2_and_buy_instruction_returns_create_then_buy_v2_prefix_in_order() {
693        let sdk = PumpSdk::new();
694        let mint = fake_pubkey(71);
695        let user = fake_pubkey(72);
696        let creator = fake_pubkey(73);
697        let fee_recipient = fake_pubkey(74);
698        let buyback_fee_recipient = fake_pubkey(75);
699        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
700
701        let ixs = sdk
702            .create_v2_and_buy_instruction(
703                mint,
704                user,
705                "Test",
706                "TST",
707                "https://example.com/metadata.json",
708                creator,
709                Pubkey::default(),
710                false,
711                false,
712                None,
713                &global,
714                1_000,
715                500_000,
716            )
717            .expect("create_v2_and_buy_instruction");
718
719        assert_eq!(ixs.len(), 6);
720        assert_eq!(
721            &ixs[0].data[..8],
722            <client::args::CreateV2 as Discriminator>::DISCRIMINATOR,
723        );
724        for i in 1..5 {
725            assert_eq!(ixs[i].data, vec![1], "expected SPL ATA idempotent ix");
726        }
727        assert_eq!(
728            &ixs[5].data[..8],
729            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
730        );
731
732        let buy_metas: Vec<Pubkey> = ixs[5].accounts.iter().map(|m| m.pubkey).collect();
733        assert!(buy_metas.contains(&constants::SPL_TOKEN_2022_PROGRAM_ID));
734        assert!(buy_metas.contains(&constants::SPL_TOKEN_PROGRAM_ID));
735        assert!(buy_metas.contains(&constants::NATIVE_MINT));
736        assert!(buy_metas.contains(&pda::pump::sharing_config(&mint).0));
737    }
738
739    #[test]
740    fn buy_v2_instruction_wires_quote_and_sharing_config_pdas() {
741        let sdk = PumpSdk::new();
742        let base_mint = fake_pubkey(81);
743        let quote_mint = fake_pubkey(82);
744        let user = fake_pubkey(83);
745        let creator = fake_pubkey(84);
746        let fee_recipient = fake_pubkey(85);
747        let buyback_fee_recipient = fake_pubkey(86);
748        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
749        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
750        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
751        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
752            creator,
753            quote_mint,
754            ..Default::default()
755        });
756
757        let ix = sdk
758            .buy_v2_instruction(
759                &global,
760                &bonding_curve,
761                base_mint,
762                quote_token_program,
763                user,
764                1_000,
765                5_000_000,
766            )
767            .expect("buy_v2_instruction");
768
769        assert_eq!(ix.program_id, crate::pump::ID);
770        assert_eq!(
771            &ix.data[..8],
772            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
773        );
774
775        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
776        assert!(metas.contains(&pda::pump::global().0));
777        assert!(metas.contains(&base_mint));
778        assert!(metas.contains(&quote_mint));
779        assert!(metas.contains(&fee_recipient));
780        assert!(metas.contains(&buyback_fee_recipient));
781        assert!(metas.contains(&pda::pump::bonding_curve(&base_mint).0));
782        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
783        assert!(metas.contains(&pda::pump::sharing_config(&base_mint).0));
784        assert!(metas.contains(&pda::pump::user_volume_accumulator(&user).0));
785        assert!(metas.contains(&pda::pump::fee_config().0));
786        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
787
788        assert!(metas
789            .contains(&pda::associated_token(&fee_recipient, &quote_token_program, &quote_mint).0));
790        assert!(metas.contains(
791            &pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0
792        ));
793
794        let signers: Vec<Pubkey> = ix
795            .accounts
796            .iter()
797            .filter(|m| m.is_signer)
798            .map(|m| m.pubkey)
799            .collect();
800        assert_eq!(signers, vec![user]);
801    }
802
803    #[test]
804    fn buy_v2_instructions_prepends_four_ata_creates() {
805        let sdk = PumpSdk::new();
806        let base_mint = fake_pubkey(91);
807        let quote_mint = fake_pubkey(92);
808        let user = fake_pubkey(93);
809        let creator = fake_pubkey(94);
810        let fee_recipient = fake_pubkey(95);
811        let buyback_fee_recipient = fake_pubkey(96);
812        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
813        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
814        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
815        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
816            creator,
817            quote_mint,
818            ..Default::default()
819        });
820
821        let ixs = sdk
822            .buy_v2_instructions(
823                &global,
824                &bonding_curve,
825                base_mint,
826                quote_token_program,
827                user,
828                1_000,
829                5_000_000,
830            )
831            .expect("buy_v2_instructions");
832
833        assert_eq!(ixs.len(), 5);
834        for ata_ix in &ixs[..4] {
835            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
836            assert_eq!(ata_ix.data, vec![1]);
837        }
838        assert_eq!(
839            &ixs[4].data[..8],
840            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
841        );
842
843        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
844        let expected_atas: [Pubkey; 4] = [
845            pda::associated_token(&user, &base_token_program, &base_mint).0,
846            pda::associated_token(&bonding_curve, &quote_token_program, &quote_mint).0,
847            pda::associated_token(&user, &quote_token_program, &quote_mint).0,
848            pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0,
849        ];
850        for (i, expected) in expected_atas.iter().enumerate() {
851            let metas: Vec<Pubkey> = ixs[i].accounts.iter().map(|m| m.pubkey).collect();
852            assert!(metas.contains(expected), "ix {i} missing expected ATA");
853        }
854    }
855
856    #[test]
857    #[allow(deprecated)]
858    fn sell_instruction_cashback_appends_uva_then_bonding_curve_v2() {
859        let sdk = PumpSdk::new();
860        let mint = fake_pubkey(101);
861        let user = fake_pubkey(102);
862        let creator = fake_pubkey(103);
863        let fee_recipient = fake_pubkey(104);
864        let buyback_fee_recipient = fake_pubkey(105);
865        let token_program = constants::SPL_TOKEN_PROGRAM_ID;
866
867        let ix = sdk.sell_instruction(
868            mint,
869            user,
870            creator,
871            fee_recipient,
872            buyback_fee_recipient,
873            100,
874            1_000_000,
875            token_program,
876            true,
877        );
878
879        assert_eq!(ix.program_id, crate::pump::ID);
880        assert_eq!(
881            &ix.data[..8],
882            <client::args::Sell as Discriminator>::DISCRIMINATOR,
883        );
884
885        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
886        assert!(metas.contains(&pda::pump::global().0));
887        assert!(metas.contains(&pda::pump::bonding_curve(&mint).0));
888        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
889        assert!(metas.contains(&pda::pump::fee_config().0));
890        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
891        assert!(metas.contains(&fee_recipient));
892        assert!(metas.contains(&pda::associated_token(&user, &token_program, &mint).0));
893
894        let n = ix.accounts.len();
895        let uva = &ix.accounts[n - 2];
896        let bcv2 = &ix.accounts[n - 1];
897        assert_eq!(uva.pubkey, pda::pump::user_volume_accumulator(&user).0);
898        assert!(uva.is_writable);
899        assert!(!uva.is_signer);
900        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&mint).0);
901        assert!(!bcv2.is_writable);
902        assert!(!bcv2.is_signer);
903    }
904
905    #[test]
906    #[allow(deprecated)]
907    fn sell_instruction_non_cashback_skips_uva() {
908        let sdk = PumpSdk::new();
909        let mint = fake_pubkey(101);
910        let user = fake_pubkey(102);
911        let creator = fake_pubkey(103);
912        let fee_recipient = fake_pubkey(104);
913        let buyback_fee_recipient = fake_pubkey(105);
914        let token_program = constants::SPL_TOKEN_PROGRAM_ID;
915
916        let ix = sdk.sell_instruction(
917            mint,
918            user,
919            creator,
920            fee_recipient,
921            buyback_fee_recipient,
922            100,
923            1_000_000,
924            token_program,
925            false,
926        );
927
928        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
929        assert!(!metas.contains(&pda::pump::user_volume_accumulator(&user).0));
930
931        let bcv2 = &ix.accounts[ix.accounts.len() - 1];
932        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&mint).0);
933        assert!(!bcv2.is_writable);
934        assert!(!bcv2.is_signer);
935    }
936
937    #[test]
938    fn sell_v2_instruction_wires_quote_and_sharing_config_pdas() {
939        let sdk = PumpSdk::new();
940        let base_mint = fake_pubkey(111);
941        let quote_mint = fake_pubkey(112);
942        let user = fake_pubkey(113);
943        let creator = fake_pubkey(114);
944        let fee_recipient = fake_pubkey(115);
945        let buyback_fee_recipient = fake_pubkey(116);
946        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
947        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
948        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
949            creator,
950            quote_mint,
951            ..Default::default()
952        });
953
954        let ix = sdk
955            .sell_v2_instruction(
956                &global,
957                &bonding_curve,
958                base_mint,
959                quote_token_program,
960                user,
961                1_000,
962                1_000_000,
963            )
964            .expect("sell_v2_instruction");
965
966        assert_eq!(ix.program_id, crate::pump::ID);
967        assert_eq!(
968            &ix.data[..8],
969            <client::args::SellV2 as Discriminator>::DISCRIMINATOR,
970        );
971
972        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
973        assert!(metas.contains(&pda::pump::global().0));
974        assert!(metas.contains(&base_mint));
975        assert!(metas.contains(&quote_mint));
976        assert!(metas.contains(&fee_recipient));
977        assert!(metas.contains(&buyback_fee_recipient));
978        assert!(metas.contains(&pda::pump::bonding_curve(&base_mint).0));
979        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
980        assert!(metas.contains(&pda::pump::sharing_config(&base_mint).0));
981        assert!(metas.contains(&pda::pump::user_volume_accumulator(&user).0));
982        assert!(metas.contains(&pda::pump::fee_config().0));
983        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
984
985        assert!(!metas.contains(&pda::pump::global_volume_accumulator().0));
986
987        assert!(metas
988            .contains(&pda::associated_token(&fee_recipient, &quote_token_program, &quote_mint).0));
989        assert!(metas.contains(
990            &pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0
991        ));
992
993        let signers: Vec<Pubkey> = ix
994            .accounts
995            .iter()
996            .filter(|m| m.is_signer)
997            .map(|m| m.pubkey)
998            .collect();
999        assert_eq!(signers, vec![user]);
1000    }
1001
1002    #[test]
1003    fn sell_v2_instructions_prepends_four_ata_creates() {
1004        let sdk = PumpSdk::new();
1005        let base_mint = fake_pubkey(121);
1006        let quote_mint = fake_pubkey(122);
1007        let user = fake_pubkey(123);
1008        let creator = fake_pubkey(124);
1009        let fee_recipient = fake_pubkey(125);
1010        let buyback_fee_recipient = fake_pubkey(126);
1011        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1012        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1013        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1014        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1015            creator,
1016            quote_mint,
1017            ..Default::default()
1018        });
1019
1020        let ixs = sdk
1021            .sell_v2_instructions(
1022                &global,
1023                &bonding_curve,
1024                base_mint,
1025                quote_token_program,
1026                user,
1027                1_000,
1028                1_000_000,
1029            )
1030            .expect("sell_v2_instructions");
1031
1032        assert_eq!(ixs.len(), 5);
1033        for ata_ix in &ixs[..4] {
1034            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
1035            assert_eq!(ata_ix.data, vec![1]);
1036        }
1037        assert_eq!(
1038            &ixs[4].data[..8],
1039            <client::args::SellV2 as Discriminator>::DISCRIMINATOR,
1040        );
1041
1042        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
1043        let expected_atas: [Pubkey; 4] = [
1044            pda::associated_token(&user, &base_token_program, &base_mint).0,
1045            pda::associated_token(&bonding_curve, &quote_token_program, &quote_mint).0,
1046            pda::associated_token(&user, &quote_token_program, &quote_mint).0,
1047            pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0,
1048        ];
1049        for (i, expected) in expected_atas.iter().enumerate() {
1050            let metas: Vec<Pubkey> = ixs[i].accounts.iter().map(|m| m.pubkey).collect();
1051            assert!(metas.contains(expected), "ix {i} missing expected ATA");
1052        }
1053    }
1054
1055    fn fake_amm_inputs() -> (
1056        Pubkey,
1057        Pubkey,
1058        Pubkey,
1059        Pubkey,
1060        Pubkey,
1061        Pubkey,
1062        Pubkey,
1063        Pubkey,
1064    ) {
1065        (
1066            fake_pubkey(0xA1),
1067            fake_pubkey(0xA2),
1068            fake_pubkey(0xA3),
1069            fake_pubkey(0xA4),
1070            fake_pubkey(0xA5),
1071            fake_pubkey(0xA6),
1072            fake_pubkey(0xA7),
1073            constants::SPL_TOKEN_PROGRAM_ID,
1074        )
1075    }
1076
1077    #[test]
1078    fn buy_amm_instruction_wires_pump_amm_pdas() {
1079        let sdk = PumpSdk::new();
1080        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
1081            fake_amm_inputs();
1082        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1083        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1084        let amm_global = amm_global_with_fees(fee_rec, buyback);
1085        let pool_state = Pool::new(PoolFromIdl {
1086            base_mint,
1087            quote_mint,
1088            coin_creator,
1089            is_cashback_coin: true,
1090            ..Default::default()
1091        });
1092
1093        let ix = sdk
1094            .buy_amm_instruction(
1095                pool,
1096                &amm_global,
1097                &pool_state,
1098                base_token_program,
1099                quote_token_program,
1100                user,
1101                1_000,
1102                500_000,
1103            )
1104            .expect("buy_amm_instruction");
1105
1106        assert_eq!(ix.program_id, crate::pump_amm::ID);
1107        assert_eq!(
1108            &ix.data[..8],
1109            <amm_client::args::Buy as Discriminator>::DISCRIMINATOR,
1110        );
1111        assert_eq!(ix.accounts.len(), 27);
1112
1113        let pubkeys: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1114        assert!(pubkeys.contains(&pda::pump_amm::global_config().0));
1115        assert!(pubkeys.contains(&pda::pump_amm::event_authority().0));
1116        assert!(pubkeys.contains(&pda::pump_amm::global_volume_accumulator().0));
1117        assert!(pubkeys.contains(&pda::pump_amm::user_volume_accumulator(&user).0));
1118        assert!(pubkeys.contains(&pda::pump_amm::fee_config().0));
1119        assert!(pubkeys.contains(&pda::pump_amm::coin_creator_vault_authority(&coin_creator).0));
1120
1121        assert_eq!(ix.accounts[0].pubkey, pool);
1122        assert!(ix.accounts[0].is_writable);
1123        assert!(!ix.accounts[0].is_signer);
1124        assert_eq!(ix.accounts[1].pubkey, user);
1125        assert!(ix.accounts[1].is_signer);
1126
1127        let n = ix.accounts.len();
1128        let cashback = &ix.accounts[n - 4];
1129        let bcv2 = &ix.accounts[n - 3];
1130        let buyback_meta = &ix.accounts[n - 2];
1131        let buyback_ata = &ix.accounts[n - 1];
1132
1133        let user_vol_accum = pda::pump_amm::user_volume_accumulator(&user).0;
1134        assert_eq!(
1135            cashback.pubkey,
1136            pda::associated_token(
1137                &user_vol_accum,
1138                &quote_token_program,
1139                &constants::NATIVE_MINT,
1140            )
1141            .0
1142        );
1143        assert!(cashback.is_writable);
1144
1145        assert_eq!(bcv2.pubkey, pda::pump_amm::pool_v2(&base_mint).0);
1146        assert!(!bcv2.is_writable);
1147
1148        assert_eq!(buyback_meta.pubkey, buyback);
1149        assert!(buyback_meta.is_writable);
1150
1151        assert_eq!(
1152            buyback_ata.pubkey,
1153            pda::associated_token(&buyback, &quote_token_program, &quote_mint).0
1154        );
1155        assert!(buyback_ata.is_writable);
1156    }
1157
1158    #[test]
1159    fn buy_amm_instruction_skips_remaining_when_no_cashback_no_creator() {
1160        let sdk = PumpSdk::new();
1161        let (pool, base_mint, quote_mint, user, _, fee_rec, buyback, _) = fake_amm_inputs();
1162        let amm_global = amm_global_with_fees(fee_rec, buyback);
1163        let pool_state = Pool::new(PoolFromIdl {
1164            base_mint,
1165            quote_mint,
1166            coin_creator: Pubkey::default(),
1167            is_cashback_coin: false,
1168            ..Default::default()
1169        });
1170
1171        let ix = sdk
1172            .buy_amm_instruction(
1173                pool,
1174                &amm_global,
1175                &pool_state,
1176                constants::SPL_TOKEN_2022_PROGRAM_ID,
1177                constants::SPL_TOKEN_PROGRAM_ID,
1178                user,
1179                1_000,
1180                500_000,
1181            )
1182            .expect("buy_amm_instruction");
1183
1184        assert_eq!(ix.accounts.len(), 25);
1185        let n = ix.accounts.len();
1186        assert_eq!(ix.accounts[n - 2].pubkey, buyback);
1187        assert!(ix.accounts[n - 2].is_writable);
1188    }
1189
1190    #[test]
1191    fn sell_amm_instruction_wires_pump_amm_pdas() {
1192        let sdk = PumpSdk::new();
1193        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
1194            fake_amm_inputs();
1195        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1196        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1197        let amm_global = amm_global_with_fees(fee_rec, buyback);
1198        let pool_state = Pool::new(PoolFromIdl {
1199            base_mint,
1200            quote_mint,
1201            coin_creator,
1202            is_cashback_coin: true,
1203            ..Default::default()
1204        });
1205
1206        let ix = sdk
1207            .sell_amm_instruction(
1208                pool,
1209                &amm_global,
1210                &pool_state,
1211                base_token_program,
1212                quote_token_program,
1213                user,
1214                1_000_000,
1215                500,
1216            )
1217            .expect("sell_amm_instruction");
1218
1219        assert_eq!(ix.program_id, crate::pump_amm::ID);
1220        assert_eq!(
1221            &ix.data[..8],
1222            <amm_client::args::Sell as Discriminator>::DISCRIMINATOR,
1223        );
1224        assert_eq!(ix.accounts.len(), 26);
1225
1226        let pubkeys: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1227        assert!(!pubkeys.contains(&pda::pump_amm::global_volume_accumulator().0));
1228
1229        let user_vol_accum = pda::pump_amm::user_volume_accumulator(&user).0;
1230        let n = ix.accounts.len();
1231        let cashback_ata = &ix.accounts[n - 5];
1232        let cashback_uva = &ix.accounts[n - 4];
1233        let bcv2 = &ix.accounts[n - 3];
1234        let buyback_meta = &ix.accounts[n - 2];
1235        let buyback_ata = &ix.accounts[n - 1];
1236
1237        assert_eq!(
1238            cashback_ata.pubkey,
1239            pda::associated_token(&user_vol_accum, &quote_token_program, &quote_mint).0
1240        );
1241        assert!(cashback_ata.is_writable);
1242
1243        assert_eq!(cashback_uva.pubkey, user_vol_accum);
1244        assert!(cashback_uva.is_writable);
1245
1246        assert_eq!(bcv2.pubkey, pda::pump_amm::pool_v2(&base_mint).0);
1247        assert!(!bcv2.is_writable);
1248
1249        assert_eq!(buyback_meta.pubkey, buyback);
1250        assert!(!buyback_meta.is_writable);
1251
1252        assert_eq!(
1253            buyback_ata.pubkey,
1254            pda::associated_token(&buyback, &quote_token_program, &quote_mint).0
1255        );
1256        assert!(buyback_ata.is_writable);
1257    }
1258
1259    #[test]
1260    fn sell_amm_instruction_skips_remaining_when_no_cashback_no_creator() {
1261        let sdk = PumpSdk::new();
1262        let (pool, base_mint, quote_mint, user, _, fee_rec, buyback, _) = fake_amm_inputs();
1263        let amm_global = amm_global_with_fees(fee_rec, buyback);
1264        let pool_state = Pool::new(PoolFromIdl {
1265            base_mint,
1266            quote_mint,
1267            coin_creator: Pubkey::default(),
1268            is_cashback_coin: false,
1269            ..Default::default()
1270        });
1271
1272        let ix = sdk
1273            .sell_amm_instruction(
1274                pool,
1275                &amm_global,
1276                &pool_state,
1277                constants::SPL_TOKEN_2022_PROGRAM_ID,
1278                constants::SPL_TOKEN_PROGRAM_ID,
1279                user,
1280                1_000_000,
1281                500,
1282            )
1283            .expect("sell_amm_instruction");
1284
1285        assert_eq!(ix.accounts.len(), 23);
1286        let n = ix.accounts.len();
1287        assert_eq!(ix.accounts[n - 2].pubkey, buyback);
1288        assert!(!ix.accounts[n - 2].is_writable);
1289    }
1290
1291    #[test]
1292    fn buy_amm_instructions_prepends_one_ata_create() {
1293        let sdk = PumpSdk::new();
1294        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
1295            fake_amm_inputs();
1296        let amm_global = amm_global_with_fees(fee_rec, buyback);
1297        let pool_state = Pool::new(PoolFromIdl {
1298            base_mint,
1299            quote_mint,
1300            coin_creator,
1301            is_cashback_coin: false,
1302            ..Default::default()
1303        });
1304
1305        let ixs = sdk
1306            .buy_amm_instructions(
1307                pool,
1308                &amm_global,
1309                &pool_state,
1310                constants::SPL_TOKEN_2022_PROGRAM_ID,
1311                constants::SPL_TOKEN_PROGRAM_ID,
1312                user,
1313                1_000,
1314                500_000,
1315            )
1316            .expect("buy_amm_instructions");
1317
1318        assert_eq!(ixs.len(), 2);
1319        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1320        assert_eq!(ixs[0].data, vec![1]);
1321        assert_eq!(ixs[1].program_id, crate::pump_amm::ID);
1322        assert_eq!(
1323            &ixs[1].data[..8],
1324            <amm_client::args::Buy as Discriminator>::DISCRIMINATOR,
1325        );
1326    }
1327
1328    #[test]
1329    fn sell_amm_instructions_prepends_one_ata_create() {
1330        let sdk = PumpSdk::new();
1331        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
1332            fake_amm_inputs();
1333        let amm_global = amm_global_with_fees(fee_rec, buyback);
1334        let pool_state = Pool::new(PoolFromIdl {
1335            base_mint,
1336            quote_mint,
1337            coin_creator,
1338            is_cashback_coin: false,
1339            ..Default::default()
1340        });
1341
1342        let ixs = sdk
1343            .sell_amm_instructions(
1344                pool,
1345                &amm_global,
1346                &pool_state,
1347                constants::SPL_TOKEN_2022_PROGRAM_ID,
1348                constants::SPL_TOKEN_PROGRAM_ID,
1349                user,
1350                1_000_000,
1351                500,
1352            )
1353            .expect("sell_amm_instructions");
1354
1355        assert_eq!(ixs.len(), 2);
1356        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1357        assert_eq!(ixs[0].data, vec![1]);
1358        assert_eq!(ixs[1].program_id, crate::pump_amm::ID);
1359        assert_eq!(
1360            &ixs[1].data[..8],
1361            <amm_client::args::Sell as Discriminator>::DISCRIMINATOR,
1362        );
1363    }
1364
1365    fn user_wsol_ata(user: &Pubkey) -> Pubkey {
1366        pda::associated_token(
1367            user,
1368            &constants::SPL_TOKEN_PROGRAM_ID,
1369            &constants::NATIVE_MINT,
1370        )
1371        .0
1372    }
1373
1374    /// Parses lamports from a system `transfer` instruction (tests).
1375    fn parse_system_transfer_lamports(ix: &solana_program::instruction::Instruction) -> u64 {
1376        assert_eq!(ix.program_id, system_program::ID);
1377        assert_eq!(ix.data.len(), 12);
1378        assert_eq!(&ix.data[..4], &2u32.to_le_bytes());
1379        u64::from_le_bytes(ix.data[4..12].try_into().unwrap())
1380    }
1381
1382    #[test]
1383    fn trade_tx_buy_bc_delegates_to_buy_v2_instructions() {
1384        let sdk = PumpSdk::new();
1385        let mint = fake_pubkey(0xB1);
1386        let user = fake_pubkey(0xB2);
1387        let creator = fake_pubkey(0xB3);
1388        let fee_recipient = fake_pubkey(0xB4);
1389        let buyback_fee_recipient = fake_pubkey(0xB5);
1390        let max_sol = 7_500_000u64;
1391        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1392        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1393            creator,
1394            quote_mint: constants::NATIVE_MINT,
1395            ..Default::default()
1396        });
1397
1398        let ixs = sdk
1399            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1400                mint,
1401                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1402                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1403                user,
1404                is_buy: true,
1405                venue: TradeVenue::BondingCurve {
1406                    global: &global,
1407                    bonding_curve: &bonding_curve,
1408                },
1409                base_amount: 1_000,
1410                sol_amount_threshold: max_sol,
1411            })
1412            .expect("trade_tx_instructions");
1413
1414        assert_eq!(ixs.len(), 3);
1415        for ata_ix in &ixs[..2] {
1416            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
1417            assert_eq!(ata_ix.data, vec![1]);
1418        }
1419        assert_eq!(ixs[2].program_id, crate::pump::ID);
1420        assert_eq!(
1421            &ixs[2].data[..8],
1422            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
1423        );
1424
1425        let expected_atas: [Pubkey; 2] = [
1426            pda::associated_token(&user, &constants::SPL_TOKEN_2022_PROGRAM_ID, &mint).0,
1427            user_wsol_ata(&user),
1428        ];
1429        for (i, expected) in expected_atas.iter().enumerate() {
1430            let metas: Vec<Pubkey> = ixs[i].accounts.iter().map(|m| m.pubkey).collect();
1431            assert!(metas.contains(expected), "ata ix {i} missing expected ATA");
1432        }
1433    }
1434
1435    #[test]
1436    fn trade_tx_sell_bc_delegates_to_sell_v2_instructions() {
1437        let sdk = PumpSdk::new();
1438        let mint = fake_pubkey(0xC1);
1439        let user = fake_pubkey(0xC2);
1440        let creator = fake_pubkey(0xC3);
1441        let fee_recipient = fake_pubkey(0xC4);
1442        let buyback_fee_recipient = fake_pubkey(0xC5);
1443        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1444        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1445            creator,
1446            quote_mint: constants::NATIVE_MINT,
1447            ..Default::default()
1448        });
1449
1450        let ixs = sdk
1451            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1452                mint,
1453                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1454                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1455                user,
1456                is_buy: false,
1457                venue: TradeVenue::BondingCurve {
1458                    global: &global,
1459                    bonding_curve: &bonding_curve,
1460                },
1461                base_amount: 1_000,
1462                sol_amount_threshold: 1_000_000,
1463            })
1464            .expect("trade_tx_instructions");
1465
1466        assert_eq!(ixs.len(), 2);
1467        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1468        assert_eq!(ixs[0].data, vec![1]);
1469        assert_eq!(ixs[1].program_id, crate::pump::ID);
1470        assert_eq!(
1471            &ixs[1].data[..8],
1472            <client::args::SellV2 as Discriminator>::DISCRIMINATOR,
1473        );
1474        let metas: Vec<Pubkey> = ixs[0].accounts.iter().map(|m| m.pubkey).collect();
1475        assert!(metas.contains(&user_wsol_ata(&user)));
1476    }
1477
1478    #[test]
1479    fn trade_tx_auto_matches_explicit_bc_when_not_complete() {
1480        let sdk = PumpSdk::new();
1481        let mint = fake_pubkey(0x70);
1482        let user = fake_pubkey(0x71);
1483        let creator = fake_pubkey(0x72);
1484        let fee_recipient = fake_pubkey(0x73);
1485        let buyback_fee_recipient = fake_pubkey(0x74);
1486        let max_sol = 7_500_000u64;
1487        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1488        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1489            creator,
1490            quote_mint: constants::NATIVE_MINT,
1491            complete: false,
1492            ..Default::default()
1493        });
1494
1495        let explicit = sdk
1496            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1497                mint,
1498                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1499                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1500                user,
1501                is_buy: true,
1502                venue: TradeVenue::BondingCurve {
1503                    global: &global,
1504                    bonding_curve: &bonding_curve,
1505                },
1506                base_amount: 1_000,
1507                sol_amount_threshold: max_sol,
1508            })
1509            .expect("explicit bc");
1510
1511        let pool = fake_pubkey(0x75);
1512        let amm_g = amm_global_with_fees(fake_pubkey(0x76), fake_pubkey(0x77));
1513        let ps = Pool::new(PoolFromIdl {
1514            base_mint: mint,
1515            quote_mint: constants::NATIVE_MINT,
1516            ..Default::default()
1517        });
1518        let auto = sdk
1519            .trade_tx_instructions(TradeTxParams {
1520                mint,
1521                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1522                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1523                user,
1524                is_buy: true,
1525                base_amount: 1_000,
1526                sol_amount_threshold: max_sol,
1527                pump_global: &global,
1528                bonding_curve: &bonding_curve,
1529                pump_pool: Some(PumpPoolCtx {
1530                    pool,
1531                    amm_global: &amm_g,
1532                    pool_state: &ps,
1533                }),
1534            })
1535            .expect("auto bc");
1536
1537        assert_eq!(explicit.len(), auto.len());
1538        for (a, b) in explicit.iter().zip(auto.iter()) {
1539            assert_eq!(a.program_id, b.program_id);
1540            assert_eq!(a.data, b.data);
1541        }
1542    }
1543
1544    #[test]
1545    fn trade_tx_auto_returns_none_when_complete_without_graduated() {
1546        let sdk = PumpSdk::new();
1547        let global = pump_global_with_fees(fake_pubkey(0x80), fake_pubkey(0x81));
1548        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1549            complete: true,
1550            ..Default::default()
1551        });
1552        assert!(sdk
1553            .trade_tx_instructions(TradeTxParams {
1554                mint: fake_pubkey(0x82),
1555                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1556                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1557                user: fake_pubkey(0x83),
1558                is_buy: true,
1559                base_amount: 1,
1560                sol_amount_threshold: 1,
1561                pump_global: &global,
1562                bonding_curve: &bonding_curve,
1563                pump_pool: None,
1564            })
1565            .is_none());
1566    }
1567
1568    #[test]
1569    fn trade_tx_auto_matches_explicit_amm_when_complete() {
1570        let sdk = PumpSdk::new();
1571        let mint = fake_pubkey(0x90);
1572        let user = fake_pubkey(0x91);
1573        let coin_creator = fake_pubkey(0x92);
1574        let fee_recipient = fake_pubkey(0x93);
1575        let buyback_fee_recipient = fake_pubkey(0x94);
1576        let pool = fake_pubkey(0x95);
1577        let max_sol = 2_500_000u64;
1578        let amm_global = amm_global_with_fees(fee_recipient, buyback_fee_recipient);
1579        let pool_state = Pool::new(PoolFromIdl {
1580            base_mint: mint,
1581            quote_mint: constants::NATIVE_MINT,
1582            coin_creator,
1583            is_cashback_coin: true,
1584            ..Default::default()
1585        });
1586        let pump_global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1587        let bonding_curve = BondingCurve::new(BondingCurveFromIdl {
1588            creator: coin_creator,
1589            quote_mint: constants::NATIVE_MINT,
1590            complete: true,
1591            ..Default::default()
1592        });
1593
1594        let explicit = sdk
1595            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1596                mint,
1597                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1598                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1599                user,
1600                is_buy: true,
1601                venue: TradeVenue::Amm {
1602                    pool,
1603                    amm_global: &amm_global,
1604                    pool_state: &pool_state,
1605                },
1606                base_amount: 1_000,
1607                sol_amount_threshold: max_sol,
1608            })
1609            .expect("explicit amm");
1610
1611        let auto = sdk
1612            .trade_tx_instructions(TradeTxParams {
1613                mint,
1614                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1615                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1616                user,
1617                is_buy: true,
1618                base_amount: 1_000,
1619                sol_amount_threshold: max_sol,
1620                pump_global: &pump_global,
1621                bonding_curve: &bonding_curve,
1622                pump_pool: Some(PumpPoolCtx {
1623                    pool,
1624                    amm_global: &amm_global,
1625                    pool_state: &pool_state,
1626                }),
1627            })
1628            .expect("auto amm");
1629
1630        assert_eq!(explicit.len(), auto.len());
1631        for (a, b) in explicit.iter().zip(auto.iter()) {
1632            assert_eq!(a.program_id, b.program_id);
1633            assert_eq!(a.data, b.data);
1634        }
1635    }
1636
1637    #[test]
1638    fn quote_trade_bc_matches_direct_buy_and_sell_quotes() {
1639        let sdk = PumpSdk::new();
1640        let g = fixture_global();
1641        let bc = fixture_bonding_curve(fake_pubkey(7));
1642        let supply = g.token_total_supply;
1643        let target = 100_000_000_000_000u64;
1644        let slip = 200u16;
1645
1646        let buy_direct = sdk
1647            .buy_quote_bonding_curve_token_out(&g, None, &bc, supply, target, slip)
1648            .expect("buy direct");
1649        let buy_auto = sdk
1650            .quote_trade(TradeQuoteParams {
1651                is_buy: true,
1652                base_amount: target,
1653                slippage_bps: slip,
1654                base_mint_supply: supply,
1655                pump_global: &g,
1656                pump_fee_config: None,
1657                bonding_curve: &bc,
1658                pump_pool: None,
1659            })
1660            .expect("auto routed")
1661            .expect("buy auto");
1662        assert_eq!(buy_direct, buy_auto);
1663
1664        let sell_direct = sdk
1665            .sell_quote_bonding_curve(&g, None, &bc, supply, target, slip)
1666            .expect("sell direct");
1667        let sell_auto = sdk
1668            .quote_trade(TradeQuoteParams {
1669                is_buy: false,
1670                base_amount: target,
1671                slippage_bps: slip,
1672                base_mint_supply: supply,
1673                pump_global: &g,
1674                pump_fee_config: None,
1675                bonding_curve: &bc,
1676                pump_pool: None,
1677            })
1678            .expect("auto routed")
1679            .expect("sell auto");
1680        assert_eq!(sell_direct, sell_auto);
1681    }
1682
1683    #[test]
1684    fn quote_trade_amm_matches_direct_buy_and_sell_quotes() {
1685        let sdk = PumpSdk::new();
1686        let g = fixture_global();
1687        let gc = fixture_amm_global_config();
1688        let pool_state = fixture_pool(fake_pubkey(0xCC));
1689        let mut bc = fixture_bonding_curve(fake_pubkey(7));
1690        bc.complete = true;
1691
1692        let base_reserve = 200_000_000_000_000u64;
1693        let quote_reserve = 30_000_000_000u64;
1694        let base_mint_supply = 1_000_000_000_000_000u64;
1695        let target = 1_000_000_000_000u64;
1696        let slip = 100u16;
1697
1698        let source = AmmQuoteSource::Pool {
1699            pool: &pool_state,
1700            base_reserve,
1701            quote_reserve,
1702            base_mint_supply,
1703        };
1704        let buy_direct = sdk
1705            .buy_quote_amm_token_out(&gc, None, source, target, slip)
1706            .expect("buy direct");
1707        let buy_auto = sdk
1708            .quote_trade(TradeQuoteParams {
1709                is_buy: true,
1710                base_amount: target,
1711                slippage_bps: slip,
1712                base_mint_supply,
1713                pump_global: &g,
1714                pump_fee_config: None,
1715                bonding_curve: &bc,
1716                pump_pool: Some(PumpPoolQuoteCtx {
1717                    amm_global: &gc,
1718                    amm_fee_config: None,
1719                    pool_state: &pool_state,
1720                    base_reserve,
1721                    quote_reserve,
1722                }),
1723            })
1724            .expect("auto routed")
1725            .expect("buy auto");
1726        assert_eq!(buy_direct, buy_auto);
1727
1728        let sell_source = AmmQuoteSource::Pool {
1729            pool: &pool_state,
1730            base_reserve,
1731            quote_reserve,
1732            base_mint_supply,
1733        };
1734        let sell_direct = sdk
1735            .sell_quote_amm(&gc, None, sell_source, target, slip)
1736            .expect("sell direct");
1737        let sell_auto = sdk
1738            .quote_trade(TradeQuoteParams {
1739                is_buy: false,
1740                base_amount: target,
1741                slippage_bps: slip,
1742                base_mint_supply,
1743                pump_global: &g,
1744                pump_fee_config: None,
1745                bonding_curve: &bc,
1746                pump_pool: Some(PumpPoolQuoteCtx {
1747                    amm_global: &gc,
1748                    amm_fee_config: None,
1749                    pool_state: &pool_state,
1750                    base_reserve,
1751                    quote_reserve,
1752                }),
1753            })
1754            .expect("auto routed")
1755            .expect("sell auto");
1756        assert_eq!(sell_direct, sell_auto);
1757    }
1758
1759    #[test]
1760    fn quote_trade_returns_none_when_complete_without_pump_pool() {
1761        let sdk = PumpSdk::new();
1762        let g = fixture_global();
1763        let mut bc = fixture_bonding_curve(fake_pubkey(7));
1764        bc.complete = true;
1765        assert!(sdk
1766            .quote_trade(TradeQuoteParams {
1767                is_buy: true,
1768                base_amount: 1,
1769                slippage_bps: 0,
1770                base_mint_supply: g.token_total_supply,
1771                pump_global: &g,
1772                pump_fee_config: None,
1773                bonding_curve: &bc,
1774                pump_pool: None,
1775            })
1776            .is_none());
1777    }
1778
1779    #[test]
1780    fn trade_tx_buy_amm_wraps_and_unwraps_with_cashback() {
1781        let sdk = PumpSdk::new();
1782        let mint = fake_pubkey(0xD1);
1783        let user = fake_pubkey(0xD2);
1784        let coin_creator = fake_pubkey(0xD3);
1785        let fee_recipient = fake_pubkey(0xD4);
1786        let buyback_fee_recipient = fake_pubkey(0xD5);
1787        let pool = fake_pubkey(0xD6);
1788        let max_sol = 2_500_000u64;
1789        let amm_global = amm_global_with_fees(fee_recipient, buyback_fee_recipient);
1790        let pool_state = Pool::new(PoolFromIdl {
1791            base_mint: mint,
1792            quote_mint: constants::NATIVE_MINT,
1793            coin_creator,
1794            is_cashback_coin: true,
1795            ..Default::default()
1796        });
1797
1798        let ixs = sdk
1799            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1800                mint,
1801                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1802                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1803                user,
1804                is_buy: true,
1805                venue: TradeVenue::Amm {
1806                    pool,
1807                    amm_global: &amm_global,
1808                    pool_state: &pool_state,
1809                },
1810                base_amount: 1_000,
1811                sol_amount_threshold: max_sol,
1812            })
1813            .expect("trade_tx_instructions");
1814
1815        assert_eq!(ixs.len(), 6);
1816        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1817        assert_eq!(ixs[1].program_id, constants::SPL_ATA_PROGRAM_ID);
1818        assert_eq!(parse_system_transfer_lamports(&ixs[2]), max_sol);
1819        assert_eq!(ixs[3].program_id, constants::SPL_TOKEN_PROGRAM_ID);
1820        assert_eq!(ixs[4].program_id, crate::pump_amm::ID);
1821        assert_eq!(
1822            &ixs[4].data[..8],
1823            <amm_client::args::Buy as Discriminator>::DISCRIMINATOR,
1824        );
1825        assert_eq!(ixs[5].program_id, constants::SPL_TOKEN_PROGRAM_ID);
1826
1827        let pubkeys: Vec<Pubkey> = ixs[4].accounts.iter().map(|m| m.pubkey).collect();
1828        let user_vol_accum = pda::pump_amm::user_volume_accumulator(&user).0;
1829        assert!(pubkeys.contains(
1830            &pda::associated_token(
1831                &user_vol_accum,
1832                &constants::SPL_TOKEN_PROGRAM_ID,
1833                &constants::NATIVE_MINT,
1834            )
1835            .0
1836        ));
1837        assert!(pubkeys.contains(&pda::pump_amm::pool_v2(&mint).0));
1838    }
1839
1840    #[test]
1841    fn trade_tx_sell_amm_unwraps_proceeds_no_wrap() {
1842        let sdk = PumpSdk::new();
1843        let mint = fake_pubkey(0xE1);
1844        let user = fake_pubkey(0xE2);
1845        let coin_creator = fake_pubkey(0xE3);
1846        let fee_recipient = fake_pubkey(0xE4);
1847        let buyback_fee_recipient = fake_pubkey(0xE5);
1848        let pool = fake_pubkey(0xE6);
1849        let amm_global = amm_global_with_fees(fee_recipient, buyback_fee_recipient);
1850        let pool_state = Pool::new(PoolFromIdl {
1851            base_mint: mint,
1852            quote_mint: constants::NATIVE_MINT,
1853            coin_creator,
1854            is_cashback_coin: false,
1855            ..Default::default()
1856        });
1857
1858        let ixs = sdk
1859            .trade_tx_instructions_with_venue(TradeTxWithVenueParams {
1860                mint,
1861                base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
1862                quote_token_program: constants::SPL_TOKEN_PROGRAM_ID,
1863                user,
1864                is_buy: false,
1865                venue: TradeVenue::Amm {
1866                    pool,
1867                    amm_global: &amm_global,
1868                    pool_state: &pool_state,
1869                },
1870                base_amount: 5_000,
1871                sol_amount_threshold: 100,
1872            })
1873            .expect("trade_tx_instructions");
1874
1875        assert_eq!(ixs.len(), 3);
1876        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
1877        let wsol_ata = user_wsol_ata(&user);
1878        assert!(ixs[0].accounts.iter().any(|m| m.pubkey == wsol_ata));
1879        assert_eq!(ixs[1].program_id, crate::pump_amm::ID);
1880        assert_eq!(
1881            &ixs[1].data[..8],
1882            <amm_client::args::Sell as Discriminator>::DISCRIMINATOR,
1883        );
1884        assert_eq!(ixs[2].program_id, constants::SPL_TOKEN_PROGRAM_ID);
1885        assert!(ixs[2].accounts.iter().any(|m| m.pubkey == wsol_ata));
1886    }
1887
1888    #[test]
1889    fn create_coin_instructions_emits_create_then_buy_v2() {
1890        let sdk = PumpSdk::new();
1891        let mint = fake_pubkey(0xF1);
1892        let user = fake_pubkey(0xF2);
1893        let creator = fake_pubkey(0xF3);
1894        let fee_recipient = fake_pubkey(0xF4);
1895        let buyback_fee_recipient = fake_pubkey(0xF5);
1896        let max_sol = 1_234_567u64;
1897        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1898
1899        let ixs = sdk
1900            .create_coin_instructions(CreateCoinParams {
1901                mint,
1902                user,
1903                creator,
1904                name: "Test".into(),
1905                symbol: "TST".into(),
1906                uri: "https://example.com/metadata.json".into(),
1907                mayhem_mode: false,
1908                cashback: false,
1909                quote_mint: Pubkey::default(),
1910                global: &global,
1911                token_amount: 1_000,
1912                max_quote_tokens: max_sol,
1913                tokenized_agent_buyback_bps: None,
1914            })
1915            .expect("create_coin_instructions");
1916
1917        assert_eq!(ixs.len(), 4);
1918        assert_eq!(
1919            &ixs[0].data[..8],
1920            <client::args::CreateV2 as Discriminator>::DISCRIMINATOR,
1921        );
1922        for ata_ix in &ixs[1..3] {
1923            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
1924            assert_eq!(ata_ix.data, vec![1]);
1925        }
1926        assert_eq!(ixs[3].program_id, crate::pump::ID);
1927        assert_eq!(
1928            &ixs[3].data[..8],
1929            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
1930        );
1931        let buy_metas: Vec<Pubkey> = ixs[3].accounts.iter().map(|m| m.pubkey).collect();
1932        assert!(buy_metas.contains(&constants::SPL_TOKEN_2022_PROGRAM_ID));
1933        assert!(buy_metas.contains(&constants::NATIVE_MINT));
1934        assert!(buy_metas.contains(&pda::pump::sharing_config(&mint).0));
1935    }
1936
1937    #[test]
1938    fn create_coin_instructions_appends_agent_initialize_when_some() {
1939        let sdk = PumpSdk::new();
1940        let mint = fake_pubkey(0xA1);
1941        let user = fake_pubkey(0xA2);
1942        let creator = fake_pubkey(0xA3);
1943        let fee_recipient = fake_pubkey(0xA4);
1944        let buyback_fee_recipient = fake_pubkey(0xA5);
1945        let global = pump_global_with_fees(fee_recipient, buyback_fee_recipient);
1946
1947        let buyback_bps = 5000u16;
1948        let with_agent = sdk
1949            .create_coin_instructions(CreateCoinParams {
1950                mint,
1951                user,
1952                creator,
1953                name: "Test".into(),
1954                symbol: "TST".into(),
1955                uri: "https://example.com/metadata.json".into(),
1956                mayhem_mode: false,
1957                cashback: false,
1958                quote_mint: Pubkey::default(),
1959                global: &global,
1960                token_amount: 1_000,
1961                max_quote_tokens: 500_000,
1962                tokenized_agent_buyback_bps: Some(buyback_bps),
1963            })
1964            .expect("create_coin_instructions Some");
1965        let without_agent = sdk
1966            .create_coin_instructions(CreateCoinParams {
1967                mint,
1968                user,
1969                creator,
1970                name: "Test".into(),
1971                symbol: "TST".into(),
1972                uri: "https://example.com/metadata.json".into(),
1973                mayhem_mode: false,
1974                cashback: false,
1975                quote_mint: Pubkey::default(),
1976                global: &global,
1977                token_amount: 1_000,
1978                max_quote_tokens: 500_000,
1979                tokenized_agent_buyback_bps: None,
1980            })
1981            .expect("create_coin_instructions None");
1982
1983        assert_eq!(
1984            with_agent.len(),
1985            without_agent.len() + 1,
1986            "Some(bps) appends exactly one extra ix"
1987        );
1988        let agent_ix = with_agent.last().expect("agent ix present");
1989        assert_eq!(agent_ix.program_id, crate::pump_agent_payments::ID);
1990        assert_eq!(
1991            &agent_ix.data[..8],
1992            <agent_client::args::AgentInitialize as Discriminator>::DISCRIMINATOR,
1993        );
1994        // args.authority (creator) + args.buyback_bps trail the discriminator.
1995        assert_eq!(&agent_ix.data[8..40], creator.as_ref());
1996        assert_eq!(
1997            u16::from_le_bytes([agent_ix.data[40], agent_ix.data[41]]),
1998            buyback_bps,
1999        );
2000        let agent_metas: Vec<Pubkey> = agent_ix.accounts.iter().map(|m| m.pubkey).collect();
2001        assert!(agent_metas.contains(&user), "user is the signer");
2002        assert!(agent_metas.contains(&pda::pump::bonding_curve(&mint).0));
2003        assert!(agent_metas.contains(&pda::pump_agent_payments::global_config().0));
2004        assert!(agent_metas.contains(&mint));
2005        assert!(agent_metas.contains(&pda::pump_agent_payments::token_agent_payments(&mint).0));
2006        assert!(agent_metas.contains(&pda::pump_agent_payments::event_authority().0));
2007        assert!(agent_metas.contains(&crate::pump_agent_payments::ID));
2008        assert!(agent_metas.contains(&pda::pump::sharing_config(&mint).0));
2009    }
2010
2011    #[test]
2012    fn program_ids_match_idl() {
2013        assert_eq!(
2014            crate::pump::ID,
2015            pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
2016        );
2017        assert_eq!(
2018            crate::pump_amm::ID,
2019            pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
2020        );
2021    }
2022
2023    fn fixture_global() -> Global {
2024        Global::new(GlobalFromIdl {
2025            fee_basis_points: 100,
2026            creator_fee_basis_points: 50,
2027            token_total_supply: 1_000_000_000_000_000,
2028            initial_real_token_reserves: 793_100_000_000_000,
2029            pool_migration_fee: 6_900_000_000,
2030            ..Default::default()
2031        })
2032    }
2033
2034    fn fixture_bonding_curve(creator: Pubkey) -> BondingCurve {
2035        BondingCurve::new(BondingCurveFromIdl {
2036            virtual_token_reserves: 1_073_000_000_000_000,
2037            virtual_quote_reserves: 30_000_000_000,
2038            real_token_reserves: 793_100_000_000_000,
2039            real_quote_reserves: 0,
2040            token_total_supply: 1_000_000_000_000_000,
2041            complete: false,
2042            creator,
2043            is_mayhem_mode: false,
2044            is_cashback_coin: false,
2045            quote_mint: constants::NATIVE_MINT,
2046        })
2047    }
2048
2049    fn fixture_amm_global_config() -> GlobalConfig {
2050        GlobalConfig::new(GlobalConfigFromIdl {
2051            lp_fee_basis_points: 20,
2052            protocol_fee_basis_points: 5,
2053            coin_creator_fee_basis_points: 5,
2054            ..Default::default()
2055        })
2056    }
2057
2058    fn fixture_pool(coin_creator: Pubkey) -> Pool {
2059        Pool::new(PoolFromIdl {
2060            base_mint: fake_pubkey(0xAA),
2061            creator: fake_pubkey(0xBB),
2062            coin_creator,
2063            quote_mint: constants::NATIVE_MINT,
2064            ..Default::default()
2065        })
2066    }
2067
2068    #[test]
2069    fn bc_buy_sol_in_matches_ts() {
2070        let sdk = PumpSdk::new();
2071        let g = fixture_global();
2072        let bc = fixture_bonding_curve(fake_pubkey(7));
2073        let q = sdk
2074            .buy_quote_bonding_curve_sol_in(
2075                &g,
2076                None,
2077                &bc,
2078                g.token_total_supply,
2079                1_000_000_000, // 1 SOL
2080                100,           // 1% slippage
2081            )
2082            .unwrap();
2083        assert_eq!(q.amount, 34_117_646_995_895);
2084        assert_eq!(q.min_out, 33_776_470_525_936);
2085        assert_eq!(q.input_amount_used, 1_000_000_000);
2086        assert_eq!(q.max_input, 1_000_000_000);
2087    }
2088
2089    #[test]
2090    fn bc_buy_token_out_matches_ts() {
2091        let sdk = PumpSdk::new();
2092        let g = fixture_global();
2093        let bc = fixture_bonding_curve(fake_pubkey(7));
2094        let q = sdk
2095            .buy_quote_bonding_curve_token_out(
2096                &g,
2097                None,
2098                &bc,
2099                g.token_total_supply,
2100                100_000_000_000_000,
2101                200, // 2% slippage
2102            )
2103            .unwrap();
2104        assert_eq!(q.amount, 3_129_496_404);
2105        assert_eq!(q.max_input, 3_192_086_332);
2106        assert_eq!(q.min_out, 100_000_000_000_000);
2107        assert_eq!(q.input_amount_used, 100_000_000_000_000);
2108    }
2109
2110    #[test]
2111    fn bc_sell_with_creator_matches_ts() {
2112        let sdk = PumpSdk::new();
2113        let g = fixture_global();
2114        let bc = fixture_bonding_curve(fake_pubkey(7));
2115        let q = sdk
2116            .sell_quote_bonding_curve(
2117                &g,
2118                None,
2119                &bc,
2120                g.token_total_supply,
2121                50_000_000_000_000,
2122                50, // 0.5% slippage
2123            )
2124            .unwrap();
2125        assert_eq!(q.amount, 1_315_672_305);
2126        assert_eq!(q.min_out, 1_309_093_943);
2127    }
2128
2129    #[test]
2130    fn bc_sell_default_creator_skips_creator_fee() {
2131        let sdk = PumpSdk::new();
2132        let g = fixture_global();
2133        let bc = fixture_bonding_curve(Pubkey::default());
2134        let q = sdk
2135            .sell_quote_bonding_curve(&g, None, &bc, g.token_total_supply, 50_000_000_000_000, 0)
2136            .unwrap();
2137        assert_eq!(q.amount, 1_322_350_845);
2138    }
2139
2140    #[test]
2141    fn amm_buy_sol_in_matches_ts() {
2142        let sdk = PumpSdk::new();
2143        let gc = fixture_amm_global_config();
2144        let pool = fixture_pool(fake_pubkey(0xCC));
2145        let q = sdk
2146            .buy_quote_amm_sol_in(
2147                &gc,
2148                None,
2149                AmmQuoteSource::Pool {
2150                    pool: &pool,
2151                    base_reserve: 200_000_000_000_000,
2152                    quote_reserve: 30_000_000_000,
2153                    base_mint_supply: 1_000_000_000_000_000,
2154                },
2155                1_000_000_000,
2156                100,
2157            )
2158            .unwrap();
2159        assert_eq!(q.amount, 6_432_936_635_069);
2160        assert_eq!(q.min_out, 6_368_607_268_718);
2161        assert_eq!(q.max_input, 1_010_000_000);
2162    }
2163
2164    #[test]
2165    fn amm_buy_token_out_matches_ts() {
2166        let sdk = PumpSdk::new();
2167        let gc = fixture_amm_global_config();
2168        let pool = fixture_pool(fake_pubkey(0xCC));
2169        let q = sdk
2170            .buy_quote_amm_token_out(
2171                &gc,
2172                None,
2173                AmmQuoteSource::Pool {
2174                    pool: &pool,
2175                    base_reserve: 200_000_000_000_000,
2176                    quote_reserve: 30_000_000_000,
2177                    base_mint_supply: 1_000_000_000_000_000,
2178                },
2179                1_000_000_000_000,
2180                100,
2181            )
2182            .unwrap();
2183        assert_eq!(q.amount, 151_206_031);
2184        assert_eq!(q.max_input, 152_718_091);
2185    }
2186
2187    #[test]
2188    fn amm_sell_with_coin_creator_matches_ts() {
2189        let sdk = PumpSdk::new();
2190        let gc = fixture_amm_global_config();
2191        let pool = fixture_pool(fake_pubkey(0xCC));
2192        let q = sdk
2193            .sell_quote_amm(
2194                &gc,
2195                None,
2196                AmmQuoteSource::Pool {
2197                    pool: &pool,
2198                    base_reserve: 200_000_000_000_000,
2199                    quote_reserve: 30_000_000_000,
2200                    base_mint_supply: 1_000_000_000_000_000,
2201                },
2202                1_000_000_000_000,
2203                100,
2204            )
2205            .unwrap();
2206        assert_eq!(q.amount, 148_805_969);
2207        assert_eq!(q.min_out, 147_317_909);
2208    }
2209
2210    #[test]
2211    fn amm_sell_default_coin_creator_skips_creator_fee() {
2212        let sdk = PumpSdk::new();
2213        let gc = fixture_amm_global_config();
2214        let pool = fixture_pool(Pubkey::default());
2215        let q = sdk
2216            .sell_quote_amm(
2217                &gc,
2218                None,
2219                AmmQuoteSource::Pool {
2220                    pool: &pool,
2221                    base_reserve: 200_000_000_000_000,
2222                    quote_reserve: 30_000_000_000,
2223                    base_mint_supply: 1_000_000_000_000_000,
2224                },
2225                1_000_000_000_000,
2226                0,
2227            )
2228            .unwrap();
2229        assert_eq!(q.amount, 148_880_596);
2230    }
2231
2232    #[test]
2233    fn zero_input_returns_zero() {
2234        let sdk = PumpSdk::new();
2235        let g = fixture_global();
2236        let bc = fixture_bonding_curve(fake_pubkey(7));
2237        let q = sdk
2238            .buy_quote_bonding_curve_sol_in(&g, None, &bc, g.token_total_supply, 0, 100)
2239            .unwrap();
2240        assert_eq!(q.amount, 0);
2241        let q = sdk
2242            .sell_quote_bonding_curve(&g, None, &bc, g.token_total_supply, 0, 100)
2243            .unwrap();
2244        assert_eq!(q.amount, 0);
2245    }
2246
2247    #[test]
2248    fn migrated_curve_returns_zero() {
2249        let sdk = PumpSdk::new();
2250        let g = fixture_global();
2251        let mut bc = fixture_bonding_curve(fake_pubkey(7));
2252        bc.virtual_token_reserves = 0;
2253        let q = sdk
2254            .buy_quote_bonding_curve_sol_in(&g, None, &bc, g.token_total_supply, 1_000_000_000, 0)
2255            .unwrap();
2256        assert_eq!(q.amount, 0);
2257    }
2258
2259    #[test]
2260    fn amm_empty_reserves_errors() {
2261        let sdk = PumpSdk::new();
2262        let gc = fixture_amm_global_config();
2263        let pool = fixture_pool(fake_pubkey(0xCC));
2264        let err = sdk
2265            .buy_quote_amm_sol_in(
2266                &gc,
2267                None,
2268                AmmQuoteSource::Pool {
2269                    pool: &pool,
2270                    base_reserve: 0,
2271                    quote_reserve: 1_000,
2272                    base_mint_supply: 1_000,
2273                },
2274                1,
2275                0,
2276            )
2277            .unwrap_err();
2278        assert_eq!(err, QuoteError::EmptyReserves);
2279        let err = sdk
2280            .sell_quote_amm(
2281                &gc,
2282                None,
2283                AmmQuoteSource::Pool {
2284                    pool: &pool,
2285                    base_reserve: 1_000,
2286                    quote_reserve: 0,
2287                    base_mint_supply: 1_000,
2288                },
2289                1,
2290                0,
2291            )
2292            .unwrap_err();
2293        assert_eq!(err, QuoteError::EmptyReserves);
2294    }
2295
2296    #[test]
2297    fn amm_buy_token_out_oversize_errors() {
2298        let sdk = PumpSdk::new();
2299        let gc = fixture_amm_global_config();
2300        let pool = fixture_pool(fake_pubkey(0xCC));
2301        let err = sdk
2302            .buy_quote_amm_token_out(
2303                &gc,
2304                None,
2305                AmmQuoteSource::Pool {
2306                    pool: &pool,
2307                    base_reserve: 1_000,
2308                    quote_reserve: 1_000,
2309                    base_mint_supply: 1_000_000,
2310                },
2311                1_000,
2312                0,
2313            )
2314            .unwrap_err();
2315        assert_eq!(err, QuoteError::BaseOutExceedsReserve);
2316        let err = sdk
2317            .buy_quote_amm_token_out(
2318                &gc,
2319                None,
2320                AmmQuoteSource::Pool {
2321                    pool: &pool,
2322                    base_reserve: 1_000,
2323                    quote_reserve: 1_000,
2324                    base_mint_supply: 1_000_000,
2325                },
2326                5_000,
2327                0,
2328            )
2329            .unwrap_err();
2330        assert_eq!(err, QuoteError::BaseOutExceedsReserve);
2331    }
2332
2333    #[test]
2334    fn bc_token_out_buy_at_real_reserves_errors() {
2335        let sdk = PumpSdk::new();
2336        let g = fixture_global();
2337        let mut bc = fixture_bonding_curve(fake_pubkey(7));
2338        bc.real_token_reserves = bc.virtual_token_reserves;
2339        let err = sdk
2340            .buy_quote_bonding_curve_token_out(
2341                &g,
2342                None,
2343                &bc,
2344                g.token_total_supply,
2345                bc.virtual_token_reserves,
2346                0,
2347            )
2348            .unwrap_err();
2349        assert_eq!(err, QuoteError::DepletedBondingCurve);
2350    }
2351
2352    #[test]
2353    fn amm_quote_bonding_curve_complete_underflow_errors() {
2354        let sdk = PumpSdk::new();
2355        let g = fixture_global();
2356        let gc = fixture_amm_global_config();
2357        let bc = fixture_bonding_curve(fake_pubkey(7)); // real_quote_reserves = 0
2358        let base_mint = fake_pubkey(0xAA);
2359        let err = sdk
2360            .buy_quote_amm_sol_in(
2361                &gc,
2362                None,
2363                AmmQuoteSource::BondingCurveComplete {
2364                    global: &g,
2365                    bonding_curve: &bc,
2366                    base_mint: &base_mint,
2367                    base_mint_supply: g.token_total_supply,
2368                },
2369                1_000_000_000,
2370                0,
2371            )
2372            .unwrap_err();
2373        assert_eq!(err, QuoteError::EmptyReserves);
2374    }
2375
2376    #[test]
2377    fn amm_quote_bonding_curve_complete_matches_formula() {
2378        let sdk = PumpSdk::new();
2379        let g = fixture_global();
2380        let gc = fixture_amm_global_config();
2381        let mut bc = fixture_bonding_curve(fake_pubkey(7));
2382        bc.real_quote_reserves = 100_000_000_000;
2383        let base_mint = fake_pubkey(0xAA);
2384
2385        let q = sdk
2386            .buy_quote_amm_sol_in(
2387                &gc,
2388                None,
2389                AmmQuoteSource::BondingCurveComplete {
2390                    global: &g,
2391                    bonding_curve: &bc,
2392                    base_mint: &base_mint,
2393                    base_mint_supply: g.token_total_supply,
2394                },
2395                1_000_000_000,
2396                0,
2397            )
2398            .unwrap();
2399
2400        let base_reserve = g.token_total_supply - g.initial_real_token_reserves;
2401        let quote_reserve = bc.real_quote_reserves - g.pool_migration_fee;
2402        let pool_creator = pda::pump::pool_authority(&base_mint).0;
2403        let ctx = AmmContext {
2404            global_config: &gc,
2405            fee_config: None,
2406            base_mint: &base_mint,
2407            pool_creator: &pool_creator,
2408            coin_creator: &bc.creator,
2409            base_reserve,
2410            quote_reserve,
2411            base_mint_supply: g.token_total_supply,
2412        };
2413        let expected = amm_math::buy_quote_input(&ctx, 1_000_000_000)
2414            .unwrap()
2415            .base_amount_out;
2416        assert_eq!(q.amount, expected);
2417    }
2418}