#![cfg(feature = "local-validator")]
mod common;
use anchor_spl::associated_token::spl_associated_token_account::instruction::create_associated_token_account_idempotent;
use anchor_spl::token::spl_token;
use solana_sdk::compute_budget::ComputeBudgetInstruction;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{read_keypair_file, Keypair, Signer};
use pump_rust_client::{constants, pda, PumpSdk};
use common::fixtures::{
USDC_QUOTE_MINT, USDC_QUOTE_MINT_AUTHORITY, USDC_QUOTE_MINT_AUTHORITY_KEYPAIR_PATH,
};
use common::{
airdrop_blocking, load_alt, make_client, make_rpc, send_v0_tx, token_balance,
DEFAULT_USER_LAMPORTS,
};
const QUOTE_MINT_AMOUNT: u64 = 1_000_000_000_000;
const BUY_BASE_AMOUNT: u64 = 100_000_000_000;
const MAX_QUOTE_COST: u64 = 500_000_000_000;
#[tokio::test]
#[ignore = "requires `cargo run --features local-validator --bin local-validator` running"]
async fn v2_custom_quote_mint_create_buy_sell() {
let rpc = make_rpc();
let client = make_client();
let sdk = PumpSdk::new();
let global = client.fetch_global().await.expect(
"fetch_global on local validator — did you run clone_devnet_accounts and start the validator?",
);
let mut recipient_owners: Vec<Pubkey> = Vec::new();
if global.fee_recipient != Pubkey::default() {
recipient_owners.push(global.fee_recipient);
}
recipient_owners.extend(
global
.fee_recipients
.iter()
.copied()
.filter(|p| *p != Pubkey::default()),
);
recipient_owners.extend(
global
.buyback_fee_recipients
.iter()
.copied()
.filter(|p| *p != Pubkey::default()),
);
recipient_owners.sort();
recipient_owners.dedup();
assert!(
!recipient_owners.is_empty(),
"Global has no fee_recipient / buyback_fee_recipient entries set"
);
let base_token_program = constants::SPL_TOKEN_2022_PROGRAM_ID;
let quote_token_program = constants::SPL_TOKEN_PROGRAM_ID;
let quote_mint_authority = read_keypair_file(USDC_QUOTE_MINT_AUTHORITY_KEYPAIR_PATH)
.expect("read USDC_QUOTE_MINT_AUTHORITY keypair");
assert_eq!(
quote_mint_authority.pubkey(),
USDC_QUOTE_MINT_AUTHORITY,
"USDC_QUOTE_MINT_AUTHORITY constant must match the on-disk keypair file"
);
let user = Keypair::new();
let mint = Keypair::new();
println!(
"[v2-quote] user = {} base_mint = {} quote_mint = {USDC_QUOTE_MINT}",
user.pubkey(),
mint.pubkey()
);
airdrop_blocking(&rpc, &user.pubkey(), DEFAULT_USER_LAMPORTS).await;
let alt = load_alt(&rpc, constants::DEVNET_ALT).await;
let user_quote_ata =
pda::associated_token(&user.pubkey(), "e_token_program, &USDC_QUOTE_MINT).0;
let user_base_ata =
pda::associated_token(&user.pubkey(), &base_token_program, &mint.pubkey()).0;
for chunk in recipient_owners.chunks(3) {
let mut ixs = vec![ComputeBudgetInstruction::set_compute_unit_limit(200_000)];
for owner in chunk {
ixs.push(create_associated_token_account_idempotent(
&user.pubkey(),
owner,
&USDC_QUOTE_MINT,
"e_token_program,
));
}
send_v0_tx(&rpc, &ixs, &user, &[&user], &alt).await;
}
let user_setup_ixs = vec![
ComputeBudgetInstruction::set_compute_unit_limit(200_000),
create_associated_token_account_idempotent(
&user.pubkey(),
&user.pubkey(),
&USDC_QUOTE_MINT,
"e_token_program,
),
spl_token::instruction::mint_to(
"e_token_program,
&USDC_QUOTE_MINT,
&user_quote_ata,
&USDC_QUOTE_MINT_AUTHORITY,
&[],
QUOTE_MINT_AMOUNT,
)
.expect("mint_to ix"),
];
send_v0_tx(
&rpc,
&user_setup_ixs,
&user,
&[&user, "e_mint_authority],
&alt,
)
.await;
let user_quote_after_setup = token_balance(&rpc, &user_quote_ata).await;
assert_eq!(
user_quote_after_setup, QUOTE_MINT_AMOUNT,
"mint_to should fund the user's quote ATA exactly"
);
let create_ixs = vec![
ComputeBudgetInstruction::set_compute_unit_limit(400_000),
sdk.create_v2_instruction(
mint.pubkey(),
user.pubkey(),
"TestQuote",
"TQ",
"https://example.com/tq.json",
user.pubkey(),
USDC_QUOTE_MINT,
false,
false,
),
];
let create_sig = send_v0_tx(&rpc, &create_ixs, &user, &[&user, &mint], &alt).await;
println!("[v2-quote] create sig: {create_sig} Mint created {{mint.pubkey()}}");
let bc_after_create = client
.fetch_bonding_curve(&mint.pubkey())
.await
.expect("fetch bonding_curve after create_v2");
assert_eq!(
bc_after_create.quote_mint, USDC_QUOTE_MINT,
"create_v2 must persist the custom quote mint on the bonding curve"
);
let mut buy_ixs = vec![ComputeBudgetInstruction::set_compute_unit_limit(400_000)];
buy_ixs.extend(
sdk.buy_v2_instructions(
&global,
&bc_after_create,
mint.pubkey(),
quote_token_program,
user.pubkey(),
BUY_BASE_AMOUNT,
MAX_QUOTE_COST,
)
.expect("buy_v2_instructions"),
);
let buy_sig = send_v0_tx(&rpc, &buy_ixs, &user, &[&user], &alt).await;
println!("[v2-quote] buy sig: {buy_sig}");
let bc_after_buy = client
.fetch_bonding_curve(&mint.pubkey())
.await
.expect("fetch bonding_curve after v2 buy");
let user_base_after_buy = token_balance(&rpc, &user_base_ata).await;
let user_quote_after_buy = token_balance(&rpc, &user_quote_ata).await;
assert_eq!(
user_base_after_buy, BUY_BASE_AMOUNT,
"buy_v2 should credit exactly BUY_BASE_AMOUNT base units"
);
assert!(
user_quote_after_buy < user_quote_after_setup,
"buy_v2 should debit some quote balance (pre={user_quote_after_setup} post={user_quote_after_buy})"
);
assert!(
bc_after_buy.real_quote_reserves > bc_after_create.real_quote_reserves,
"buy_v2 should grow real_quote_reserves (pre={} post={})",
bc_after_create.real_quote_reserves,
bc_after_buy.real_quote_reserves,
);
let sell_amount = user_base_after_buy;
let mut sell_ixs = vec![ComputeBudgetInstruction::set_compute_unit_limit(400_000)];
sell_ixs.extend(
sdk.sell_v2_instructions(
&global,
&bc_after_buy,
mint.pubkey(),
quote_token_program,
user.pubkey(),
sell_amount,
1,
)
.expect("sell_v2_instructions"),
);
let sell_sig = send_v0_tx(&rpc, &sell_ixs, &user, &[&user], &alt).await;
println!("[v2-quote] sell sig: {sell_sig}");
let bc_after_sell = client
.fetch_bonding_curve(&mint.pubkey())
.await
.expect("fetch bonding_curve after v2 sell");
let user_base_after_sell = token_balance(&rpc, &user_base_ata).await;
let user_quote_after_sell = token_balance(&rpc, &user_quote_ata).await;
assert_eq!(
user_base_after_sell, 0,
"sell_v2 should drain the user's base balance"
);
assert!(
user_quote_after_sell > user_quote_after_buy,
"sell_v2 should credit quote proceeds (pre={user_quote_after_buy} post={user_quote_after_sell})"
);
assert!(
bc_after_sell.real_quote_reserves < bc_after_buy.real_quote_reserves,
"sell_v2 should shrink real_quote_reserves (pre={} post={})",
bc_after_buy.real_quote_reserves,
bc_after_sell.real_quote_reserves,
);
}