Skip to main content

pump_swap_sdk/
instruction.rs

1use crate::constants::{
2    EVENT_AUTHORITY, FEE_PROGRAM, GLOBAL_CONFIG, GLOBAL_VOLUME_ACCUMULATOR, PUMP_CREATOR_VAULT,
3    PUMP_SWAP_PROGRAM_ID, PUMPFUN_EVENT_AUTHORITY, PUMPFUN_PROGRAM, WRAPPED_SOL_MINT,
4};
5use crate::state::PoolInfo;
6use crate::util::{
7    calc_lp_mint_pda, calc_user_pool_token_account, fee_config_pda, find_coin_creator_vault_ata,
8    find_coin_creator_vault_authority, find_user_vol_accumulator, pick_buyback_fee_recipient,
9    pick_protocol_fee_recipient, pool_v2_pda, user_volume_accumulator_quote_ata,
10};
11use anyhow::Result;
12use bytemuck::{Pod, Zeroable};
13use solana_sdk::instruction::{AccountMeta, Instruction};
14use solana_sdk::pubkey::Pubkey;
15use solana_sdk::system_program;
16
17/// Common trait for serializing instructions to `Vec<u8>`.
18pub trait ToInstructionBytes {
19    fn to_vec(&self) -> Vec<u8>;
20}
21
22impl<T: Pod> ToInstructionBytes for T {
23    fn to_vec(&self) -> Vec<u8> {
24        bytemuck::bytes_of(self).to_vec()
25    }
26}
27
28/// pump-amm `buy` instruction args — buy an *exact* amount of base out by
29/// spending up to `max_quote_amount_in` of quote.
30///
31/// On-chain layout: `[discriminator(8) | base_amount_out(8) |
32/// max_quote_amount_in(8) | track_volume(1)]` = 25 bytes.
33#[derive(Clone, Copy, Debug, Default, PartialEq)]
34pub struct BuyInstruction {
35    pub base_amount_out: u64,
36    pub max_quote_amount_in: u64,
37    /// Tells the program whether to write this trade's quote-in into the
38    /// caller's `user_volume_accumulator` for cashback / incentive tracking.
39    pub track_volume: bool,
40}
41
42impl BuyInstruction {
43    pub const DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
44
45    pub fn new(base_amount_out: u64, max_quote_amount_in: u64, track_volume: bool) -> Self {
46        Self {
47            base_amount_out,
48            max_quote_amount_in,
49            track_volume,
50        }
51    }
52
53    pub fn to_vec(&self) -> Vec<u8> {
54        let mut buf = Vec::with_capacity(25);
55        buf.extend_from_slice(&Self::DISCRIMINATOR);
56        buf.extend_from_slice(&self.base_amount_out.to_le_bytes());
57        buf.extend_from_slice(&self.max_quote_amount_in.to_le_bytes());
58        buf.push(self.track_volume as u8);
59        buf
60    }
61}
62
63/// pump-amm `buy_exact_quote_in` instruction args — spend an *exact* amount
64/// of quote, getting at least `min_base_amount_out` of base.
65///
66/// On-chain layout: `[discriminator(8) | spendable_quote_in(8) |
67/// min_base_amount_out(8) | track_volume(1)]` = 25 bytes.
68#[derive(Clone, Copy, Debug, Default, PartialEq)]
69pub struct BuyExactQuoteInInstruction {
70    pub spendable_quote_in: u64,
71    pub min_base_amount_out: u64,
72    pub track_volume: bool,
73}
74
75impl BuyExactQuoteInInstruction {
76    pub const DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
77
78    pub fn new(spendable_quote_in: u64, min_base_amount_out: u64, track_volume: bool) -> Self {
79        Self {
80            spendable_quote_in,
81            min_base_amount_out,
82            track_volume,
83        }
84    }
85
86    pub fn to_vec(&self) -> Vec<u8> {
87        let mut buf = Vec::with_capacity(25);
88        buf.extend_from_slice(&Self::DISCRIMINATOR);
89        buf.extend_from_slice(&self.spendable_quote_in.to_le_bytes());
90        buf.extend_from_slice(&self.min_base_amount_out.to_le_bytes());
91        buf.push(self.track_volume as u8);
92        buf
93    }
94}
95
96#[repr(C)]
97#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
98pub struct SellInstruction {
99    pub discriminator: [u8; 8],
100    pub base_amount_in: u64,
101    pub min_quote_amount_out: u64,
102}
103
104impl SellInstruction {
105    pub fn new(base_amount_in: u64, min_quote_amount_out: u64) -> Self {
106        Self {
107            discriminator: [51, 230, 133, 164, 1, 127, 131, 173],
108            base_amount_in,
109            min_quote_amount_out,
110        }
111    }
112}
113
114/// pump-amm `deposit` instruction args — add liquidity to a pool, minting
115/// `lp_token_amount_out` LP tokens in exchange for up to `max_base_amount_in`
116/// base and `max_quote_amount_in` quote.
117#[repr(C)]
118#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
119pub struct DepositInstruction {
120    pub discriminator: [u8; 8],
121    pub lp_token_amount_out: u64,
122    pub max_base_amount_in: u64,
123    pub max_quote_amount_in: u64,
124}
125
126impl DepositInstruction {
127    pub fn new(
128        lp_token_amount_out: u64,
129        max_base_amount_in: u64,
130        max_quote_amount_in: u64,
131    ) -> Self {
132        Self {
133            discriminator: [242, 35, 198, 137, 82, 225, 242, 182],
134            lp_token_amount_out,
135            max_base_amount_in,
136            max_quote_amount_in,
137        }
138    }
139}
140
141/// pump-amm `claim_cashback` instruction args — takes no arguments; the
142/// `user_volume_accumulator` PDA tells the program how much to pay out.
143#[derive(Clone, Copy, Debug, Default, PartialEq)]
144pub struct ClaimCashbackInstruction;
145
146impl ClaimCashbackInstruction {
147    pub const DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
148
149    pub fn to_vec(&self) -> Vec<u8> {
150        Self::DISCRIMINATOR.to_vec()
151    }
152}
153
154#[repr(C)]
155#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable)]
156pub struct CreatePoolInstruction {
157    pub discriminator: [u8; 8],
158    pub index: u16,
159    pub base_amount_in: u64,
160    pub quote_amount_in: u64,
161    pub coin_creator: Pubkey,
162}
163
164impl CreatePoolInstruction {
165    pub fn new(base_amount_in: u64, quote_amount_in: u64, coin_creator: Pubkey) -> Self {
166        Self {
167            discriminator: [233, 146, 209, 142, 207, 104, 64, 188],
168            index: 0,
169            base_amount_in,
170            quote_amount_in,
171            coin_creator,
172        }
173    }
174
175    #[allow(clippy::wrong_self_convention)]
176    fn to_vec(&self) -> Vec<u8> {
177        let mut buf: Vec<u8> = Vec::with_capacity(size_of::<Self>());
178        buf.extend_from_slice(&self.discriminator);
179        buf.extend_from_slice(&self.index.to_le_bytes());
180        buf.extend_from_slice(&self.base_amount_in.to_le_bytes());
181        buf.extend_from_slice(&self.quote_amount_in.to_le_bytes());
182        buf.extend_from_slice(&self.coin_creator.to_bytes());
183        buf
184    }
185}
186
187#[repr(C)]
188#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
189pub struct WithdrawInstruction {
190    pub discriminator: [u8; 8],
191    pub lp_token_amount_in: u64,
192    pub min_base_amount_out: u64,
193    pub min_quote_amount_out: u64,
194}
195
196impl WithdrawInstruction {
197    pub fn new(
198        lp_token_amount_in: u64,
199        min_base_amount_out: u64,
200        min_quote_amount_out: u64,
201    ) -> Self {
202        Self {
203            discriminator: [183, 18, 70, 156, 148, 109, 161, 34],
204            lp_token_amount_in,
205            min_base_amount_out,
206            min_quote_amount_out,
207        }
208    }
209}
210
211/// Build a pump-amm `buy` instruction (exact-base-out) for the current
212/// canonical IDL plus cashback / buyback `remaining_accounts`.
213///
214/// `track_volume = true` makes the program write this trade's quote-in to
215/// the caller's `user_volume_accumulator` PDA, which is required for any
216/// future `claim_cashback` payout to include it.
217///
218/// All non-input accounts are derived internally from `pool_info` and `user`:
219/// the protocol-fee-recipient ATA, creator-vault PDAs, volume-accumulator
220/// PDAs, fee-config PDA, plus the trailing `remaining_accounts` required by
221/// the live program (cashback ATA when `pool_info.is_cashback_coin`,
222/// `pool-v2` PDA when `pool_info.coin_creator != default`, and a randomly-
223/// selected buyback fee recipient + its ATA).
224///
225/// `pool_info.base_token_program` and `pool_info.quote_token_program` decide
226/// whether each side uses `spl_token` or `spl_token_2022`.
227pub fn make_buy_instruction(
228    base_amount_out: u64,
229    max_quote_amount_in: u64,
230    track_volume: bool,
231    pool_info: &PoolInfo,
232    user: &Pubkey,
233    user_base_token_account: &Pubkey,
234    user_quote_token_account: &Pubkey,
235) -> Result<Instruction> {
236    let data = BuyInstruction::new(base_amount_out, max_quote_amount_in, track_volume).to_vec();
237    Ok(Instruction {
238        program_id: PUMP_SWAP_PROGRAM_ID,
239        accounts: swap_accounts(
240            pool_info,
241            user,
242            user_base_token_account,
243            user_quote_token_account,
244            SwapKind::Buy,
245        ),
246        data,
247    })
248}
249
250/// Build a pump-amm `buy_exact_quote_in` instruction (exact-quote-in) — the
251/// "spend exactly X SOL, get at least Y tokens" entry point most trader
252/// bots want. Account layout is identical to [`make_buy_instruction`];
253/// only the instruction data (discriminator + args) differs.
254pub fn make_buy_exact_quote_in_instruction(
255    spendable_quote_in: u64,
256    min_base_amount_out: u64,
257    track_volume: bool,
258    pool_info: &PoolInfo,
259    user: &Pubkey,
260    user_base_token_account: &Pubkey,
261    user_quote_token_account: &Pubkey,
262) -> Result<Instruction> {
263    let data =
264        BuyExactQuoteInInstruction::new(spendable_quote_in, min_base_amount_out, track_volume)
265            .to_vec();
266    Ok(Instruction {
267        program_id: PUMP_SWAP_PROGRAM_ID,
268        accounts: swap_accounts(
269            pool_info,
270            user,
271            user_base_token_account,
272            user_quote_token_account,
273            SwapKind::Buy,
274        ),
275        data,
276    })
277}
278
279#[derive(Clone, Copy)]
280enum SwapKind {
281    Buy,
282    Sell,
283}
284
285/// Account list shared by `buy` / `buy_exact_quote_in` (23 IDL + 1–4
286/// `remaining_accounts`) and `sell` (21 IDL + 1–3 `remaining_accounts`).
287fn swap_accounts(
288    pool_info: &PoolInfo,
289    user: &Pubkey,
290    user_base_token_account: &Pubkey,
291    user_quote_token_account: &Pubkey,
292    kind: SwapKind,
293) -> Vec<AccountMeta> {
294    let protocol_fee_recipient = pick_protocol_fee_recipient();
295    let protocol_fee_recipient_ta =
296        spl_associated_token_account::get_associated_token_address_with_program_id(
297            &protocol_fee_recipient,
298            &pool_info.quote_mint,
299            &pool_info.quote_token_program,
300        );
301    let creator_vault_authority = find_coin_creator_vault_authority(&pool_info.coin_creator);
302    let creator_vault_ata = find_coin_creator_vault_ata(
303        &creator_vault_authority,
304        &pool_info.quote_token_program,
305        &pool_info.quote_mint,
306    );
307    let fee_config = fee_config_pda();
308
309    let mut accounts = vec![
310        AccountMeta::new(pool_info.pool, false),
311        AccountMeta::new(*user, true),
312        AccountMeta::new_readonly(GLOBAL_CONFIG, false),
313        AccountMeta::new_readonly(pool_info.base_mint, false),
314        AccountMeta::new_readonly(pool_info.quote_mint, false),
315        AccountMeta::new(*user_base_token_account, false),
316        AccountMeta::new(*user_quote_token_account, false),
317        AccountMeta::new(pool_info.pool_base_token_account, false),
318        AccountMeta::new(pool_info.pool_quote_token_account, false),
319        AccountMeta::new_readonly(protocol_fee_recipient, false),
320        AccountMeta::new(protocol_fee_recipient_ta, false),
321        AccountMeta::new_readonly(pool_info.base_token_program, false),
322        AccountMeta::new_readonly(pool_info.quote_token_program, false),
323        AccountMeta::new_readonly(system_program::ID, false),
324        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
325        AccountMeta::new_readonly(EVENT_AUTHORITY, false),
326        AccountMeta::new_readonly(PUMP_SWAP_PROGRAM_ID, false),
327        AccountMeta::new(creator_vault_ata, false),
328        AccountMeta::new_readonly(creator_vault_authority, false),
329    ];
330
331    if matches!(kind, SwapKind::Buy) {
332        accounts.push(AccountMeta::new_readonly(GLOBAL_VOLUME_ACCUMULATOR, false));
333        accounts.push(AccountMeta::new(find_user_vol_accumulator(user), false));
334    }
335
336    accounts.push(AccountMeta::new_readonly(fee_config, false));
337    accounts.push(AccountMeta::new_readonly(FEE_PROGRAM, false));
338
339    let is_sell = matches!(kind, SwapKind::Sell);
340    append_swap_remaining_accounts(&mut accounts, pool_info, user, is_sell);
341    accounts
342}
343
344/// Build a pump-amm `sell` instruction for the current canonical IDL plus
345/// cashback / buyback `remaining_accounts`.
346///
347/// Sell omits the global/user volume accumulators that Buy carries (those
348/// move into `remaining_accounts` only when `pool_info.is_cashback_coin`).
349pub fn make_sell_instruction(
350    base_amount_in: u64,
351    min_quote_amount_out: u64,
352    pool_info: &PoolInfo,
353    user: &Pubkey,
354    user_base_token_account: &Pubkey,
355    user_quote_token_account: &Pubkey,
356) -> Result<Instruction> {
357    let data = SellInstruction::new(base_amount_in, min_quote_amount_out).to_vec();
358    Ok(Instruction {
359        program_id: PUMP_SWAP_PROGRAM_ID,
360        accounts: swap_accounts(
361            pool_info,
362            user,
363            user_base_token_account,
364            user_quote_token_account,
365            SwapKind::Sell,
366        ),
367        data,
368    })
369}
370
371/// Build a pump-amm `deposit` instruction — add liquidity, minting LP tokens.
372///
373/// The user must pre-create their LP-token ATA (`user_pool_token_account`)
374/// for `lp_mint = calc_lp_mint_pda(pool)`. Use
375/// [`PumpSwapClient::deposit_into_wsol_pool`](crate::client::PumpSwapClient::deposit_into_wsol_pool)
376/// for the full convenience flow including ATA creation.
377#[allow(clippy::too_many_arguments)]
378pub fn make_deposit_instruction(
379    lp_token_amount_out: u64,
380    max_base_amount_in: u64,
381    max_quote_amount_in: u64,
382    pool_info: &PoolInfo,
383    user: &Pubkey,
384    user_base_token_account: &Pubkey,
385    user_quote_token_account: &Pubkey,
386    user_pool_token_account: &Pubkey,
387) -> Result<Instruction> {
388    let data =
389        DepositInstruction::new(lp_token_amount_out, max_base_amount_in, max_quote_amount_in)
390            .to_vec();
391
392    let accounts = vec![
393        AccountMeta::new(pool_info.pool, false),
394        AccountMeta::new_readonly(GLOBAL_CONFIG, false),
395        AccountMeta::new_readonly(*user, true),
396        AccountMeta::new_readonly(pool_info.base_mint, false),
397        AccountMeta::new_readonly(pool_info.quote_mint, false),
398        AccountMeta::new(pool_info.lp_mint, false),
399        AccountMeta::new(*user_base_token_account, false),
400        AccountMeta::new(*user_quote_token_account, false),
401        AccountMeta::new(*user_pool_token_account, false),
402        AccountMeta::new(pool_info.pool_base_token_account, false),
403        AccountMeta::new(pool_info.pool_quote_token_account, false),
404        AccountMeta::new_readonly(spl_token::ID, false),
405        AccountMeta::new_readonly(spl_token_2022::ID, false),
406        AccountMeta::new_readonly(EVENT_AUTHORITY, false),
407        AccountMeta::new_readonly(PUMP_SWAP_PROGRAM_ID, false),
408    ];
409
410    Ok(Instruction {
411        program_id: PUMP_SWAP_PROGRAM_ID,
412        accounts,
413        data,
414    })
415}
416
417/// Build a pump-amm `claim_cashback` instruction — pays the caller their
418/// accrued cashback from the `user_volume_accumulator` PDA, denominated in
419/// the given `quote_mint` (typically WSOL).
420///
421/// The caller's `user_volume_accumulator` and its quote-mint ATA must
422/// already exist — they're created lazily by the `buy` / `buy_exact_quote_in`
423/// flow when `track_volume = true`.
424pub fn make_claim_cashback_instruction(
425    user: &Pubkey,
426    quote_mint: &Pubkey,
427    quote_token_program: &Pubkey,
428) -> Result<Instruction> {
429    let user_volume_accumulator = find_user_vol_accumulator(user);
430    let user_volume_accumulator_quote_ta =
431        spl_associated_token_account::get_associated_token_address_with_program_id(
432            &user_volume_accumulator,
433            quote_mint,
434            quote_token_program,
435        );
436    let user_quote_ta = spl_associated_token_account::get_associated_token_address_with_program_id(
437        user,
438        quote_mint,
439        quote_token_program,
440    );
441
442    // Per IDL, `user` is mut but NOT marked signer — the program looks the
443    // user up via PDA seeds, so anyone can trigger a cashback claim that
444    // pays out to that user's wsol ATA.
445    let accounts = vec![
446        AccountMeta::new(*user, false),
447        AccountMeta::new(user_volume_accumulator, false),
448        AccountMeta::new_readonly(*quote_mint, false),
449        AccountMeta::new_readonly(*quote_token_program, false),
450        AccountMeta::new(user_volume_accumulator_quote_ta, false),
451        AccountMeta::new(user_quote_ta, false),
452        AccountMeta::new_readonly(system_program::ID, false),
453        AccountMeta::new_readonly(EVENT_AUTHORITY, false),
454        AccountMeta::new_readonly(PUMP_SWAP_PROGRAM_ID, false),
455    ];
456
457    Ok(Instruction {
458        program_id: PUMP_SWAP_PROGRAM_ID,
459        accounts,
460        data: ClaimCashbackInstruction.to_vec(),
461    })
462}
463
464/// Append the live program's `remaining_accounts` for Buy / Sell. Mirrors
465/// the pump-fun npm SDK `offlinePumpAmm` logic.
466///
467/// Order:
468/// 1. (if cashback) `user_volume_accumulator_quote_ata` (writable). Sell also
469///    pushes `user_volume_accumulator` (writable) immediately after.
470/// 2. (if `pool.coin_creator != default`) `pool_v2_pda(base_mint)` (read-only).
471/// 3. Always: `buyback_fee_recipient` (read-only),
472///    `buyback_fee_recipient_quote_ata` (writable).
473fn append_swap_remaining_accounts(
474    accounts: &mut Vec<AccountMeta>,
475    pool_info: &PoolInfo,
476    user: &Pubkey,
477    is_sell: bool,
478) {
479    if pool_info.is_cashback_coin {
480        let cashback_ata = user_volume_accumulator_quote_ata(
481            user,
482            &pool_info.quote_mint,
483            &pool_info.quote_token_program,
484        );
485        accounts.push(AccountMeta::new(cashback_ata, false));
486        if is_sell {
487            accounts.push(AccountMeta::new(find_user_vol_accumulator(user), false));
488        }
489    }
490
491    if pool_info.coin_creator != Pubkey::default() {
492        accounts.push(AccountMeta::new_readonly(
493            pool_v2_pda(&pool_info.base_mint),
494            false,
495        ));
496    }
497
498    let buyback_recipient = pick_buyback_fee_recipient();
499    let buyback_recipient_ta =
500        spl_associated_token_account::get_associated_token_address_with_program_id(
501            &buyback_recipient,
502            &pool_info.quote_mint,
503            &pool_info.quote_token_program,
504        );
505    accounts.push(AccountMeta::new_readonly(buyback_recipient, false));
506    accounts.push(AccountMeta::new(buyback_recipient_ta, false));
507}
508
509/// Build a pump-amm `create_pool` instruction for a WSOL-quoted pool.
510#[allow(clippy::too_many_arguments)]
511pub fn create_pool_instruction(
512    base_amount_in: u64,
513    quote_amount_in: u64,
514    pool: &Pubkey,
515    creator: &Pubkey,
516    coin_creator: &Pubkey,
517    base_mint: &Pubkey,
518    quote_mint: &Pubkey,
519    user_base_token_account: &Pubkey,
520    user_quote_token_account: &Pubkey,
521    pool_base_token_account: &Pubkey,
522    pool_quote_token_account: &Pubkey,
523) -> Result<Instruction> {
524    let data = CreatePoolInstruction::new(base_amount_in, quote_amount_in, *coin_creator).to_vec();
525
526    let lp_mint = calc_lp_mint_pda(pool).0;
527
528    let accounts = vec![
529        AccountMeta::new(*pool, false),
530        AccountMeta::new_readonly(GLOBAL_CONFIG, false),
531        AccountMeta::new(*creator, true),
532        AccountMeta::new_readonly(*base_mint, false),
533        AccountMeta::new_readonly(*quote_mint, false),
534        AccountMeta::new(lp_mint, false),
535        AccountMeta::new(*user_base_token_account, false),
536        AccountMeta::new(*user_quote_token_account, false),
537        AccountMeta::new(calc_user_pool_token_account(creator, &lp_mint).0, false),
538        AccountMeta::new(*pool_base_token_account, false),
539        AccountMeta::new(*pool_quote_token_account, false),
540        AccountMeta::new_readonly(system_program::ID, false),
541        AccountMeta::new_readonly(spl_token_2022::ID, false),
542        AccountMeta::new_readonly(spl_token::ID, false),
543        AccountMeta::new_readonly(spl_token::ID, false),
544        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
545        AccountMeta::new_readonly(EVENT_AUTHORITY, false),
546        AccountMeta::new_readonly(PUMP_SWAP_PROGRAM_ID, false),
547    ];
548
549    Ok(Instruction {
550        program_id: PUMP_SWAP_PROGRAM_ID,
551        accounts,
552        data,
553    })
554}
555
556/// Build a pump-amm `withdraw` instruction for a WSOL-quoted pool.
557#[allow(clippy::too_many_arguments)]
558pub fn withdraw_instruction(
559    pool: &Pubkey,
560    user: &Pubkey,
561    base_mint: &Pubkey,
562    base_ata: &Pubkey,
563    quote_ata: &Pubkey,
564    lp_token_amount_in: u64,
565    min_base_amount_out: u64,
566    min_quote_amount_out: u64,
567) -> Result<Instruction> {
568    let data = WithdrawInstruction::new(
569        lp_token_amount_in,
570        min_base_amount_out,
571        min_quote_amount_out,
572    )
573    .to_vec();
574
575    let lp_mint = calc_lp_mint_pda(pool).0;
576    let user_pool_token_account = calc_user_pool_token_account(user, &lp_mint).0;
577
578    let pool_base_ata = spl_associated_token_account::get_associated_token_address(pool, base_mint);
579    let pool_quote_ata =
580        spl_associated_token_account::get_associated_token_address(pool, &WRAPPED_SOL_MINT);
581
582    let accounts = vec![
583        AccountMeta::new(*pool, false),
584        AccountMeta::new_readonly(GLOBAL_CONFIG, false),
585        AccountMeta::new_readonly(*user, true),
586        AccountMeta::new_readonly(*base_mint, false),
587        AccountMeta::new_readonly(WRAPPED_SOL_MINT, false),
588        AccountMeta::new(lp_mint, false),
589        AccountMeta::new(*base_ata, false),
590        AccountMeta::new(*quote_ata, false),
591        AccountMeta::new(user_pool_token_account, false),
592        AccountMeta::new(pool_base_ata, false),
593        AccountMeta::new(pool_quote_ata, false),
594        AccountMeta::new_readonly(spl_token::ID, false),
595        AccountMeta::new_readonly(spl_token_2022::ID, false),
596        AccountMeta::new_readonly(EVENT_AUTHORITY, false),
597        AccountMeta::new_readonly(PUMP_SWAP_PROGRAM_ID, false),
598    ];
599
600    Ok(Instruction {
601        program_id: PUMP_SWAP_PROGRAM_ID,
602        accounts,
603        data,
604    })
605}
606
607/// Build the pump-amm instruction that moves accrued creator fees from the
608/// coin-creator vault back into the pump.fun program's creator vault, so they
609/// can subsequently be distributed via [`distribute_creator_fees_instruction`].
610pub fn transfer_creator_fees_to_pump_instruction(coin_creator: &Pubkey) -> Result<Instruction> {
611    let coin_creator_vault_authority = find_coin_creator_vault_authority(coin_creator);
612    let coin_creator_vault_ata = find_coin_creator_vault_ata(
613        &coin_creator_vault_authority,
614        &spl_token::ID,
615        &WRAPPED_SOL_MINT,
616    );
617
618    let accounts = vec![
619        AccountMeta::new_readonly(WRAPPED_SOL_MINT, false),
620        AccountMeta::new_readonly(spl_token::ID, false),
621        AccountMeta::new_readonly(system_program::ID, false),
622        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
623        AccountMeta::new_readonly(*coin_creator, false),
624        AccountMeta::new(coin_creator_vault_authority, false),
625        AccountMeta::new(coin_creator_vault_ata, false),
626        AccountMeta::new(PUMP_CREATOR_VAULT, false),
627        AccountMeta::new_readonly(EVENT_AUTHORITY, false),
628        AccountMeta::new_readonly(PUMP_SWAP_PROGRAM_ID, false),
629    ];
630
631    Ok(Instruction {
632        program_id: PUMP_SWAP_PROGRAM_ID,
633        accounts,
634        data: vec![139, 52, 134, 85, 228, 229, 108, 241],
635    })
636}
637
638/// pump.fun `distribute_creator_fees` instruction — pays out fees from the pump
639/// program's creator vault to the admin/sharing destinations.
640pub fn distribute_creator_fees_instruction(
641    mint: &Pubkey,
642    bonding_curve: &Pubkey,
643    sharing_config: &Pubkey,
644    admin_account: &Pubkey,
645) -> Result<Instruction> {
646    let accounts = vec![
647        AccountMeta::new_readonly(*mint, false),
648        AccountMeta::new_readonly(*bonding_curve, false),
649        AccountMeta::new_readonly(*sharing_config, false),
650        AccountMeta::new(PUMP_CREATOR_VAULT, false),
651        AccountMeta::new_readonly(system_program::ID, false),
652        AccountMeta::new_readonly(PUMPFUN_EVENT_AUTHORITY, false),
653        AccountMeta::new_readonly(PUMPFUN_PROGRAM, false),
654        AccountMeta::new(*admin_account, true),
655    ];
656
657    Ok(Instruction {
658        program_id: PUMPFUN_PROGRAM,
659        accounts,
660        data: vec![165, 114, 103, 0, 121, 206, 247, 81],
661    })
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667
668    fn pk(byte: u8) -> Pubkey {
669        Pubkey::new_from_array([byte; 32])
670    }
671
672    fn pool_info(coin_creator: Pubkey, is_cashback_coin: bool) -> PoolInfo {
673        PoolInfo {
674            pool: pk(1),
675            base_mint: pk(2),
676            quote_mint: WRAPPED_SOL_MINT,
677            lp_mint: pk(3),
678            pool_base_token_account: pk(4),
679            pool_quote_token_account: pk(5),
680            creator: pk(6),
681            coin_creator,
682            is_cashback_coin,
683            base_token_program: spl_token::ID,
684            quote_token_program: spl_token::ID,
685        }
686    }
687
688    #[test]
689    fn swap_instruction_account_counts_include_required_remaining_accounts() {
690        let user = pk(7);
691        let user_base_ata = pk(8);
692        let user_quote_ata = pk(9);
693
694        let classic = pool_info(Pubkey::default(), false);
695        assert_eq!(
696            make_buy_instruction(1, 2, true, &classic, &user, &user_base_ata, &user_quote_ata)
697                .unwrap()
698                .accounts
699                .len(),
700            25
701        );
702        assert_eq!(
703            make_buy_exact_quote_in_instruction(
704                1,
705                2,
706                true,
707                &classic,
708                &user,
709                &user_base_ata,
710                &user_quote_ata,
711            )
712            .unwrap()
713            .accounts
714            .len(),
715            25
716        );
717        assert_eq!(
718            make_sell_instruction(1, 2, &classic, &user, &user_base_ata, &user_quote_ata)
719                .unwrap()
720                .accounts
721                .len(),
722            23
723        );
724
725        let creator_pool = pool_info(pk(10), false);
726        assert_eq!(
727            make_buy_instruction(
728                1,
729                2,
730                true,
731                &creator_pool,
732                &user,
733                &user_base_ata,
734                &user_quote_ata
735            )
736            .unwrap()
737            .accounts
738            .len(),
739            26
740        );
741        assert_eq!(
742            make_sell_instruction(1, 2, &creator_pool, &user, &user_base_ata, &user_quote_ata)
743                .unwrap()
744                .accounts
745                .len(),
746            24
747        );
748
749        let cashback_creator_pool = pool_info(pk(10), true);
750        assert_eq!(
751            make_buy_instruction(
752                1,
753                2,
754                true,
755                &cashback_creator_pool,
756                &user,
757                &user_base_ata,
758                &user_quote_ata,
759            )
760            .unwrap()
761            .accounts
762            .len(),
763            27
764        );
765        assert_eq!(
766            make_sell_instruction(
767                1,
768                2,
769                &cashback_creator_pool,
770                &user,
771                &user_base_ata,
772                &user_quote_ata,
773            )
774            .unwrap()
775            .accounts
776            .len(),
777            26
778        );
779    }
780
781    #[test]
782    fn buy_instruction_data_layout_matches_idl() {
783        let bytes = BuyInstruction::new(50_000_000, 564_953_706, true).to_vec();
784        assert_eq!(bytes.len(), 25, "data must be exactly 25 bytes");
785        assert_eq!(&bytes[0..8], &BuyInstruction::DISCRIMINATOR);
786        assert_eq!(
787            u64::from_le_bytes(bytes[8..16].try_into().unwrap()),
788            50_000_000
789        );
790        assert_eq!(
791            u64::from_le_bytes(bytes[16..24].try_into().unwrap()),
792            564_953_706
793        );
794        assert_eq!(bytes[24], 1);
795
796        let bytes_off = BuyInstruction::new(1, 2, false).to_vec();
797        assert_eq!(bytes_off[24], 0);
798    }
799
800    #[test]
801    fn buy_exact_quote_in_data_layout_matches_idl() {
802        // Reproduces the exact on-chain instruction data observed in tx
803        // 5EogpJNF...WoqH (spend 0.05 SOL, min 564_953_706 base out, track on).
804        let bytes = BuyExactQuoteInInstruction::new(50_000_000, 564_953_706, true).to_vec();
805        assert_eq!(bytes.len(), 25);
806        assert_eq!(&bytes[0..8], &BuyExactQuoteInInstruction::DISCRIMINATOR);
807        assert_eq!(
808            u64::from_le_bytes(bytes[8..16].try_into().unwrap()),
809            50_000_000
810        );
811        assert_eq!(
812            u64::from_le_bytes(bytes[16..24].try_into().unwrap()),
813            564_953_706
814        );
815        assert_eq!(bytes[24], 1);
816    }
817
818    #[test]
819    fn deposit_instruction_account_count() {
820        let pool = pool_info(Pubkey::default(), false);
821        let user = pk(7);
822        assert_eq!(
823            make_deposit_instruction(1, 2, 3, &pool, &user, &pk(8), &pk(9), &pk(10))
824                .unwrap()
825                .accounts
826                .len(),
827            15
828        );
829    }
830
831    #[test]
832    fn claim_cashback_instruction_account_count() {
833        let user = pk(7);
834        assert_eq!(
835            make_claim_cashback_instruction(&user, &WRAPPED_SOL_MINT, &spl_token::ID)
836                .unwrap()
837                .accounts
838                .len(),
839            9
840        );
841    }
842}