v4.0.11 switched the fast-submit (`wait_tx_confirmed = false`) result loop from `wait_for_all_submitted` to `wait_for_first_submitted`. The new function returns as soon as one SWQOS route's HTTP submit completes and drains whatever signatures are in `ResultCollector.results` at that moment — slower routes' signatures arrive after the caller has returned. This is correct for fire-and-forget low-latency submits, but breaks callers that do their own on-chain confirmation against a pinned durable nonce. Only one of the submitted txs can land (nonce is consumed by whichever lands first), but the caller has no way of knowing in advance which route it will be. Their post-submit logic feeds every returned signature to `getSignatureStatuses` to find the landed one — with only the fastest HTTP responder returned (often not the one that lands), the landed tx isn't in the polled set. Adds an opt-in `SwapParams.wait_for_all_submits: bool` (default false, plumbed through `TradeBuyParams` / `TradeSellParams`). When true and `wait_tx_confirmed = false`, `execute_parallel` uses `wait_for_all_submitted` (still in the codebase, was `#[allow(dead_code)]`) so every submitted signature is returned. Zero behaviour change for current users.
1298 lines
49 KiB
Rust
1298 lines
49 KiB
Rust
//! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)).
|
|
//!
|
|
//! The SDK selects the legacy or V2 on-chain layout from `PumpFunParams.quote_mint`.
|
|
//! `Pubkey::default()` and the Solscan SOL sentinel keep the smaller legacy SOL layout;
|
|
//! explicit WSOL or USDC uses the V2 27/26-account unified metas.
|
|
//! Default (`false`) keeps the smaller legacy SOL-paired instruction layout for latency.
|
|
|
|
use crate::{
|
|
common::bonding_curve::BondingCurveAccount,
|
|
common::spl_token::close_account,
|
|
constants::{trade::trade::DEFAULT_SLIPPAGE, TOKEN_PROGRAM_2022},
|
|
trading::core::{
|
|
params::{PumpFunParams, SwapParams},
|
|
traits::InstructionBuilder,
|
|
},
|
|
};
|
|
use crate::{
|
|
instruction::{
|
|
token_account_setup::{
|
|
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
|
push_create_user_token_account,
|
|
},
|
|
utils::pumpfun::{
|
|
accounts, get_bonding_curve_pda, get_user_volume_accumulator_pda,
|
|
global_constants::{self},
|
|
pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix_with_fee_sharing,
|
|
},
|
|
},
|
|
utils::calc::{
|
|
common::{calculate_with_slippage_buy, calculate_with_slippage_sell},
|
|
pumpfun::{get_buy_token_amount_from_sol_amount, get_sell_sol_amount_from_token_amount},
|
|
},
|
|
};
|
|
use anyhow::{anyhow, Result};
|
|
use solana_sdk::{
|
|
instruction::{AccountMeta, Instruction},
|
|
pubkey::Pubkey,
|
|
signer::Signer,
|
|
};
|
|
|
|
#[inline]
|
|
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey {
|
|
let tp = protocol_params.token_program;
|
|
if tp == Pubkey::default() {
|
|
TOKEN_PROGRAM_2022
|
|
} else {
|
|
tp
|
|
}
|
|
}
|
|
|
|
/// Resolve quote mint and its token program from PumpFunParams.
|
|
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` means legacy SOL-paired; use WSOL mint when a
|
|
/// downstream V2 helper needs a concrete SPL quote mint.
|
|
#[inline]
|
|
fn effective_quote_mint_and_token_program(protocol_params: &PumpFunParams) -> (Pubkey, Pubkey) {
|
|
let curve_quote_mint = protocol_params.bonding_curve.effective_quote_mint();
|
|
let quote_mint = if protocol_params.quote_mint != Pubkey::default() {
|
|
BondingCurveAccount::normalize_quote_mint(protocol_params.quote_mint)
|
|
} else if curve_quote_mint != Pubkey::default() {
|
|
curve_quote_mint
|
|
} else {
|
|
crate::constants::WSOL_TOKEN_ACCOUNT
|
|
};
|
|
(quote_mint, crate::constants::TOKEN_PROGRAM)
|
|
}
|
|
|
|
#[inline]
|
|
fn is_sol_quote_mint(quote_mint: &Pubkey) -> bool {
|
|
*quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
|
|| *quote_mint == crate::constants::SOL_TOKEN_ACCOUNT
|
|
}
|
|
|
|
#[inline]
|
|
fn validate_v2_buy_quote_mint(input_mint: &Pubkey, quote_mint: &Pubkey) -> Result<()> {
|
|
if is_sol_quote_mint(quote_mint) {
|
|
if *input_mint == crate::constants::SOL_TOKEN_ACCOUNT
|
|
|| *input_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
|
{
|
|
return Ok(());
|
|
}
|
|
} else if input_mint == quote_mint {
|
|
return Ok(());
|
|
}
|
|
Err(anyhow!(
|
|
"PumpFun V2 buy input_mint {} does not match quote_mint {}; USDC quote pools must be bought with USDC, not SOL",
|
|
input_mint,
|
|
quote_mint
|
|
))
|
|
}
|
|
|
|
#[inline]
|
|
fn validate_v2_sell_quote_mint(output_mint: &Pubkey, quote_mint: &Pubkey) -> Result<()> {
|
|
if is_sol_quote_mint(quote_mint) {
|
|
if *output_mint == crate::constants::SOL_TOKEN_ACCOUNT
|
|
|| *output_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
|
{
|
|
return Ok(());
|
|
}
|
|
} else if output_mint == quote_mint {
|
|
return Ok(());
|
|
}
|
|
Err(anyhow!(
|
|
"PumpFun V2 sell output_mint {} does not match quote_mint {}; USDC quote pools settle to USDC, not SOL",
|
|
output_mint,
|
|
quote_mint
|
|
))
|
|
}
|
|
|
|
pub struct PumpFunInstructionBuilder;
|
|
|
|
#[async_trait::async_trait]
|
|
impl InstructionBuilder for PumpFunInstructionBuilder {
|
|
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
build_buy(params)
|
|
}
|
|
|
|
async fn build_sell_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
build_sell(params)
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn should_use_v2_layout(params: &SwapParams) -> Result<bool> {
|
|
let protocol_params = params
|
|
.protocol_params
|
|
.as_any()
|
|
.downcast_ref::<PumpFunParams>()
|
|
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
|
let (quote_mint, _) = effective_quote_mint_and_token_program(protocol_params);
|
|
let explicit_v2_quote = protocol_params.quote_mint != Pubkey::default()
|
|
&& protocol_params.quote_mint != crate::constants::SOL_TOKEN_ACCOUNT;
|
|
Ok(explicit_v2_quote || !is_sol_quote_mint("e_mint))
|
|
}
|
|
|
|
#[inline]
|
|
fn build_buy(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
if should_use_v2_layout(params)? {
|
|
build_buy_unified(params)
|
|
} else {
|
|
build_buy_legacy(params)
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn build_sell(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
if should_use_v2_layout(params)? {
|
|
build_sell_unified(params)
|
|
} else {
|
|
build_sell_legacy(params)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// V1 (default) — 18 metas, no quote_mint ATA create
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn build_buy_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
use crate::instruction::pumpfun_ix_data::{
|
|
encode_pumpfun_buy_exact_quote_in_ix_data, encode_pumpfun_buy_ix_data, PumpFunIxVersion,
|
|
};
|
|
use crate::instruction::utils::pumpfun::{
|
|
get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random,
|
|
};
|
|
|
|
let protocol_params = params
|
|
.protocol_params
|
|
.as_any()
|
|
.downcast_ref::<PumpFunParams>()
|
|
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
|
|
|
let lamports_in = params.input_amount.unwrap_or(0);
|
|
if lamports_in == 0 {
|
|
return Err(anyhow!("Amount cannot be zero"));
|
|
}
|
|
|
|
let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE);
|
|
|
|
let bonding_curve = &protocol_params.bonding_curve;
|
|
let creator = protocol_params.effective_creator_for_trade();
|
|
let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing(
|
|
&creator,
|
|
protocol_params.creator_vault,
|
|
¶ms.output_mint,
|
|
protocol_params.fee_sharing_creator_vault_if_active,
|
|
)
|
|
.ok_or_else(|| anyhow!("creator_vault PDA derivation failed (creator={})", creator))?;
|
|
|
|
let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| {
|
|
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
|
|
})?;
|
|
|
|
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
|
let token_program = effective_pump_mint_token_program(protocol_params);
|
|
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
|
crate::constants::TOKEN_PROGRAM_2022_META
|
|
} else {
|
|
crate::constants::TOKEN_PROGRAM_META
|
|
};
|
|
|
|
let associated_bonding_curve =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&bonding_curve_addr,
|
|
¶ms.output_mint,
|
|
&token_program,
|
|
);
|
|
|
|
let user_token_account =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
|
¶ms.payer.pubkey(),
|
|
¶ms.output_mint,
|
|
&token_program,
|
|
params.open_seed_optimize,
|
|
);
|
|
|
|
let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
|
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
|
|
|
let mut instructions = Vec::with_capacity(2);
|
|
|
|
if params.create_output_mint_ata {
|
|
instructions.extend(
|
|
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
|
¶ms.payer.pubkey(),
|
|
¶ms.payer.pubkey(),
|
|
¶ms.output_mint,
|
|
&token_program,
|
|
params.open_seed_optimize,
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Legacy buy path ──
|
|
let track_volume_val = if bonding_curve.is_cashback_coin { 1u8 } else { 0u8 };
|
|
let ix_version = PumpFunIxVersion::Legacy { track_volume: track_volume_val };
|
|
let buy_data = if let Some(token_amount) = params.fixed_output_amount {
|
|
encode_pumpfun_buy_ix_data(token_amount, lamports_in, ix_version)
|
|
} else {
|
|
let buy_token_amount = get_buy_token_amount_from_sol_amount(
|
|
bonding_curve.virtual_token_reserves as u128,
|
|
bonding_curve.virtual_sol_reserves as u128,
|
|
bonding_curve.real_token_reserves as u128,
|
|
creator,
|
|
lamports_in,
|
|
);
|
|
if params.use_exact_sol_amount.unwrap_or(true) {
|
|
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
|
encode_pumpfun_buy_exact_quote_in_ix_data(lamports_in, min_tokens_out, ix_version)
|
|
} else {
|
|
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
|
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, ix_version)
|
|
}
|
|
};
|
|
|
|
let fee_recipient_meta =
|
|
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
|
|
|
let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.output_mint).ok_or_else(|| {
|
|
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint)
|
|
})?;
|
|
let mut metas: Vec<AccountMeta> = vec![
|
|
global_constants::GLOBAL_ACCOUNT_META,
|
|
fee_recipient_meta,
|
|
AccountMeta::new_readonly(params.output_mint, false),
|
|
AccountMeta::new(bonding_curve_addr, false),
|
|
AccountMeta::new(associated_bonding_curve, false),
|
|
AccountMeta::new(user_token_account, false),
|
|
AccountMeta::new(params.payer.pubkey(), true),
|
|
crate::constants::SYSTEM_PROGRAM_META,
|
|
token_program_meta,
|
|
AccountMeta::new(creator_vault_account, false),
|
|
accounts::EVENT_AUTHORITY_META,
|
|
accounts::PUMPFUN_META,
|
|
accounts::GLOBAL_VOLUME_ACCUMULATOR_META,
|
|
AccountMeta::new(user_volume_accumulator, false),
|
|
accounts::FEE_CONFIG_META,
|
|
accounts::FEE_PROGRAM_META,
|
|
];
|
|
metas.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
|
metas.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
|
|
|
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, buy_data.as_slice(), metas));
|
|
|
|
Ok(instructions)
|
|
}
|
|
|
|
fn build_sell_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
use crate::instruction::pumpfun_ix_data::{encode_pumpfun_sell_ix_data, PumpFunIxVersion};
|
|
use crate::instruction::utils::pumpfun::{
|
|
get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random,
|
|
is_phantom_default_creator_vault,
|
|
};
|
|
|
|
let protocol_params = params
|
|
.protocol_params
|
|
.as_any()
|
|
.downcast_ref::<PumpFunParams>()
|
|
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
|
|
|
let token_amount = if let Some(amount) = params.input_amount {
|
|
if amount == 0 {
|
|
return Err(anyhow!("Amount cannot be zero"));
|
|
}
|
|
amount
|
|
} else {
|
|
return Err(anyhow!("Amount token is required"));
|
|
};
|
|
|
|
let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE);
|
|
|
|
let bonding_curve = &protocol_params.bonding_curve;
|
|
|
|
// 卖出时:优先信任观测到的 creator_vault(来自 gRPC / pump fee 事件)。
|
|
// 不要通过 effective_creator 推导覆盖,避免 creator 被修改后推导出错 → Anchor 2006。
|
|
let creator_vault_account = if protocol_params.creator_vault != Pubkey::default()
|
|
&& !is_phantom_default_creator_vault(&protocol_params.creator_vault)
|
|
{
|
|
protocol_params.creator_vault
|
|
} else {
|
|
resolve_creator_vault_for_ix_with_fee_sharing(
|
|
&bonding_curve.creator,
|
|
protocol_params.creator_vault,
|
|
¶ms.input_mint,
|
|
protocol_params.fee_sharing_creator_vault_if_active,
|
|
)
|
|
.ok_or_else(|| {
|
|
anyhow!("creator_vault PDA derivation failed (curve_creator={})", bonding_curve.creator)
|
|
})?
|
|
};
|
|
|
|
let min_sol_output = if let Some(fixed) = params.fixed_output_amount {
|
|
fixed
|
|
} else {
|
|
let creator = protocol_params.effective_creator_for_trade();
|
|
let sol_amount = get_sell_sol_amount_from_token_amount(
|
|
bonding_curve.virtual_token_reserves as u128,
|
|
bonding_curve.virtual_sol_reserves as u128,
|
|
creator,
|
|
token_amount,
|
|
);
|
|
calculate_with_slippage_sell(sol_amount, slippage_bp)
|
|
};
|
|
|
|
let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| {
|
|
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
|
|
})?;
|
|
|
|
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
|
let token_program = effective_pump_mint_token_program(protocol_params);
|
|
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
|
crate::constants::TOKEN_PROGRAM_2022_META
|
|
} else {
|
|
crate::constants::TOKEN_PROGRAM_META
|
|
};
|
|
|
|
let associated_bonding_curve =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&bonding_curve_addr,
|
|
¶ms.input_mint,
|
|
&token_program,
|
|
);
|
|
|
|
let user_token_account =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
|
¶ms.payer.pubkey(),
|
|
¶ms.input_mint,
|
|
&token_program,
|
|
params.open_seed_optimize,
|
|
);
|
|
|
|
let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
|
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
|
|
|
let mut instructions = Vec::with_capacity(2);
|
|
let sell_data = encode_pumpfun_sell_ix_data(
|
|
token_amount,
|
|
min_sol_output,
|
|
PumpFunIxVersion::Legacy { track_volume: 0 },
|
|
);
|
|
|
|
let fee_recipient_meta =
|
|
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
|
|
|
let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.input_mint).ok_or_else(|| {
|
|
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.input_mint)
|
|
})?;
|
|
let mut metas: Vec<AccountMeta> = vec![
|
|
global_constants::GLOBAL_ACCOUNT_META,
|
|
fee_recipient_meta,
|
|
AccountMeta::new_readonly(params.input_mint, false),
|
|
AccountMeta::new(bonding_curve_addr, false),
|
|
AccountMeta::new(associated_bonding_curve, false),
|
|
AccountMeta::new(user_token_account, false),
|
|
AccountMeta::new(params.payer.pubkey(), true),
|
|
crate::constants::SYSTEM_PROGRAM_META,
|
|
AccountMeta::new(creator_vault_account, false),
|
|
token_program_meta,
|
|
accounts::EVENT_AUTHORITY_META,
|
|
accounts::PUMPFUN_META,
|
|
accounts::FEE_CONFIG_META,
|
|
accounts::FEE_PROGRAM_META,
|
|
];
|
|
|
|
if bonding_curve.is_cashback_coin {
|
|
metas.push(AccountMeta::new(user_volume_accumulator, false));
|
|
}
|
|
|
|
metas.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
|
metas.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
|
|
|
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, sell_data.as_slice(), metas));
|
|
|
|
if protocol_params.close_token_account_when_sell.unwrap_or(false) || params.close_input_mint_ata
|
|
{
|
|
instructions.push(close_account(
|
|
&token_program,
|
|
&user_token_account,
|
|
¶ms.payer.pubkey(),
|
|
¶ms.payer.pubkey(),
|
|
&[¶ms.payer.pubkey()],
|
|
)?);
|
|
}
|
|
|
|
Ok(instructions)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// V2 — 27 metas, quote_mint ATA create
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn build_buy_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
use crate::instruction::pumpfun_ix_data::{
|
|
encode_pumpfun_buy_exact_quote_in_ix_data, encode_pumpfun_buy_ix_data, PumpFunIxVersion,
|
|
};
|
|
use crate::instruction::utils::pumpfun::{
|
|
get_buyback_fee_recipient_random, get_fee_sharing_config_pda,
|
|
};
|
|
|
|
let protocol_params = params
|
|
.protocol_params
|
|
.as_any()
|
|
.downcast_ref::<PumpFunParams>()
|
|
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
|
|
|
let lamports_in = params.input_amount.unwrap_or(0);
|
|
if lamports_in == 0 {
|
|
return Err(anyhow!("Amount cannot be zero"));
|
|
}
|
|
|
|
let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE);
|
|
|
|
let bonding_curve = &protocol_params.bonding_curve;
|
|
let creator = protocol_params.effective_creator_for_trade();
|
|
let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing(
|
|
&creator,
|
|
protocol_params.creator_vault,
|
|
¶ms.output_mint,
|
|
protocol_params.fee_sharing_creator_vault_if_active,
|
|
)
|
|
.ok_or_else(|| anyhow!("creator_vault PDA derivation failed (creator={})", creator))?;
|
|
|
|
let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| {
|
|
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
|
|
})?;
|
|
|
|
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
|
let base_token_program = effective_pump_mint_token_program(protocol_params);
|
|
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
|
|
crate::constants::TOKEN_PROGRAM_2022_META
|
|
} else {
|
|
crate::constants::TOKEN_PROGRAM_META
|
|
};
|
|
|
|
let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params);
|
|
validate_v2_buy_quote_mint(¶ms.input_mint, "e_mint)?;
|
|
let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META;
|
|
|
|
let associated_base_bonding_curve =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&bonding_curve_addr,
|
|
¶ms.output_mint,
|
|
&base_token_program,
|
|
);
|
|
|
|
let associated_base_user =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
|
¶ms.payer.pubkey(),
|
|
¶ms.output_mint,
|
|
&base_token_program,
|
|
params.open_seed_optimize,
|
|
);
|
|
|
|
let fee_recipient_meta =
|
|
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
|
let fee_recipient_pk = fee_recipient_meta.pubkey;
|
|
let buyback_fee_recipient = get_buyback_fee_recipient_random();
|
|
|
|
let associated_quote_fee_recipient =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&fee_recipient_pk,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
let associated_quote_buyback_fee_recipient =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&buyback_fee_recipient,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
let associated_quote_bonding_curve =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&bonding_curve_addr,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
let associated_quote_user =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
|
¶ms.payer.pubkey(),
|
|
"e_mint,
|
|
"e_token_program,
|
|
params.open_seed_optimize,
|
|
);
|
|
let associated_creator_vault =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&creator_vault_account,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
|
|
let sharing_config = get_fee_sharing_config_pda(¶ms.output_mint).ok_or_else(|| {
|
|
anyhow!("sharing_config PDA derivation failed for mint {}", params.output_mint)
|
|
})?;
|
|
|
|
let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
|
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
|
let associated_user_volume_accumulator =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&user_volume_accumulator,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
|
|
let mut instructions = Vec::with_capacity(6);
|
|
|
|
if params.create_output_mint_ata {
|
|
instructions.extend(
|
|
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
|
¶ms.payer.pubkey(),
|
|
¶ms.payer.pubkey(),
|
|
¶ms.output_mint,
|
|
&base_token_program,
|
|
params.open_seed_optimize,
|
|
),
|
|
);
|
|
}
|
|
|
|
let (buy_data, quote_amount_to_fund) = if let Some(token_amount) = params.fixed_output_amount {
|
|
(encode_pumpfun_buy_ix_data(token_amount, lamports_in, PumpFunIxVersion::V2), lamports_in)
|
|
} else {
|
|
let buy_token_amount = get_buy_token_amount_from_sol_amount(
|
|
bonding_curve.virtual_token_reserves as u128,
|
|
bonding_curve.virtual_sol_reserves as u128,
|
|
bonding_curve.real_token_reserves as u128,
|
|
creator,
|
|
lamports_in,
|
|
);
|
|
if params.use_exact_sol_amount.unwrap_or(true) {
|
|
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
|
(
|
|
encode_pumpfun_buy_exact_quote_in_ix_data(
|
|
lamports_in,
|
|
min_tokens_out,
|
|
PumpFunIxVersion::V2,
|
|
),
|
|
lamports_in,
|
|
)
|
|
} else {
|
|
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
|
(
|
|
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, PumpFunIxVersion::V2),
|
|
max_sol_cost,
|
|
)
|
|
}
|
|
};
|
|
|
|
if params.create_input_mint_ata {
|
|
push_create_or_wrap_user_token_account(
|
|
&mut instructions,
|
|
¶ms.payer.pubkey(),
|
|
"e_mint,
|
|
"e_token_program,
|
|
quote_amount_to_fund,
|
|
params.open_seed_optimize,
|
|
);
|
|
}
|
|
|
|
let metas: Vec<AccountMeta> = vec![
|
|
global_constants::GLOBAL_ACCOUNT_META,
|
|
AccountMeta::new_readonly(params.output_mint, false),
|
|
AccountMeta::new_readonly(quote_mint, false),
|
|
base_token_program_meta,
|
|
quote_token_program_meta,
|
|
AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false),
|
|
fee_recipient_meta,
|
|
AccountMeta::new(associated_quote_fee_recipient, false),
|
|
AccountMeta::new(buyback_fee_recipient, false),
|
|
AccountMeta::new(associated_quote_buyback_fee_recipient, false),
|
|
AccountMeta::new(bonding_curve_addr, false),
|
|
AccountMeta::new(associated_base_bonding_curve, false),
|
|
AccountMeta::new(associated_quote_bonding_curve, false),
|
|
AccountMeta::new(params.payer.pubkey(), true),
|
|
AccountMeta::new(associated_base_user, false),
|
|
AccountMeta::new(associated_quote_user, false),
|
|
AccountMeta::new(creator_vault_account, false),
|
|
AccountMeta::new(associated_creator_vault, false),
|
|
AccountMeta::new_readonly(sharing_config, false),
|
|
accounts::GLOBAL_VOLUME_ACCUMULATOR_META,
|
|
AccountMeta::new(user_volume_accumulator, false),
|
|
AccountMeta::new(associated_user_volume_accumulator, false),
|
|
accounts::FEE_CONFIG_META,
|
|
accounts::FEE_PROGRAM_META,
|
|
crate::constants::SYSTEM_PROGRAM_META,
|
|
accounts::EVENT_AUTHORITY_META,
|
|
accounts::PUMPFUN_META,
|
|
];
|
|
|
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, buy_data.as_slice(), metas));
|
|
|
|
if params.close_input_mint_ata {
|
|
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
|
}
|
|
|
|
Ok(instructions)
|
|
}
|
|
|
|
fn build_sell_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
use crate::instruction::pumpfun_ix_data::{encode_pumpfun_sell_ix_data, PumpFunIxVersion};
|
|
use crate::instruction::utils::pumpfun::{
|
|
get_buyback_fee_recipient_random, get_fee_sharing_config_pda,
|
|
is_phantom_default_creator_vault,
|
|
};
|
|
|
|
let protocol_params = params
|
|
.protocol_params
|
|
.as_any()
|
|
.downcast_ref::<PumpFunParams>()
|
|
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
|
|
|
let token_amount = if let Some(amount) = params.input_amount {
|
|
if amount == 0 {
|
|
return Err(anyhow!("Amount cannot be zero"));
|
|
}
|
|
amount
|
|
} else {
|
|
return Err(anyhow!("Amount token is required"));
|
|
};
|
|
|
|
let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE);
|
|
|
|
let bonding_curve = &protocol_params.bonding_curve;
|
|
|
|
// 卖出时:优先信任观测到的 creator_vault(来自 gRPC / pump fee 事件)。
|
|
// 不要通过 effective_creator 推导覆盖,避免 creator 被修改后推导出错 → Anchor 2006。
|
|
let creator_vault_account = if protocol_params.creator_vault != Pubkey::default()
|
|
&& !is_phantom_default_creator_vault(&protocol_params.creator_vault)
|
|
{
|
|
protocol_params.creator_vault
|
|
} else {
|
|
resolve_creator_vault_for_ix_with_fee_sharing(
|
|
&bonding_curve.creator,
|
|
protocol_params.creator_vault,
|
|
¶ms.input_mint,
|
|
protocol_params.fee_sharing_creator_vault_if_active,
|
|
)
|
|
.ok_or_else(|| {
|
|
anyhow!("creator_vault PDA derivation failed (curve_creator={})", bonding_curve.creator)
|
|
})?
|
|
};
|
|
|
|
let min_sol_output = if let Some(fixed) = params.fixed_output_amount {
|
|
fixed
|
|
} else {
|
|
let creator = protocol_params.effective_creator_for_trade();
|
|
let sol_amount = get_sell_sol_amount_from_token_amount(
|
|
bonding_curve.virtual_token_reserves as u128,
|
|
bonding_curve.virtual_sol_reserves as u128,
|
|
creator,
|
|
token_amount,
|
|
);
|
|
calculate_with_slippage_sell(sol_amount, slippage_bp)
|
|
};
|
|
|
|
let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| {
|
|
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
|
|
})?;
|
|
|
|
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
|
let base_token_program = effective_pump_mint_token_program(protocol_params);
|
|
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
|
|
crate::constants::TOKEN_PROGRAM_2022_META
|
|
} else {
|
|
crate::constants::TOKEN_PROGRAM_META
|
|
};
|
|
|
|
let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params);
|
|
validate_v2_sell_quote_mint(¶ms.output_mint, "e_mint)?;
|
|
let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META;
|
|
|
|
let associated_base_bonding_curve =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&bonding_curve_addr,
|
|
¶ms.input_mint,
|
|
&base_token_program,
|
|
);
|
|
|
|
let associated_base_user =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
|
¶ms.payer.pubkey(),
|
|
¶ms.input_mint,
|
|
&base_token_program,
|
|
params.open_seed_optimize,
|
|
);
|
|
|
|
let fee_recipient_meta =
|
|
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
|
let fee_recipient_pk = fee_recipient_meta.pubkey;
|
|
let buyback_fee_recipient = get_buyback_fee_recipient_random();
|
|
|
|
let associated_quote_fee_recipient =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&fee_recipient_pk,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
let associated_quote_buyback_fee_recipient =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&buyback_fee_recipient,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
let associated_quote_bonding_curve =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&bonding_curve_addr,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
let associated_quote_user =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
|
¶ms.payer.pubkey(),
|
|
"e_mint,
|
|
"e_token_program,
|
|
params.open_seed_optimize,
|
|
);
|
|
let associated_creator_vault =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&creator_vault_account,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
|
|
let sharing_config = get_fee_sharing_config_pda(¶ms.input_mint).ok_or_else(|| {
|
|
anyhow!("sharing_config PDA derivation failed for mint {}", params.input_mint)
|
|
})?;
|
|
|
|
let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
|
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
|
let associated_user_volume_accumulator =
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&user_volume_accumulator,
|
|
"e_mint,
|
|
"e_token_program,
|
|
);
|
|
|
|
let mut instructions = Vec::with_capacity(4);
|
|
|
|
if params.create_output_mint_ata {
|
|
push_create_user_token_account(
|
|
&mut instructions,
|
|
¶ms.payer.pubkey(),
|
|
"e_mint,
|
|
"e_token_program,
|
|
params.open_seed_optimize,
|
|
);
|
|
}
|
|
|
|
let sell_data = encode_pumpfun_sell_ix_data(token_amount, min_sol_output, PumpFunIxVersion::V2);
|
|
|
|
let metas: Vec<AccountMeta> = vec![
|
|
global_constants::GLOBAL_ACCOUNT_META,
|
|
AccountMeta::new_readonly(params.input_mint, false),
|
|
AccountMeta::new_readonly(quote_mint, false),
|
|
base_token_program_meta,
|
|
quote_token_program_meta,
|
|
AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false),
|
|
fee_recipient_meta,
|
|
AccountMeta::new(associated_quote_fee_recipient, false),
|
|
AccountMeta::new(buyback_fee_recipient, false),
|
|
AccountMeta::new(associated_quote_buyback_fee_recipient, false),
|
|
AccountMeta::new(bonding_curve_addr, false),
|
|
AccountMeta::new(associated_base_bonding_curve, false),
|
|
AccountMeta::new(associated_quote_bonding_curve, false),
|
|
AccountMeta::new(params.payer.pubkey(), true),
|
|
AccountMeta::new(associated_base_user, false),
|
|
AccountMeta::new(associated_quote_user, false),
|
|
AccountMeta::new(creator_vault_account, false),
|
|
AccountMeta::new(associated_creator_vault, false),
|
|
AccountMeta::new_readonly(sharing_config, false),
|
|
AccountMeta::new(user_volume_accumulator, false),
|
|
AccountMeta::new(associated_user_volume_accumulator, false),
|
|
accounts::FEE_CONFIG_META,
|
|
accounts::FEE_PROGRAM_META,
|
|
crate::constants::SYSTEM_PROGRAM_META,
|
|
accounts::EVENT_AUTHORITY_META,
|
|
accounts::PUMPFUN_META,
|
|
];
|
|
|
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, sell_data.as_slice(), metas));
|
|
|
|
if protocol_params.close_token_account_when_sell.unwrap_or(false) || params.close_input_mint_ata
|
|
{
|
|
instructions.push(close_account(
|
|
&base_token_program,
|
|
&associated_base_user,
|
|
¶ms.payer.pubkey(),
|
|
¶ms.payer.pubkey(),
|
|
&[¶ms.payer.pubkey()],
|
|
)?);
|
|
}
|
|
|
|
if params.close_output_mint_ata {
|
|
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
|
}
|
|
|
|
Ok(instructions)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared: claim_cashback (independent of V1/V2)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Claim cashback (UserVolumeAccumulator → user lamports).
|
|
pub fn claim_cashback_pumpfun_instruction(payer: &Pubkey) -> Option<Instruction> {
|
|
const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
|
|
let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?;
|
|
let ix_accounts = vec![
|
|
AccountMeta::new(*payer, true),
|
|
AccountMeta::new(user_volume_accumulator, false),
|
|
crate::constants::SYSTEM_PROGRAM_META,
|
|
];
|
|
let ix =
|
|
Instruction::new_with_bytes(accounts::PUMPFUN, &CLAIM_CASHBACK_DISCRIMINATOR, ix_accounts);
|
|
Some(ix)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
common::{bonding_curve::BondingCurveAccount, GasFeeStrategy},
|
|
constants::TOKEN_PROGRAM,
|
|
trading::core::params::{DexParamEnum, PumpFunParams, SwapParams},
|
|
};
|
|
use solana_sdk::signature::Keypair;
|
|
use std::sync::Arc;
|
|
|
|
fn pump_mint() -> Pubkey {
|
|
"E3JvmGcGFDzhu2Cnxyeq5BRvN7HH9JZUsfAUh2v8pump".parse().unwrap()
|
|
}
|
|
|
|
fn swap_params_for_buy(mint: Pubkey, token_program: Pubkey) -> SwapParams {
|
|
let bonding_curve =
|
|
crate::instruction::utils::pumpfun::get_bonding_curve_pda(&mint).unwrap();
|
|
let creator = Pubkey::new_unique();
|
|
let creator_vault =
|
|
crate::instruction::utils::pumpfun::get_creator_vault_pda(&creator).unwrap();
|
|
let bc = BondingCurveAccount {
|
|
account: bonding_curve,
|
|
virtual_token_reserves: global_constants::INITIAL_VIRTUAL_TOKEN_RESERVES,
|
|
virtual_sol_reserves: global_constants::INITIAL_VIRTUAL_SOL_RESERVES,
|
|
real_token_reserves: global_constants::INITIAL_REAL_TOKEN_RESERVES,
|
|
creator,
|
|
..Default::default()
|
|
};
|
|
let params = PumpFunParams {
|
|
bonding_curve: Arc::new(bc),
|
|
associated_bonding_curve: Pubkey::default(),
|
|
observed_trade_creator: Some(creator),
|
|
creator_vault,
|
|
fee_sharing_creator_vault_if_active: None,
|
|
token_program,
|
|
close_token_account_when_sell: None,
|
|
fee_recipient: global_constants::FEE_RECIPIENT,
|
|
quote_mint: Pubkey::default(),
|
|
};
|
|
|
|
SwapParams {
|
|
rpc: None,
|
|
payer: Arc::new(Keypair::new()),
|
|
trade_type: crate::swqos::TradeType::Buy,
|
|
input_mint: crate::constants::SOL_TOKEN_ACCOUNT,
|
|
input_token_program: None,
|
|
output_mint: mint,
|
|
output_token_program: None,
|
|
input_amount: Some(10_000_000),
|
|
slippage_basis_points: Some(300),
|
|
address_lookup_table_account: None,
|
|
recent_blockhash: None,
|
|
wait_tx_confirmed: false,
|
|
protocol_params: DexParamEnum::PumpFun(params),
|
|
open_seed_optimize: true,
|
|
swqos_clients: Arc::new(Vec::new()),
|
|
middleware_manager: None,
|
|
durable_nonce: None,
|
|
with_tip: true,
|
|
create_input_mint_ata: false,
|
|
close_input_mint_ata: false,
|
|
create_output_mint_ata: true,
|
|
close_output_mint_ata: false,
|
|
fixed_output_amount: None,
|
|
gas_fee_strategy: GasFeeStrategy::new(),
|
|
simulate: false,
|
|
log_enabled: false,
|
|
wait_for_all_submits: false,
|
|
use_dedicated_sender_threads: false,
|
|
sender_thread_cores: None,
|
|
max_sender_concurrency: 0,
|
|
effective_core_ids: Arc::new(Vec::new()),
|
|
check_min_tip: false,
|
|
grpc_recv_us: None,
|
|
use_exact_sol_amount: Some(true),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_claim_cashback_instruction() {
|
|
let payer = Pubkey::new_unique();
|
|
let ix = claim_cashback_pumpfun_instruction(&payer).unwrap();
|
|
assert_eq!(ix.accounts.len(), 3);
|
|
assert_eq!(ix.accounts[0].pubkey, payer);
|
|
}
|
|
|
|
#[test]
|
|
fn pump_suffix_buy_respects_explicit_legacy_token_program() {
|
|
crate::common::seed::set_default_rents();
|
|
let params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
|
let instructions = build_buy(¶ms).unwrap();
|
|
|
|
assert_eq!(instructions.len(), 3);
|
|
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
|
|
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM);
|
|
assert_eq!(instructions[2].accounts.len(), 18);
|
|
assert_eq!(instructions[2].data.len(), 25);
|
|
}
|
|
|
|
#[test]
|
|
fn non_pump_buy_respects_explicit_legacy_token_program() {
|
|
crate::common::seed::set_default_rents();
|
|
let params = swap_params_for_buy(Pubkey::new_unique(), TOKEN_PROGRAM);
|
|
let instructions = build_buy(¶ms).unwrap();
|
|
|
|
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_v1_fixed_output_uses_buy_with_max_input_budget() {
|
|
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
|
params.create_output_mint_ata = false;
|
|
params.fixed_output_amount = Some(42);
|
|
params.use_exact_sol_amount = Some(true);
|
|
|
|
let instructions = build_buy(¶ms).unwrap();
|
|
let ix = instructions.last().unwrap();
|
|
|
|
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
|
|
assert_eq!(ix.data.len(), 25);
|
|
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
|
assert_eq!(
|
|
u64::from_le_bytes(ix.data[16..24].try_into().unwrap()),
|
|
params.input_amount.unwrap()
|
|
);
|
|
assert_eq!(ix.data[24], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_v2_fixed_output_uses_buy_with_max_input_budget() {
|
|
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
|
params.create_output_mint_ata = false;
|
|
params.fixed_output_amount = Some(42);
|
|
params.use_exact_sol_amount = Some(true);
|
|
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
|
*protocol_params =
|
|
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
|
}
|
|
|
|
let instructions = build_buy(¶ms).unwrap();
|
|
let ix = instructions.last().unwrap();
|
|
|
|
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
|
|
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
|
assert_eq!(
|
|
u64::from_le_bytes(ix.data[16..24].try_into().unwrap()),
|
|
params.input_amount.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_v2_regular_buy_wraps_max_quote_budget() {
|
|
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
|
params.create_output_mint_ata = false;
|
|
params.create_input_mint_ata = true;
|
|
params.use_exact_sol_amount = Some(false);
|
|
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
|
*protocol_params =
|
|
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
|
}
|
|
|
|
let instructions = build_buy(¶ms).unwrap();
|
|
let transfer_ix = &instructions[1];
|
|
let system_ix = bincode::deserialize::<
|
|
solana_system_interface::instruction::SystemInstruction,
|
|
>(&transfer_ix.data)
|
|
.unwrap();
|
|
let expected = crate::utils::calc::common::calculate_with_slippage_buy(
|
|
params.input_amount.unwrap(),
|
|
params.slippage_basis_points.unwrap(),
|
|
);
|
|
|
|
match system_ix {
|
|
solana_system_interface::instruction::SystemInstruction::Transfer { lamports } => {
|
|
assert_eq!(lamports, expected);
|
|
}
|
|
other => panic!("unexpected system instruction: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_usdc_dev_trade_uses_usdc_initial_quote_reserves() {
|
|
let mint = pump_mint();
|
|
let dev_quote_amount = 707_080;
|
|
let params = PumpFunParams::from_dev_trade_with_quote_mint(
|
|
mint,
|
|
55_855_975_892_641,
|
|
dev_quote_amount,
|
|
Pubkey::new_unique(),
|
|
Pubkey::default(),
|
|
Pubkey::default(),
|
|
Pubkey::default(),
|
|
None,
|
|
global_constants::FEE_RECIPIENT,
|
|
TOKEN_PROGRAM,
|
|
false,
|
|
Some(false),
|
|
crate::constants::USDC_TOKEN_ACCOUNT,
|
|
);
|
|
|
|
assert_eq!(params.quote_mint, crate::constants::USDC_TOKEN_ACCOUNT);
|
|
assert_eq!(params.bonding_curve.quote_mint, crate::constants::USDC_TOKEN_ACCOUNT);
|
|
assert_eq!(
|
|
params.bonding_curve.virtual_sol_reserves,
|
|
global_constants::INITIAL_VIRTUAL_USDC_RESERVES + dev_quote_amount
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_with_quote_mint_adjusts_dev_trade_quote_reserves() {
|
|
let mint = pump_mint();
|
|
let dev_quote_amount = 707_080;
|
|
let params = PumpFunParams::from_dev_trade(
|
|
mint,
|
|
55_855_975_892_641,
|
|
dev_quote_amount,
|
|
Pubkey::new_unique(),
|
|
Pubkey::default(),
|
|
Pubkey::default(),
|
|
Pubkey::default(),
|
|
None,
|
|
global_constants::FEE_RECIPIENT,
|
|
TOKEN_PROGRAM,
|
|
false,
|
|
Some(false),
|
|
)
|
|
.with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
|
|
|
assert_eq!(
|
|
params.bonding_curve.virtual_sol_reserves,
|
|
global_constants::INITIAL_VIRTUAL_USDC_RESERVES + dev_quote_amount
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_usdc_trade_event_preserves_virtual_quote_reserves() {
|
|
let mint = pump_mint();
|
|
let virtual_quote_reserves = 4_527_693_121;
|
|
let real_quote_reserves = 235_693_121;
|
|
let params = PumpFunParams::from_trade(
|
|
Pubkey::default(),
|
|
Pubkey::default(),
|
|
mint,
|
|
crate::constants::USDC_TOKEN_ACCOUNT,
|
|
Pubkey::new_unique(),
|
|
Pubkey::default(),
|
|
944_144_024_107_359,
|
|
virtual_quote_reserves,
|
|
737_244_024_107_359,
|
|
real_quote_reserves,
|
|
None,
|
|
global_constants::FEE_RECIPIENT,
|
|
TOKEN_PROGRAM,
|
|
false,
|
|
Some(false),
|
|
);
|
|
|
|
assert_eq!(params.quote_mint, crate::constants::USDC_TOKEN_ACCOUNT);
|
|
assert_eq!(params.bonding_curve.virtual_quote_reserves(), virtual_quote_reserves);
|
|
assert_eq!(params.bonding_curve.real_quote_reserves(), real_quote_reserves);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_solscan_sol_quote_mint_keeps_legacy_layout() {
|
|
let mint = pump_mint();
|
|
let params = PumpFunParams::from_trade(
|
|
Pubkey::default(),
|
|
Pubkey::default(),
|
|
mint,
|
|
crate::constants::SOL_TOKEN_ACCOUNT,
|
|
Pubkey::new_unique(),
|
|
Pubkey::default(),
|
|
944_144_024_107_359,
|
|
30_235_693_121,
|
|
737_244_024_107_359,
|
|
235_693_121,
|
|
None,
|
|
global_constants::FEE_RECIPIENT,
|
|
TOKEN_PROGRAM,
|
|
false,
|
|
Some(false),
|
|
);
|
|
|
|
assert_eq!(params.quote_mint, Pubkey::default());
|
|
assert_eq!(params.bonding_curve.quote_mint, crate::constants::WSOL_TOKEN_ACCOUNT);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_with_solscan_sol_quote_mint_selects_v1() {
|
|
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
|
params.create_output_mint_ata = false;
|
|
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
|
*protocol_params =
|
|
protocol_params.clone().with_quote_mint(crate::constants::SOL_TOKEN_ACCOUNT);
|
|
assert_eq!(protocol_params.quote_mint, Pubkey::default());
|
|
assert_eq!(
|
|
protocol_params.bonding_curve.quote_mint,
|
|
crate::constants::WSOL_TOKEN_ACCOUNT
|
|
);
|
|
}
|
|
|
|
let ix = build_buy(¶ms).unwrap().pop().unwrap();
|
|
assert_eq!(
|
|
&ix.data[..8],
|
|
crate::instruction::utils::pumpfun::BUY_EXACT_SOL_IN_DISCRIMINATOR
|
|
);
|
|
assert_eq!(ix.accounts.len(), 18);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_v2_usdc_buy_rejects_sol_input() {
|
|
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
|
params.create_output_mint_ata = false;
|
|
params.input_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
|
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
|
*protocol_params =
|
|
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
|
}
|
|
|
|
let err = build_buy(¶ms).unwrap_err().to_string();
|
|
assert!(err.contains("USDC quote pools must be bought with USDC"));
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_usdc_quote_mint_selects_v2_without_global_flag() {
|
|
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
|
params.create_output_mint_ata = false;
|
|
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
|
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
|
*protocol_params =
|
|
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
|
}
|
|
|
|
let ix = build_buy(¶ms).unwrap().pop().unwrap();
|
|
assert_eq!(
|
|
&ix.data[..8],
|
|
crate::instruction::utils::pumpfun::BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR
|
|
);
|
|
assert_eq!(ix.accounts.len(), 27);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_v2_usdc_buy_uses_idl_accounts_17_18_19() {
|
|
let mint = pump_mint();
|
|
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
|
params.create_output_mint_ata = false;
|
|
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
|
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
|
*protocol_params =
|
|
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
|
}
|
|
|
|
let associated_creator_vault =
|
|
if let DexParamEnum::PumpFun(protocol_params) = ¶ms.protocol_params {
|
|
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
|
&protocol_params.creator_vault,
|
|
&crate::constants::USDC_TOKEN_ACCOUNT,
|
|
&crate::constants::TOKEN_PROGRAM,
|
|
)
|
|
} else {
|
|
Pubkey::default()
|
|
};
|
|
let ix = build_buy(¶ms).unwrap().pop().unwrap();
|
|
assert_eq!(ix.accounts.len(), 27);
|
|
assert_eq!(ix.accounts[17].pubkey, associated_creator_vault);
|
|
assert_eq!(
|
|
ix.accounts[18].pubkey,
|
|
crate::instruction::utils::pumpfun::get_fee_sharing_config_pda(&mint).unwrap()
|
|
);
|
|
assert_eq!(ix.accounts[19].pubkey, accounts::GLOBAL_VOLUME_ACCUMULATOR);
|
|
assert_eq!(ix.accounts[22].pubkey, accounts::FEE_CONFIG);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_v2_buyback_fee_recipient_is_writable() {
|
|
let mint = pump_mint();
|
|
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
|
params.create_output_mint_ata = false;
|
|
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
|
*protocol_params =
|
|
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
|
}
|
|
|
|
let buy_ix = build_buy(¶ms).unwrap().pop().unwrap();
|
|
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&buy_ix.accounts[8].pubkey));
|
|
assert!(buy_ix.accounts[8].is_writable);
|
|
|
|
params.trade_type = crate::swqos::TradeType::Sell;
|
|
params.input_mint = mint;
|
|
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
|
let sell_ix = build_sell(¶ms).unwrap().pop().unwrap();
|
|
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&sell_ix.accounts[8].pubkey));
|
|
assert!(sell_ix.accounts[8].is_writable);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_v1_sell_fixed_output_uses_min_sol_directly() {
|
|
let mint = pump_mint();
|
|
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
|
params.trade_type = crate::swqos::TradeType::Sell;
|
|
params.input_mint = mint;
|
|
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
|
params.create_output_mint_ata = false;
|
|
params.fixed_output_amount = Some(42);
|
|
|
|
let instructions = build_sell(¶ms).unwrap();
|
|
let ix = instructions.last().unwrap();
|
|
|
|
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_DISCRIMINATOR);
|
|
assert_eq!(ix.data.len(), 24);
|
|
assert_eq!(
|
|
u64::from_le_bytes(ix.data[8..16].try_into().unwrap()),
|
|
params.input_amount.unwrap()
|
|
);
|
|
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
|
}
|
|
|
|
#[test]
|
|
fn pumpfun_v2_sell_fixed_output_uses_min_sol_directly() {
|
|
let mint = pump_mint();
|
|
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
|
params.trade_type = crate::swqos::TradeType::Sell;
|
|
params.input_mint = mint;
|
|
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
|
params.create_output_mint_ata = false;
|
|
params.fixed_output_amount = Some(42);
|
|
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
|
*protocol_params =
|
|
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
|
}
|
|
|
|
let instructions = build_sell(¶ms).unwrap();
|
|
let ix = instructions.last().unwrap();
|
|
|
|
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_V2_DISCRIMINATOR);
|
|
assert_eq!(ix.data.len(), 24);
|
|
assert_eq!(
|
|
u64::from_le_bytes(ix.data[8..16].try_into().unwrap()),
|
|
params.input_amount.unwrap()
|
|
);
|
|
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
|
}
|
|
}
|