From 75bfbf6d54e04e2fd15148be394528335f130842 Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Wed, 6 May 2026 00:25:54 +0800 Subject: [PATCH] fix(pumpfun): trust explicit ix creator_vault over creator-derived PDA - resolve_creator_vault_for_ix_with_fee_sharing uses non-placeholder ix vault as-is. - When ix vault missing, fall back to fee_sharing_creator_vault_if_active then PDA(creator). - Add Pump troubleshooting CN doc; extend PumpFunParams field documentation. - Bump to v4.0.7. Co-authored-by: Cursor --- Cargo.toml | 2 +- README.md | 4 + README_CN.md | 5 + docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md | 208 +++++++ src/client/mod.rs | 4 +- src/common/fast_fn.rs | 2 + src/instruction/pumpfun.rs | 140 +++-- src/instruction/utils/pumpfun.rs | 640 ++++++++++----------- src/perf/hardware_optimizations.rs | 8 +- src/swqos/blockrazor.rs | 2 +- src/trading/core/params/pumpfun.rs | 38 +- 11 files changed, 632 insertions(+), 421 deletions(-) create mode 100644 docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md mode change 100755 => 100644 src/instruction/pumpfun.rs diff --git a/Cargo.toml b/Cargo.toml index a61eeca..1765ab8 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "4.0.6" +version = "4.0.7" edition = "2021" authors = [ "William ", diff --git a/README.md b/README.md index 0eff4cc..6fe68ec 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,10 @@ PumpFun and PumpSwap support **cashback** for eligible tokens: part of the tradi - The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params. - **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback. +#### PumpFun: troubleshooting (on-chain errors) + +For **Anchor 2006 / `NotAuthorized` (6000) / wrong token program / BuyZeroAmount (6020) / slippage (6042)** and related issues, see **[docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)** (Chinese). An English appendix may be added later. + #### PumpFun: Creator Rewards Sharing (creator_vault) Some PumpFun coins use **Creator Rewards Sharing**, so the on-chain `creator_vault` can differ from the default derivation. If you reuse cached params from a **buy** when **selling**, you may see program error **2006 (seeds constraint violated)**. To avoid this: diff --git a/README_CN.md b/README_CN.md index 051c3a5..a8b05f2 100755 --- a/README_CN.md +++ b/README_CN.md @@ -53,6 +53,7 @@ - [🔍 地址查找表](#-地址查找表) - [🔍 Nonce 缓存](#-nonce-缓存) - [💰 Cashback 支持(PumpFun / PumpSwap)](#-cashback-支持pumpfun--pumpswap) + - [Pump.fun 常见链上错误与排错(文档)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md) - [🛡️ MEV 保护服务](#️-mev-保护服务) - [📁 项目结构](#-项目结构) - [📄 许可证](#-许可证) @@ -334,6 +335,10 @@ PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还 - **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`。 - **领取返现**:使用 `client.claim_cashback_pumpfun()` 和 `client.claim_cashback_pumpswap(...)` 领取累计的返现。 +#### PumpFun:常见错误与排错思路 + +实盘集成时若遇 **Anchor 2006、`NotAuthorized`(6000)、Token program 不匹配、6020/6042** 等,请参阅专门文档:**[Pump.fun 常见链上错误与处理思路(中文)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)**。 + #### PumpFun:Creator Rewards Sharing(creator_vault) 部分 PumpFun 代币启用了 **Creator Rewards Sharing**,链上 `creator_vault` 可能与默认推导结果不同。若在**卖出**时复用**买入**时缓存的 params,可能触发程序错误 **2006(seeds constraint violated)**。建议: diff --git a/docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md b/docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md new file mode 100644 index 0000000..658a23f --- /dev/null +++ b/docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md @@ -0,0 +1,208 @@ +# Pump.fun(Bonding Curve)常见链上错误与处理思路 + +本文档面向使用 **sol-trade-sdk** 组装 Pump.fun Program(`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`)买卖交易的集成方,汇总**实战中高频**的失败形态、日志特征,以及与 SDK 参数的对应关系和**推荐处理方式**。 + +> 自定义错误码以仓库内 `idl/pump.json`、`idl/pump_fees.json` 为准;**2006** 等部分错误来自 **Anchor 框架**,不在 Pump 自定义枚举里。 + +--- + +## 1. Anchor `2006` / `ConstraintSeeds`(`creator_vault`) + +### 现象 + +- Solana Explorer / `simulateTransaction`:**Program Error: custom program error: 2006**,或 Anchor 文案 **A seeds constraint was violated**。 +- 日志里常见于账户 **`creator_vault`**:打印 **Left**(你传入的 pubkey)与 **Right**(程序按当前 curve 推导的 PDA)。 + +### 含义 + +Pump 校验 `creator_vault` 必须满足程序的 **PDAs seeds**(与 bonding curve 上记录的 **creator / fee-sharing 布局**一致)。传入地址与程序期望不一致即失败。 + +### 常见成因 + +1. **`bonding_curve.creator` 与链上不符**:事件/缓存里用的是 create 交易的 `creator` 字段、`user`,或陈旧快照,与 curve 账户内真实 creator 不一致;据此推导或缓存的 vault 会错。 +2. **买单侧「旧 vault」被沿用到卖单**:买入时 ix 里的 `creator_vault` 在后续 trade 语境下已不再与程序约束一致(例如 creator / sharing 语义在链上演进),卖单仍填旧地址 → **Left ≠ Right**。 +3. **Creator Rewards / pump-fees 分成布局**:部分 mint 使用 `sharing-config` 相关 seeds,单靠 `PDA(["creator-vault"], bonding_curve.creator)` 不足以覆盖全部历史状态;Stale 的 offline hint 也会把 resolve 引向错误 vault。 +4. **Phantom vault**:历史上若用 `Pubkey::default()` 推导出的占位 vault(SDK 常量 `phantom_default_creator_vault`),链上必定失败。 + +### SDK 侧处理思路 + +| 方向 | 做法 | +|------|------| +| **买入 / 卖出指令**(同一套解析) | 使用 `resolve_creator_vault_for_ix_with_fee_sharing`(`src/instruction/utils/pumpfun.rs`):有 **非 default、非 phantom** 的 ix / 解析器回填 **`creator_vault` 时原样采用**(**不会**再根据 `creator` 做 `get_creator_vault_pda` 覆盖费分成等非传统布局);**未传 ix vault** 时依次:`fee_sharing_creator_vault_if_active` hint → `PDA(effective_creator)`。`PumpFunInstructionBuilder` 买卖均走此逻辑。 | +| **权威对齐** | 低延迟路径外,可 **`PumpFunParams::from_mint_by_rpc`** 或由解析器 **`fill_trade_accounts`**(如 sol-parser-sdk)持续刷新 `creator` / `creator_vault`;必要时对 **fee-sharing** 使用 `fetch_fee_sharing_creator_vault_if_active` / `refresh_fee_sharing_creator_vault_from_rpc`。 | +| **bonding_curve 账户地址** | 指令构建时使用 **`get_bonding_curve_pda(mint)`** 作为 canonical bonding curve pubkey,避免缓存中的曲线地址错位导致读到错误 **creator**。 | + +集成方若在 **bot** 侧维护持仓快照,建议在**每笔**解析到的 Pump trade 后刷新缓存/仓位中的 `creator` 与 `creator_vault`,避免「只写一次建仓快照」。 + +--- + +## 2. Pump `6000` `NotAuthorized`(常见:`feeRecipient`) + +### 现象 + +- 日志:`AnchorError thrown in programs/pump/src/fee_recipient.rs` 或 **`Error Code: NotAuthorized`**(与 Global 授权的 fee recipient 池有关)。 + +### 含义 + +账户 **#2 fee recipient** 不是当前 Pump **Global / 协议**允许使用的收款地址之一,或与 **Mayhem / 非 Mayhem** 池不一致。 + +### 常见成因 + +1. 使用了**过期**或**错误池**的 fee recipient(静态列表落后于主网轮换)。 +2. **`mayhem_mode` 与 `fee_recipient` 不匹配**:声明 Mayhem 却传普通池地址,或相反(见 `reconcile_mayhem_mode_for_trade`,`src/instruction/utils/pumpfun.rs`)。 + +### SDK 侧处理思路 + +| 方向 | 做法 | +|------|------| +| **优先事件** | 使用 gRPC / 解析器里的 **`tradeEvent.feeRecipient`** 或同笔 **create_v2 + buy** 观测到的 fee recipient。 | +| **纠偏** | `PumpFunParams::from_trade` 会对 `mayhem_mode` 与 `fee_recipient` 做池一致性纠偏;发单前若事件缺省,可走 `pump_fun_fee_recipient_meta`(按 `is_mayhem_mode` 从静态池选)。 | +| **提交前清空** | 若你明确希望与 npm / 官方 SDK 一致的「按池随机」,可在业务层 **`fee_recipient = default`**,由 builder 再走静态池(与 README 中的 Cashback/Mayhem 说明一致)。 | + +--- + +## 3. SPL:Token **`initializeAccount3`**——`incorrect program id for instruction` + +### 现象 + +- 内联指令里 **`Token Program: initializeAccount3`**(或 Token-2022 等价指令)报错 **`incorrect program id for instruction`**。 + +### 含义 + +为 **Mint** 创建用户 ATA 时,使用的 **token program(Legacy SPL vs Token-2022)** 与 **Mint 的实际 owner(`mint.owner`)** 不一致。 + +### 常见成因 + +1. Pump 新发多为 **Token-2022**,但代码写死 **`Tokenkeg…`**。 +2. 少数 Legacy mint(`Tokenkeg…`),却被强制按 Token-2022 建账。 + +### SDK 侧处理思路 + +- **`PumpFunParams::token_program`** 必须与非 default 的 **mint owner** 一致;从 **gRPC / 解析结果** 带入,**勿**在已明确 program 时再用「仅按 `.pump` 后缀猜 Token-2022」覆盖(业务层若做后缀启发,应仅在 `token_program == default` 时生效)。 + +--- + +## 4. Pump `6020` `BuyZeroAmount` + +### 现象 + +- `buy` / `buy_exact_sol_in` 报 **Buy zero amount**。 + +### 常见成因 + +- `min_tokens_out == 0`(或等价路径算出可买 **0 枚**),协议直接拒绝。 +- 使用 **Create / Shred** 事件构造曲线时 **virtual / real 储备全 0**,本地定价算出 **0**。 + +### SDK 侧处理思路 + +- 对「首买 / 无储备」场景用 **`PumpFunParams::from_dev_trade`** 或按协议初值回填虚拟储备(与 `global_constants` 一致),再算 **`min_tokens_out`**。 +- 适当 **放宽买入滑点**(`slippage_basis_points`),避免估算代币量略小于链上。 + +--- + +## 5. Pump `6042` `BuySlippageBelowMinTokensOut` + +### 现象 + +- 文案:**Slippage: Would buy less tokens than expected min_tokens_out**。 + +### 含义 + +链上实际可成交代币数量 **小于** 指令参数 **`min_tokens_out`**。 + +### 常见成因 + +- 市价波动、SOL 竞价导致曲线状态与本地快照不一致。 +- 本地 **`get_buy_token_amount_from_sol_amount`** 所用 **creator / 费率假设**与链上 **pfee CPI**(动态费率)不一致,**预估偏多**。 + +### SDK 侧处理思路 + +- **提高滑点容忍**(或降低 **`min_tokens_out`**)。 +- 尽量用 **较新**的 **virtual/real reserves**(来自最近一次 trade 解析或简短 RPC)。 +- 若运行在 **狙击手**等极端延迟场景,需接受:**保守的 min_out**(更大滑点)换成功率。 + +--- + +## 6. Pump `6024` `Overflow` 与其它算术类错误 + +### 现象 + +- `6024 Overflow`、`6025 Truncation`、`6026 DivisionByZero`(见 IDL)。 + +### 常见成因 + +- 指令参数 **`amount` / SOL / lamports** 与曲线状态组合不合法(例如极端大卖、或为 0 与后续计算冲突)。 +- SDK 外传入了 **不合理的储备快照**。 + +### SDK 侧处理思路 + +- 校验 **买入/卖出金额 > 0**、与余额一致。 +- 使用 **`from_mint_by_rpc`** 或与链一致的储备后再算 **`min_sol_output` / `min_tokens_out`**。 + +--- + +## 7. Pump `6027` `NotEnoughRemainingAccounts`(返现等) + +### 现象 + +- 返现代币等路径要求 **remaining accounts**(例如 `UserVolumeAccumulator`),数量不足。 + +### 常见成因 + +- **`is_cashback_coin`(或等价标志)为 true**,但组装指令时 **未追加**所需账户。 + +### SDK 侧处理思路 + +- **`PumpFunParams::from_trade` / `from_dev_trade`** 传入正确的 **`is_cashback_coin`**(来自事件)。 +- README 中与 **Cashback** 章节一致:**事件路径必须带标志**,不能默认 false。 + +--- + +## 8. Pump `6022` `SellZeroAmount` + +### 含义 + +卖出代币数量为 **0**。在业务层过滤即可。 + +--- + +## 9. 与 pump-fees / Creator 迁移相关的错误(`6049`–`6053` 等) + +IDL 中例如: + +- **`6049` `CreatorMigratedToSharingConfig`** +- **`6050` `UnableToDistributeCreatorVaultMigratedToSharingConfig`** +- **`6053` `BondingCurveAndSharingConfigCreatorMismatch`** + +### 思路 + +这些是 **creator / fee-sharing** 生命周期中的**专用分支**,与一般买卖路径不同。若仿真或清算类指令触发: + +- 以 **Pump / pump-fees 官方文档** 为准使用 **`distribute_creator_fees`**、**reset_fee_sharing_config** 等; +- **`creator_vault` resolve** 需结合 **`fetch_fee_sharing_creator_vault_if_active`** 与链上 **`SharingConfig` 状态**,避免离线 deduce 过时。 + +--- + +## 10. 调试清单(推荐给集成方) + +1. **记下失败指令索引** + **Simulation / explorer 展开的账户列表**,重点核对:**mint、bonding_curve、associated_bonding_curve、creator_vault、fee_recipient、token_program**。 +2. **对比 Anchor 日志里的 Left / Right**(针对 2006)与本地 `PumpFunParams` 打印是否一致。 +3. **`mint.owner`** 与 **`PumpFunParams.token_program`** 是否一致。 +4. **`bonding_curve` 地址**是否与 **`get_bonding_curve_pda(mint)`** 一致。 +5. **`mayhem_mode` ↔ `fee_recipient`** 是否同池。 +6. 低延迟不足以覆盖 **creator 演进** 时,是否在卖前引入了 **RPC 或最新 trade** 刷新。 + +--- + +## 参考代码入口(本仓库) + +| 主题 | 路径 | +|------|------| +| Buy / Sell vault resolve(显式 ix `creator_vault` → `fee_sharing` hint → `PDA(effective_creator)`) | `src/instruction/utils/pumpfun.rs` — `resolve_creator_vault_for_ix_with_fee_sharing`;`effective_creator_for_trade` 见 `src/trading/core/params/pumpfun.rs` | +| Fee recipient / Mayhem | `src/instruction/utils/pumpfun.rs` — `pump_fun_fee_recipient_meta`, `reconcile_mayhem_mode_for_trade` | +| 指令构建 | `src/instruction/pumpfun.rs` — `PumpFunInstructionBuilder` | +| Params | `src/trading/core/params/pumpfun.rs` — `PumpFunParams::{from_trade, from_dev_trade, from_mint_by_rpc, refresh_fee_sharing_creator_vault_from_rpc}` | + +--- + +如需英文版或对 PumpSwap/其它 DEX 的同类文档,可在 `docs/` 下按相同结构扩展。 diff --git a/src/client/mod.rs b/src/client/mod.rs index f11a5a1..d87d6df 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -372,7 +372,7 @@ pub struct TradeBuyParams { /// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input. /// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes. pub use_exact_sol_amount: Option, - /// 可选:事件收到时间(微秒,与 sol-parser-sdk 的 metadata.grpc_recv_us / clock::now_micros 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点,打印起点→提交耗时。 + /// Optional upstream receive timestamp (e.g. gRPC recv) in microseconds for latency tracing. pub grpc_recv_us: Option, } @@ -418,7 +418,7 @@ pub struct TradeSellParams { pub gas_fee_strategy: GasFeeStrategy, /// Whether to simulate the transaction instead of executing it pub simulate: bool, - /// 可选:事件收到时间(微秒,与 sol-parser-sdk clock 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点。 + /// Optional upstream receive timestamp (e.g. gRPC recv) in microseconds for latency tracing. pub grpc_recv_us: Option, } diff --git a/src/common/fast_fn.rs b/src/common/fast_fn.rs index 78e806a..8330021 100644 --- a/src/common/fast_fn.rs +++ b/src/common/fast_fn.rs @@ -154,6 +154,8 @@ pub enum PdaCacheKey { PumpFunUserVolume(Pubkey), PumpFunBondingCurve(Pubkey), PumpFunBondingCurveV2(Pubkey), + /// Pump-fees program `sharing-config` PDA for a mint (`feeSharingConfigPda`). + PumpFunFeeSharingConfig(Pubkey), PumpFunCreatorVault(Pubkey), BonkPool(Pubkey, Pubkey), BonkVault(Pubkey, Pubkey), diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs old mode 100755 new mode 100644 index db89128..83b5a5f --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -1,3 +1,5 @@ +//! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)). + use crate::{ common::spl_token::close_account, constants::{trade::trade::DEFAULT_SLIPPAGE, TOKEN_PROGRAM_2022}, @@ -26,30 +28,37 @@ use anyhow::{anyhow, Result}; use solana_sdk::instruction::AccountMeta; use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer}; -/// Instruction builder for PumpFun protocol +#[inline] +fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey { + let tp = protocol_params.token_program; + if tp == Pubkey::default() { + TOKEN_PROGRAM_2022 + } else { + tp + } +} + pub struct PumpFunInstructionBuilder; #[async_trait::async_trait] impl InstructionBuilder for PumpFunInstructionBuilder { async fn build_buy_instructions(&self, params: &SwapParams) -> Result> { - // ======================================== - // Parameter validation and basic data preparation - // ======================================== let protocol_params = params .protocol_params .as_any() .downcast_ref::() .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; - if params.input_amount.unwrap_or(0) == 0 { + 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; - // 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_with_fee_sharing( + 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, @@ -62,9 +71,6 @@ impl InstructionBuilder for PumpFunInstructionBuilder { ) })?; - // ======================================== - // Trade calculation and account address preparation - // ======================================== let buy_token_amount = match params.fixed_output_amount { Some(amount) => amount, None => get_buy_token_amount_from_sol_amount( @@ -72,25 +78,19 @@ impl InstructionBuilder for PumpFunInstructionBuilder { bonding_curve.virtual_sol_reserves as u128, bonding_curve.real_token_reserves as u128, creator, - params.input_amount.unwrap_or(0), + lamports_in, ), }; - let max_sol_cost = calculate_with_slippage_buy( - params.input_amount.unwrap_or(0), - params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), - ); + let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp); - // 始终用 mint 推导 canonical bonding curve PDA。缓存里的 `bonding_curve.account` 可能指向其它池子, - // 会导致链上读到错误 `creator`,从而 creator_vault seeds 与传入的 vault 不一致(Anchor 2006)。 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) })?; - // Determine token program based on mayhem mode let is_mayhem_mode = bonding_curve.is_mayhem_mode; - let token_program = protocol_params.token_program; - let token_program_meta = if protocol_params.token_program == TOKEN_PROGRAM_2022 { + let token_program = effective_pump_mint_token_program(protocol_params); + let token_program_meta = if token_program == TOKEN_PROGRAM_2022 { crate::constants::TOKEN_PROGRAM_2022_META } else { crate::constants::TOKEN_PROGRAM_META @@ -114,14 +114,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder { let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()) .ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?; - // ======================================== - // 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 if params.create_output_mint_ata { instructions.extend( crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed( @@ -135,12 +129,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder { } 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), - ); + let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp); encode_pumpfun_buy_exact_sol_in_ix_data( - params.input_amount.unwrap_or(0), + lamports_in, min_tokens_out, TRACK_VOLUME_TRUE, ) @@ -148,14 +139,13 @@ impl InstructionBuilder for PumpFunInstructionBuilder { 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 = 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 accounts: Vec = vec![ + let mut metas: Vec = vec![ global_constants::GLOBAL_ACCOUNT_META, fee_recipient_meta, AccountMeta::new_readonly(params.output_mint, false), @@ -165,7 +155,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder { AccountMeta::new(params.payer.pubkey(), true), crate::constants::SYSTEM_PROGRAM_META, token_program_meta, - AccountMeta::new(creator_vault_pda, false), + AccountMeta::new(creator_vault_account, false), accounts::EVENT_AUTHORITY_META, accounts::PUMPFUN_META, accounts::GLOBAL_VOLUME_ACCUMULATOR_META, @@ -173,19 +163,22 @@ impl InstructionBuilder for PumpFunInstructionBuilder { accounts::FEE_CONFIG_META, accounts::FEE_PROGRAM_META, ]; - accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false)); // remainingAccounts: @pump-fun/pump-sdk 要求末尾传 bondingCurveV2Pda(mint),勿删 - // Apr 2026: extra protocol fee recipient after bonding-curve-v2 (writable) - accounts.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false)); + metas.push(AccountMeta::new_readonly(bonding_curve_v2, false)); + metas.push(AccountMeta::new( + get_protocol_extra_fee_recipient_random(), + false, + )); - instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, accounts)); + instructions.push(Instruction::new_with_bytes( + accounts::PUMPFUN, + &buy_data, + metas, + )); Ok(instructions) } async fn build_sell_instructions(&self, params: &SwapParams) -> Result> { - // ======================================== - // Parameter validation and basic data preparation - // ======================================== let protocol_params = params .protocol_params .as_any() @@ -201,9 +194,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder { 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; - let creator = bonding_curve.creator; - let creator_vault_pda = resolve_creator_vault_for_ix_with_fee_sharing( + 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.input_mint, @@ -216,9 +211,6 @@ impl InstructionBuilder for PumpFunInstructionBuilder { ) })?; - // ======================================== - // Trade calculation and account address preparation - // ======================================== let sol_amount = get_sell_sol_amount_from_token_amount( bonding_curve.virtual_token_reserves as u128, bonding_curve.virtual_sol_reserves as u128, @@ -228,20 +220,16 @@ impl InstructionBuilder for PumpFunInstructionBuilder { let min_sol_output = match params.fixed_output_amount { Some(fixed) => fixed, - None => calculate_with_slippage_sell( - sol_amount, - params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), - ), + None => calculate_with_slippage_sell(sol_amount, slippage_bp), }; let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| { anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint) })?; - // Determine token program based on mayhem mode let is_mayhem_mode = bonding_curve.is_mayhem_mode; - let token_program = protocol_params.token_program; - let token_program_meta = if protocol_params.token_program == TOKEN_PROGRAM_2022 { + let token_program = effective_pump_mint_token_program(protocol_params); + let token_program_meta = if token_program == TOKEN_PROGRAM_2022 { crate::constants::TOKEN_PROGRAM_2022_META } else { crate::constants::TOKEN_PROGRAM_META @@ -262,17 +250,12 @@ impl InstructionBuilder for PumpFunInstructionBuilder { params.open_seed_optimize, ); - // ======================================== - // Build instructions - // ======================================== 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 mut accounts: Vec = vec![ + let mut metas: Vec = vec![ global_constants::GLOBAL_ACCOUNT_META, fee_recipient_meta, AccountMeta::new_readonly(params.input_mint, false), @@ -281,7 +264,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder { AccountMeta::new(user_token_account, false), AccountMeta::new(params.payer.pubkey(), true), crate::constants::SYSTEM_PROGRAM_META, - AccountMeta::new(creator_vault_pda, false), + AccountMeta::new(creator_vault_account, false), token_program_meta, accounts::EVENT_AUTHORITY_META, accounts::PUMPFUN_META, @@ -289,23 +272,28 @@ impl InstructionBuilder for PumpFunInstructionBuilder { accounts::FEE_PROGRAM_META, ]; - // Cashback: Bonding Curve Sell expects UserVolumeAccumulator PDA at 0th remaining account (writable) if bonding_curve.is_cashback_coin { let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()) .ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?; - accounts.push(AccountMeta::new(user_volume_accumulator, false)); + metas.push(AccountMeta::new(user_volume_accumulator, false)); } - // remainingAccounts: @pump-fun/pump-sdk sell 要求末尾传 bondingCurveV2Pda(mint)(cashback 时在 user_volume_accumulator 之后),勿删 + 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) })?; - accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false)); - accounts.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false)); + metas.push(AccountMeta::new_readonly(bonding_curve_v2, false)); + metas.push(AccountMeta::new( + get_protocol_extra_fee_recipient_random(), + false, + )); - instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, accounts)); + instructions.push(Instruction::new_with_bytes( + accounts::PUMPFUN, + &sell_data, + metas, + )); - // Optional: Close token account if protocol_params.close_token_account_when_sell.unwrap_or(false) || params.close_input_mint_ata { @@ -322,16 +310,20 @@ impl InstructionBuilder for PumpFunInstructionBuilder { } } -/// Claim cashback for Bonding Curve (Pump program). Transfers native lamports from UserVolumeAccumulator to user. +/// Claim cashback (UserVolumeAccumulator → user lamports). pub fn claim_cashback_pumpfun_instruction(payer: &Pubkey) -> Option { const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197]; let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?; - let accounts = vec![ - AccountMeta::new(*payer, true), // user (signer, writable) - AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable, not signer) + let ix_accounts = vec![ + AccountMeta::new(*payer, true), + AccountMeta::new(user_volume_accumulator, false), crate::constants::SYSTEM_PROGRAM_META, accounts::EVENT_AUTHORITY_META, accounts::PUMPFUN_META, ]; - Some(Instruction::new_with_bytes(accounts::PUMPFUN, &CLAIM_CASHBACK_DISCRIMINATOR, accounts)) + Some(Instruction::new_with_bytes( + accounts::PUMPFUN, + &CLAIM_CASHBACK_DISCRIMINATOR, + ix_accounts, + )) } diff --git a/src/instruction/utils/pumpfun.rs b/src/instruction/utils/pumpfun.rs index bd8cb9f..7b2e24a 100644 --- a/src/instruction/utils/pumpfun.rs +++ b/src/instruction/utils/pumpfun.rs @@ -1,3 +1,7 @@ +//! Pump.fun bonding-curve utilities (flat module): PDAs, fee `#2`, pump-fees `SharingConfig`, cold RPC. +//! +//! Hot swap ix assembly stays sync; async helpers at file bottom. Layout matches `@pump-fun/pump-sdk`. + use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient}; use anyhow::anyhow; use rand::seq::IndexedRandom; @@ -7,231 +11,181 @@ use solana_sdk::{ }; 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; +// --- seeds ------------------------------------------------------------- -/// 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 - pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve"; - /// Seed for bonding curve v2 PDA (required by program upgrade, readonly at end of account list) - pub const BONDING_CURVE_V2_SEED: &[u8] = b"bonding-curve-v2"; - /// Seed for creator vault PDAs - pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault"; - - /// Seed for metadata PDAs - pub const METADATA_SEED: &[u8] = b"metadata"; - - /// Seed for user volume accumulator PDAs - pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator"; - - /// Seed for global volume accumulator PDAs - pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator"; - - pub const FEE_CONFIG_SEED: &[u8] = b"fee_config"; - - /// `feeSharingConfig` PDA under pump-fees (`@pump-fun/pump-sdk` `feeSharingConfigPda`) - pub const SHARING_CONFIG_SEED: &[u8] = b"sharing-config"; +/// Seed for bonding curve PDAs (`["bonding-curve", mint]`). +pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve"; +/// Seed for bonding curve v2 PDA (`["bonding-curve-v2", mint]`). +pub const BONDING_CURVE_V2_SEED: &[u8] = b"bonding-curve-v2"; +/// Creator vault PDA seeds prefix (`["creator-vault", authority]`). +pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault"; +/// Metadata PDA seeds prefix. +pub const METADATA_SEED: &[u8] = b"metadata"; +/// User volume accumulator for cashback / bonding-curve UX. +pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator"; +/// Global volume accumulator. +pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator"; +pub const FEE_CONFIG_SEED: &[u8] = b"fee_config"; +/// `feeSharingConfig` PDA under pump-fees (`feeSharingConfigPda`). +pub const SHARING_CONFIG_SEED: &[u8] = b"sharing-config"; } + pub mod global_constants { - use solana_sdk::{pubkey, pubkey::Pubkey}; - pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000; - pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000; +use solana_sdk::{pubkey, pubkey::Pubkey}; - pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000; +pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000; +pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000; +pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000; +pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000; +pub const FEE_BASIS_POINTS: u64 = 95; +pub const ENABLE_MIGRATE: bool = false; +pub const POOL_MIGRATION_FEE: u64 = 15_000_001; +pub const CREATOR_FEE: u64 = 30; +pub const SCALE: u64 = 1_000_000; +pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; +pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; - pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000; +pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); +pub const FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta = + solana_sdk::instruction::AccountMeta { + pubkey: FEE_RECIPIENT, + is_signer: false, + is_writable: true, + }; - pub const FEE_BASIS_POINTS: u64 = 95; +pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [ + pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"), + pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"), + pubkey!("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"), + pubkey!("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"), + pubkey!("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"), + pubkey!("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"), + pubkey!("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"), + pubkey!("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"), +]; +pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0]; +pub const MAYHEM_FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta = + solana_sdk::instruction::AccountMeta { + pubkey: MAYHEM_FEE_RECIPIENT, + is_signer: false, + is_writable: true, + }; - pub const ENABLE_MIGRATE: bool = false; +pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); +pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta = + solana_sdk::instruction::AccountMeta { + pubkey: GLOBAL_ACCOUNT, + is_signer: false, + is_writable: false, + }; - pub const POOL_MIGRATION_FEE: u64 = 15_000_001; +pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF"); +pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"); - pub const CREATOR_FEE: u64 = 30; +pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); +pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); +pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); +pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); +pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); +pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); +pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"); - pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals - - pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports - - pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL - - /// Public key for the fee recipient - pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); - pub const FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta = - solana_sdk::instruction::AccountMeta { - pubkey: FEE_RECIPIENT, - is_signer: false, - is_writable: true, - }; - - /// Mayhem fee recipients (pump-public-docs: use any one randomly) - pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [ - pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"), - pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"), - pubkey!("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"), - pubkey!("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"), - pubkey!("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"), - pubkey!("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"), - pubkey!("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"), - pubkey!("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"), - ]; - pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0]; - pub const MAYHEM_FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta = - solana_sdk::instruction::AccountMeta { - pubkey: MAYHEM_FEE_RECIPIENT, - is_signer: false, - is_writable: true, - }; - - /// Public key for the global PDA - pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); - pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta = - solana_sdk::instruction::AccountMeta { - pubkey: GLOBAL_ACCOUNT, - is_signer: false, - is_writable: false, - }; - - /// Public key for the authority - pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF"); - - /// Public key for the withdraw authority - pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"); - - pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1 - pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2 - pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3 - pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4 - pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5 - pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6 - pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"); - // Pump.fun AMM: Protocol Fee 7 - - /// Protocol extra fee recipients (Apr 2026 breaking upgrade). One is appended after `bonding-curve-v2`, **writable**. - /// See: - pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [ - pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"), - pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"), - pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"), - pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"), - pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"), - pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"), - pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"), - pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"), - ]; +pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [ + pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"), + pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"), + pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"), + pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"), + pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"), + pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"), + pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"), + pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"), +]; } -/// Constants related to program accounts and authorities + pub mod accounts { - use solana_sdk::{pubkey, pubkey::Pubkey}; - /// Public key for the Pump.fun program - pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); - /// Public key for the MPL Token Metadata program - pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); +use solana_sdk::{pubkey, pubkey::Pubkey}; - /// Authority for program events - pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"); +pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); +pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); +pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"); +pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = + pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); +pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); +pub const FEE_PROGRAM: Pubkey = pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"); +pub const GLOBAL_VOLUME_ACCUMULATOR: Pubkey = + pubkey!("Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y"); +pub const FEE_CONFIG: Pubkey = pubkey!("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt"); - /// Associated Token Program ID - pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = - pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); +pub const PUMPFUN_META: solana_sdk::instruction::AccountMeta = + solana_sdk::instruction::AccountMeta { + pubkey: PUMPFUN, + is_signer: false, + is_writable: false, + }; - pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); +pub const EVENT_AUTHORITY_META: solana_sdk::instruction::AccountMeta = + solana_sdk::instruction::AccountMeta { + pubkey: EVENT_AUTHORITY, + is_signer: false, + is_writable: false, + }; - pub const FEE_PROGRAM: Pubkey = pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"); +pub const FEE_PROGRAM_META: solana_sdk::instruction::AccountMeta = + solana_sdk::instruction::AccountMeta { + pubkey: FEE_PROGRAM, + is_signer: false, + is_writable: false, + }; - pub const GLOBAL_VOLUME_ACCUMULATOR: Pubkey = - pubkey!("Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y"); +pub const GLOBAL_VOLUME_ACCUMULATOR_META: solana_sdk::instruction::AccountMeta = + solana_sdk::instruction::AccountMeta { + pubkey: GLOBAL_VOLUME_ACCUMULATOR, + is_signer: false, + is_writable: true, + }; - pub const FEE_CONFIG: Pubkey = pubkey!("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt"); - - // META - pub const PUMPFUN_META: solana_sdk::instruction::AccountMeta = - solana_sdk::instruction::AccountMeta { - pubkey: PUMPFUN, - is_signer: false, - is_writable: false, - }; - - pub const EVENT_AUTHORITY_META: solana_sdk::instruction::AccountMeta = - solana_sdk::instruction::AccountMeta { - pubkey: EVENT_AUTHORITY, - is_signer: false, - is_writable: false, - }; - - pub const FEE_PROGRAM_META: solana_sdk::instruction::AccountMeta = - solana_sdk::instruction::AccountMeta { - pubkey: FEE_PROGRAM, - is_signer: false, - is_writable: false, - }; - - pub const GLOBAL_VOLUME_ACCUMULATOR_META: solana_sdk::instruction::AccountMeta = - solana_sdk::instruction::AccountMeta { - pubkey: GLOBAL_VOLUME_ACCUMULATOR, - is_signer: false, - is_writable: true, - }; - - pub const FEE_CONFIG_META: solana_sdk::instruction::AccountMeta = - solana_sdk::instruction::AccountMeta { - pubkey: FEE_CONFIG, - is_signer: false, - is_writable: false, - }; +pub const FEE_CONFIG_META: solana_sdk::instruction::AccountMeta = + solana_sdk::instruction::AccountMeta { + pubkey: FEE_CONFIG, + is_signer: false, + is_writable: false, + }; } -/// Instruction discriminators for PumpFun program + +// --- Anchor / layout constants --------------------------------------- + + +/// Minimum bonding curve account data length (`sdk.ts` `BONDING_CURVE_NEW_SIZE`). +pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151; + 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 EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229]; + 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; +pub(crate) const SHARING_CONFIG_STATUS_ACTIVE: u8 = 1; + + +// --- Fee recipient pools ----------------------------------------------- -/// Check if a pubkey is one of the Mayhem fee recipients #[inline] pub fn is_mayhem_fee_recipient(pubkey: &Pubkey) -> bool { - global_constants::MAYHEM_FEE_RECIPIENTS.iter().any(|p| p == pubkey) + global_constants::MAYHEM_FEE_RECIPIENTS.contains(pubkey) } -/// Check if a pubkey is a Pump.fun AMM protocol fee recipient (PUMPFUN_AMM_FEE_1..7) #[inline] pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool { pubkey == &global_constants::PUMPFUN_AMM_FEE_1 @@ -243,14 +197,11 @@ 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, @@ -275,7 +226,6 @@ pub fn reconcile_mayhem_mode_for_trade( } } -/// 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); @@ -287,18 +237,18 @@ pub fn fee_recipient_ok_for_bonding_curve_mode(pk: &Pubkey, is_mayhem_mode: bool } } -/// 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 .choose(&mut rand::rng()) .unwrap_or(&global_constants::MAYHEM_FEE_RECIPIENTS[0]); - AccountMeta { pubkey: recipient, is_signer: false, is_writable: true } + 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] = &[ @@ -321,7 +271,6 @@ pub fn get_standard_fee_recipient_meta_random() -> AccountMeta { } } -/// Random entry from [`global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS`] (must be last account after bonding-curve-v2, writable). #[inline] pub fn get_protocol_extra_fee_recipient_random() -> Pubkey { *global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS @@ -329,14 +278,13 @@ 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`);未提供或与 `is_mayhem_mode` 池不一致时按 mayhem 从静态池随机(避免 NotAuthorized)。 #[inline] -pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta { - let trust_stream = from_stream != Pubkey::default() - && fee_recipient_ok_for_bonding_curve_mode(&from_stream, is_mayhem_mode); - if trust_stream { +pub fn pump_fun_fee_recipient_meta(observed_fee_recipient: Pubkey, is_mayhem_mode: bool) -> AccountMeta { + let trust_observation = observed_fee_recipient != Pubkey::default() + && fee_recipient_ok_for_bonding_curve_mode(&observed_fee_recipient, is_mayhem_mode); + if trust_observation { AccountMeta { - pubkey: from_stream, + pubkey: observed_fee_recipient, is_signer: false, is_writable: true, } @@ -347,12 +295,29 @@ pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> } } -pub struct Symbol; +// --- Extend bonding curve (cold path) -------------------------------- -impl Symbol { - pub const SOLANA: &'static str = "solana"; +#[inline] +pub fn extend_bonding_curve_account_instruction( + bonding_curve: &Pubkey, + user: &Pubkey, +) -> Instruction { + Instruction::new_with_bytes( + accounts::PUMPFUN, + &EXTEND_ACCOUNT_DISCRIMINATOR, + vec![ + AccountMeta::new(*bonding_curve, false), + AccountMeta::new(*user, true), + crate::constants::SYSTEM_PROGRAM_META, + accounts::EVENT_AUTHORITY_META, + accounts::PUMPFUN_META, + ], + ) } + +// --- Cached PDAs + creator_vault resolve ------------------------------ + #[inline] pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option { crate::common::fast_fn::get_cached_pda( @@ -360,13 +325,11 @@ pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option { || { let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_SEED, mint.as_ref()]; let program_id: &Pubkey = &accounts::PUMPFUN; - let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); - pda.map(|pubkey| pubkey.0) + Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0) }, ) } -/// Bonding curve v2 PDA (seeds: ["bonding-curve-v2", mint]). Required at end of buy/sell/buy_exact_sol_in accounts. #[inline] pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option { crate::common::fast_fn::get_cached_pda( @@ -374,8 +337,7 @@ pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option { || { let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_V2_SEED, mint.as_ref()]; let program_id: &Pubkey = &accounts::PUMPFUN; - let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); - pda.map(|pubkey| pubkey.0) + Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0) }, ) } @@ -385,7 +347,6 @@ pub fn get_creator(creator_vault_pda: &Pubkey) -> Pubkey { if creator_vault_pda.eq(&Pubkey::default()) { Pubkey::default() } else { - // Fast check against cached default creator vault static DEFAULT_CREATOR_VAULT: std::sync::LazyLock> = std::sync::LazyLock::new(|| get_creator_vault_pda(&Pubkey::default())); match DEFAULT_CREATOR_VAULT.as_ref() { @@ -402,24 +363,25 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option { || { let seeds: &[&[u8]; 2] = &[seeds::CREATOR_VAULT_SEED, creator.as_ref()]; let program_id: &Pubkey = &accounts::PUMPFUN; - let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); - pda.map(|pubkey| pubkey.0) + Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0) }, ) } -/// `feeSharingConfig` PDA per mint (`pump-sdk` `feeSharingConfigPda` → `pump-fees` program). #[inline] pub fn get_fee_sharing_config_pda(mint: &Pubkey) -> Option { - Pubkey::try_find_program_address( - &[seeds::SHARING_CONFIG_SEED, mint.as_ref()], - &accounts::FEE_PROGRAM, + crate::common::fast_fn::get_cached_pda( + crate::common::fast_fn::PdaCacheKey::PumpFunFeeSharingConfig(*mint), + || { + Pubkey::try_find_program_address( + &[seeds::SHARING_CONFIG_SEED, mint.as_ref()], + &accounts::FEE_PROGRAM, + ) + .map(|(p, _)| p) + }, ) - .map(|(p, _)| p) } -/// PDA of `["creator-vault", Pubkey::default()]`. Never use as a real vault — it is only produced when -/// `creator` was missing and code incorrectly derived a vault; on-chain this fails with Anchor 2006. #[inline] pub fn phantom_default_creator_vault() -> Pubkey { solana_sdk::pubkey!("2DR3iqRPVThyRLVJnwjPW1qiGWrp8RUFfHVjMbZyhdNc") @@ -430,21 +392,6 @@ pub fn is_phantom_default_creator_vault(pk: &Pubkey) -> bool { *pk == phantom_default_creator_vault() } -/// Resolve `creator_vault` for Pump buy/sell account #10. -/// -/// - If `creator` is **missing** in the outer trade-event borsh (`Pubkey::default()`) but -/// `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)` → 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, @@ -454,78 +401,75 @@ pub fn resolve_creator_vault_for_ix( 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). +/// Resolves Pump.fun bonding-curve buy/sell **account `#10` (`creator_vault`)** for ix assembly. +/// +/// **Priority (highest first)** +/// 1. **Explicit `creator_vault`** from ix / parser / cached observation when non-default and not the phantom +/// sentinel — **always used as-is** (no remap to [`get_creator_vault_pda`] from `creator`); +/// fee-sharing / multi-party layouts rely on upstream passing the vault the program expects (`pfee…` / `update_fee_shares`). +/// 2. If ix vault missing: optional `fee_sharing_creator_vault_if_active` hint (non-default, non-phantom). +/// 3. Else: [`get_creator_vault_pda`] from `creator` when `creator` is known. #[inline] pub fn resolve_creator_vault_for_ix_with_fee_sharing( creator: &Pubkey, creator_vault_from_event: Pubkey, - mint: &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_vault_from_event != Pubkey::default() && creator_vault_from_event != phantom { + return Some(creator_vault_from_event); + } + + if let Some(v) = fee_sharing_creator_vault_if_active { + if v != Pubkey::default() && v != phantom { + return Some(v); + } + } if *creator == Pubkey::default() { - if creator_vault_from_event == Pubkey::default() { - return None; - } - if creator_vault_from_event == phantom { - return None; - } - return Some(creator_vault_from_event); + return None; } - 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); - } - // 仅当 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) + get_creator_vault_pda(creator) } -/// 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 fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option { + crate::common::fast_fn::get_cached_pda( + crate::common::fast_fn::PdaCacheKey::PumpFunUserVolume(*user), + || { + let seed: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()]; + let program_id: &Pubkey = &accounts::PUMPFUN; + Pubkey::try_find_program_address(seed, program_id).map(|pubkey| pubkey.0) + }, + ) +} + +#[inline] +pub fn get_buy_price( + amount: u64, + virtual_sol_reserves: u64, + virtual_token_reserves: u64, + real_token_reserves: u64, +) -> u64 { + if amount == 0 { + return 0; + } + + let n: u128 = (virtual_sol_reserves as u128) * (virtual_token_reserves as u128); + let i: u128 = (virtual_sol_reserves as u128) + (amount as u128); + let r: u128 = n / i + 1; + let s: u128 = (virtual_token_reserves as u128) - r; + let s_u64 = s as u64; + + s_u64.min(real_token_reserves) +} + + +// --- RPC ------------------------------------------------------------- + #[inline] pub async fn fetch_fee_sharing_creator_vault_if_active( rpc: &SolanaRpcClient, @@ -551,7 +495,7 @@ pub async fn fetch_fee_sharing_creator_vault_if_active( let mint_on_chain = Pubkey::new_from_array( d[11..43] .try_into() - .map_err(|_| anyhow::anyhow!("SharingConfig mint slice"))?, + .map_err(|_| anyhow!("SharingConfig mint slice"))?, ); if mint_on_chain != *mint { return Ok(None); @@ -559,26 +503,13 @@ pub async fn fetch_fee_sharing_creator_vault_if_active( 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( - crate::common::fast_fn::PdaCacheKey::PumpFunUserVolume(*user), - || { - let seeds: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()]; - let program_id: &Pubkey = &accounts::PUMPFUN; - let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); - pda.map(|pubkey| pubkey.0) - }, - ) -} - #[inline] pub async fn fetch_bonding_curve_account( rpc: &SolanaRpcClient, mint: &Pubkey, ) -> Result<(Arc, Pubkey), anyhow::Error> { let bonding_curve_pda: Pubkey = - get_bonding_curve_pda(mint).ok_or(anyhow!("Bonding curve not found"))?; + get_bonding_curve_pda(mint).ok_or_else(|| anyhow!("Bonding curve not found"))?; let account = rpc.get_account(&bonding_curve_pda).await?; if account.data.is_empty() { @@ -592,29 +523,16 @@ pub async fn fetch_bonding_curve_account( Ok((Arc::new(bonding_curve), bonding_curve_pda)) } -#[inline] -pub fn get_buy_price( - amount: u64, - virtual_sol_reserves: u64, - virtual_token_reserves: u64, - real_token_reserves: u64, -) -> u64 { - if amount == 0 { - return 0; - } - let n: u128 = (virtual_sol_reserves as u128) * (virtual_token_reserves as u128); - let i: u128 = (virtual_sol_reserves as u128) + (amount as u128); - let r: u128 = n / i + 1; - let s: u128 = (virtual_token_reserves as u128) - r; - let s_u64 = s as u64; - - s_u64.min(real_token_reserves) -} #[cfg(test)] mod tests { - use super::*; + use super::{ + global_constants, + phantom_default_creator_vault, pump_fun_fee_recipient_meta, + reconcile_mayhem_mode_for_trade, resolve_creator_vault_for_ix, + resolve_creator_vault_for_ix_with_fee_sharing, *, + }; use solana_sdk::pubkey::Pubkey; #[test] @@ -651,7 +569,11 @@ mod tests { #[test] fn default_creator_yields_fixed_creator_vault() { let v = get_creator_vault_pda(&Pubkey::default()).unwrap(); - assert_eq!(v, phantom_default_creator_vault(), "phantom vault constant must match PDA(default creator)"); + assert_eq!( + v, + phantom_default_creator_vault(), + "phantom vault constant must match PDA(default creator)" + ); } #[test] @@ -675,13 +597,17 @@ mod tests { fn resolve_rejects_phantom_vault_when_creator_borsh_is_default() { let mint = Pubkey::new_unique(); assert_eq!( - resolve_creator_vault_for_ix(&Pubkey::default(), phantom_default_creator_vault(), &mint), + resolve_creator_vault_for_ix( + &Pubkey::default(), + phantom_default_creator_vault(), + &mint, + ), None ); } #[test] - fn resolve_prefers_fee_sharing_vault_when_rpc_hint_and_differs_from_creator_pda() { + fn resolve_prefers_creator_pda_ix_vault_even_when_fee_sharing_hint_differs() { let creator = Pubkey::new_unique(); let mint = Pubkey::new_unique(); let v_derived = get_creator_vault_pda(&creator).unwrap(); @@ -694,7 +620,28 @@ mod tests { &mint, Some(vs), ); - assert_eq!(resolved, Some(vs)); + assert_eq!( + resolved, + Some(v_derived), + "observed ix uses PDA(creator); stale hint must not override → wrong vault / Anchor 2006" + ); + } + + #[test] + fn resolve_trusts_ix_fee_sharing_vault_when_creator_known() { + let creator = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap(); + let vs = get_creator_vault_pda(&sharing_pk).unwrap(); + let creator_vault = get_creator_vault_pda(&creator).unwrap(); + assert_ne!(vs, creator_vault); + let resolved = + resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, Some(vs)); + assert_eq!( + resolved, + Some(vs), + "explicit ix creator_vault (e.g. fee-sharing #8) wins; no remap to PDA(creator)" + ); } #[test] @@ -709,7 +656,19 @@ mod tests { } #[test] - fn resolve_stale_fee_sharing_vault_falls_back_to_creator_pda_when_hint_inactive() { + fn resolve_ix_vault_always_wins_when_non_default_even_if_creator_pda_differs() { + let creator = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let v_derived = get_creator_vault_pda(&creator).unwrap(); + let ix_other = Pubkey::new_unique(); + assert_ne!(ix_other, v_derived); + let resolved = + resolve_creator_vault_for_ix_with_fee_sharing(&creator, ix_other, &mint, None); + assert_eq!(resolved, Some(ix_other)); + } + + #[test] + fn resolve_fee_sharing_ix_vault_used_as_is_even_without_hint() { let creator = Pubkey::new_unique(); let mint = Pubkey::new_unique(); let v_derived = get_creator_vault_pda(&creator).unwrap(); @@ -717,11 +676,22 @@ mod tests { 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)" + assert_eq!(resolved, Some(vs)); + } + + #[test] + fn resolve_falls_back_to_fee_sharing_hint_when_ix_vault_placeholder() { + let creator = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap(); + let vs = get_creator_vault_pda(&sharing_pk).unwrap(); + let resolved = resolve_creator_vault_for_ix_with_fee_sharing( + &creator, + Pubkey::default(), + &mint, + Some(vs), ); + assert_eq!(resolved, Some(vs)); } #[test] diff --git a/src/perf/hardware_optimizations.rs b/src/perf/hardware_optimizations.rs index 368c24b..7815817 100644 --- a/src/perf/hardware_optimizations.rs +++ b/src/perf/hardware_optimizations.rs @@ -447,23 +447,23 @@ impl BranchOptimizer { /// Prefetch: load cache line at ptr into L1. Caller must ensure ptr is valid, read-only, no concurrent write. 预取:将 ptr 所在缓存行加载到 L1;调用方需保证有效、只读、无并发写。 #[inline(always)] - pub unsafe fn prefetch_read_data(ptr: *const T) { + pub unsafe fn prefetch_read_data(_ptr: *const T) { #[cfg(target_arch = "x86_64")] { use std::arch::x86_64::_mm_prefetch; use std::arch::x86_64::_MM_HINT_T0; - _mm_prefetch(ptr as *const i8, _MM_HINT_T0); + _mm_prefetch(_ptr as *const i8, _MM_HINT_T0); } } /// Prefetch for write (T1 hint). 写预取(T1 提示)。 #[inline(always)] - pub unsafe fn prefetch_write_data(ptr: *const T) { + pub unsafe fn prefetch_write_data(_ptr: *const T) { #[cfg(target_arch = "x86_64")] { use std::arch::x86_64::_mm_prefetch; use std::arch::x86_64::_MM_HINT_T1; - _mm_prefetch(ptr as *const i8, _MM_HINT_T1); + _mm_prefetch(_ptr as *const i8, _MM_HINT_T1); } } } diff --git a/src/swqos/blockrazor.rs b/src/swqos/blockrazor.rs index 16f4a44..a7e4c3b 100644 --- a/src/swqos/blockrazor.rs +++ b/src/swqos/blockrazor.rs @@ -429,7 +429,7 @@ impl BlockRazorClient { let (content, _signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?; - let mut query_params: Vec<(&str, &str)> = vec![ + let query_params: Vec<(&str, &str)> = vec![ ("auth", auth_token.as_str()), // mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection). // revertProtection is unrelated to MEV; not set. diff --git a/src/trading/core/params/pumpfun.rs b/src/trading/core/params/pumpfun.rs index c146eae..e8f6b9b 100644 --- a/src/trading/core/params/pumpfun.rs +++ b/src/trading/core/params/pumpfun.rs @@ -12,15 +12,24 @@ use std::sync::Arc; /// **Creator vault**: Pump buy/sell pass `creator_vault` = `PDA(["creator-vault", authority])`. /// Usually `authority` is [`BondingCurveAccount::creator`]; with **Creator Rewards Sharing** it is /// `fee_sharing_config_pda(mint)` (see [`fetch_fee_sharing_creator_vault_if_active`](crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active)). -/// Keep `bonding_curve.creator` in sync with chain; ix building uses [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing). +/// **Buy/sell**:`creator_vault` 及(若可得)**`tradeEvent` / CPI 日志中的 `creator`** 优先于陈旧的曲线快照; +/// ix 组装与链下询价见 [`Self::effective_creator_for_trade`]、[`crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing`]。 #[derive(Clone)] pub struct PumpFunParams { pub bonding_curve: Arc, pub associated_bonding_curve: Pubkey, - /// Resolved by [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing): ix vault when it matches `PDA(creator)`, fee-sharing vault, or RPC hint. + /// 最新一笔可观测 trade 的 **`tradeEvent.creator`(日志)**。当 `Some` 且非 default 时, + /// **优先于** `bonding_curve.creator` 用于链下 creator-fee 询价与 `creator_vault` 在缺省 ix 时的推导。 + /// Pump 上 creator 可能随交易推进,调用方应在每次解析到带 `creator` 的 trade 后更新(如 `.with_observed_trade_creator`)。 + pub observed_trade_creator: Option, + /// Resolved by [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing): + /// **显式 `creator_vault`(非 default、非 phantom)永远优先并按原值使用**,不会再用 `creator` 重算覆盖; + /// 未传时再按 `fee_sharing_creator_vault_if_active`、`PDA(effective_creator)`(见 [`Self::effective_creator_for_trade`])。 pub creator_vault: Pubkey, /// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc). pub fee_sharing_creator_vault_if_active: Option, + /// SPL Token or Token-2022 program id owning the **mint** (from gRPC / parser / cache). + /// **`Pubkey::default()`**:ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致);显式传入 Legacy 或 Token-2022 id 可覆盖该默认值。 pub token_program: Pubkey, /// Whether to close token account when selling, only effective during sell operations pub close_token_account_when_sell: Option, @@ -38,6 +47,7 @@ impl PumpFunParams { Self { bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }), associated_bonding_curve: Pubkey::default(), + observed_trade_creator: None, creator_vault: creator_vault, fee_sharing_creator_vault_if_active: None, token_program: token_program, @@ -87,6 +97,7 @@ impl PumpFunParams { Self { bonding_curve: Arc::new(bonding_curve_account), associated_bonding_curve: associated_bonding_curve, + observed_trade_creator: (creator != Pubkey::default()).then_some(creator), creator_vault: creator_vault_resolved, fee_sharing_creator_vault_if_active: None, close_token_account_when_sell: close_token_account_when_sell, @@ -142,6 +153,7 @@ impl PumpFunParams { Self { bonding_curve: Arc::new(bonding_curve), associated_bonding_curve: associated_bonding_curve, + observed_trade_creator: (creator != Pubkey::default()).then_some(creator), creator_vault: creator_vault_resolved, fee_sharing_creator_vault_if_active: None, close_token_account_when_sell: close_token_account_when_sell, @@ -150,6 +162,7 @@ impl PumpFunParams { } } + /// 仅 RPC 读取曲线快照;[`Self::observed_trade_creator`] 为 `None`,便于 bot 缓存合并时用粘性的 trade 日志 creator 覆盖陈旧曲线推导。 pub async fn from_mint_by_rpc( rpc: &SolanaRpcClient, mint: &Pubkey, @@ -189,6 +202,7 @@ impl PumpFunParams { Ok(Self { bonding_curve: Arc::new(bonding_curve), associated_bonding_curve: associated_bonding_curve, + observed_trade_creator: None, creator_vault, fee_sharing_creator_vault_if_active, close_token_account_when_sell: None, @@ -197,6 +211,15 @@ impl PumpFunParams { }) } + /// 链下公式与 **`creator_vault` 推导回退**:**日志/事件 `creator`**(若已写入 `observed_trade_creator`) + /// 优先,否则使用 `bonding_curve.creator`。 + #[inline] + pub fn effective_creator_for_trade(&self) -> Pubkey { + self.observed_trade_creator + .filter(|c| *c != Pubkey::default()) + .unwrap_or(self.bonding_curve.creator) + } + /// One `getAccount` on pump-fees `SharingConfig` + re-resolves [`Self::creator_vault`]. Call before sell /// when params come from gRPC/cache so migrated fee-sharing mints do not hit Anchor 2006. pub async fn refresh_fee_sharing_creator_vault_from_rpc( @@ -207,7 +230,7 @@ impl PumpFunParams { self.fee_sharing_creator_vault_if_active = crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint) .await?; - let c = self.bonding_curve.creator; + let c = self.effective_creator_for_trade(); if let Some(v) = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing( &c, @@ -221,13 +244,20 @@ impl PumpFunParams { Ok(self) } - /// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`]. + /// Updates the cached `creator_vault` field only. Buy/sell ix use [`Self::effective_creator_for_trade`] + resolve. #[inline] pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self { self.creator_vault = creator_vault; self } + /// 覆盖 **最新一笔 trade 日志中的 `creator`**(`tradeEvent.creator`)。`None` 或 default 会清除覆盖。 + #[inline] + pub fn with_observed_trade_creator(mut self, c: Option) -> Self { + self.observed_trade_creator = c.filter(|x| *x != Pubkey::default()); + self + } + /// Override fee-sharing vault hint (e.g. from an off-chain indexer). `None` clears the hint. #[inline] pub fn with_fee_sharing_creator_vault_if_active(