Skip to main content

pump_rust_client/
sdk.rs

1use anchor_lang::{InstructionData, ToAccountMetas};
2use solana_program::{
3    instruction::{AccountMeta, Instruction},
4    pubkey::Pubkey,
5    system_program, sysvar,
6};
7
8use crate::math::amm::{
9    self as amm_math, AmmContext, BuyBaseInputResult, BuyQuoteInputResult, SellBaseInputResult,
10};
11use crate::math::bonding_curve as bc_math;
12use crate::math::utils::{add_slippage, sub_slippage};
13pub use crate::math::QuoteError;
14use crate::math::QuoteResult;
15use crate::pump_amm::{
16    client as amm_client, types::OptionBool as AmmOptionBool, ID as PUMP_AMM_PROGRAM_ID,
17};
18use crate::state::pump_amm::{FeeConfig as AmmFeeConfig, GlobalConfig, Pool};
19use crate::state::{BondingCurve, FeeConfig, Global};
20use crate::token::{
21    create_associated_token_account_idempotent, unwrap_sol_instruction, wrap_sol_instructions,
22};
23use crate::{constants, pda, pump::client, pump::types::OptionBool};
24
25/// Result of a quote computation. `amount` is the primary output (tokens for
26/// SOL-input buys; lamports for token-input buys and for sells).
27/// `min_out` and `max_input` are slippage-adjusted bounds where applicable.
28/// For **AMM** quote-in buys (`buy_quote_amm_sol_in`), `max_input` is the
29/// slippage-inflated quote ceiling (mirrors `maxQuote` in
30/// `@pump-fun/pump-swap-sdk` `buyQuoteInput`). For sells, `min_out` is the
31/// quote floor. For token-out buys, `max_input` is the max quote you may spend.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct Quote {
34    pub amount: u64,
35    pub min_out: u64,
36    pub input_amount_used: u64,
37    pub max_input: u64,
38}
39
40/// Routing for [`PumpSdk::trade_tx_instructions`]. The bonding-curve path
41/// uses `buy_v2` / `sell_v2`; the AMM path targets the supplied pool with
42/// `buy_amm` / `sell_amm`.
43#[derive(Clone, Copy, Debug)]
44pub enum TradeVenue {
45    BondingCurve,
46    Amm { pool: Pubkey },
47}
48
49/// How [`PumpSdk::buy_quote_amm_sol_in`], [`PumpSdk::buy_quote_amm_token_out`], and
50/// [`PumpSdk::sell_quote_amm`] obtain reserves and pool identity.
51///
52/// Prefer [`AmmQuoteSource::Pool`] with vault balances from RPC (same contract as
53/// `@pump-fun/pump-swap-sdk` `OnlinePumpAmmSdk.swapSolanaState`). Use
54/// [`AmmQuoteSource::BondingCurveComplete`] only when the curve is finished and the pool
55/// is not available yet — reserves are a **heuristic** from pump `Global` +
56/// `BondingCurve`, not live AMM state.
57#[derive(Clone, Copy, Debug)]
58pub enum AmmQuoteSource<'a> {
59    Pool {
60        pool: &'a Pool,
61        base_reserve: u64,
62        quote_reserve: u64,
63        base_mint_supply: u64,
64    },
65    BondingCurveComplete {
66        global: &'a Global,
67        bonding_curve: &'a BondingCurve,
68        base_mint: &'a Pubkey,
69        base_mint_supply: u64,
70    },
71}
72
73/// Inputs to [`PumpSdk::trade_tx_instructions`]. Quote mint is hard-wired to
74/// wSOL: `sol_amount_threshold` is the SOL ceiling on buy and the SOL floor
75/// on sell. Caller pre-computes amounts via the existing `*_quote_*` helpers.
76#[derive(Clone, Copy, Debug)]
77pub struct TradeTxParams {
78    pub mint: Pubkey,
79    pub base_token_program: Pubkey,
80    pub user: Pubkey,
81    pub creator: Pubkey,
82    pub fee_recipient: Pubkey,
83    pub buyback_fee_recipient: Pubkey,
84    pub is_buy: bool,
85    pub venue: TradeVenue,
86    /// Only consumed by the AMM venue; ignored for `BondingCurve`.
87    pub is_cashback_coin: bool,
88    pub base_amount: u64,
89    pub sol_amount_threshold: u64,
90}
91
92/// Inputs to [`PumpSdk::create_coin_instructions`]. Mint is the freshly
93/// generated keypair pubkey; the caller signs the resulting tx with that
94/// keypair plus the user.
95#[derive(Clone, Debug)]
96pub struct CreateCoinParams {
97    pub mint: Pubkey,
98    pub user: Pubkey,
99    pub creator: Pubkey,
100    pub name: String,
101    pub symbol: String,
102    pub uri: String,
103    pub mayhem_mode: bool,
104    pub cashback: bool,
105    pub fee_recipient: Pubkey,
106    pub buyback_fee_recipient: Pubkey,
107    pub token_amount: u64,
108    pub max_sol_cost: u64,
109}
110
111/// Offline instruction builders. No RPC calls are made and does not require RPC connection url.
112#[derive(Clone, Copy, Debug, Default)]
113pub struct PumpSdk;
114
115impl PumpSdk {
116    pub const fn new() -> Self {
117        Self
118    }
119
120    fn map_amm_quote_source<R>(
121        global_config: &GlobalConfig,
122        fee_config: Option<&AmmFeeConfig>,
123        source: AmmQuoteSource<'_>,
124        with: impl FnOnce(AmmContext<'_>) -> QuoteResult<R>,
125    ) -> QuoteResult<R> {
126        match source {
127            AmmQuoteSource::Pool {
128                pool,
129                base_reserve,
130                quote_reserve,
131                base_mint_supply,
132            } => {
133                let ctx = AmmContext {
134                    global_config,
135                    fee_config,
136                    base_mint: &pool.base_mint,
137                    pool_creator: &pool.creator,
138                    coin_creator: &pool.coin_creator,
139                    base_reserve,
140                    quote_reserve,
141                    base_mint_supply,
142                };
143                with(ctx)
144            }
145            AmmQuoteSource::BondingCurveComplete {
146                global,
147                bonding_curve,
148                base_mint,
149                base_mint_supply,
150            } => {
151                let (base_reserve, quote_reserve) = estimated_reserves(global, bonding_curve)?;
152                let pool_creator_pk = pda::pump::pool_authority(base_mint).0;
153                let ctx = AmmContext {
154                    global_config,
155                    fee_config,
156                    base_mint,
157                    pool_creator: &pool_creator_pk,
158                    coin_creator: &bonding_curve.creator,
159                    base_reserve,
160                    quote_reserve,
161                    base_mint_supply,
162                };
163                with(ctx)
164            }
165        }
166    }
167
168    /// v1 create: SPL Token + Metaplex metadata. The TS SDK marks the
169    /// matching helper `@deprecated` in favor of `create_v2_instruction`,
170    /// but it remains supported on-chain.
171    #[deprecated(note = "Use create_v2_instruction instead")]
172    pub fn create_instruction(
173        &self,
174        mint: Pubkey,
175        user: Pubkey,
176        name: impl Into<String>,
177        symbol: impl Into<String>,
178        uri: impl Into<String>,
179        creator: Pubkey,
180    ) -> Instruction {
181        let token_program = constants::SPL_TOKEN_PROGRAM_ID;
182        let bonding_curve = pda::pump::bonding_curve(&mint).0;
183        let accounts = client::accounts::Create {
184            mint,
185            mint_authority: pda::pump::mint_authority().0,
186            bonding_curve,
187            associated_bonding_curve: pda::associated_token(&bonding_curve, &token_program, &mint)
188                .0,
189            global: pda::pump::global().0,
190            mpl_token_metadata: constants::MPL_TOKEN_METADATA_PROGRAM_ID,
191            metadata: pda::pump::metadata(&mint).0,
192            user,
193            system_program: system_program::ID,
194            token_program,
195            associated_token_program: constants::SPL_ATA_PROGRAM_ID,
196            rent: sysvar::rent::ID,
197            event_authority: pda::pump::event_authority().0,
198            program: crate::pump::ID,
199        };
200        let args = client::args::Create {
201            name: name.into(),
202            symbol: symbol.into(),
203            uri: uri.into(),
204            creator,
205        };
206        Instruction {
207            program_id: crate::pump::ID,
208            accounts: accounts.to_account_metas(None),
209            data: args.data(),
210        }
211    }
212
213    /// v2 create: Token-2022 + Mayhem PDAs.
214    pub fn create_v2_instruction(
215        &self,
216        mint: Pubkey,
217        user: Pubkey,
218        name: impl Into<String>,
219        symbol: impl Into<String>,
220        uri: impl Into<String>,
221        creator: Pubkey,
222        mayhem_mode: bool,
223        cashback: bool,
224    ) -> Instruction {
225        let token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
226        let bonding_curve = pda::pump::bonding_curve(&mint).0;
227        let accounts = client::accounts::CreateV2 {
228            mint,
229            mint_authority: pda::pump::mint_authority().0,
230            bonding_curve,
231            associated_bonding_curve: pda::associated_token(&bonding_curve, &token_program, &mint)
232                .0,
233            global: pda::pump::global().0,
234            user,
235            system_program: system_program::ID,
236            token_program,
237            associated_token_program: constants::SPL_ATA_PROGRAM_ID,
238            mayhem_program_id: constants::MAYHEM_PROGRAM_ID,
239            global_params: pda::mayhem::global_params().0,
240            sol_vault: pda::mayhem::sol_vault().0,
241            mayhem_state: pda::mayhem::mayhem_state(&mint).0,
242            mayhem_token_vault: pda::mayhem::mayhem_token_vault(&mint).0,
243            event_authority: pda::pump::event_authority().0,
244            program: crate::pump::ID,
245        };
246        let args = client::args::CreateV2 {
247            name: name.into(),
248            symbol: symbol.into(),
249            uri: uri.into(),
250            creator,
251            is_mayhem_mode: mayhem_mode,
252            is_cashback_enabled: OptionBool(cashback),
253        };
254        Instruction {
255            program_id: crate::pump::ID,
256            accounts: accounts.to_account_metas(None),
257            data: args.data(),
258        }
259    }
260
261    /// Buy tokens from a bonding curve. The caller picks `fee_recipient` and
262    /// `buyback_fee_recipient` from the on-chain `Global` state. `max_sol_cost`
263    /// is the slippage-adjusted ceiling on SOL spent. `token_program` is the
264    /// SPL Token program for the mint (Token-2022 for v2 coins, classic SPL
265    /// Token otherwise).
266    ///
267    /// Remaining accounts (in order, mirrors `pump-sdk-internal`'s
268    /// `getBuyInstructionInternal`): `bonding_curve_v2` (readonly),
269    /// `buyback_fee_recipient` (writable).
270    #[deprecated(note = "Use buy_v2_instruction instead")]
271    pub fn buy_instruction(
272        &self,
273        mint: Pubkey,
274        user: Pubkey,
275        creator: Pubkey,
276        fee_recipient: Pubkey,
277        buyback_fee_recipient: Pubkey,
278        amount: u64,
279        max_sol_cost: u64,
280        token_program: Pubkey,
281    ) -> Instruction {
282        let bonding_curve = pda::pump::bonding_curve(&mint).0;
283        let accounts = client::accounts::Buy {
284            global: pda::pump::global().0,
285            fee_recipient,
286            mint,
287            bonding_curve,
288            associated_bonding_curve: pda::associated_token(&bonding_curve, &token_program, &mint)
289                .0,
290            associated_user: pda::associated_token(&user, &token_program, &mint).0,
291            user,
292            system_program: system_program::ID,
293            token_program,
294            creator_vault: pda::pump::creator_vault(&creator).0,
295            event_authority: pda::pump::event_authority().0,
296            program: crate::pump::ID,
297            global_volume_accumulator: pda::pump::global_volume_accumulator().0,
298            user_volume_accumulator: pda::pump::user_volume_accumulator(&user).0,
299            fee_config: pda::pump::fee_config().0,
300            fee_program: constants::FEE_PROGRAM_ID,
301        };
302        let args = client::args::Buy {
303            amount,
304            max_sol_cost,
305            track_volume: OptionBool(true),
306        };
307        let mut metas = accounts.to_account_metas(None);
308        metas.push(AccountMeta::new_readonly(
309            pda::pump::bonding_curve_v2(&mint).0,
310            false,
311        ));
312        metas.push(AccountMeta::new(buyback_fee_recipient, false));
313        Instruction {
314            program_id: crate::pump::ID,
315            accounts: metas,
316            data: args.data(),
317        }
318    }
319
320    /// Buy tokens with an arbitrary quote mint. Unlike `buy_instruction`, the
321    /// program takes the quote asset on its own token program and routes a
322    /// share of fees through the per-mint `sharing_config`.
323    /// Caller picks `fee_recipient` and `buyback_fee_recipient` from `Global`.
324    pub fn buy_v2_instruction(
325        &self,
326        base_mint: Pubkey,
327        quote_mint: Pubkey,
328        base_token_program: Pubkey,
329        quote_token_program: Pubkey,
330        user: Pubkey,
331        creator: Pubkey,
332        fee_recipient: Pubkey,
333        buyback_fee_recipient: Pubkey,
334        amount: u64,
335        max_sol_cost: u64,
336    ) -> Instruction {
337        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
338        let creator_vault = pda::pump::creator_vault(&creator).0;
339        let user_volume_accumulator = pda::pump::user_volume_accumulator(&user).0;
340        let accounts = client::accounts::BuyV2 {
341            global: pda::pump::global().0,
342            base_mint,
343            quote_mint,
344            base_token_program,
345            quote_token_program,
346            associated_token_program: constants::SPL_ATA_PROGRAM_ID,
347            fee_recipient,
348            associated_quote_fee_recipient: pda::associated_token(
349                &fee_recipient,
350                &quote_token_program,
351                &quote_mint,
352            )
353            .0,
354            buyback_fee_recipient,
355            associated_quote_buyback_fee_recipient: pda::associated_token(
356                &buyback_fee_recipient,
357                &quote_token_program,
358                &quote_mint,
359            )
360            .0,
361            bonding_curve,
362            associated_base_bonding_curve: pda::associated_token(
363                &bonding_curve,
364                &base_token_program,
365                &base_mint,
366            )
367            .0,
368            associated_quote_bonding_curve: pda::associated_token(
369                &bonding_curve,
370                &quote_token_program,
371                &quote_mint,
372            )
373            .0,
374            user,
375            associated_base_user: pda::associated_token(&user, &base_token_program, &base_mint).0,
376            associated_quote_user: pda::associated_token(&user, &quote_token_program, &quote_mint)
377                .0,
378            creator_vault,
379            associated_creator_vault: pda::associated_token(
380                &creator_vault,
381                &quote_token_program,
382                &quote_mint,
383            )
384            .0,
385            sharing_config: pda::pump::sharing_config(&base_mint).0,
386            global_volume_accumulator: pda::pump::global_volume_accumulator().0,
387            user_volume_accumulator,
388            associated_user_volume_accumulator: pda::associated_token(
389                &user_volume_accumulator,
390                &quote_token_program,
391                &quote_mint,
392            )
393            .0,
394            fee_config: pda::pump::fee_config().0,
395            fee_program: constants::FEE_PROGRAM_ID,
396            system_program: system_program::ID,
397            event_authority: pda::pump::event_authority().0,
398            program: crate::pump::ID,
399        };
400        let args = client::args::BuyV2 {
401            amount,
402            max_sol_cost,
403        };
404        Instruction {
405            program_id: crate::pump::ID,
406            accounts: accounts.to_account_metas(None),
407            data: args.data(),
408        }
409    }
410
411    /// `buy_v2` preceded by idempotent ATA creates for every account the
412    /// program does NOT lazy-initialize. From `programs/pump/src/buy_v2.rs`,
413    /// `validate_quote_token_account` only initializes the creator-vault,
414    /// user-volume-accumulator, and quote-fee-recipient quote ATAs
415    /// (`should_initialize = true`); everything else must already exist.
416    /// We emit 4 prefixes:
417    ///   1. `associated_base_user` (always required)
418    ///   2. `associated_quote_bonding_curve`
419    ///   3. `associated_quote_user`
420    ///   4. `associated_quote_buyback_fee_recipient`
421    /// (2-4) are only enforced for non-legacy curves but are idempotent and
422    /// cheap when already present.
423    pub fn buy_v2_instructions(
424        &self,
425        base_mint: Pubkey,
426        quote_mint: Pubkey,
427        base_token_program: Pubkey,
428        quote_token_program: Pubkey,
429        user: Pubkey,
430        creator: Pubkey,
431        fee_recipient: Pubkey,
432        buyback_fee_recipient: Pubkey,
433        amount: u64,
434        max_sol_cost: u64,
435    ) -> Vec<Instruction> {
436        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
437        vec![
438            create_associated_token_account_idempotent(
439                &user,
440                &user,
441                &base_mint,
442                &base_token_program,
443            ),
444            create_associated_token_account_idempotent(
445                &user,
446                &bonding_curve,
447                &quote_mint,
448                &quote_token_program,
449            ),
450            create_associated_token_account_idempotent(
451                &user,
452                &user,
453                &quote_mint,
454                &quote_token_program,
455            ),
456            create_associated_token_account_idempotent(
457                &user,
458                &buyback_fee_recipient,
459                &quote_mint,
460                &quote_token_program,
461            ),
462            self.buy_v2_instruction(
463                base_mint,
464                quote_mint,
465                base_token_program,
466                quote_token_program,
467                user,
468                creator,
469                fee_recipient,
470                buyback_fee_recipient,
471                amount,
472                max_sol_cost,
473            ),
474        ]
475    }
476
477    /// Sell tokens back to a bonding curve. The caller picks `fee_recipient`
478    /// from the on-chain `Global` state. `min_sol_output` is the
479    /// slippage-adjusted floor on SOL received. `token_program` is the SPL
480    /// Token program for the mint (Token-2022 for v2 coins, classic SPL Token
481    /// otherwise).
482    ///
483    /// Remaining accounts (in order, mirrors the on-chain handler's cashback
484    /// path): `user_volume_accumulator` (writable), `bonding_curve_v2`
485    /// (readonly). Always appended; the program falls back to
486    /// `creator_vault` when these are absent or invalid.
487    #[deprecated(note = "Use sell_v2_instruction instead")]
488    pub fn sell_instruction(
489        &self,
490        mint: Pubkey,
491        user: Pubkey,
492        creator: Pubkey,
493        fee_recipient: Pubkey,
494        amount: u64,
495        min_sol_output: u64,
496        token_program: Pubkey,
497    ) -> Instruction {
498        let bonding_curve = pda::pump::bonding_curve(&mint).0;
499        let accounts = client::accounts::Sell {
500            global: pda::pump::global().0,
501            fee_recipient,
502            mint,
503            bonding_curve,
504            associated_bonding_curve: pda::associated_token(&bonding_curve, &token_program, &mint)
505                .0,
506            associated_user: pda::associated_token(&user, &token_program, &mint).0,
507            user,
508            system_program: system_program::ID,
509            creator_vault: pda::pump::creator_vault(&creator).0,
510            token_program,
511            event_authority: pda::pump::event_authority().0,
512            program: crate::pump::ID,
513            fee_config: pda::pump::fee_config().0,
514            fee_program: constants::FEE_PROGRAM_ID,
515        };
516        let args = client::args::Sell {
517            amount,
518            min_sol_output,
519        };
520        let mut metas = accounts.to_account_metas(None);
521        metas.push(AccountMeta::new(
522            pda::pump::user_volume_accumulator(&user).0,
523            false,
524        ));
525        metas.push(AccountMeta::new_readonly(
526            pda::pump::bonding_curve_v2(&mint).0,
527            false,
528        ));
529        Instruction {
530            program_id: crate::pump::ID,
531            accounts: metas,
532            data: args.data(),
533        }
534    }
535
536    /// Sell tokens for an arbitrary quote mint. Mirror of `buy_v2_instruction`
537    /// on the sell side. Routes a share of fees through the per-mint
538    /// `sharing_config`. Caller picks `fee_recipient` and
539    /// `buyback_fee_recipient` from `Global`.
540    pub fn sell_v2_instruction(
541        &self,
542        base_mint: Pubkey,
543        quote_mint: Pubkey,
544        base_token_program: Pubkey,
545        quote_token_program: Pubkey,
546        user: Pubkey,
547        creator: Pubkey,
548        fee_recipient: Pubkey,
549        buyback_fee_recipient: Pubkey,
550        amount: u64,
551        min_sol_output: u64,
552    ) -> Instruction {
553        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
554        let creator_vault = pda::pump::creator_vault(&creator).0;
555        let user_volume_accumulator = pda::pump::user_volume_accumulator(&user).0;
556        let accounts = client::accounts::SellV2 {
557            global: pda::pump::global().0,
558            base_mint,
559            quote_mint,
560            base_token_program,
561            quote_token_program,
562            associated_token_program: constants::SPL_ATA_PROGRAM_ID,
563            fee_recipient,
564            associated_quote_fee_recipient: pda::associated_token(
565                &fee_recipient,
566                &quote_token_program,
567                &quote_mint,
568            )
569            .0,
570            buyback_fee_recipient,
571            associated_quote_buyback_fee_recipient: pda::associated_token(
572                &buyback_fee_recipient,
573                &quote_token_program,
574                &quote_mint,
575            )
576            .0,
577            bonding_curve,
578            associated_base_bonding_curve: pda::associated_token(
579                &bonding_curve,
580                &base_token_program,
581                &base_mint,
582            )
583            .0,
584            associated_quote_bonding_curve: pda::associated_token(
585                &bonding_curve,
586                &quote_token_program,
587                &quote_mint,
588            )
589            .0,
590            user,
591            associated_base_user: pda::associated_token(&user, &base_token_program, &base_mint).0,
592            associated_quote_user: pda::associated_token(&user, &quote_token_program, &quote_mint)
593                .0,
594            creator_vault,
595            associated_creator_vault: pda::associated_token(
596                &creator_vault,
597                &quote_token_program,
598                &quote_mint,
599            )
600            .0,
601            sharing_config: pda::pump::sharing_config(&base_mint).0,
602            user_volume_accumulator,
603            associated_user_volume_accumulator: pda::associated_token(
604                &user_volume_accumulator,
605                &quote_token_program,
606                &quote_mint,
607            )
608            .0,
609            fee_config: pda::pump::fee_config().0,
610            fee_program: constants::FEE_PROGRAM_ID,
611            system_program: system_program::ID,
612            event_authority: pda::pump::event_authority().0,
613            program: crate::pump::ID,
614        };
615        let args = client::args::SellV2 {
616            amount,
617            min_sol_output,
618        };
619        Instruction {
620            program_id: crate::pump::ID,
621            accounts: accounts.to_account_metas(None),
622            data: args.data(),
623        }
624    }
625
626    /// `sell_v2` preceded by idempotent ATA creates for every account the
627    /// program does NOT lazy-initialize. Mirrors `buy_v2_instructions`. We
628    /// emit 4 prefixes:
629    ///   1. `associated_base_user` (user must hold the base token)
630    ///   2. `associated_quote_bonding_curve`
631    ///   3. `associated_quote_user` (user receives quote)
632    ///   4. `associated_quote_buyback_fee_recipient`
633    pub fn sell_v2_instructions(
634        &self,
635        base_mint: Pubkey,
636        quote_mint: Pubkey,
637        base_token_program: Pubkey,
638        quote_token_program: Pubkey,
639        user: Pubkey,
640        creator: Pubkey,
641        fee_recipient: Pubkey,
642        buyback_fee_recipient: Pubkey,
643        amount: u64,
644        min_sol_output: u64,
645    ) -> Vec<Instruction> {
646        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
647        vec![
648            create_associated_token_account_idempotent(
649                &user,
650                &user,
651                &base_mint,
652                &base_token_program,
653            ),
654            create_associated_token_account_idempotent(
655                &user,
656                &bonding_curve,
657                &quote_mint,
658                &quote_token_program,
659            ),
660            create_associated_token_account_idempotent(
661                &user,
662                &user,
663                &quote_mint,
664                &quote_token_program,
665            ),
666            create_associated_token_account_idempotent(
667                &user,
668                &buyback_fee_recipient,
669                &quote_mint,
670                &quote_token_program,
671            ),
672            self.sell_v2_instruction(
673                base_mint,
674                quote_mint,
675                base_token_program,
676                quote_token_program,
677                user,
678                creator,
679                fee_recipient,
680                buyback_fee_recipient,
681                amount,
682                min_sol_output,
683            ),
684        ]
685    }
686
687    /// `create_v2` + idempotent ATA + `buy`, in that order.
688    /// Mirrors `createV2AndBuyInstructions` in `pump-sdk/src/sdk.ts`.
689    /// `fee_recipient` is supplied by the caller (typically picked from
690    /// `Global`). Quote is wSOL on the classic SPL Token program — the
691    /// only sensible quote at creation time. For other quotes, call
692    /// `buy_v2_instruction` directly.
693    pub fn create_v2_and_buy_instruction(
694        &self,
695        mint: Pubkey,
696        user: Pubkey,
697        name: impl Into<String>,
698        symbol: impl Into<String>,
699        uri: impl Into<String>,
700        creator: Pubkey,
701        mayhem_mode: bool,
702        cashback: bool,
703        fee_recipient: Pubkey,
704        buyback_fee_recipient: Pubkey,
705        amount: u64,
706        max_sol_cost: u64,
707    ) -> Vec<Instruction> {
708        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
709        let quote_mint = constants::NATIVE_MINT;
710        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
711        let buy_v2_instructions = self.buy_v2_instructions(
712            mint,
713            quote_mint,
714            base_token_program,
715            quote_token_program,
716            user,
717            creator,
718            fee_recipient,
719            buyback_fee_recipient,
720            amount,
721            max_sol_cost,
722        );
723        let mut instructions = vec![self.create_v2_instruction(
724            mint,
725            user,
726            name,
727            symbol,
728            uri,
729            creator,
730            mayhem_mode,
731            cashback,
732        )];
733
734        instructions.extend(buy_v2_instructions);
735
736        instructions
737    }
738
739    pub fn extend_account_ix(&self, mint: Pubkey, user: Pubkey) -> Instruction {
740        let accounts = client::accounts::ExtendAccount {
741            account: pda::pump::bonding_curve(&mint).0,
742            user,
743            system_program: system_program::ID,
744            event_authority: pda::pump::event_authority().0,
745            program: crate::pump::ID,
746        };
747        Instruction {
748            program_id: crate::pump::ID,
749            accounts: accounts.to_account_metas(None),
750            data: client::args::ExtendAccount.data(),
751        }
752    }
753
754    /// Buy on a graduated `pump_amm` pool. Mirrors `PUMP_AMM_SDK.buyInstructions`'
755    /// inner IX from `pump-swap-sdk/src/sdk/offlinePumpAmm.ts`. Caller picks
756    /// `protocol_fee_recipient` from `GlobalConfig` (via `getFeeRecipient`) and
757    /// `buyback_fee_recipient` likewise (via `getBuybackFeeRecipient`).
758    /// `coin_creator` and `is_cashback_coin` come from the on-chain `Pool`.
759    /// Pass `Pubkey::default()` for `coin_creator` to skip the
760    /// `bonding_curve_v2` remaining account on default-creator pools.
761    /// `track_volume` is hardcoded to true (matches the TS SDK).
762    #[allow(clippy::too_many_arguments)]
763    pub fn buy_amm_instruction(
764        &self,
765        pool: Pubkey,
766        base_mint: Pubkey,
767        quote_mint: Pubkey,
768        base_token_program: Pubkey,
769        quote_token_program: Pubkey,
770        user: Pubkey,
771        coin_creator: Pubkey,
772        protocol_fee_recipient: Pubkey,
773        buyback_fee_recipient: Pubkey,
774        is_cashback_coin: bool,
775        base_amount_out: u64,
776        max_quote_amount_in: u64,
777    ) -> Instruction {
778        let coin_creator_vault_authority =
779            pda::pump_amm::coin_creator_vault_authority(&coin_creator).0;
780        let user_volume_accumulator = pda::pump_amm::user_volume_accumulator(&user).0;
781        let accounts = amm_client::accounts::Buy {
782            pool,
783            user,
784            global_config: pda::pump_amm::global_config().0,
785            base_mint,
786            quote_mint,
787            user_base_token_account: pda::associated_token(&user, &base_token_program, &base_mint)
788                .0,
789            user_quote_token_account: pda::associated_token(
790                &user,
791                &quote_token_program,
792                &quote_mint,
793            )
794            .0,
795            pool_base_token_account: pda::associated_token(&pool, &base_token_program, &base_mint)
796                .0,
797            pool_quote_token_account: pda::associated_token(
798                &pool,
799                &quote_token_program,
800                &quote_mint,
801            )
802            .0,
803            protocol_fee_recipient,
804            protocol_fee_recipient_token_account: pda::associated_token(
805                &protocol_fee_recipient,
806                &quote_token_program,
807                &quote_mint,
808            )
809            .0,
810            base_token_program,
811            quote_token_program,
812            system_program: system_program::ID,
813            associated_token_program: constants::SPL_ATA_PROGRAM_ID,
814            event_authority: pda::pump_amm::event_authority().0,
815            program: PUMP_AMM_PROGRAM_ID,
816            coin_creator_vault_ata: pda::associated_token(
817                &coin_creator_vault_authority,
818                &quote_token_program,
819                &quote_mint,
820            )
821            .0,
822            coin_creator_vault_authority,
823            global_volume_accumulator: pda::pump_amm::global_volume_accumulator().0,
824            user_volume_accumulator,
825            fee_config: pda::pump_amm::fee_config().0,
826            fee_program: constants::FEE_PROGRAM_ID,
827        };
828        let args = amm_client::args::Buy {
829            base_amount_out,
830            max_quote_amount_in,
831            track_volume: AmmOptionBool(true),
832        };
833        let mut metas = accounts.to_account_metas(None);
834        if is_cashback_coin {
835            metas.push(AccountMeta::new(
836                pda::associated_token(
837                    &user_volume_accumulator,
838                    &quote_token_program,
839                    &constants::NATIVE_MINT,
840                )
841                .0,
842                false,
843            ));
844        }
845        if coin_creator != Pubkey::default() {
846            metas.push(AccountMeta::new_readonly(
847                pda::pump::bonding_curve_v2(&base_mint).0,
848                false,
849            ));
850        }
851        metas.push(AccountMeta::new(buyback_fee_recipient, false));
852        metas.push(AccountMeta::new(
853            pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0,
854            false,
855        ));
856        Instruction {
857            program_id: PUMP_AMM_PROGRAM_ID,
858            accounts: metas,
859            data: args.data(),
860        }
861    }
862
863    /// `buy_amm` preceded by an idempotent ATA-create for the user's base ATA
864    /// (the asset being received). Wsol management for the quote side is the
865    /// caller's responsibility; the existing `buy_v2_instructions` wrapper
866    /// follows the same convention.
867    #[allow(clippy::too_many_arguments)]
868    pub fn buy_amm_instructions(
869        &self,
870        pool: Pubkey,
871        base_mint: Pubkey,
872        quote_mint: Pubkey,
873        base_token_program: Pubkey,
874        quote_token_program: Pubkey,
875        user: Pubkey,
876        coin_creator: Pubkey,
877        protocol_fee_recipient: Pubkey,
878        buyback_fee_recipient: Pubkey,
879        is_cashback_coin: bool,
880        base_amount_out: u64,
881        max_quote_amount_in: u64,
882    ) -> Vec<Instruction> {
883        vec![
884            create_associated_token_account_idempotent(
885                &user,
886                &user,
887                &base_mint,
888                &base_token_program,
889            ),
890            self.buy_amm_instruction(
891                pool,
892                base_mint,
893                quote_mint,
894                base_token_program,
895                quote_token_program,
896                user,
897                coin_creator,
898                protocol_fee_recipient,
899                buyback_fee_recipient,
900                is_cashback_coin,
901                base_amount_out,
902                max_quote_amount_in,
903            ),
904        ]
905    }
906
907    /// Sell on a graduated `pump_amm` pool. Mirrors `PUMP_AMM_SDK.sellInstructions`'
908    /// inner IX from `pump-swap-sdk/src/sdk/offlinePumpAmm.ts`. The remaining-account
909    /// layout differs from buy in three places (cashback adds two accounts not one,
910    /// the cashback ATA uses `quote_mint` not `NATIVE_MINT`, and
911    /// `buyback_fee_recipient` is read-only). See `buy_amm_instruction` for the
912    /// rest of the parameter contract.
913    #[allow(clippy::too_many_arguments)]
914    pub fn sell_amm_instruction(
915        &self,
916        pool: Pubkey,
917        base_mint: Pubkey,
918        quote_mint: Pubkey,
919        base_token_program: Pubkey,
920        quote_token_program: Pubkey,
921        user: Pubkey,
922        coin_creator: Pubkey,
923        protocol_fee_recipient: Pubkey,
924        buyback_fee_recipient: Pubkey,
925        is_cashback_coin: bool,
926        base_amount_in: u64,
927        min_quote_amount_out: u64,
928    ) -> Instruction {
929        let coin_creator_vault_authority =
930            pda::pump_amm::coin_creator_vault_authority(&coin_creator).0;
931        let user_volume_accumulator = pda::pump_amm::user_volume_accumulator(&user).0;
932        let accounts = amm_client::accounts::Sell {
933            pool,
934            user,
935            global_config: pda::pump_amm::global_config().0,
936            base_mint,
937            quote_mint,
938            user_base_token_account: pda::associated_token(&user, &base_token_program, &base_mint)
939                .0,
940            user_quote_token_account: pda::associated_token(
941                &user,
942                &quote_token_program,
943                &quote_mint,
944            )
945            .0,
946            pool_base_token_account: pda::associated_token(&pool, &base_token_program, &base_mint)
947                .0,
948            pool_quote_token_account: pda::associated_token(
949                &pool,
950                &quote_token_program,
951                &quote_mint,
952            )
953            .0,
954            protocol_fee_recipient,
955            protocol_fee_recipient_token_account: pda::associated_token(
956                &protocol_fee_recipient,
957                &quote_token_program,
958                &quote_mint,
959            )
960            .0,
961            base_token_program,
962            quote_token_program,
963            system_program: system_program::ID,
964            associated_token_program: constants::SPL_ATA_PROGRAM_ID,
965            event_authority: pda::pump_amm::event_authority().0,
966            program: PUMP_AMM_PROGRAM_ID,
967            coin_creator_vault_ata: pda::associated_token(
968                &coin_creator_vault_authority,
969                &quote_token_program,
970                &quote_mint,
971            )
972            .0,
973            coin_creator_vault_authority,
974            fee_config: pda::pump_amm::fee_config().0,
975            fee_program: constants::FEE_PROGRAM_ID,
976        };
977        let args = amm_client::args::Sell {
978            base_amount_in,
979            min_quote_amount_out,
980        };
981        let mut metas = accounts.to_account_metas(None);
982        if is_cashback_coin {
983            metas.push(AccountMeta::new(
984                pda::associated_token(&user_volume_accumulator, &quote_token_program, &quote_mint)
985                    .0,
986                false,
987            ));
988            metas.push(AccountMeta::new(user_volume_accumulator, false));
989        }
990        if coin_creator != Pubkey::default() {
991            metas.push(AccountMeta::new_readonly(
992                pda::pump::bonding_curve_v2(&base_mint).0,
993                false,
994            ));
995        }
996        metas.push(AccountMeta::new_readonly(buyback_fee_recipient, false));
997        metas.push(AccountMeta::new(
998            pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0,
999            false,
1000        ));
1001        Instruction {
1002            program_id: PUMP_AMM_PROGRAM_ID,
1003            accounts: metas,
1004            data: args.data(),
1005        }
1006    }
1007
1008    /// `sell_amm` preceded by an idempotent ATA-create for the user's quote ATA
1009    /// (the asset being received).
1010    #[allow(clippy::too_many_arguments)]
1011    pub fn sell_amm_instructions(
1012        &self,
1013        pool: Pubkey,
1014        base_mint: Pubkey,
1015        quote_mint: Pubkey,
1016        base_token_program: Pubkey,
1017        quote_token_program: Pubkey,
1018        user: Pubkey,
1019        coin_creator: Pubkey,
1020        protocol_fee_recipient: Pubkey,
1021        buyback_fee_recipient: Pubkey,
1022        is_cashback_coin: bool,
1023        base_amount_in: u64,
1024        min_quote_amount_out: u64,
1025    ) -> Vec<Instruction> {
1026        vec![
1027            create_associated_token_account_idempotent(
1028                &user,
1029                &user,
1030                &quote_mint,
1031                &quote_token_program,
1032            ),
1033            self.sell_amm_instruction(
1034                pool,
1035                base_mint,
1036                quote_mint,
1037                base_token_program,
1038                quote_token_program,
1039                user,
1040                coin_creator,
1041                protocol_fee_recipient,
1042                buyback_fee_recipient,
1043                is_cashback_coin,
1044                base_amount_in,
1045                min_quote_amount_out,
1046            ),
1047        ]
1048    }
1049
1050    // ------------------------------------------------------------------
1051    // Complete trade flows (mirror `TradeTxService` in
1052    // `backends/blockchain-client/src/trade-tx/trade-tx.service.ts`).
1053    //
1054    // Quote is hard-wired to wSOL. SOL is wrapped via system_transfer +
1055    // sync_native before the swap (buy only) and the user's wSOL ATA is
1056    // closed after the swap (always), so any residual or proceeds become
1057    // lamports without a second tx.
1058    // ------------------------------------------------------------------
1059
1060    /// Build a complete buy/sell against either the bonding curve (`buy_v2`/
1061    /// `sell_v2`) or a graduated `pump_amm` pool. Mirrors `TradeTxService.tradeTx`.
1062    pub fn trade_tx_instructions(&self, params: TradeTxParams) -> Vec<Instruction> {
1063        let TradeTxParams {
1064            mint,
1065            base_token_program,
1066            user,
1067            creator,
1068            fee_recipient,
1069            buyback_fee_recipient,
1070            is_buy,
1071            venue,
1072            is_cashback_coin,
1073            base_amount,
1074            sol_amount_threshold,
1075        } = params;
1076        let quote_mint = constants::NATIVE_MINT;
1077        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1078
1079        match venue {
1080            TradeVenue::BondingCurve => {
1081                let bonding_curve = pda::pump::bonding_curve(&mint).0;
1082                let mut ixs = vec![
1083                    create_associated_token_account_idempotent(
1084                        &user,
1085                        &user,
1086                        &mint,
1087                        &base_token_program,
1088                    ),
1089                    create_associated_token_account_idempotent(
1090                        &user,
1091                        &bonding_curve,
1092                        &quote_mint,
1093                        &quote_token_program,
1094                    ),
1095                    create_associated_token_account_idempotent(
1096                        &user,
1097                        &user,
1098                        &quote_mint,
1099                        &quote_token_program,
1100                    ),
1101                    create_associated_token_account_idempotent(
1102                        &user,
1103                        &buyback_fee_recipient,
1104                        &quote_mint,
1105                        &quote_token_program,
1106                    ),
1107                ];
1108                if is_buy {
1109                    ixs.extend(wrap_sol_instructions(&user, sol_amount_threshold));
1110                    ixs.push(self.buy_v2_instruction(
1111                        mint,
1112                        quote_mint,
1113                        base_token_program,
1114                        quote_token_program,
1115                        user,
1116                        creator,
1117                        fee_recipient,
1118                        buyback_fee_recipient,
1119                        base_amount,
1120                        sol_amount_threshold,
1121                    ));
1122                } else {
1123                    ixs.push(self.sell_v2_instruction(
1124                        mint,
1125                        quote_mint,
1126                        base_token_program,
1127                        quote_token_program,
1128                        user,
1129                        creator,
1130                        fee_recipient,
1131                        buyback_fee_recipient,
1132                        base_amount,
1133                        sol_amount_threshold,
1134                    ));
1135                }
1136                ixs.push(unwrap_sol_instruction(&user));
1137                ixs
1138            }
1139            TradeVenue::Amm { pool } => {
1140                let mut ixs: Vec<Instruction> = Vec::with_capacity(6);
1141                if is_buy {
1142                    ixs.push(create_associated_token_account_idempotent(
1143                        &user,
1144                        &user,
1145                        &mint,
1146                        &base_token_program,
1147                    ));
1148                    ixs.push(create_associated_token_account_idempotent(
1149                        &user,
1150                        &user,
1151                        &quote_mint,
1152                        &quote_token_program,
1153                    ));
1154                    ixs.extend(wrap_sol_instructions(&user, sol_amount_threshold));
1155                    ixs.push(self.buy_amm_instruction(
1156                        pool,
1157                        mint,
1158                        quote_mint,
1159                        base_token_program,
1160                        quote_token_program,
1161                        user,
1162                        creator,
1163                        fee_recipient,
1164                        buyback_fee_recipient,
1165                        is_cashback_coin,
1166                        base_amount,
1167                        sol_amount_threshold,
1168                    ));
1169                } else {
1170                    ixs.push(create_associated_token_account_idempotent(
1171                        &user,
1172                        &user,
1173                        &quote_mint,
1174                        &quote_token_program,
1175                    ));
1176                    ixs.push(self.sell_amm_instruction(
1177                        pool,
1178                        mint,
1179                        quote_mint,
1180                        base_token_program,
1181                        quote_token_program,
1182                        user,
1183                        creator,
1184                        fee_recipient,
1185                        buyback_fee_recipient,
1186                        is_cashback_coin,
1187                        base_amount,
1188                        sol_amount_threshold,
1189                    ));
1190                }
1191                ixs.push(unwrap_sol_instruction(&user));
1192                ixs
1193            }
1194        }
1195    }
1196
1197    /// Build a complete `create_v2` + initial-buy with full SOL wrap/unwrap.
1198    /// Mirrors `TradeTxService.createCoin` minus the optional tokenized-agent
1199    /// step (no Rust `pump-agent-payments` SDK in this repo).
1200    pub fn create_coin_instructions(&self, params: CreateCoinParams) -> Vec<Instruction> {
1201        let CreateCoinParams {
1202            mint,
1203            user,
1204            creator,
1205            name,
1206            symbol,
1207            uri,
1208            mayhem_mode,
1209            cashback,
1210            fee_recipient,
1211            buyback_fee_recipient,
1212            token_amount,
1213            max_sol_cost,
1214        } = params;
1215        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1216        let quote_mint = constants::NATIVE_MINT;
1217        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1218        let bonding_curve = pda::pump::bonding_curve(&mint).0;
1219
1220        let mut ixs: Vec<Instruction> = Vec::with_capacity(9);
1221        ixs.push(self.create_v2_instruction(
1222            mint,
1223            user,
1224            name,
1225            symbol,
1226            uri,
1227            creator,
1228            mayhem_mode,
1229            cashback,
1230        ));
1231        ixs.push(create_associated_token_account_idempotent(
1232            &user,
1233            &user,
1234            &mint,
1235            &base_token_program,
1236        ));
1237        ixs.push(create_associated_token_account_idempotent(
1238            &user,
1239            &bonding_curve,
1240            &quote_mint,
1241            &quote_token_program,
1242        ));
1243        ixs.push(create_associated_token_account_idempotent(
1244            &user,
1245            &user,
1246            &quote_mint,
1247            &quote_token_program,
1248        ));
1249        ixs.push(create_associated_token_account_idempotent(
1250            &user,
1251            &buyback_fee_recipient,
1252            &quote_mint,
1253            &quote_token_program,
1254        ));
1255        ixs.extend(wrap_sol_instructions(&user, max_sol_cost));
1256        ixs.push(self.buy_v2_instruction(
1257            mint,
1258            quote_mint,
1259            base_token_program,
1260            quote_token_program,
1261            user,
1262            creator,
1263            fee_recipient,
1264            buyback_fee_recipient,
1265            token_amount,
1266            max_sol_cost,
1267        ));
1268        ixs.push(unwrap_sol_instruction(&user));
1269        ixs
1270    }
1271
1272    // ------------------------------------------------------------------
1273    // Quoting
1274    //
1275    // Bonding curve: mirrors `getQuote` / bonding-curve branches in the
1276    // frontend store (`stores/blockchainStore/store.ts`).
1277    //
1278    // **Pump AMM** (canonical): match `@pump-fun/pump-swap-sdk` — load pool
1279    // `GlobalConfig` + optional `FeeConfig`, decode `Pool`, then set
1280    // `base_reserve` / `quote_reserve` to the **live SPL token amounts** on
1281    // `pool.pool_base_token_account` and `pool.pool_quote_token_account`
1282    // (same as `OnlinePumpAmmSdk.swapSolanaState` in TS). Pass
1283    // `base_mint_supply` from the base mint account (`supply` field), same as
1284    // the SDK. Pass [`AmmQuoteSource::Pool`] with those balances. For a
1285    // completed curve before the pool exists, [`AmmQuoteSource::BondingCurveComplete`]
1286    // synthesizes reserves (heuristic only).
1287    //
1288    // `slippage_bps` is in basis points (1% = 100). All inputs and outputs
1289    // are atomic units (lamports / token base units).
1290    // ------------------------------------------------------------------
1291
1292    /// Bonding curve buy quoting where the user specifies SOL in.
1293    pub fn buy_quote_bonding_curve_sol_in(
1294        &self,
1295        global: &Global,
1296        fee_config: Option<&FeeConfig>,
1297        bonding_curve: &BondingCurve,
1298        mint_supply: u64,
1299        sol_amount: u64,
1300        slippage_bps: u16,
1301    ) -> QuoteResult<Quote> {
1302        let tokens_out = bc_math::buy_token_amount_from_sol_amount(
1303            global,
1304            fee_config,
1305            bonding_curve,
1306            mint_supply,
1307            sol_amount,
1308        )?;
1309        Ok(Quote {
1310            amount: tokens_out,
1311            min_out: sub_slippage(tokens_out, slippage_bps),
1312            input_amount_used: sol_amount,
1313            max_input: sol_amount,
1314        })
1315    }
1316
1317    /// Bonding curve buy quoting where the user specifies token amount out.
1318    pub fn buy_quote_bonding_curve_token_out(
1319        &self,
1320        global: &Global,
1321        fee_config: Option<&FeeConfig>,
1322        bonding_curve: &BondingCurve,
1323        mint_supply: u64,
1324        token_amount: u64,
1325        slippage_bps: u16,
1326    ) -> QuoteResult<Quote> {
1327        let sol_cost = bc_math::buy_sol_amount_from_token_amount(
1328            global,
1329            fee_config,
1330            bonding_curve,
1331            mint_supply,
1332            token_amount,
1333        )?;
1334        Ok(Quote {
1335            amount: sol_cost,
1336            // Desired tokens out is fixed; slippage is on the SOL side.
1337            min_out: token_amount,
1338            input_amount_used: token_amount,
1339            max_input: add_slippage(sol_cost, slippage_bps),
1340        })
1341    }
1342
1343    /// Bonding curve sell quoting.
1344    pub fn sell_quote_bonding_curve(
1345        &self,
1346        global: &Global,
1347        fee_config: Option<&FeeConfig>,
1348        bonding_curve: &BondingCurve,
1349        mint_supply: u64,
1350        token_amount: u64,
1351        slippage_bps: u16,
1352    ) -> QuoteResult<Quote> {
1353        let sol_out = bc_math::sell_sol_amount_from_token_amount(
1354            global,
1355            fee_config,
1356            bonding_curve,
1357            mint_supply,
1358            token_amount,
1359        )?;
1360        Ok(Quote {
1361            amount: sol_out,
1362            min_out: sub_slippage(sol_out, slippage_bps),
1363            input_amount_used: token_amount,
1364            max_input: token_amount,
1365        })
1366    }
1367
1368    /// AMM buy quoting where the user specifies quote (e.g. wSOL) in.
1369    pub fn buy_quote_amm_sol_in(
1370        &self,
1371        global_config: &GlobalConfig,
1372        fee_config: Option<&AmmFeeConfig>,
1373        source: AmmQuoteSource<'_>,
1374        sol_amount: u64,
1375        slippage_bps: u16,
1376    ) -> QuoteResult<Quote> {
1377        Self::map_amm_quote_source(global_config, fee_config, source, |ctx| {
1378            let BuyQuoteInputResult {
1379                base_amount_out, ..
1380            } = amm_math::buy_quote_input(&ctx, sol_amount)?;
1381            Ok(Quote {
1382                amount: base_amount_out,
1383                min_out: sub_slippage(base_amount_out, slippage_bps),
1384                input_amount_used: sol_amount,
1385                max_input: add_slippage(sol_amount, slippage_bps),
1386            })
1387        })
1388    }
1389
1390    /// AMM buy quoting where the user specifies token amount out.
1391    pub fn buy_quote_amm_token_out(
1392        &self,
1393        global_config: &GlobalConfig,
1394        fee_config: Option<&AmmFeeConfig>,
1395        source: AmmQuoteSource<'_>,
1396        token_amount: u64,
1397        slippage_bps: u16,
1398    ) -> QuoteResult<Quote> {
1399        Self::map_amm_quote_source(global_config, fee_config, source, |ctx| {
1400            let BuyBaseInputResult { total_quote_in, .. } =
1401                amm_math::buy_base_input(&ctx, token_amount)?;
1402            Ok(Quote {
1403                amount: total_quote_in,
1404                min_out: token_amount,
1405                input_amount_used: token_amount,
1406                max_input: add_slippage(total_quote_in, slippage_bps),
1407            })
1408        })
1409    }
1410
1411    /// AMM sell quoting.
1412    pub fn sell_quote_amm(
1413        &self,
1414        global_config: &GlobalConfig,
1415        fee_config: Option<&AmmFeeConfig>,
1416        source: AmmQuoteSource<'_>,
1417        token_amount: u64,
1418        slippage_bps: u16,
1419    ) -> QuoteResult<Quote> {
1420        Self::map_amm_quote_source(global_config, fee_config, source, |ctx| {
1421            let SellBaseInputResult {
1422                final_quote_out, ..
1423            } = amm_math::sell_base_input(&ctx, token_amount)?;
1424            Ok(Quote {
1425                amount: final_quote_out,
1426                min_out: sub_slippage(final_quote_out, slippage_bps),
1427                input_amount_used: token_amount,
1428                max_input: token_amount,
1429            })
1430        })
1431    }
1432}
1433
1434/// Synthesized reserves when using [`AmmQuoteSource::BondingCurveComplete`].
1435/// Mirrors the legacy app formula (`store.ts`):
1436/// `token_total_supply - initial_real_token_reserves` and
1437/// `real_quote_reserves - pool_migration_fee`.
1438fn estimated_reserves(global: &Global, bonding_curve: &BondingCurve) -> QuoteResult<(u64, u64)> {
1439    let base_reserve = global
1440        .token_total_supply
1441        .checked_sub(global.initial_real_token_reserves)
1442        .ok_or(QuoteError::EmptyReserves)?;
1443    let quote_reserve = bonding_curve
1444        .real_quote_reserves
1445        .checked_sub(global.pool_migration_fee)
1446        .ok_or(QuoteError::EmptyReserves)?;
1447    if base_reserve == 0 || quote_reserve == 0 {
1448        return Err(QuoteError::EmptyReserves);
1449    }
1450    Ok((base_reserve, quote_reserve))
1451}
1452
1453#[cfg(test)]
1454mod tests {
1455    use super::*;
1456    use anchor_lang::Discriminator;
1457    use solana_program::{pubkey, pubkey::Pubkey};
1458
1459    fn fake_pubkey(seed: u8) -> Pubkey {
1460        Pubkey::new_from_array([seed; 32])
1461    }
1462
1463    #[test]
1464    #[allow(deprecated)]
1465    fn create_instruction_wires_metadata_pdas() {
1466        let sdk = PumpSdk::new();
1467        let mint = fake_pubkey(51);
1468        let user = fake_pubkey(52);
1469        let creator = fake_pubkey(53);
1470
1471        let ix = sdk.create_instruction(
1472            mint,
1473            user,
1474            "Test",
1475            "TST",
1476            "https://example.com/metadata.json",
1477            creator,
1478        );
1479
1480        assert_eq!(ix.program_id, crate::pump::ID);
1481        assert_eq!(
1482            &ix.data[..8],
1483            <client::args::Create as Discriminator>::DISCRIMINATOR,
1484        );
1485        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1486        assert!(metas.contains(&pda::pump::metadata(&mint).0));
1487        assert!(metas.contains(&constants::MPL_TOKEN_METADATA_PROGRAM_ID));
1488        assert!(metas.contains(&constants::SPL_TOKEN_PROGRAM_ID));
1489        assert!(metas.contains(&pda::pump::bonding_curve(&mint).0));
1490        assert!(metas.contains(&sysvar::rent::ID));
1491    }
1492
1493    #[test]
1494    fn create_v2_instruction_wires_mayhem_pdas() {
1495        let sdk = PumpSdk::new();
1496        let mint = fake_pubkey(21);
1497        let user = fake_pubkey(22);
1498        let creator = fake_pubkey(23);
1499
1500        let ix = sdk.create_v2_instruction(
1501            mint,
1502            user,
1503            "Test",
1504            "TST",
1505            "https://example.com/metadata.json",
1506            creator,
1507            false,
1508            false,
1509        );
1510
1511        assert_eq!(ix.program_id, crate::pump::ID);
1512        assert_eq!(
1513            &ix.data[..8],
1514            <client::args::CreateV2 as Discriminator>::DISCRIMINATOR,
1515        );
1516        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1517        assert!(metas.contains(&pda::mayhem::global_params().0));
1518        assert!(metas.contains(&pda::mayhem::sol_vault().0));
1519        assert!(metas.contains(&pda::mayhem::mayhem_state(&mint).0));
1520        assert!(metas.contains(&pda::mayhem::mayhem_token_vault(&mint).0));
1521        assert!(metas.contains(&constants::MAYHEM_PROGRAM_ID));
1522        assert!(metas.contains(&constants::SPL_TOKEN_2022_PROGRAM_ID));
1523    }
1524
1525    #[test]
1526    #[allow(deprecated)]
1527    fn buy_instruction_appends_bonding_curve_v2_then_buyback_fee_recipient() {
1528        let sdk = PumpSdk::new();
1529        let mint = fake_pubkey(61);
1530        let user = fake_pubkey(62);
1531        let creator = fake_pubkey(63);
1532        let fee_recipient = fake_pubkey(64);
1533        let buyback_fee_recipient = fake_pubkey(65);
1534        let token_program = constants::SPL_TOKEN_PROGRAM_ID;
1535
1536        let ix = sdk.buy_instruction(
1537            mint,
1538            user,
1539            creator,
1540            fee_recipient,
1541            buyback_fee_recipient,
1542            100,
1543            5_000_000,
1544            token_program,
1545        );
1546
1547        assert_eq!(ix.program_id, crate::pump::ID);
1548        assert_eq!(
1549            &ix.data[..8],
1550            <client::args::Buy as Discriminator>::DISCRIMINATOR,
1551        );
1552
1553        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1554        assert!(metas.contains(&pda::pump::global().0));
1555        assert!(metas.contains(&pda::pump::bonding_curve(&mint).0));
1556        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
1557        assert!(metas.contains(&pda::pump::user_volume_accumulator(&user).0));
1558        assert!(metas.contains(&pda::pump::fee_config().0));
1559        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
1560        assert!(metas.contains(&fee_recipient));
1561
1562        // Tail of the account list is exactly [bonding_curve_v2 ro, buyback_fee_recipient rw].
1563        let n = ix.accounts.len();
1564        let bcv2 = &ix.accounts[n - 2];
1565        let buyback = &ix.accounts[n - 1];
1566        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&mint).0);
1567        assert!(!bcv2.is_writable);
1568        assert!(!bcv2.is_signer);
1569        assert_eq!(buyback.pubkey, buyback_fee_recipient);
1570        assert!(buyback.is_writable);
1571        assert!(!buyback.is_signer);
1572    }
1573
1574    #[test]
1575    fn create_v2_and_buy_instruction_returns_create_then_buy_v2_prefix_in_order() {
1576        let sdk = PumpSdk::new();
1577        let mint = fake_pubkey(71);
1578        let user = fake_pubkey(72);
1579        let creator = fake_pubkey(73);
1580        let fee_recipient = fake_pubkey(74);
1581        let buyback_fee_recipient = fake_pubkey(75);
1582
1583        let ixs = sdk.create_v2_and_buy_instruction(
1584            mint,
1585            user,
1586            "Test",
1587            "TST",
1588            "https://example.com/metadata.json",
1589            creator,
1590            false,
1591            false,
1592            fee_recipient,
1593            buyback_fee_recipient,
1594            1_000,
1595            500_000,
1596        );
1597
1598        // create_v2 + buy_v2_instructions (4 idempotent ATA creates + buy_v2).
1599        assert_eq!(ixs.len(), 6);
1600        assert_eq!(
1601            &ixs[0].data[..8],
1602            <client::args::CreateV2 as Discriminator>::DISCRIMINATOR,
1603        );
1604        for i in 1..5 {
1605            assert_eq!(ixs[i].data, vec![1], "expected SPL ATA idempotent ix");
1606        }
1607        assert_eq!(
1608            &ixs[5].data[..8],
1609            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
1610        );
1611
1612        // Buy should be a v2 buy with base=Token-2022, quote=wSOL/SPL Token.
1613        let buy_metas: Vec<Pubkey> = ixs[5].accounts.iter().map(|m| m.pubkey).collect();
1614        assert!(buy_metas.contains(&constants::SPL_TOKEN_2022_PROGRAM_ID));
1615        assert!(buy_metas.contains(&constants::SPL_TOKEN_PROGRAM_ID));
1616        assert!(buy_metas.contains(&constants::NATIVE_MINT));
1617        assert!(buy_metas.contains(&pda::pump::sharing_config(&mint).0));
1618    }
1619
1620    #[test]
1621    fn buy_v2_instruction_wires_quote_and_sharing_config_pdas() {
1622        let sdk = PumpSdk::new();
1623        let base_mint = fake_pubkey(81);
1624        let quote_mint = fake_pubkey(82);
1625        let user = fake_pubkey(83);
1626        let creator = fake_pubkey(84);
1627        let fee_recipient = fake_pubkey(85);
1628        let buyback_fee_recipient = fake_pubkey(86);
1629        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1630        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1631
1632        let ix = sdk.buy_v2_instruction(
1633            base_mint,
1634            quote_mint,
1635            base_token_program,
1636            quote_token_program,
1637            user,
1638            creator,
1639            fee_recipient,
1640            buyback_fee_recipient,
1641            1_000,
1642            5_000_000,
1643        );
1644
1645        assert_eq!(ix.program_id, crate::pump::ID);
1646        assert_eq!(
1647            &ix.data[..8],
1648            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
1649        );
1650
1651        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1652        assert!(metas.contains(&pda::pump::global().0));
1653        assert!(metas.contains(&base_mint));
1654        assert!(metas.contains(&quote_mint));
1655        assert!(metas.contains(&fee_recipient));
1656        assert!(metas.contains(&buyback_fee_recipient));
1657        assert!(metas.contains(&pda::pump::bonding_curve(&base_mint).0));
1658        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
1659        assert!(metas.contains(&pda::pump::sharing_config(&base_mint).0));
1660        assert!(metas.contains(&pda::pump::user_volume_accumulator(&user).0));
1661        assert!(metas.contains(&pda::pump::fee_config().0));
1662        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
1663
1664        // Quote-side ATAs derive against quote_token_program + quote_mint.
1665        assert!(metas
1666            .contains(&pda::associated_token(&fee_recipient, &quote_token_program, &quote_mint).0));
1667        assert!(metas.contains(
1668            &pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0
1669        ));
1670
1671        // Caller is the only signer.
1672        let signers: Vec<Pubkey> = ix
1673            .accounts
1674            .iter()
1675            .filter(|m| m.is_signer)
1676            .map(|m| m.pubkey)
1677            .collect();
1678        assert_eq!(signers, vec![user]);
1679    }
1680
1681    #[test]
1682    fn buy_v2_instructions_prepends_four_ata_creates() {
1683        let sdk = PumpSdk::new();
1684        let base_mint = fake_pubkey(91);
1685        let quote_mint = fake_pubkey(92);
1686        let user = fake_pubkey(93);
1687        let creator = fake_pubkey(94);
1688        let fee_recipient = fake_pubkey(95);
1689        let buyback_fee_recipient = fake_pubkey(96);
1690        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1691        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1692
1693        let ixs = sdk.buy_v2_instructions(
1694            base_mint,
1695            quote_mint,
1696            base_token_program,
1697            quote_token_program,
1698            user,
1699            creator,
1700            fee_recipient,
1701            buyback_fee_recipient,
1702            1_000,
1703            5_000_000,
1704        );
1705
1706        // [base_user_ata, quote_bonding_curve_ata, quote_user_ata, quote_buyback_ata, buy_v2]
1707        assert_eq!(ixs.len(), 5);
1708        for ata_ix in &ixs[..4] {
1709            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
1710            // ATA program's idempotent-create discriminator is `1`.
1711            assert_eq!(ata_ix.data, vec![1]);
1712        }
1713        assert_eq!(
1714            &ixs[4].data[..8],
1715            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
1716        );
1717
1718        // The four ATA-create ixs must derive against the same (mint, token_program)
1719        // pairs the on-chain program expects.
1720        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
1721        let expected_atas: [Pubkey; 4] = [
1722            pda::associated_token(&user, &base_token_program, &base_mint).0,
1723            pda::associated_token(&bonding_curve, &quote_token_program, &quote_mint).0,
1724            pda::associated_token(&user, &quote_token_program, &quote_mint).0,
1725            pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0,
1726        ];
1727        for (i, expected) in expected_atas.iter().enumerate() {
1728            let metas: Vec<Pubkey> = ixs[i].accounts.iter().map(|m| m.pubkey).collect();
1729            assert!(metas.contains(expected), "ix {i} missing expected ATA");
1730        }
1731    }
1732
1733    #[test]
1734    #[allow(deprecated)]
1735    fn sell_instruction_appends_user_volume_accumulator_then_bonding_curve_v2() {
1736        let sdk = PumpSdk::new();
1737        let mint = fake_pubkey(101);
1738        let user = fake_pubkey(102);
1739        let creator = fake_pubkey(103);
1740        let fee_recipient = fake_pubkey(104);
1741        let token_program = constants::SPL_TOKEN_PROGRAM_ID;
1742
1743        let ix = sdk.sell_instruction(
1744            mint,
1745            user,
1746            creator,
1747            fee_recipient,
1748            100,
1749            1_000_000,
1750            token_program,
1751        );
1752
1753        assert_eq!(ix.program_id, crate::pump::ID);
1754        assert_eq!(
1755            &ix.data[..8],
1756            <client::args::Sell as Discriminator>::DISCRIMINATOR,
1757        );
1758
1759        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1760        assert!(metas.contains(&pda::pump::global().0));
1761        assert!(metas.contains(&pda::pump::bonding_curve(&mint).0));
1762        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
1763        assert!(metas.contains(&pda::pump::fee_config().0));
1764        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
1765        assert!(metas.contains(&fee_recipient));
1766        assert!(metas.contains(&pda::associated_token(&user, &token_program, &mint).0));
1767
1768        // Tail of the account list is exactly [user_volume_accumulator rw, bonding_curve_v2 ro].
1769        let n = ix.accounts.len();
1770        let uva = &ix.accounts[n - 2];
1771        let bcv2 = &ix.accounts[n - 1];
1772        assert_eq!(uva.pubkey, pda::pump::user_volume_accumulator(&user).0);
1773        assert!(uva.is_writable);
1774        assert!(!uva.is_signer);
1775        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&mint).0);
1776        assert!(!bcv2.is_writable);
1777        assert!(!bcv2.is_signer);
1778    }
1779
1780    #[test]
1781    fn sell_v2_instruction_wires_quote_and_sharing_config_pdas() {
1782        let sdk = PumpSdk::new();
1783        let base_mint = fake_pubkey(111);
1784        let quote_mint = fake_pubkey(112);
1785        let user = fake_pubkey(113);
1786        let creator = fake_pubkey(114);
1787        let fee_recipient = fake_pubkey(115);
1788        let buyback_fee_recipient = fake_pubkey(116);
1789        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1790        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1791
1792        let ix = sdk.sell_v2_instruction(
1793            base_mint,
1794            quote_mint,
1795            base_token_program,
1796            quote_token_program,
1797            user,
1798            creator,
1799            fee_recipient,
1800            buyback_fee_recipient,
1801            1_000,
1802            1_000_000,
1803        );
1804
1805        assert_eq!(ix.program_id, crate::pump::ID);
1806        assert_eq!(
1807            &ix.data[..8],
1808            <client::args::SellV2 as Discriminator>::DISCRIMINATOR,
1809        );
1810
1811        let metas: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1812        assert!(metas.contains(&pda::pump::global().0));
1813        assert!(metas.contains(&base_mint));
1814        assert!(metas.contains(&quote_mint));
1815        assert!(metas.contains(&fee_recipient));
1816        assert!(metas.contains(&buyback_fee_recipient));
1817        assert!(metas.contains(&pda::pump::bonding_curve(&base_mint).0));
1818        assert!(metas.contains(&pda::pump::creator_vault(&creator).0));
1819        assert!(metas.contains(&pda::pump::sharing_config(&base_mint).0));
1820        assert!(metas.contains(&pda::pump::user_volume_accumulator(&user).0));
1821        assert!(metas.contains(&pda::pump::fee_config().0));
1822        assert!(metas.contains(&constants::FEE_PROGRAM_ID));
1823
1824        // sell_v2 has no global_volume_accumulator (unlike buy_v2).
1825        assert!(!metas.contains(&pda::pump::global_volume_accumulator().0));
1826
1827        // Quote-side ATAs derive against quote_token_program + quote_mint.
1828        assert!(metas
1829            .contains(&pda::associated_token(&fee_recipient, &quote_token_program, &quote_mint).0));
1830        assert!(metas.contains(
1831            &pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0
1832        ));
1833
1834        // Caller is the only signer.
1835        let signers: Vec<Pubkey> = ix
1836            .accounts
1837            .iter()
1838            .filter(|m| m.is_signer)
1839            .map(|m| m.pubkey)
1840            .collect();
1841        assert_eq!(signers, vec![user]);
1842    }
1843
1844    #[test]
1845    fn sell_v2_instructions_prepends_four_ata_creates() {
1846        let sdk = PumpSdk::new();
1847        let base_mint = fake_pubkey(121);
1848        let quote_mint = fake_pubkey(122);
1849        let user = fake_pubkey(123);
1850        let creator = fake_pubkey(124);
1851        let fee_recipient = fake_pubkey(125);
1852        let buyback_fee_recipient = fake_pubkey(126);
1853        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1854        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1855
1856        let ixs = sdk.sell_v2_instructions(
1857            base_mint,
1858            quote_mint,
1859            base_token_program,
1860            quote_token_program,
1861            user,
1862            creator,
1863            fee_recipient,
1864            buyback_fee_recipient,
1865            1_000,
1866            1_000_000,
1867        );
1868
1869        // [base_user_ata, quote_bonding_curve_ata, quote_user_ata, quote_buyback_ata, sell_v2]
1870        assert_eq!(ixs.len(), 5);
1871        for ata_ix in &ixs[..4] {
1872            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
1873            // ATA program's idempotent-create discriminator is `1`.
1874            assert_eq!(ata_ix.data, vec![1]);
1875        }
1876        assert_eq!(
1877            &ixs[4].data[..8],
1878            <client::args::SellV2 as Discriminator>::DISCRIMINATOR,
1879        );
1880
1881        // The four ATA-create ixs must derive against the same (mint, token_program)
1882        // pairs the on-chain program expects.
1883        let bonding_curve = pda::pump::bonding_curve(&base_mint).0;
1884        let expected_atas: [Pubkey; 4] = [
1885            pda::associated_token(&user, &base_token_program, &base_mint).0,
1886            pda::associated_token(&bonding_curve, &quote_token_program, &quote_mint).0,
1887            pda::associated_token(&user, &quote_token_program, &quote_mint).0,
1888            pda::associated_token(&buyback_fee_recipient, &quote_token_program, &quote_mint).0,
1889        ];
1890        for (i, expected) in expected_atas.iter().enumerate() {
1891            let metas: Vec<Pubkey> = ixs[i].accounts.iter().map(|m| m.pubkey).collect();
1892            assert!(metas.contains(expected), "ix {i} missing expected ATA");
1893        }
1894    }
1895
1896    // ------------------------------------------------------------------
1897    // pump_amm buy / sell instruction tests.
1898    //
1899    // Mirror the IDL ordering at `idls/pump_amm.json` (buy: 23 base accounts,
1900    // sell: 21) and the remaining-account layout from
1901    // `pump-swap-sdk/src/sdk/offlinePumpAmm.ts:638-882`.
1902    // ------------------------------------------------------------------
1903
1904    fn fake_amm_inputs() -> (
1905        Pubkey,
1906        Pubkey,
1907        Pubkey,
1908        Pubkey,
1909        Pubkey,
1910        Pubkey,
1911        Pubkey,
1912        Pubkey,
1913    ) {
1914        (
1915            fake_pubkey(0xA1),               // pool
1916            fake_pubkey(0xA2),               // base_mint
1917            fake_pubkey(0xA3),               // quote_mint
1918            fake_pubkey(0xA4),               // user
1919            fake_pubkey(0xA5),               // coin_creator
1920            fake_pubkey(0xA6),               // protocol_fee_recipient
1921            fake_pubkey(0xA7),               // buyback_fee_recipient
1922            constants::SPL_TOKEN_PROGRAM_ID, // quote_token_program (placeholder)
1923        )
1924    }
1925
1926    #[test]
1927    fn buy_amm_instruction_wires_pump_amm_pdas() {
1928        let sdk = PumpSdk::new();
1929        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
1930            fake_amm_inputs();
1931        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
1932        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
1933
1934        let ix = sdk.buy_amm_instruction(
1935            pool,
1936            base_mint,
1937            quote_mint,
1938            base_token_program,
1939            quote_token_program,
1940            user,
1941            coin_creator,
1942            fee_rec,
1943            buyback,
1944            true, // is_cashback_coin
1945            1_000,
1946            500_000,
1947        );
1948
1949        assert_eq!(ix.program_id, crate::pump_amm::ID);
1950        assert_eq!(
1951            &ix.data[..8],
1952            <amm_client::args::Buy as Discriminator>::DISCRIMINATOR,
1953        );
1954        // 23 IDL accounts + 1 cashback ATA + 1 bonding_curve_v2 + 2 buyback (recipient + ATA).
1955        assert_eq!(ix.accounts.len(), 27);
1956
1957        let pubkeys: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
1958        // Spot-check IDL-derived PDAs are present.
1959        assert!(pubkeys.contains(&pda::pump_amm::global_config().0));
1960        assert!(pubkeys.contains(&pda::pump_amm::event_authority().0));
1961        assert!(pubkeys.contains(&pda::pump_amm::global_volume_accumulator().0));
1962        assert!(pubkeys.contains(&pda::pump_amm::user_volume_accumulator(&user).0));
1963        assert!(pubkeys.contains(&pda::pump_amm::fee_config().0));
1964        assert!(pubkeys.contains(&pda::pump_amm::coin_creator_vault_authority(&coin_creator).0));
1965
1966        // First account is the pool (writable, not signer); user is signer.
1967        assert_eq!(ix.accounts[0].pubkey, pool);
1968        assert!(ix.accounts[0].is_writable);
1969        assert!(!ix.accounts[0].is_signer);
1970        assert_eq!(ix.accounts[1].pubkey, user);
1971        assert!(ix.accounts[1].is_signer);
1972
1973        // Tail layout: [cashback_ata rw, bonding_curve_v2 ro, buyback rw, buyback_ata rw].
1974        let n = ix.accounts.len();
1975        let cashback = &ix.accounts[n - 4];
1976        let bcv2 = &ix.accounts[n - 3];
1977        let buyback_meta = &ix.accounts[n - 2];
1978        let buyback_ata = &ix.accounts[n - 1];
1979
1980        // Cashback ATA on buy uses NATIVE_MINT, not quote_mint.
1981        let user_vol_accum = pda::pump_amm::user_volume_accumulator(&user).0;
1982        assert_eq!(
1983            cashback.pubkey,
1984            pda::associated_token(
1985                &user_vol_accum,
1986                &quote_token_program,
1987                &constants::NATIVE_MINT,
1988            )
1989            .0
1990        );
1991        assert!(cashback.is_writable);
1992
1993        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&base_mint).0);
1994        assert!(!bcv2.is_writable);
1995
1996        // buyback_fee_recipient is WRITABLE on buy.
1997        assert_eq!(buyback_meta.pubkey, buyback);
1998        assert!(buyback_meta.is_writable);
1999
2000        assert_eq!(
2001            buyback_ata.pubkey,
2002            pda::associated_token(&buyback, &quote_token_program, &quote_mint).0
2003        );
2004        assert!(buyback_ata.is_writable);
2005    }
2006
2007    #[test]
2008    fn buy_amm_instruction_skips_remaining_when_no_cashback_no_creator() {
2009        let sdk = PumpSdk::new();
2010        let (pool, base_mint, quote_mint, user, _, fee_rec, buyback, _) = fake_amm_inputs();
2011
2012        let ix = sdk.buy_amm_instruction(
2013            pool,
2014            base_mint,
2015            quote_mint,
2016            constants::SPL_TOKEN_2022_PROGRAM_ID,
2017            constants::SPL_TOKEN_PROGRAM_ID,
2018            user,
2019            Pubkey::default(), // default coin_creator -> no bonding_curve_v2 remaining
2020            fee_rec,
2021            buyback,
2022            false, // no cashback
2023            1_000,
2024            500_000,
2025        );
2026
2027        // 23 IDL accounts + 0 + 0 + 2 (always-on buyback pair) = 25.
2028        assert_eq!(ix.accounts.len(), 25);
2029        let n = ix.accounts.len();
2030        assert_eq!(ix.accounts[n - 2].pubkey, buyback);
2031        assert!(ix.accounts[n - 2].is_writable);
2032    }
2033
2034    #[test]
2035    fn sell_amm_instruction_wires_pump_amm_pdas() {
2036        let sdk = PumpSdk::new();
2037        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
2038            fake_amm_inputs();
2039        let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
2040        let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
2041
2042        let ix = sdk.sell_amm_instruction(
2043            pool,
2044            base_mint,
2045            quote_mint,
2046            base_token_program,
2047            quote_token_program,
2048            user,
2049            coin_creator,
2050            fee_rec,
2051            buyback,
2052            true, // is_cashback_coin
2053            1_000_000,
2054            500,
2055        );
2056
2057        assert_eq!(ix.program_id, crate::pump_amm::ID);
2058        assert_eq!(
2059            &ix.data[..8],
2060            <amm_client::args::Sell as Discriminator>::DISCRIMINATOR,
2061        );
2062        // 21 IDL accounts + 2 cashback + 1 bonding_curve_v2 + 2 buyback = 26.
2063        assert_eq!(ix.accounts.len(), 26);
2064
2065        // Sell does NOT include global_volume_accumulator (unlike buy).
2066        let pubkeys: Vec<Pubkey> = ix.accounts.iter().map(|m| m.pubkey).collect();
2067        assert!(!pubkeys.contains(&pda::pump_amm::global_volume_accumulator().0));
2068
2069        let user_vol_accum = pda::pump_amm::user_volume_accumulator(&user).0;
2070        let n = ix.accounts.len();
2071        // Tail: [cashback_ata rw, user_vol_accum rw, bonding_curve_v2 ro, buyback ro, buyback_ata rw]
2072        let cashback_ata = &ix.accounts[n - 5];
2073        let cashback_uva = &ix.accounts[n - 4];
2074        let bcv2 = &ix.accounts[n - 3];
2075        let buyback_meta = &ix.accounts[n - 2];
2076        let buyback_ata = &ix.accounts[n - 1];
2077
2078        // Cashback ATA on sell uses quote_mint (not NATIVE_MINT — the buy side does that).
2079        assert_eq!(
2080            cashback_ata.pubkey,
2081            pda::associated_token(&user_vol_accum, &quote_token_program, &quote_mint).0
2082        );
2083        assert!(cashback_ata.is_writable);
2084
2085        assert_eq!(cashback_uva.pubkey, user_vol_accum);
2086        assert!(cashback_uva.is_writable);
2087
2088        assert_eq!(bcv2.pubkey, pda::pump::bonding_curve_v2(&base_mint).0);
2089        assert!(!bcv2.is_writable);
2090
2091        // buyback_fee_recipient is READ-ONLY on sell (writable on buy).
2092        assert_eq!(buyback_meta.pubkey, buyback);
2093        assert!(!buyback_meta.is_writable);
2094
2095        assert_eq!(
2096            buyback_ata.pubkey,
2097            pda::associated_token(&buyback, &quote_token_program, &quote_mint).0
2098        );
2099        assert!(buyback_ata.is_writable);
2100    }
2101
2102    #[test]
2103    fn sell_amm_instruction_skips_remaining_when_no_cashback_no_creator() {
2104        let sdk = PumpSdk::new();
2105        let (pool, base_mint, quote_mint, user, _, fee_rec, buyback, _) = fake_amm_inputs();
2106
2107        let ix = sdk.sell_amm_instruction(
2108            pool,
2109            base_mint,
2110            quote_mint,
2111            constants::SPL_TOKEN_2022_PROGRAM_ID,
2112            constants::SPL_TOKEN_PROGRAM_ID,
2113            user,
2114            Pubkey::default(),
2115            fee_rec,
2116            buyback,
2117            false,
2118            1_000_000,
2119            500,
2120        );
2121
2122        // 21 IDL accounts + 0 + 0 + 2 (always-on buyback pair) = 23.
2123        assert_eq!(ix.accounts.len(), 23);
2124        let n = ix.accounts.len();
2125        // buyback is read-only on sell.
2126        assert_eq!(ix.accounts[n - 2].pubkey, buyback);
2127        assert!(!ix.accounts[n - 2].is_writable);
2128    }
2129
2130    #[test]
2131    fn buy_amm_instructions_prepends_one_ata_create() {
2132        let sdk = PumpSdk::new();
2133        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
2134            fake_amm_inputs();
2135
2136        let ixs = sdk.buy_amm_instructions(
2137            pool,
2138            base_mint,
2139            quote_mint,
2140            constants::SPL_TOKEN_2022_PROGRAM_ID,
2141            constants::SPL_TOKEN_PROGRAM_ID,
2142            user,
2143            coin_creator,
2144            fee_rec,
2145            buyback,
2146            false,
2147            1_000,
2148            500_000,
2149        );
2150
2151        assert_eq!(ixs.len(), 2);
2152        // First is the SPL ATA program's idempotent instruction (single byte 1).
2153        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
2154        assert_eq!(ixs[0].data, vec![1]);
2155        // Second is the AMM buy.
2156        assert_eq!(ixs[1].program_id, crate::pump_amm::ID);
2157        assert_eq!(
2158            &ixs[1].data[..8],
2159            <amm_client::args::Buy as Discriminator>::DISCRIMINATOR,
2160        );
2161    }
2162
2163    #[test]
2164    fn sell_amm_instructions_prepends_one_ata_create() {
2165        let sdk = PumpSdk::new();
2166        let (pool, base_mint, quote_mint, user, coin_creator, fee_rec, buyback, _) =
2167            fake_amm_inputs();
2168
2169        let ixs = sdk.sell_amm_instructions(
2170            pool,
2171            base_mint,
2172            quote_mint,
2173            constants::SPL_TOKEN_2022_PROGRAM_ID,
2174            constants::SPL_TOKEN_PROGRAM_ID,
2175            user,
2176            coin_creator,
2177            fee_rec,
2178            buyback,
2179            false,
2180            1_000_000,
2181            500,
2182        );
2183
2184        assert_eq!(ixs.len(), 2);
2185        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
2186        assert_eq!(ixs[0].data, vec![1]);
2187        assert_eq!(ixs[1].program_id, crate::pump_amm::ID);
2188        assert_eq!(
2189            &ixs[1].data[..8],
2190            <amm_client::args::Sell as Discriminator>::DISCRIMINATOR,
2191        );
2192    }
2193
2194    // ------------------------------------------------------------------
2195    // Complete trade-flow tests (trade_tx_instructions / create_coin_instructions).
2196    // ------------------------------------------------------------------
2197
2198    fn user_wsol_ata(user: &Pubkey) -> Pubkey {
2199        pda::associated_token(
2200            user,
2201            &constants::SPL_TOKEN_PROGRAM_ID,
2202            &constants::NATIVE_MINT,
2203        )
2204        .0
2205    }
2206
2207    /// `system_instruction::transfer` is a system-program instruction whose
2208    /// data is `[2u32 LE, lamports u64 LE]`. Pulls out the lamports for assertions.
2209    fn parse_system_transfer_lamports(ix: &Instruction) -> u64 {
2210        assert_eq!(ix.program_id, system_program::ID);
2211        assert_eq!(ix.data.len(), 12);
2212        assert_eq!(&ix.data[..4], &2u32.to_le_bytes());
2213        u64::from_le_bytes(ix.data[4..12].try_into().unwrap())
2214    }
2215
2216    #[test]
2217    fn trade_tx_buy_bc_wraps_sol_and_unwraps_residual() {
2218        let sdk = PumpSdk::new();
2219        let mint = fake_pubkey(0xB1);
2220        let user = fake_pubkey(0xB2);
2221        let creator = fake_pubkey(0xB3);
2222        let fee_recipient = fake_pubkey(0xB4);
2223        let buyback_fee_recipient = fake_pubkey(0xB5);
2224        let max_sol = 7_500_000u64;
2225
2226        let ixs = sdk.trade_tx_instructions(TradeTxParams {
2227            mint,
2228            base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
2229            user,
2230            creator,
2231            fee_recipient,
2232            buyback_fee_recipient,
2233            is_buy: true,
2234            venue: TradeVenue::BondingCurve,
2235            is_cashback_coin: false,
2236            base_amount: 1_000,
2237            sol_amount_threshold: max_sol,
2238        });
2239
2240        // [ata×4, system_transfer, sync_native, buy_v2, close_account]
2241        assert_eq!(ixs.len(), 8);
2242        for ata_ix in &ixs[..4] {
2243            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
2244            assert_eq!(ata_ix.data, vec![1]);
2245        }
2246        assert_eq!(parse_system_transfer_lamports(&ixs[4]), max_sol);
2247        assert_eq!(ixs[5].program_id, constants::SPL_TOKEN_PROGRAM_ID);
2248        assert_eq!(ixs[6].program_id, crate::pump::ID);
2249        assert_eq!(
2250            &ixs[6].data[..8],
2251            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
2252        );
2253        assert_eq!(ixs[7].program_id, constants::SPL_TOKEN_PROGRAM_ID);
2254
2255        // sync_native and close_account both target the user's wSOL ATA.
2256        let wsol_ata = user_wsol_ata(&user);
2257        assert!(ixs[5].accounts.iter().any(|m| m.pubkey == wsol_ata));
2258        assert!(ixs[7].accounts.iter().any(|m| m.pubkey == wsol_ata));
2259
2260        // ATA prefixes match the four (mint, token_program) pairs the program expects.
2261        let bonding_curve = pda::pump::bonding_curve(&mint).0;
2262        let expected_atas: [Pubkey; 4] = [
2263            pda::associated_token(&user, &constants::SPL_TOKEN_2022_PROGRAM_ID, &mint).0,
2264            pda::associated_token(
2265                &bonding_curve,
2266                &constants::SPL_TOKEN_PROGRAM_ID,
2267                &constants::NATIVE_MINT,
2268            )
2269            .0,
2270            wsol_ata,
2271            pda::associated_token(
2272                &buyback_fee_recipient,
2273                &constants::SPL_TOKEN_PROGRAM_ID,
2274                &constants::NATIVE_MINT,
2275            )
2276            .0,
2277        ];
2278        for (i, expected) in expected_atas.iter().enumerate() {
2279            let metas: Vec<Pubkey> = ixs[i].accounts.iter().map(|m| m.pubkey).collect();
2280            assert!(metas.contains(expected), "ata ix {i} missing expected ATA");
2281        }
2282    }
2283
2284    #[test]
2285    fn trade_tx_sell_bc_unwraps_proceeds_no_wrap() {
2286        let sdk = PumpSdk::new();
2287        let mint = fake_pubkey(0xC1);
2288        let user = fake_pubkey(0xC2);
2289        let creator = fake_pubkey(0xC3);
2290        let fee_recipient = fake_pubkey(0xC4);
2291        let buyback_fee_recipient = fake_pubkey(0xC5);
2292
2293        let ixs = sdk.trade_tx_instructions(TradeTxParams {
2294            mint,
2295            base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
2296            user,
2297            creator,
2298            fee_recipient,
2299            buyback_fee_recipient,
2300            is_buy: false,
2301            venue: TradeVenue::BondingCurve,
2302            is_cashback_coin: false,
2303            base_amount: 1_000,
2304            sol_amount_threshold: 1_000_000,
2305        });
2306
2307        // [ata×4, sell_v2, close_account] — no system_transfer or sync_native on sell.
2308        assert_eq!(ixs.len(), 6);
2309        for ata_ix in &ixs[..4] {
2310            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
2311            assert_eq!(ata_ix.data, vec![1]);
2312        }
2313        assert_eq!(ixs[4].program_id, crate::pump::ID);
2314        assert_eq!(
2315            &ixs[4].data[..8],
2316            <client::args::SellV2 as Discriminator>::DISCRIMINATOR,
2317        );
2318        assert_eq!(ixs[5].program_id, constants::SPL_TOKEN_PROGRAM_ID);
2319        assert!(ixs[5]
2320            .accounts
2321            .iter()
2322            .any(|m| m.pubkey == user_wsol_ata(&user)));
2323    }
2324
2325    #[test]
2326    fn trade_tx_buy_amm_wraps_and_unwraps_with_cashback() {
2327        let sdk = PumpSdk::new();
2328        let mint = fake_pubkey(0xD1);
2329        let user = fake_pubkey(0xD2);
2330        let coin_creator = fake_pubkey(0xD3);
2331        let fee_recipient = fake_pubkey(0xD4);
2332        let buyback_fee_recipient = fake_pubkey(0xD5);
2333        let pool = fake_pubkey(0xD6);
2334        let max_sol = 2_500_000u64;
2335
2336        let ixs = sdk.trade_tx_instructions(TradeTxParams {
2337            mint,
2338            base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
2339            user,
2340            creator: coin_creator,
2341            fee_recipient,
2342            buyback_fee_recipient,
2343            is_buy: true,
2344            venue: TradeVenue::Amm { pool },
2345            is_cashback_coin: true,
2346            base_amount: 1_000,
2347            sol_amount_threshold: max_sol,
2348        });
2349
2350        // [ata_base, ata_wsol, system_transfer, sync_native, buy_amm, close_account]
2351        assert_eq!(ixs.len(), 6);
2352        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
2353        assert_eq!(ixs[1].program_id, constants::SPL_ATA_PROGRAM_ID);
2354        assert_eq!(parse_system_transfer_lamports(&ixs[2]), max_sol);
2355        assert_eq!(ixs[3].program_id, constants::SPL_TOKEN_PROGRAM_ID);
2356        assert_eq!(ixs[4].program_id, crate::pump_amm::ID);
2357        assert_eq!(
2358            &ixs[4].data[..8],
2359            <amm_client::args::Buy as Discriminator>::DISCRIMINATOR,
2360        );
2361        assert_eq!(ixs[5].program_id, constants::SPL_TOKEN_PROGRAM_ID);
2362
2363        // The buy_amm instruction must include both the cashback remaining account
2364        // (NATIVE_MINT-flavoured) and the bonding_curve_v2 readonly tail (non-default creator).
2365        let pubkeys: Vec<Pubkey> = ixs[4].accounts.iter().map(|m| m.pubkey).collect();
2366        let user_vol_accum = pda::pump_amm::user_volume_accumulator(&user).0;
2367        assert!(pubkeys.contains(
2368            &pda::associated_token(
2369                &user_vol_accum,
2370                &constants::SPL_TOKEN_PROGRAM_ID,
2371                &constants::NATIVE_MINT,
2372            )
2373            .0
2374        ));
2375        assert!(pubkeys.contains(&pda::pump::bonding_curve_v2(&mint).0));
2376    }
2377
2378    #[test]
2379    fn trade_tx_sell_amm_unwraps_proceeds_no_wrap() {
2380        let sdk = PumpSdk::new();
2381        let mint = fake_pubkey(0xE1);
2382        let user = fake_pubkey(0xE2);
2383        let coin_creator = fake_pubkey(0xE3);
2384        let fee_recipient = fake_pubkey(0xE4);
2385        let buyback_fee_recipient = fake_pubkey(0xE5);
2386        let pool = fake_pubkey(0xE6);
2387
2388        let ixs = sdk.trade_tx_instructions(TradeTxParams {
2389            mint,
2390            base_token_program: constants::SPL_TOKEN_2022_PROGRAM_ID,
2391            user,
2392            creator: coin_creator,
2393            fee_recipient,
2394            buyback_fee_recipient,
2395            is_buy: false,
2396            venue: TradeVenue::Amm { pool },
2397            is_cashback_coin: false,
2398            base_amount: 5_000,
2399            sol_amount_threshold: 100,
2400        });
2401
2402        // [ata_wsol, sell_amm, close_account]
2403        assert_eq!(ixs.len(), 3);
2404        assert_eq!(ixs[0].program_id, constants::SPL_ATA_PROGRAM_ID);
2405        let wsol_ata = user_wsol_ata(&user);
2406        assert!(ixs[0].accounts.iter().any(|m| m.pubkey == wsol_ata));
2407        assert_eq!(ixs[1].program_id, crate::pump_amm::ID);
2408        assert_eq!(
2409            &ixs[1].data[..8],
2410            <amm_client::args::Sell as Discriminator>::DISCRIMINATOR,
2411        );
2412        assert_eq!(ixs[2].program_id, constants::SPL_TOKEN_PROGRAM_ID);
2413        assert!(ixs[2].accounts.iter().any(|m| m.pubkey == wsol_ata));
2414    }
2415
2416    #[test]
2417    fn create_coin_instructions_emits_full_create_buy_with_wrap() {
2418        let sdk = PumpSdk::new();
2419        let mint = fake_pubkey(0xF1);
2420        let user = fake_pubkey(0xF2);
2421        let creator = fake_pubkey(0xF3);
2422        let fee_recipient = fake_pubkey(0xF4);
2423        let buyback_fee_recipient = fake_pubkey(0xF5);
2424        let max_sol = 1_234_567u64;
2425
2426        let ixs = sdk.create_coin_instructions(CreateCoinParams {
2427            mint,
2428            user,
2429            creator,
2430            name: "Test".into(),
2431            symbol: "TST".into(),
2432            uri: "https://example.com/metadata.json".into(),
2433            mayhem_mode: false,
2434            cashback: false,
2435            fee_recipient,
2436            buyback_fee_recipient,
2437            token_amount: 1_000,
2438            max_sol_cost: max_sol,
2439        });
2440
2441        // [create_v2, ata×4, system_transfer, sync_native, buy_v2, close_account]
2442        assert_eq!(ixs.len(), 9);
2443        assert_eq!(
2444            &ixs[0].data[..8],
2445            <client::args::CreateV2 as Discriminator>::DISCRIMINATOR,
2446        );
2447        for ata_ix in &ixs[1..5] {
2448            assert_eq!(ata_ix.program_id, constants::SPL_ATA_PROGRAM_ID);
2449            assert_eq!(ata_ix.data, vec![1]);
2450        }
2451        assert_eq!(parse_system_transfer_lamports(&ixs[5]), max_sol);
2452        assert_eq!(ixs[6].program_id, constants::SPL_TOKEN_PROGRAM_ID);
2453        assert_eq!(ixs[7].program_id, crate::pump::ID);
2454        assert_eq!(
2455            &ixs[7].data[..8],
2456            <client::args::BuyV2 as Discriminator>::DISCRIMINATOR,
2457        );
2458        // Buy is v2 with base=Token-2022, quote=wSOL/SPL Token.
2459        let buy_metas: Vec<Pubkey> = ixs[7].accounts.iter().map(|m| m.pubkey).collect();
2460        assert!(buy_metas.contains(&constants::SPL_TOKEN_2022_PROGRAM_ID));
2461        assert!(buy_metas.contains(&constants::NATIVE_MINT));
2462        assert!(buy_metas.contains(&pda::pump::sharing_config(&mint).0));
2463
2464        assert_eq!(ixs[8].program_id, constants::SPL_TOKEN_PROGRAM_ID);
2465        let wsol_ata = user_wsol_ata(&user);
2466        assert!(ixs[8].accounts.iter().any(|m| m.pubkey == wsol_ata));
2467    }
2468
2469    /// Sanity: ensure the IDL-derived program IDs match the canonical addresses
2470    /// from the TS SDK so we don't silently regress against a future IDL change.
2471    #[test]
2472    fn program_ids_match_idl() {
2473        assert_eq!(
2474            crate::pump::ID,
2475            pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
2476        );
2477        assert_eq!(
2478            crate::pump_amm::ID,
2479            pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
2480        );
2481    }
2482
2483    // ------------------------------------------------------------------
2484    // Quote parity tests.
2485    //
2486    // Reference values were generated by a Python port of the TS algorithm
2487    // and cross-verified against `@pump-fun/pump-sdk-internal@1.0.0-devnet.29`
2488    // and `@pump-fun/pump-swap-sdk@1.15.0` directly (see commit history /
2489    // sibling repro script `tools/quote_parity_check.cjs`). If the on-chain
2490    // formulas change, regenerate the constants.
2491    // ------------------------------------------------------------------
2492
2493    use crate::state::pump_amm::{GlobalConfig, Pool};
2494    use crate::state::{BondingCurve, Global};
2495
2496    /// Standard pump global with flat fees (1% protocol, 0.5% creator) and
2497    /// realistic supply / migration fee. All other fields zero/default.
2498    fn fixture_global() -> Global {
2499        Global {
2500            fee_basis_points: 100,
2501            creator_fee_basis_points: 50,
2502            token_total_supply: 1_000_000_000_000_000,
2503            initial_real_token_reserves: 793_100_000_000_000,
2504            pool_migration_fee: 6_900_000_000,
2505            ..Default::default()
2506        }
2507    }
2508
2509    /// Pre-trade bonding curve, real creator (so creator fee applies).
2510    fn fixture_bonding_curve(creator: Pubkey) -> BondingCurve {
2511        BondingCurve {
2512            virtual_token_reserves: 1_073_000_000_000_000,
2513            virtual_quote_reserves: 30_000_000_000,
2514            real_token_reserves: 793_100_000_000_000,
2515            real_quote_reserves: 0,
2516            token_total_supply: 1_000_000_000_000_000,
2517            complete: false,
2518            creator,
2519            is_mayhem_mode: false,
2520            is_cashback_coin: false,
2521            quote_mint: constants::NATIVE_MINT,
2522        }
2523    }
2524
2525    fn fixture_amm_global_config() -> GlobalConfig {
2526        GlobalConfig {
2527            lp_fee_basis_points: 20,
2528            protocol_fee_basis_points: 5,
2529            coin_creator_fee_basis_points: 5,
2530            ..Default::default()
2531        }
2532    }
2533
2534    /// Non-pump pool: `creator` deliberately not the pool-authority PDA so
2535    /// the no-fee-config path is exercised purely against `GlobalConfig`.
2536    fn fixture_pool(coin_creator: Pubkey) -> Pool {
2537        Pool {
2538            base_mint: fake_pubkey(0xAA),
2539            creator: fake_pubkey(0xBB),
2540            coin_creator,
2541            quote_mint: constants::NATIVE_MINT,
2542            ..Default::default()
2543        }
2544    }
2545
2546    // -------- Bonding curve parity --------
2547
2548    #[test]
2549    fn bc_buy_sol_in_matches_ts() {
2550        let sdk = PumpSdk::new();
2551        let g = fixture_global();
2552        let bc = fixture_bonding_curve(fake_pubkey(7));
2553        let q = sdk
2554            .buy_quote_bonding_curve_sol_in(
2555                &g,
2556                None,
2557                &bc,
2558                g.token_total_supply,
2559                1_000_000_000, // 1 SOL
2560                100,           // 1% slippage
2561            )
2562            .unwrap();
2563        assert_eq!(q.amount, 34_117_646_995_895);
2564        assert_eq!(q.min_out, 33_776_470_525_936);
2565        assert_eq!(q.input_amount_used, 1_000_000_000);
2566        assert_eq!(q.max_input, 1_000_000_000);
2567    }
2568
2569    #[test]
2570    fn bc_buy_token_out_matches_ts() {
2571        let sdk = PumpSdk::new();
2572        let g = fixture_global();
2573        let bc = fixture_bonding_curve(fake_pubkey(7));
2574        let q = sdk
2575            .buy_quote_bonding_curve_token_out(
2576                &g,
2577                None,
2578                &bc,
2579                g.token_total_supply,
2580                100_000_000_000_000,
2581                200, // 2% slippage
2582            )
2583            .unwrap();
2584        assert_eq!(q.amount, 3_129_496_404);
2585        assert_eq!(q.max_input, 3_192_086_332);
2586        assert_eq!(q.min_out, 100_000_000_000_000);
2587        assert_eq!(q.input_amount_used, 100_000_000_000_000);
2588    }
2589
2590    #[test]
2591    fn bc_sell_with_creator_matches_ts() {
2592        let sdk = PumpSdk::new();
2593        let g = fixture_global();
2594        let bc = fixture_bonding_curve(fake_pubkey(7));
2595        let q = sdk
2596            .sell_quote_bonding_curve(
2597                &g,
2598                None,
2599                &bc,
2600                g.token_total_supply,
2601                50_000_000_000_000,
2602                50, // 0.5% slippage
2603            )
2604            .unwrap();
2605        assert_eq!(q.amount, 1_315_672_305);
2606        assert_eq!(q.min_out, 1_309_093_943);
2607    }
2608
2609    #[test]
2610    fn bc_sell_default_creator_skips_creator_fee() {
2611        let sdk = PumpSdk::new();
2612        let g = fixture_global();
2613        let bc = fixture_bonding_curve(Pubkey::default());
2614        let q = sdk
2615            .sell_quote_bonding_curve(&g, None, &bc, g.token_total_supply, 50_000_000_000_000, 0)
2616            .unwrap();
2617        // Output is strictly larger than the with-creator case because the
2618        // 0.5% creator slice is not subtracted.
2619        assert_eq!(q.amount, 1_322_350_845);
2620    }
2621
2622    // -------- AMM parity --------
2623
2624    #[test]
2625    fn amm_buy_sol_in_matches_ts() {
2626        let sdk = PumpSdk::new();
2627        let gc = fixture_amm_global_config();
2628        let pool = fixture_pool(fake_pubkey(0xCC));
2629        let q = sdk
2630            .buy_quote_amm_sol_in(
2631                &gc,
2632                None,
2633                AmmQuoteSource::Pool {
2634                    pool: &pool,
2635                    base_reserve: 200_000_000_000_000,
2636                    quote_reserve: 30_000_000_000,
2637                    base_mint_supply: 1_000_000_000_000_000,
2638                },
2639                1_000_000_000,
2640                100,
2641            )
2642            .unwrap();
2643        assert_eq!(q.amount, 6_432_936_635_069);
2644        assert_eq!(q.min_out, 6_368_607_268_718);
2645        assert_eq!(q.max_input, 1_010_000_000); // pump-swap-sdk buyQuoteInput maxQuote @ 1% bps
2646    }
2647
2648    #[test]
2649    fn amm_buy_token_out_matches_ts() {
2650        let sdk = PumpSdk::new();
2651        let gc = fixture_amm_global_config();
2652        let pool = fixture_pool(fake_pubkey(0xCC));
2653        let q = sdk
2654            .buy_quote_amm_token_out(
2655                &gc,
2656                None,
2657                AmmQuoteSource::Pool {
2658                    pool: &pool,
2659                    base_reserve: 200_000_000_000_000,
2660                    quote_reserve: 30_000_000_000,
2661                    base_mint_supply: 1_000_000_000_000_000,
2662                },
2663                1_000_000_000_000,
2664                100,
2665            )
2666            .unwrap();
2667        assert_eq!(q.amount, 151_206_031);
2668        assert_eq!(q.max_input, 152_718_091);
2669    }
2670
2671    #[test]
2672    fn amm_sell_with_coin_creator_matches_ts() {
2673        let sdk = PumpSdk::new();
2674        let gc = fixture_amm_global_config();
2675        let pool = fixture_pool(fake_pubkey(0xCC));
2676        let q = sdk
2677            .sell_quote_amm(
2678                &gc,
2679                None,
2680                AmmQuoteSource::Pool {
2681                    pool: &pool,
2682                    base_reserve: 200_000_000_000_000,
2683                    quote_reserve: 30_000_000_000,
2684                    base_mint_supply: 1_000_000_000_000_000,
2685                },
2686                1_000_000_000_000,
2687                100,
2688            )
2689            .unwrap();
2690        assert_eq!(q.amount, 148_805_969);
2691        assert_eq!(q.min_out, 147_317_909);
2692    }
2693
2694    #[test]
2695    fn amm_sell_default_coin_creator_skips_creator_fee() {
2696        let sdk = PumpSdk::new();
2697        let gc = fixture_amm_global_config();
2698        let pool = fixture_pool(Pubkey::default());
2699        let q = sdk
2700            .sell_quote_amm(
2701                &gc,
2702                None,
2703                AmmQuoteSource::Pool {
2704                    pool: &pool,
2705                    base_reserve: 200_000_000_000_000,
2706                    quote_reserve: 30_000_000_000,
2707                    base_mint_supply: 1_000_000_000_000_000,
2708                },
2709                1_000_000_000_000,
2710                0,
2711            )
2712            .unwrap();
2713        assert_eq!(q.amount, 148_880_596);
2714    }
2715
2716    // -------- Edge cases --------
2717
2718    #[test]
2719    fn zero_input_returns_zero() {
2720        let sdk = PumpSdk::new();
2721        let g = fixture_global();
2722        let bc = fixture_bonding_curve(fake_pubkey(7));
2723        let q = sdk
2724            .buy_quote_bonding_curve_sol_in(&g, None, &bc, g.token_total_supply, 0, 100)
2725            .unwrap();
2726        assert_eq!(q.amount, 0);
2727        let q = sdk
2728            .sell_quote_bonding_curve(&g, None, &bc, g.token_total_supply, 0, 100)
2729            .unwrap();
2730        assert_eq!(q.amount, 0);
2731    }
2732
2733    #[test]
2734    fn migrated_curve_returns_zero() {
2735        let sdk = PumpSdk::new();
2736        let g = fixture_global();
2737        let mut bc = fixture_bonding_curve(fake_pubkey(7));
2738        bc.virtual_token_reserves = 0; // signals already-migrated curve
2739        let q = sdk
2740            .buy_quote_bonding_curve_sol_in(&g, None, &bc, g.token_total_supply, 1_000_000_000, 0)
2741            .unwrap();
2742        assert_eq!(q.amount, 0);
2743    }
2744
2745    // -------- Error paths --------
2746
2747    #[test]
2748    fn amm_empty_reserves_errors() {
2749        let sdk = PumpSdk::new();
2750        let gc = fixture_amm_global_config();
2751        let pool = fixture_pool(fake_pubkey(0xCC));
2752        let err = sdk
2753            .buy_quote_amm_sol_in(
2754                &gc,
2755                None,
2756                AmmQuoteSource::Pool {
2757                    pool: &pool,
2758                    base_reserve: 0,
2759                    quote_reserve: 1_000,
2760                    base_mint_supply: 1_000,
2761                },
2762                1,
2763                0,
2764            )
2765            .unwrap_err();
2766        assert_eq!(err, QuoteError::EmptyReserves);
2767        let err = sdk
2768            .sell_quote_amm(
2769                &gc,
2770                None,
2771                AmmQuoteSource::Pool {
2772                    pool: &pool,
2773                    base_reserve: 1_000,
2774                    quote_reserve: 0,
2775                    base_mint_supply: 1_000,
2776                },
2777                1,
2778                0,
2779            )
2780            .unwrap_err();
2781        assert_eq!(err, QuoteError::EmptyReserves);
2782    }
2783
2784    #[test]
2785    fn amm_buy_token_out_oversize_errors() {
2786        let sdk = PumpSdk::new();
2787        let gc = fixture_amm_global_config();
2788        let pool = fixture_pool(fake_pubkey(0xCC));
2789        // base_out == base_reserve (would deplete pool)
2790        let err = sdk
2791            .buy_quote_amm_token_out(
2792                &gc,
2793                None,
2794                AmmQuoteSource::Pool {
2795                    pool: &pool,
2796                    base_reserve: 1_000,
2797                    quote_reserve: 1_000,
2798                    base_mint_supply: 1_000_000,
2799                },
2800                1_000,
2801                0,
2802            )
2803            .unwrap_err();
2804        assert_eq!(err, QuoteError::BaseOutExceedsReserve);
2805        // base_out > base_reserve
2806        let err = sdk
2807            .buy_quote_amm_token_out(
2808                &gc,
2809                None,
2810                AmmQuoteSource::Pool {
2811                    pool: &pool,
2812                    base_reserve: 1_000,
2813                    quote_reserve: 1_000,
2814                    base_mint_supply: 1_000_000,
2815                },
2816                5_000,
2817                0,
2818            )
2819            .unwrap_err();
2820        assert_eq!(err, QuoteError::BaseOutExceedsReserve);
2821    }
2822
2823    #[test]
2824    fn bc_token_out_buy_at_real_reserves_errors() {
2825        let sdk = PumpSdk::new();
2826        let g = fixture_global();
2827        let mut bc = fixture_bonding_curve(fake_pubkey(7));
2828        // Force degenerate state: real_token_reserves = virtual_token_reserves.
2829        bc.real_token_reserves = bc.virtual_token_reserves;
2830        let err = sdk
2831            .buy_quote_bonding_curve_token_out(
2832                &g,
2833                None,
2834                &bc,
2835                g.token_total_supply,
2836                bc.virtual_token_reserves,
2837                0,
2838            )
2839            .unwrap_err();
2840        assert_eq!(err, QuoteError::DepletedBondingCurve);
2841    }
2842
2843    #[test]
2844    fn amm_quote_bonding_curve_complete_underflow_errors() {
2845        let sdk = PumpSdk::new();
2846        let g = fixture_global();
2847        let gc = fixture_amm_global_config();
2848        let bc = fixture_bonding_curve(fake_pubkey(7)); // real_quote_reserves = 0
2849        let base_mint = fake_pubkey(0xAA);
2850        let err = sdk
2851            .buy_quote_amm_sol_in(
2852                &gc,
2853                None,
2854                AmmQuoteSource::BondingCurveComplete {
2855                    global: &g,
2856                    bonding_curve: &bc,
2857                    base_mint: &base_mint,
2858                    base_mint_supply: g.token_total_supply,
2859                },
2860                1_000_000_000,
2861                0,
2862            )
2863            .unwrap_err();
2864        assert_eq!(err, QuoteError::EmptyReserves);
2865    }
2866
2867    #[test]
2868    fn amm_quote_bonding_curve_complete_matches_formula() {
2869        let sdk = PumpSdk::new();
2870        let g = fixture_global();
2871        let gc = fixture_amm_global_config();
2872        let mut bc = fixture_bonding_curve(fake_pubkey(7));
2873        bc.real_quote_reserves = 100_000_000_000;
2874        let base_mint = fake_pubkey(0xAA);
2875
2876        let q = sdk
2877            .buy_quote_amm_sol_in(
2878                &gc,
2879                None,
2880                AmmQuoteSource::BondingCurveComplete {
2881                    global: &g,
2882                    bonding_curve: &bc,
2883                    base_mint: &base_mint,
2884                    base_mint_supply: g.token_total_supply,
2885                },
2886                1_000_000_000,
2887                0,
2888            )
2889            .unwrap();
2890
2891        let base_reserve = g.token_total_supply - g.initial_real_token_reserves;
2892        let quote_reserve = bc.real_quote_reserves - g.pool_migration_fee;
2893        let pool_creator = pda::pump::pool_authority(&base_mint).0;
2894        let ctx = AmmContext {
2895            global_config: &gc,
2896            fee_config: None,
2897            base_mint: &base_mint,
2898            pool_creator: &pool_creator,
2899            coin_creator: &bc.creator,
2900            base_reserve,
2901            quote_reserve,
2902            base_mint_supply: g.token_total_supply,
2903        };
2904        let expected = amm_math::buy_quote_input(&ctx, 1_000_000_000)
2905            .unwrap()
2906            .base_amount_out;
2907        assert_eq!(q.amount, expected);
2908    }
2909}