2026-05-06 00:25:54 +08:00
|
|
|
//! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)).
|
2026-05-09 07:15:07 +08:00
|
|
|
//!
|
|
|
|
|
//! Runtime flag: set `SwapParams.use_pumpfun_v2 = true` to use `buy_v2` / `sell_v2` /
|
2026-05-15 01:08:33 +08:00
|
|
|
//! `buy_exact_quote_in_v2` (27/26-account unified metas, quote_mint support).
|
|
|
|
|
//! Default (`false`) keeps the smaller legacy SOL-paired instruction layout for latency.
|
2026-05-06 00:25:54 +08:00
|
|
|
|
2025-09-08 17:12:29 +08:00
|
|
|
use crate::{
|
2025-10-05 22:27:04 +08:00
|
|
|
common::spl_token::close_account,
|
2025-11-18 10:04:46 +08:00
|
|
|
constants::{trade::trade::DEFAULT_SLIPPAGE, TOKEN_PROGRAM_2022},
|
2025-10-05 22:27:04 +08:00
|
|
|
trading::core::{
|
2025-09-22 00:05:20 +08:00
|
|
|
params::{PumpFunParams, SwapParams},
|
2025-09-08 17:12:29 +08:00
|
|
|
traits::InstructionBuilder,
|
2025-10-05 22:27:04 +08:00
|
|
|
},
|
2025-07-10 02:38:06 +08:00
|
|
|
};
|
|
|
|
|
use crate::{
|
2026-05-16 06:42:50 +08:00
|
|
|
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,
|
|
|
|
|
},
|
2025-07-10 18:14:21 +08:00
|
|
|
},
|
2025-08-18 18:00:14 +08:00
|
|
|
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},
|
|
|
|
|
},
|
2025-07-10 02:38:06 +08:00
|
|
|
};
|
2025-09-08 17:12:29 +08:00
|
|
|
use anyhow::{anyhow, Result};
|
2025-09-08 10:58:43 +08:00
|
|
|
use solana_sdk::instruction::AccountMeta;
|
2025-09-08 17:12:29 +08:00
|
|
|
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
|
2025-07-10 02:38:06 +08:00
|
|
|
|
2026-05-06 00:25:54 +08:00
|
|
|
#[inline]
|
2026-05-19 06:21:37 +08:00
|
|
|
fn is_pump_suffix_mint(mint: &Pubkey) -> bool {
|
|
|
|
|
mint.to_string().ends_with("pump")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
|
fn effective_pump_mint_token_program(mint: &Pubkey, protocol_params: &PumpFunParams) -> Pubkey {
|
2026-05-06 00:25:54 +08:00
|
|
|
let tp = protocol_params.token_program;
|
2026-05-19 06:21:37 +08:00
|
|
|
if tp == Pubkey::default() || tp == TOKEN_PROGRAM_2022 {
|
|
|
|
|
return TOKEN_PROGRAM_2022;
|
2026-05-06 00:25:54 +08:00
|
|
|
}
|
2026-05-19 06:21:37 +08:00
|
|
|
if is_pump_suffix_mint(mint) {
|
|
|
|
|
return TOKEN_PROGRAM_2022;
|
|
|
|
|
}
|
|
|
|
|
tp
|
2026-05-06 00:25:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 07:15:07 +08:00
|
|
|
/// Resolve quote mint and its token program from PumpFunParams.
|
|
|
|
|
/// `Pubkey::default()` means legacy SOL-paired → use WSOL mint for V2 instructions.
|
|
|
|
|
#[inline]
|
|
|
|
|
fn effective_quote_mint_and_token_program(protocol_params: &PumpFunParams) -> (Pubkey, Pubkey) {
|
|
|
|
|
let quote_mint = if protocol_params.quote_mint == Pubkey::default() {
|
|
|
|
|
crate::constants::WSOL_TOKEN_ACCOUNT
|
|
|
|
|
} else {
|
|
|
|
|
protocol_params.quote_mint
|
|
|
|
|
};
|
|
|
|
|
(quote_mint, crate::constants::TOKEN_PROGRAM)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-10 02:38:06 +08:00
|
|
|
pub struct PumpFunInstructionBuilder;
|
|
|
|
|
|
|
|
|
|
#[async_trait::async_trait]
|
|
|
|
|
impl InstructionBuilder for PumpFunInstructionBuilder {
|
2025-09-22 00:05:20 +08:00
|
|
|
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
2026-05-09 07:15:07 +08:00
|
|
|
if params.use_pumpfun_v2 {
|
|
|
|
|
build_buy_v2(params)
|
2025-11-10 15:30:24 +08:00
|
|
|
} else {
|
2026-05-09 07:15:07 +08:00
|
|
|
build_buy_v1(params)
|
2025-09-10 00:18:31 +08:00
|
|
|
}
|
2025-07-10 02:38:06 +08:00
|
|
|
}
|
|
|
|
|
|
2025-09-22 00:05:20 +08:00
|
|
|
async fn build_sell_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
2026-05-09 07:15:07 +08:00
|
|
|
if params.use_pumpfun_v2 {
|
|
|
|
|
build_sell_v2(params)
|
2025-07-10 02:38:06 +08:00
|
|
|
} else {
|
2026-05-09 07:15:07 +08:00
|
|
|
build_sell_v1(params)
|
2026-05-08 20:48:11 +08:00
|
|
|
}
|
2025-07-10 02:38:06 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-18 22:58:14 +08:00
|
|
|
|
2026-05-09 07:15:07 +08:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// V1 (default) — 18 metas, no quote_mint ATA create
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
|
|
|
use crate::instruction::pumpfun_ix_data::{
|
|
|
|
|
encode_pumpfun_buy_exact_sol_in_ix_data, encode_pumpfun_buy_ix_data,
|
|
|
|
|
};
|
|
|
|
|
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,
|
|
|
|
|
)
|
2026-05-15 01:08:33 +08:00
|
|
|
.ok_or_else(|| anyhow!("creator_vault PDA derivation failed (creator={})", creator))?;
|
2026-05-09 07:15:07 +08:00
|
|
|
|
|
|
|
|
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;
|
2026-05-15 01:08:33 +08:00
|
|
|
let token_program = effective_pump_mint_token_program(¶ms.output_mint, protocol_params);
|
2026-05-09 07:15:07 +08:00
|
|
|
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 };
|
2026-05-16 06:42:50 +08:00
|
|
|
let buy_data = if let Some(token_amount) = params.fixed_output_amount {
|
|
|
|
|
encode_pumpfun_buy_ix_data(token_amount, lamports_in, track_volume_val)
|
2026-05-09 07:15:07 +08:00
|
|
|
} else {
|
2026-05-16 06:42:50 +08:00
|
|
|
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_sol_in_ix_data(lamports_in, min_tokens_out, track_volume_val)
|
|
|
|
|
} else {
|
|
|
|
|
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
|
|
|
|
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, track_volume_val)
|
|
|
|
|
}
|
2026-05-09 07:15:07 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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));
|
2026-05-15 01:08:33 +08:00
|
|
|
metas.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
|
2026-05-09 07:15:07 +08:00
|
|
|
|
2026-05-15 01:08:33 +08:00
|
|
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, metas));
|
2026-05-09 07:15:07 +08:00
|
|
|
|
|
|
|
|
Ok(instructions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
|
|
|
use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_ix_data;
|
|
|
|
|
use crate::instruction::utils::pumpfun::{
|
|
|
|
|
get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random,
|
2026-05-09 14:41:22 +08:00
|
|
|
is_phantom_default_creator_vault,
|
2026-05-09 07:15:07 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-09 14:41:22 +08:00
|
|
|
|
|
|
|
|
// 卖出时:优先信任观测到的 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,
|
2026-05-09 07:15:07 +08:00
|
|
|
)
|
2026-05-09 14:41:22 +08:00
|
|
|
.ok_or_else(|| {
|
2026-05-15 01:08:33 +08:00
|
|
|
anyhow!("creator_vault PDA derivation failed (curve_creator={})", bonding_curve.creator)
|
2026-05-09 14:41:22 +08:00
|
|
|
})?
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-16 06:42:50 +08:00
|
|
|
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)
|
2026-05-09 07:15:07 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-15 01:08:33 +08:00
|
|
|
let token_program = effective_pump_mint_token_program(¶ms.input_mint, protocol_params);
|
2026-05-09 07:15:07 +08:00
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-09 08:18:58 +08:00
|
|
|
if bonding_curve.is_cashback_coin {
|
2026-05-09 07:15:07 +08:00
|
|
|
metas.push(AccountMeta::new(user_volume_accumulator, false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
metas.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
2026-05-15 01:08:33 +08:00
|
|
|
metas.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
|
2026-05-09 07:15:07 +08:00
|
|
|
|
2026-05-15 01:08:33 +08:00
|
|
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, metas));
|
2026-05-09 07:15:07 +08:00
|
|
|
|
2026-05-15 01:08:33 +08:00
|
|
|
if protocol_params.close_token_account_when_sell.unwrap_or(false) || params.close_input_mint_ata
|
2026-05-09 07:15:07 +08:00
|
|
|
{
|
|
|
|
|
instructions.push(close_account(
|
|
|
|
|
&token_program,
|
|
|
|
|
&user_token_account,
|
|
|
|
|
¶ms.payer.pubkey(),
|
|
|
|
|
¶ms.payer.pubkey(),
|
|
|
|
|
&[¶ms.payer.pubkey()],
|
|
|
|
|
)?);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(instructions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// V2 (opt-in via `use_pumpfun_v2 = true`) — 27 metas, quote_mint ATA create
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
|
|
|
use crate::instruction::pumpfun_ix_data::{
|
|
|
|
|
encode_pumpfun_buy_exact_quote_in_v2_ix_data, encode_pumpfun_buy_v2_ix_data,
|
|
|
|
|
};
|
|
|
|
|
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,
|
|
|
|
|
)
|
2026-05-15 01:08:33 +08:00
|
|
|
.ok_or_else(|| anyhow!("creator_vault PDA derivation failed (creator={})", creator))?;
|
2026-05-09 07:15:07 +08:00
|
|
|
|
|
|
|
|
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;
|
2026-05-15 01:08:33 +08:00
|
|
|
let base_token_program =
|
|
|
|
|
effective_pump_mint_token_program(¶ms.output_mint, protocol_params);
|
2026-05-09 07:15:07 +08:00
|
|
|
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);
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-15 01:08:33 +08:00
|
|
|
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)
|
|
|
|
|
})?;
|
2026-05-09 07:15:07 +08:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-16 06:42:50 +08:00
|
|
|
let mut instructions = Vec::with_capacity(6);
|
2026-05-09 07:15:07 +08:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 06:42:50 +08:00
|
|
|
let (buy_data, quote_amount_to_fund) = if let Some(token_amount) = params.fixed_output_amount {
|
|
|
|
|
(encode_pumpfun_buy_v2_ix_data(token_amount, lamports_in), 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_v2_ix_data(lamports_in, min_tokens_out), lamports_in)
|
|
|
|
|
} else {
|
|
|
|
|
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
|
|
|
|
(encode_pumpfun_buy_v2_ix_data(buy_token_amount, max_sol_cost), max_sol_cost)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-09 07:15:07 +08:00
|
|
|
if params.create_input_mint_ata {
|
2026-05-16 06:42:50 +08:00
|
|
|
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,
|
2026-05-09 07:15:07 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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_readonly(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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-15 01:08:33 +08:00
|
|
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, metas));
|
2026-05-09 07:15:07 +08:00
|
|
|
|
2026-05-16 06:42:50 +08:00
|
|
|
if params.close_input_mint_ata {
|
|
|
|
|
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 07:15:07 +08:00
|
|
|
Ok(instructions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
|
|
|
|
use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_v2_ix_data;
|
|
|
|
|
use crate::instruction::utils::pumpfun::{
|
|
|
|
|
get_buyback_fee_recipient_random, get_fee_sharing_config_pda,
|
2026-05-09 14:41:22 +08:00
|
|
|
is_phantom_default_creator_vault,
|
2026-05-09 07:15:07 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-09 14:41:22 +08:00
|
|
|
|
|
|
|
|
// 卖出时:优先信任观测到的 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,
|
2026-05-09 07:15:07 +08:00
|
|
|
)
|
2026-05-09 14:41:22 +08:00
|
|
|
.ok_or_else(|| {
|
2026-05-15 01:08:33 +08:00
|
|
|
anyhow!("creator_vault PDA derivation failed (curve_creator={})", bonding_curve.creator)
|
2026-05-09 14:41:22 +08:00
|
|
|
})?
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-16 06:42:50 +08:00
|
|
|
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)
|
2026-05-09 07:15:07 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-15 01:08:33 +08:00
|
|
|
let base_token_program = effective_pump_mint_token_program(¶ms.input_mint, protocol_params);
|
2026-05-09 07:15:07 +08:00
|
|
|
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);
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-16 06:42:50 +08:00
|
|
|
let mut instructions = Vec::with_capacity(4);
|
2026-05-09 07:15:07 +08:00
|
|
|
|
|
|
|
|
if params.create_output_mint_ata {
|
2026-05-16 06:42:50 +08:00
|
|
|
push_create_user_token_account(
|
|
|
|
|
&mut instructions,
|
|
|
|
|
¶ms.payer.pubkey(),
|
|
|
|
|
"e_mint,
|
|
|
|
|
"e_token_program,
|
|
|
|
|
params.open_seed_optimize,
|
2026-05-09 07:15:07 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let sell_data = encode_pumpfun_sell_v2_ix_data(token_amount, min_sol_output);
|
|
|
|
|
|
|
|
|
|
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_readonly(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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-15 01:08:33 +08:00
|
|
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, metas));
|
2026-05-09 07:15:07 +08:00
|
|
|
|
2026-05-15 01:08:33 +08:00
|
|
|
if protocol_params.close_token_account_when_sell.unwrap_or(false) || params.close_input_mint_ata
|
2026-05-09 07:15:07 +08:00
|
|
|
{
|
|
|
|
|
instructions.push(close_account(
|
|
|
|
|
&base_token_program,
|
|
|
|
|
&associated_base_user,
|
|
|
|
|
¶ms.payer.pubkey(),
|
|
|
|
|
¶ms.payer.pubkey(),
|
|
|
|
|
&[¶ms.payer.pubkey()],
|
|
|
|
|
)?);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 06:42:50 +08:00
|
|
|
if params.close_output_mint_ata {
|
|
|
|
|
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 07:15:07 +08:00
|
|
|
Ok(instructions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Shared: claim_cashback (independent of V1/V2)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-05-06 00:25:54 +08:00
|
|
|
/// Claim cashback (UserVolumeAccumulator → user lamports).
|
2026-02-18 22:58:14 +08:00
|
|
|
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)?;
|
2026-05-06 00:25:54 +08:00
|
|
|
let ix_accounts = vec![
|
|
|
|
|
AccountMeta::new(*payer, true),
|
|
|
|
|
AccountMeta::new(user_volume_accumulator, false),
|
2026-02-18 22:58:14 +08:00
|
|
|
crate::constants::SYSTEM_PROGRAM_META,
|
|
|
|
|
];
|
2026-05-15 01:08:33 +08:00
|
|
|
let ix =
|
|
|
|
|
Instruction::new_with_bytes(accounts::PUMPFUN, &CLAIM_CASHBACK_DISCRIMINATOR, ix_accounts);
|
2026-05-09 07:15:07 +08:00
|
|
|
Some(ix)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
2026-05-15 01:08:33 +08:00
|
|
|
use crate::{
|
|
|
|
|
common::{bonding_curve::BondingCurveAccount, GasFeeStrategy},
|
2026-05-19 06:21:37 +08:00
|
|
|
constants::{TOKEN_PROGRAM, TOKEN_PROGRAM_2022},
|
2026-05-15 01:08:33 +08:00
|
|
|
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(),
|
|
|
|
|
use_v2_ix: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
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),
|
|
|
|
|
use_pumpfun_v2: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-09 07:15:07 +08:00
|
|
|
|
|
|
|
|
#[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);
|
|
|
|
|
}
|
2026-05-15 01:08:33 +08:00
|
|
|
|
|
|
|
|
#[test]
|
2026-05-19 06:21:37 +08:00
|
|
|
fn pump_suffix_buy_forces_token_2022_even_when_params_legacy() {
|
2026-05-15 01:08:33 +08:00
|
|
|
crate::common::seed::set_default_rents();
|
|
|
|
|
let params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
|
|
|
|
let instructions = build_buy_v1(¶ms).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(instructions.len(), 3);
|
2026-05-19 06:21:37 +08:00
|
|
|
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM_2022);
|
|
|
|
|
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM_2022);
|
2026-05-15 01:08:33 +08:00
|
|
|
assert_eq!(instructions[2].accounts.len(), 18);
|
2026-05-16 06:42:50 +08:00
|
|
|
assert_eq!(instructions[2].data.len(), 25);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 06:21:37 +08:00
|
|
|
#[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_v1(¶ms).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 06:42:50 +08:00
|
|
|
#[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_v1(¶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);
|
|
|
|
|
|
|
|
|
|
let instructions = build_buy_v2(¶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);
|
|
|
|
|
|
|
|
|
|
let instructions = build_buy_v2(¶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_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_v1(¶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);
|
|
|
|
|
|
|
|
|
|
let instructions = build_sell_v2(¶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);
|
2026-05-15 01:08:33 +08:00
|
|
|
}
|
2026-02-18 22:58:14 +08:00
|
|
|
}
|