fix(instruction): complete pump ix modules and pumpfun utils after params split

Adds pumpfun_ix_data / pumpswap_ix_data; exports used by trading::core::params::PumpFunParams.
chore(release): bump version to 4.0.6

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0xfnzero
2026-05-04 14:32:47 +08:00
co-authored by Cursor
parent 55c13e2f25
commit 184f1cc583
7 changed files with 351 additions and 77 deletions
+199 -10
View File
@@ -219,6 +219,12 @@ pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
pub const BUY_EXACT_SOL_IN_DISCRIMINATOR: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
/// Anchor account discriminator for `SharingConfig` on `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` (`pump_fees.json`).
pub const SHARING_CONFIG_ACCOUNT_DISCRIMINATOR: [u8; 8] = [216, 74, 9, 0, 56, 140, 93, 75];
/// `ConfigStatus::Active` as serialized by Anchor (enum variant index; `Paused` = 0).
const SHARING_CONFIG_STATUS_ACTIVE: u8 = 1;
/// Check if a pubkey is one of the Mayhem fee recipients
#[inline]
pub fn is_mayhem_fee_recipient(pubkey: &Pubkey) -> bool {
@@ -237,6 +243,50 @@ pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
}
/// Non-mayhem bonding-curve fee pool: primary fee recipient + AMM protocol fee slots (`getFeeRecipient` when `mayhemMode === false`).
#[inline]
pub fn is_standard_bonding_fee_recipient(pubkey: &Pubkey) -> bool {
*pubkey == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(pubkey)
}
/// Bonding-curve trade: `mayhemMode` 与账户 #2 fee recipient 必须同属 Mayhem 池或同属普通池,否则会 Anchor `NotAuthorized`fee_recipient 校验)。
/// gRPC 偶尔只带对一侧的字段时,用 fee 地址与静态池对齐。
#[inline]
pub fn reconcile_mayhem_mode_for_trade(
mayhem_from_event: Option<bool>,
fee_recipient: &Pubkey,
) -> bool {
if *fee_recipient == Pubkey::default() {
return mayhem_from_event.unwrap_or(false);
}
let fee_m = is_mayhem_fee_recipient(fee_recipient);
let fee_s = is_standard_bonding_fee_recipient(fee_recipient);
match mayhem_from_event {
Some(log_m) => {
if fee_m && !log_m {
true
} else if fee_s && log_m && !fee_m {
false
} else {
log_m
}
}
None => fee_m,
}
}
/// Whether `pk` is allowed as account #2 for this bonding-curve mode. Unknown pubkeys (not in static pools) are accepted — chain is authoritative.
#[inline]
pub fn fee_recipient_ok_for_bonding_curve_mode(pk: &Pubkey, is_mayhem_mode: bool) -> bool {
let is_m = is_mayhem_fee_recipient(pk);
let is_s = is_standard_bonding_fee_recipient(pk);
if is_mayhem_mode {
!(is_s && !is_m)
} else {
!(is_m && !is_s)
}
}
/// 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]
@@ -279,10 +329,12 @@ pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
.unwrap_or(&global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
}
/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供时按 mayhem 从静态池随机
/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供或与 `is_mayhem_mode` 池不一致时按 mayhem 从静态池随机(避免 NotAuthorized
#[inline]
pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
if from_stream != Pubkey::default() {
let trust_stream = from_stream != Pubkey::default()
&& fee_recipient_ok_for_bonding_curve_mode(&from_stream, is_mayhem_mode);
if trust_stream {
AccountMeta {
pubkey: from_stream,
is_signer: false,
@@ -384,17 +436,42 @@ pub fn is_phantom_default_creator_vault(pk: &Pubkey) -> bool {
/// `creator_vault` was filled from **instruction accounts** (e.g. `fill_trade_accounts` index 9),
/// **trust that vault** — unless it equals [`phantom_default_creator_vault`] (bad derivation / cache).
/// - If event `creator_vault` is **missing** → [`get_creator_vault_pda`]`(creator)` (never `PDA(default)`).
/// - If it **matches** `PDA(creator)` or `PDA(fee_sharing_config(mint))` → use it (fast path, matches ix).
/// - If it **does not match** either (e.g. stale vault but `creator` from tradeEvent is correct) → use
/// [`get_creator_vault_pda`]`(creator)` so seeds match on-chain bonding curve (fixes 2006 Left≠Right).
/// - If it **matches** `PDA(creator)` → use it (fast path).
/// - If **Creator Rewards Sharing is active** (`fee_sharing_creator_vault_if_active` / RPC) and the event
/// vault matches `PDA(["creator-vault", fee_sharing_config_pda])` → use it.
/// - If `fee_sharing_creator_vault_if_active` is **None**, do **not** trust a stale event vault that merely
/// equals the theoretical sharing-layout PDA — fall back to `PDA(creator)` (sharing may have migrated off).
/// - If the event vault is some other stale value but `creator` is correct → `PDA(creator)` (fixes 2006 Left≠Right).
///
/// For **Creator Rewards Sharing** (`fee_sharing_creator_vault_if_active`), prefer
/// [`resolve_creator_vault_for_ix_with_fee_sharing`] or [`fetch_fee_sharing_creator_vault_if_active`].
#[inline]
pub fn resolve_creator_vault_for_ix(
creator: &Pubkey,
creator_vault_from_event: Pubkey,
mint: &Pubkey,
) -> Option<Pubkey> {
resolve_creator_vault_for_ix_with_fee_sharing(creator, creator_vault_from_event, mint, None)
}
/// Same as [`resolve_creator_vault_for_ix`], but when `fee_sharing_creator_vault_if_active` is
/// `Some(PDA(["creator-vault", fee_sharing_config_pda(mint)]))` from an RPC hint, overrides stale
/// `PDA(creator)` so sell/buy match on-chain seeds (Anchor 2006).
#[inline]
pub fn resolve_creator_vault_for_ix_with_fee_sharing(
creator: &Pubkey,
creator_vault_from_event: Pubkey,
mint: &Pubkey,
fee_sharing_creator_vault_if_active: Option<Pubkey>,
) -> Option<Pubkey> {
let phantom = phantom_default_creator_vault();
let fee_sharing_vault: Option<Pubkey> = fee_sharing_creator_vault_if_active.and_then(|v| {
let sharing_pk = get_fee_sharing_config_pda(mint)?;
let expected = get_creator_vault_pda(&sharing_pk)?;
if v == expected { Some(v) } else { None }
});
if *creator == Pubkey::default() {
if creator_vault_from_event == Pubkey::default() {
return None;
@@ -405,27 +482,83 @@ pub fn resolve_creator_vault_for_ix(
return Some(creator_vault_from_event);
}
// Real creator: poisoned cache may hold phantom vault — always remap to PDA(creator).
if creator_vault_from_event == phantom {
if let Some(vs) = fee_sharing_vault {
let v_derived = get_creator_vault_pda(creator)?;
if vs != v_derived {
return Some(vs);
}
}
return get_creator_vault_pda(creator);
}
let v_derived = get_creator_vault_pda(creator)?;
if let Some(vs) = fee_sharing_vault {
if vs != v_derived
&& (creator_vault_from_event == Pubkey::default()
|| creator_vault_from_event == v_derived
|| creator_vault_from_event != vs)
{
return Some(vs);
}
}
if creator_vault_from_event == Pubkey::default() {
return Some(v_derived);
}
if creator_vault_from_event == v_derived {
return Some(creator_vault_from_event);
}
if let Some(sharing) = get_fee_sharing_config_pda(mint) {
let v_sharing = get_creator_vault_pda(&sharing)?;
if creator_vault_from_event == v_sharing {
return Some(creator_vault_from_event);
// 仅当 RPC / 调用方已确认 Creator Rewards Sharing **仍 Active**hint 为 Some)时,才信任
// `creator_vault == PDA(["creator-vault", fee_sharing_config])`。hint 为 None 时可能是「已停用」或从未拉取:
// 此时旧缓存里的 fee-sharing vault 必须回退到 `PDA(creator)`,否则易与链上 seeds 不一致(2006)。
if fee_sharing_creator_vault_if_active.is_some() {
if let Some(sharing) = get_fee_sharing_config_pda(mint) {
let v_sharing = get_creator_vault_pda(&sharing)?;
if creator_vault_from_event == v_sharing {
return Some(creator_vault_from_event);
}
}
}
Some(v_derived)
}
/// Read pump-fees `SharingConfig` for `mint`; if **Active** and `mint` matches, return
/// `PDA(["creator-vault", fee_sharing_config_pda])` on Pump program (same as npm `pump-sdk`).
#[inline]
pub async fn fetch_fee_sharing_creator_vault_if_active(
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Option<Pubkey>, anyhow::Error> {
let Some(config_pda) = get_fee_sharing_config_pda(mint) else {
return Ok(None);
};
let acc = match rpc.get_account(&config_pda).await {
Ok(a) => a,
Err(_) => return Ok(None),
};
if acc.owner != accounts::FEE_PROGRAM {
return Ok(None);
}
let d = acc.data.as_slice();
if d.len() < 43 || d[..8] != SHARING_CONFIG_ACCOUNT_DISCRIMINATOR {
return Ok(None);
}
if d[10] != SHARING_CONFIG_STATUS_ACTIVE {
return Ok(None);
}
let mint_on_chain = Pubkey::new_from_array(
d[11..43]
.try_into()
.map_err(|_| anyhow::anyhow!("SharingConfig mint slice"))?,
);
if mint_on_chain != *mint {
return Ok(None);
}
Ok(get_creator_vault_pda(&config_pda))
}
#[inline]
pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
crate::common::fast_fn::get_cached_pda(
@@ -547,6 +680,23 @@ mod tests {
);
}
#[test]
fn resolve_prefers_fee_sharing_vault_when_rpc_hint_and_differs_from_creator_pda() {
let creator = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let v_derived = get_creator_vault_pda(&creator).unwrap();
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
assert_ne!(v_derived, vs);
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(
&creator,
v_derived,
&mint,
Some(vs),
);
assert_eq!(resolved, Some(vs));
}
#[test]
fn resolve_remaps_phantom_vault_when_creator_known() {
let creator = Pubkey::new_unique();
@@ -557,4 +707,43 @@ mod tests {
Some(expected)
);
}
#[test]
fn resolve_stale_fee_sharing_vault_falls_back_to_creator_pda_when_hint_inactive() {
let creator = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let v_derived = get_creator_vault_pda(&creator).unwrap();
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
assert_ne!(v_derived, vs);
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, None);
assert_eq!(
resolved,
Some(v_derived),
"hint None: stale sharing-layout vault in cache must not win over PDA(creator)"
);
}
#[test]
fn reconcile_mayhem_prefers_fee_when_log_says_false_but_fee_is_mayhem_pool() {
let fee = global_constants::MAYHEM_FEE_RECIPIENTS[0];
assert!(reconcile_mayhem_mode_for_trade(Some(false), &fee));
}
#[test]
fn reconcile_mayhem_prefers_fee_when_log_says_true_but_fee_is_standard_pool() {
let fee = global_constants::PUMPFUN_AMM_FEE_4;
assert!(!reconcile_mayhem_mode_for_trade(Some(true), &fee));
}
#[test]
fn pump_fee_meta_rejects_standard_fee_when_building_mayhem_ix() {
let fee = global_constants::PUMPFUN_AMM_FEE_4;
let m = pump_fun_fee_recipient_meta(fee, true);
assert!(
global_constants::MAYHEM_FEE_RECIPIENTS.contains(&m.pubkey),
"expected fallback to mayhem pool, got {}",
m.pubkey
);
}
}