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