fix(pumpfun): prefer event creator_vault; align fees with pump-sdk
- Use protocol_params.creator_vault for buy/sell when non-default to avoid ConstraintSeeds (2006) when gRPC/event creator drifts from on-chain bonding curve - Preserve event creator_vault in PumpFunParams::from_trade/from_dev_trade instead of always overwriting with PDA(creator) - Fee recipient: prefer gRPC fee_recipient; else random from CURRENT_FEE_RECIPIENTS pool (standard) or MAYHEM pool, matching @pump-fun/pump-sdk fees.ts - Add cold-path helpers: extend_bonding_curve_account_instruction, PUMP_BONDING_CURVE_MIN_DATA_LEN Made-with: Cursor
This commit is contained in:
+54
-18
@@ -8,8 +8,9 @@ use crate::{
|
||||
};
|
||||
use crate::{
|
||||
instruction::utils::pumpfun::{
|
||||
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator,
|
||||
get_mayhem_fee_recipient_meta_random, get_user_volume_accumulator_pda,
|
||||
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator_vault_pda,
|
||||
get_mayhem_fee_recipient_meta_random, get_standard_fee_recipient_meta_random,
|
||||
get_user_volume_accumulator_pda,
|
||||
global_constants::{self},
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
},
|
||||
@@ -42,8 +43,19 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
}
|
||||
|
||||
let bonding_curve = &protocol_params.bonding_curve;
|
||||
let creator_vault_pda = protocol_params.creator_vault;
|
||||
let creator = get_creator(&creator_vault_pda);
|
||||
// creator_vault must match PDA(["creator-vault", bonding_curve.creator_on_chain]). Events/gRPC
|
||||
// sometimes have a stale `creator` but a correct `creator_vault` from parsed ix; prefer non-default.
|
||||
let creator = bonding_curve.creator;
|
||||
let creator_vault_pda = if protocol_params.creator_vault != Pubkey::default() {
|
||||
protocol_params.creator_vault
|
||||
} else {
|
||||
get_creator_vault_pda(&creator).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"creator_vault PDA derivation failed (creator={})",
|
||||
creator
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
@@ -106,6 +118,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
// Hot path: no RPC here (latency). For legacy curves <151 bytes, use
|
||||
// `extend_bonding_curve_account_instruction` from a cold path or separate tx.
|
||||
let mut instructions = Vec::with_capacity(2);
|
||||
|
||||
// Create associated token account
|
||||
@@ -142,12 +156,19 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
buy_data[24..26].copy_from_slice(&track_volume);
|
||||
}
|
||||
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
global_constants::FEE_RECIPIENT_META
|
||||
};
|
||||
// Fee recipient: prefer gRPC/event pubkey (matches live trades); else @pump-fun/pump-sdk getFeeRecipient.
|
||||
let fee_recipient_meta =
|
||||
if protocol_params.fee_recipient != Pubkey::default() {
|
||||
AccountMeta {
|
||||
pubkey: protocol_params.fee_recipient,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
} else if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
get_standard_fee_recipient_meta_random()
|
||||
};
|
||||
|
||||
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)
|
||||
@@ -197,8 +218,17 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
};
|
||||
|
||||
let bonding_curve = &protocol_params.bonding_curve;
|
||||
let creator_vault_pda = protocol_params.creator_vault;
|
||||
let creator = get_creator(&creator_vault_pda);
|
||||
let creator = bonding_curve.creator;
|
||||
let creator_vault_pda = if protocol_params.creator_vault != Pubkey::default() {
|
||||
protocol_params.creator_vault
|
||||
} else {
|
||||
get_creator_vault_pda(&creator).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"creator_vault PDA derivation failed (creator={})",
|
||||
creator
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
@@ -264,12 +294,18 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
sell_data[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
sell_data[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
|
||||
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
global_constants::FEE_RECIPIENT_META
|
||||
};
|
||||
let fee_recipient_meta =
|
||||
if protocol_params.fee_recipient != Pubkey::default() {
|
||||
AccountMeta {
|
||||
pubkey: protocol_params.fee_recipient,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
} else if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
get_standard_fee_recipient_meta_random()
|
||||
};
|
||||
|
||||
let mut accounts: Vec<AccountMeta> = vec![
|
||||
global_constants::GLOBAL_ACCOUNT_META,
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
|
||||
use anyhow::anyhow;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
// --- Aligned with official `@pump-fun/pump-sdk` (npm) ---
|
||||
// - `src/fees.ts` `getFeeRecipient(global, mayhemMode)` — fee recipient pools
|
||||
// - `src/bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient`
|
||||
// - `src/sdk.ts` `BONDING_CURVE_NEW_SIZE` (151) + `extendAccountInstruction` — **not** called from the
|
||||
// trade hot path here (no RPC in `PumpFunInstructionBuilder`); use these helpers from a cold path if needed.
|
||||
|
||||
/// Minimum bonding curve account data length after protocol upgrades (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
|
||||
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
|
||||
|
||||
/// Anchor discriminator for `extend_account` (`pump.json`); same as `PumpSdk.extendAccountInstruction`.
|
||||
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
|
||||
|
||||
/// Build `extend_account` for bonding curve (cold path / separate tx only — do not add RPC to hot-path builds).
|
||||
#[inline]
|
||||
pub fn extend_bonding_curve_account_instruction(bonding_curve: &Pubkey, user: &Pubkey) -> Instruction {
|
||||
Instruction {
|
||||
program_id: accounts::PUMPFUN,
|
||||
accounts: vec![
|
||||
AccountMeta::new(*bonding_curve, false),
|
||||
AccountMeta::new(*user, true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
],
|
||||
data: EXTEND_ACCOUNT_DISCRIMINATOR.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
/// Seed for bonding curve PDAs
|
||||
@@ -190,7 +221,8 @@ pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
|
||||
}
|
||||
|
||||
/// Returns a random Mayhem fee recipient AccountMeta (pump-public-docs: Bonding Curve 2nd account = Mayhem fee recipient; use any one randomly).
|
||||
/// Mayhem: random among `Global.reservedFeeRecipient` + `Global.reservedFeeRecipients` (`fees.ts` `getFeeRecipient` when `mayhemMode === true`).
|
||||
/// Uses hardcoded `MAYHEM_FEE_RECIPIENTS`; prefer gRPC/event `PumpFunParams.fee_recipient` when set.
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
|
||||
@@ -199,6 +231,30 @@ pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
|
||||
}
|
||||
|
||||
/// Non-mayhem: random among `Global::fee_recipient` + `Global::fee_recipients[0..7]`.
|
||||
/// Same pubkey set as `bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient` and `fees.ts` `getFeeRecipient` when `mayhemMode === false`.
|
||||
#[inline]
|
||||
pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
|
||||
const POOL: &[Pubkey] = &[
|
||||
global_constants::FEE_RECIPIENT,
|
||||
global_constants::PUMPFUN_AMM_FEE_1,
|
||||
global_constants::PUMPFUN_AMM_FEE_2,
|
||||
global_constants::PUMPFUN_AMM_FEE_3,
|
||||
global_constants::PUMPFUN_AMM_FEE_4,
|
||||
global_constants::PUMPFUN_AMM_FEE_5,
|
||||
global_constants::PUMPFUN_AMM_FEE_6,
|
||||
global_constants::PUMPFUN_AMM_FEE_7,
|
||||
];
|
||||
let recipient = *POOL
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::FEE_RECIPIENT);
|
||||
AccountMeta {
|
||||
pubkey: recipient,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Symbol;
|
||||
|
||||
impl Symbol {
|
||||
|
||||
+27
-10
@@ -119,20 +119,22 @@ impl std::fmt::Debug for SwapParams {
|
||||
/// PumpFun protocol specific parameters
|
||||
/// Configuration parameters specific to PumpFun trading protocol.
|
||||
///
|
||||
/// **Creator Rewards Sharing**: Some coins use a dynamic `creator_vault` (fee-sharing config).
|
||||
/// Always use the latest on-chain creator/vault when building params for **sell**; do not reuse
|
||||
/// cached params from buy. Either fetch fresh data via RPC, or pass `creator_vault` from gRPC
|
||||
/// using [`from_trade`](PumpFunParams::from_trade) / [`from_dev_trade`](PumpFunParams::from_dev_trade),
|
||||
/// or override with [`with_creator_vault`](PumpFunParams::with_creator_vault).
|
||||
/// **Creator vault**: Pump buy/sell instructions always pass `creator_vault` =
|
||||
/// `PDA(["creator-vault", bonding_curve.creator])` derived from [`BondingCurveAccount::creator`].
|
||||
/// Keep `bonding_curve.creator` in sync with chain (gRPC / RPC); stale `creator_vault` in this struct
|
||||
/// does not affect ix building.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
/// Creator vault PDA. For Creator Rewards Sharing coins this can change; pass latest from gRPC when selling.
|
||||
/// From events/parsed ix when set; else derived from `bonding_curve.creator`. Buy/sell prefer non-default.
|
||||
pub creator_vault: Pubkey,
|
||||
pub token_program: Pubkey,
|
||||
/// Whether to close token account when selling, only effective during sell operations
|
||||
pub close_token_account_when_sell: Option<bool>,
|
||||
/// Fee recipient for buy/sell account #2. When set from gRPC (matches `@pump-fun/pump-sdk` `fees.ts` / observed trades), mirrors on-chain choice.
|
||||
/// `Pubkey::default()` uses the same random pools as `getFeeRecipient` / `getStaticRandomFeeRecipient` in the npm SDK.
|
||||
pub fee_recipient: Pubkey,
|
||||
}
|
||||
|
||||
impl PumpFunParams {
|
||||
@@ -147,6 +149,7 @@ impl PumpFunParams {
|
||||
creator_vault: creator_vault,
|
||||
token_program: token_program,
|
||||
close_token_account_when_sell: Some(close_token_account_when_sell),
|
||||
fee_recipient: Pubkey::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,12 +182,19 @@ impl PumpFunParams {
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = if creator_vault != Pubkey::default() {
|
||||
creator_vault
|
||||
} else {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve_account),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault,
|
||||
creator_vault: creator_vault_resolved,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,12 +230,19 @@ impl PumpFunParams {
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = if creator_vault != Pubkey::default() {
|
||||
creator_vault
|
||||
} else {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault,
|
||||
creator_vault: creator_vault_resolved,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,11 +279,11 @@ impl PumpFunParams {
|
||||
creator_vault: creator_vault.unwrap(),
|
||||
close_token_account_when_sell: None,
|
||||
token_program: mint_account.owner,
|
||||
fee_recipient: Pubkey::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Override `creator_vault` with a value from gRPC/event (e.g. for Creator Rewards Sharing).
|
||||
/// Use when selling so the instruction uses the latest on-chain vault and avoids "seeds constraint violated" (2006).
|
||||
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`].
|
||||
#[inline]
|
||||
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
|
||||
self.creator_vault = creator_vault;
|
||||
|
||||
Reference in New Issue
Block a user