From 184f1cc5839c178b08bdf73ecfce568afd393ba4 Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Mon, 4 May 2026 14:32:47 +0800 Subject: [PATCH] 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 --- Cargo.toml | 2 +- src/instruction/mod.rs | 2 + src/instruction/pumpfun.rs | 41 +++--- src/instruction/pumpfun_ix_data.rs | 47 +++++++ src/instruction/pumpswap.rs | 76 +++++----- src/instruction/pumpswap_ix_data.rs | 51 +++++++ src/instruction/utils/pumpfun.rs | 209 ++++++++++++++++++++++++++-- 7 files changed, 351 insertions(+), 77 deletions(-) create mode 100644 src/instruction/pumpfun_ix_data.rs create mode 100644 src/instruction/pumpswap_ix_data.rs diff --git a/Cargo.toml b/Cargo.toml index 050c7c9..a61eeca 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "4.0.5" +version = "4.0.6" edition = "2021" authors = [ "William ", diff --git a/src/instruction/mod.rs b/src/instruction/mod.rs index f9264b9..9927971 100755 --- a/src/instruction/mod.rs +++ b/src/instruction/mod.rs @@ -1,5 +1,7 @@ pub mod bonk; pub mod meteora_damm_v2; +pub(crate) mod pumpfun_ix_data; +pub(crate) mod pumpswap_ix_data; pub mod pumpfun; pub mod pumpswap; pub mod raydium_amm_v4; diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index 25e7ec4..db89128 100755 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -7,12 +7,15 @@ use crate::{ }, }; use crate::{ + instruction::pumpfun_ix_data::{ + encode_pumpfun_buy_exact_sol_in_ix_data, encode_pumpfun_buy_ix_data, + encode_pumpfun_sell_ix_data, TRACK_VOLUME_TRUE, + }, instruction::utils::pumpfun::{ accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random, get_user_volume_accumulator_pda, - pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix, + pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix_with_fee_sharing, global_constants::{self}, - BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR, }, utils::calc::{ common::{calculate_with_slippage_buy, calculate_with_slippage_sell}, @@ -46,10 +49,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder { // creator_vault must be PDA(creator) per bonding curve. Event vault: use only if == derived; // if stream sends a mismatched vault (wrong token / stale), fall back to derived. let creator = bonding_curve.creator; - let creator_vault_pda = resolve_creator_vault_for_ix( + let creator_vault_pda = 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!( @@ -130,26 +134,19 @@ impl InstructionBuilder for PumpFunInstructionBuilder { ); } - // IDL: buy/buy_exact_sol_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true) - let track_volume = if bonding_curve.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false) - let mut buy_data = [0u8; 26]; - if params.use_exact_sol_amount.unwrap_or(true) { - // buy_exact_sol_in(spendable_sol_in: u64, min_tokens_out: u64, track_volume) + let buy_data = if params.use_exact_sol_amount.unwrap_or(true) { let min_tokens_out = calculate_with_slippage_sell( buy_token_amount, params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), ); - buy_data[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR); - buy_data[8..16].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes()); - buy_data[16..24].copy_from_slice(&min_tokens_out.to_le_bytes()); - buy_data[24..26].copy_from_slice(&track_volume); + encode_pumpfun_buy_exact_sol_in_ix_data( + params.input_amount.unwrap_or(0), + min_tokens_out, + TRACK_VOLUME_TRUE, + ) } else { - // buy(token_amount: u64, max_sol_cost: u64, track_volume) - buy_data[..8].copy_from_slice(&BUY_DISCRIMINATOR); - buy_data[8..16].copy_from_slice(&buy_token_amount.to_le_bytes()); - buy_data[16..24].copy_from_slice(&max_sol_cost.to_le_bytes()); - buy_data[24..26].copy_from_slice(&track_volume); - } + encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, TRACK_VOLUME_TRUE) + }; // Fee recipient: gRPC/ShredStream 填入的 `PumpFunParams.fee_recipient`(同笔 create_v2+buy 或 trade 日志)优先;热路径无 RPC。 let fee_recipient_meta = @@ -206,10 +203,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder { let bonding_curve = &protocol_params.bonding_curve; let creator = bonding_curve.creator; - let creator_vault_pda = resolve_creator_vault_for_ix( + let creator_vault_pda = resolve_creator_vault_for_ix_with_fee_sharing( &creator, protocol_params.creator_vault, ¶ms.input_mint, + protocol_params.fee_sharing_creator_vault_if_active, ) .ok_or_else(|| { anyhow!( @@ -269,10 +267,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder { // ======================================== let mut instructions = Vec::with_capacity(2); - let mut sell_data = [0u8; 24]; - sell_data[..8].copy_from_slice(&SELL_DISCRIMINATOR); - 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()); + 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); diff --git a/src/instruction/pumpfun_ix_data.rs b/src/instruction/pumpfun_ix_data.rs new file mode 100644 index 0000000..0f064b0 --- /dev/null +++ b/src/instruction/pumpfun_ix_data.rs @@ -0,0 +1,47 @@ +//! Pump.fun 曲线 `buy` / `buy_exact_sol_in` / `sell` 的 **instruction data** 栈上编码(热路径零堆分配)。 +//! +//! 与 `@pump-fun/pump-sdk` Anchor `coder.instruction.encode` 对齐:`OptionBool` 在 ix 参数中为 **1 字节**。 + +use crate::instruction::utils::pumpfun::{ + BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR, +}; + +/// 与官方 `getBuyInstructionInternal` 一致:`track_volume = true`。 +pub const TRACK_VOLUME_TRUE: u8 = 1; + +#[inline(always)] +pub fn encode_pumpfun_buy_ix_data( + token_amount: u64, + max_sol_cost: u64, + track_volume: u8, +) -> [u8; 25] { + let mut d = [0u8; 25]; + d[..8].copy_from_slice(&BUY_DISCRIMINATOR); + d[8..16].copy_from_slice(&token_amount.to_le_bytes()); + d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes()); + d[24] = track_volume; + d +} + +#[inline(always)] +pub fn encode_pumpfun_buy_exact_sol_in_ix_data( + spendable_sol_in: u64, + min_tokens_out: u64, + track_volume: u8, +) -> [u8; 25] { + let mut d = [0u8; 25]; + d[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR); + d[8..16].copy_from_slice(&spendable_sol_in.to_le_bytes()); + d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes()); + d[24] = track_volume; + d +} + +#[inline(always)] +pub fn encode_pumpfun_sell_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] { + let mut d = [0u8; 24]; + d[..8].copy_from_slice(&SELL_DISCRIMINATOR); + d[8..16].copy_from_slice(&token_amount.to_le_bytes()); + d[16..24].copy_from_slice(&min_sol_output.to_le_bytes()); + d +} diff --git a/src/instruction/pumpswap.rs b/src/instruction/pumpswap.rs index d3e691f..c4e8bcc 100755 --- a/src/instruction/pumpswap.rs +++ b/src/instruction/pumpswap.rs @@ -1,10 +1,13 @@ use crate::{ constants::trade::trade::DEFAULT_SLIPPAGE, + instruction::pumpswap_ix_data::{ + encode_pumpswap_buy_exact_quote_in_ix_data, encode_pumpswap_buy_ix_data, + encode_pumpswap_buy_two_args, encode_pumpswap_sell_ix_data, + }, instruction::utils::pumpswap::{ accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda, get_protocol_extra_fee_recipient_random, get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata, get_user_volume_accumulator_wsol_ata, - BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR, }, trading::{ common::wsol_manager, @@ -221,35 +224,35 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { false, )); - // Create instruction data(buy/buy_exact_quote_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true);sell 仅两参数) - let track_volume = if protocol_params.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false) - let data: Vec = if quote_is_wsol_or_usdc { - let mut buf = [0u8; 26]; - if params.use_exact_sol_amount.unwrap_or(true) { + // buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。 + let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 }; + if quote_is_wsol_or_usdc { + let ix_data = if params.use_exact_sol_amount.unwrap_or(true) { let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell( token_amount, params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), ); - buf[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR); - buf[8..16].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes()); - buf[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes()); - buf[24..26].copy_from_slice(&track_volume); + encode_pumpswap_buy_exact_quote_in_ix_data( + params.input_amount.unwrap_or(0), + min_base_amount_out, + track_volume, + ) } else { - buf[..8].copy_from_slice(&BUY_DISCRIMINATOR); - buf[8..16].copy_from_slice(&token_amount.to_le_bytes()); - buf[16..24].copy_from_slice(&sol_amount.to_le_bytes()); - buf[24..26].copy_from_slice(&track_volume); - } - buf.to_vec() + encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume) + }; + instructions.push(Instruction::new_with_bytes( + accounts::AMM_PROGRAM, + &ix_data, + accounts, + )); } else { - let mut buf = [0u8; 24]; - buf[..8].copy_from_slice(&SELL_DISCRIMINATOR); - buf[8..16].copy_from_slice(&sol_amount.to_le_bytes()); - buf[16..24].copy_from_slice(&token_amount.to_le_bytes()); - buf.to_vec() - }; - - instructions.push(Instruction { program_id: accounts::AMM_PROGRAM, accounts, data }); + let ix_data = encode_pumpswap_sell_ix_data(sol_amount, token_amount); + instructions.push(Instruction::new_with_bytes( + accounts::AMM_PROGRAM, + &ix_data, + accounts, + )); + } if close_wsol_ata { // Close wSOL ATA account, reclaim rent instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey())); @@ -433,27 +436,14 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { false, )); - // Create instruction data - let mut data = [0u8; 24]; - if quote_is_wsol_or_usdc { - data[..8].copy_from_slice(&SELL_DISCRIMINATOR); - // base_amount_in - data[8..16].copy_from_slice(&token_amount.to_le_bytes()); - // min_quote_amount_out - data[16..24].copy_from_slice(&sol_amount.to_le_bytes()); + // 栈数组 + `new_with_bytes`,避免 `data.to_vec()`。 + let ix_data = if quote_is_wsol_or_usdc { + encode_pumpswap_sell_ix_data(token_amount, sol_amount) } else { - data[..8].copy_from_slice(&BUY_DISCRIMINATOR); - // base_amount_out - data[8..16].copy_from_slice(&sol_amount.to_le_bytes()); - // max_quote_amount_in - data[16..24].copy_from_slice(&token_amount.to_le_bytes()); - } + encode_pumpswap_buy_two_args(sol_amount, token_amount) + }; - instructions.push(Instruction { - program_id: accounts::AMM_PROGRAM, - accounts, - data: data.to_vec(), - }); + instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts)); if close_wsol_ata { instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey())); diff --git a/src/instruction/pumpswap_ix_data.rs b/src/instruction/pumpswap_ix_data.rs new file mode 100644 index 0000000..50dd86d --- /dev/null +++ b/src/instruction/pumpswap_ix_data.rs @@ -0,0 +1,51 @@ +//! PumpSwap AMM `buy` / `buy_exact_quote_in` / `sell` instruction **data**(栈数组、无 `Vec` 分配)。 + +use crate::instruction::utils::pumpswap::{ + BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR, +}; + +#[inline(always)] +pub fn encode_pumpswap_buy_two_args(base_amount_out: u64, max_quote_amount_in: u64) -> [u8; 24] { + let mut d = [0u8; 24]; + d[..8].copy_from_slice(&BUY_DISCRIMINATOR); + d[8..16].copy_from_slice(&base_amount_out.to_le_bytes()); + d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes()); + d +} + +#[inline(always)] +pub fn encode_pumpswap_buy_ix_data( + base_amount_out: u64, + max_quote_amount_in: u64, + track_volume: u8, +) -> [u8; 25] { + let mut d = [0u8; 25]; + d[..8].copy_from_slice(&BUY_DISCRIMINATOR); + d[8..16].copy_from_slice(&base_amount_out.to_le_bytes()); + d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes()); + d[24] = track_volume; + d +} + +#[inline(always)] +pub fn encode_pumpswap_buy_exact_quote_in_ix_data( + spendable_quote_in: u64, + min_base_amount_out: u64, + track_volume: u8, +) -> [u8; 25] { + let mut d = [0u8; 25]; + d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR); + d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes()); + d[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes()); + d[24] = track_volume; + d +} + +#[inline(always)] +pub fn encode_pumpswap_sell_ix_data(base_amount_in: u64, min_quote_amount_out: u64) -> [u8; 24] { + let mut d = [0u8; 24]; + d[..8].copy_from_slice(&SELL_DISCRIMINATOR); + d[8..16].copy_from_slice(&base_amount_in.to_le_bytes()); + d[16..24].copy_from_slice(&min_quote_amount_out.to_le_bytes()); + d +} diff --git a/src/instruction/utils/pumpfun.rs b/src/instruction/utils/pumpfun.rs index 8fc8a65..bd8cb9f 100644 --- a/src/instruction/utils/pumpfun.rs +++ b/src/instruction/utils/pumpfun.rs @@ -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, + 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 { + 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, ) -> Option { let phantom = phantom_default_creator_vault(); + let fee_sharing_vault: Option = 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, 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 { 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 + ); + } }