Compare commits

...

6 Commits

Author SHA1 Message Date
0xfnzero 75bfbf6d54 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 <cursoragent@cursor.com>
2026-05-06 00:25:54 +08:00
0xfnzero 184f1cc583 fix(instruction): complete pump ix modules and pumpfun utils after params split
Adds pumpfun_ix_data / pumpswap_ix_data; exports used by trading::core::params::PumpFunParams.
chore(release): bump version to 4.0.6

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 14:32:47 +08:00
0xfnzero 55c13e2f25 chore(release): bump version to 4.0.5
Prepare GitHub release for client/params refactor and rlib-only library target.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 14:29:23 +08:00
0xfnzero fda211ea87 refactor: extract TradingClient façade to client/mod.rs
refactor: split trading/core/params into per-protocol modules + dex_swap
build: crate-type rlib only (drop unused cdylib)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 13:46:11 +08:00
0xfnzero 85d2c602f3 fix(calc): return 0 for zero sell amount in slippage helper
calculate_with_slippage_sell previously fell through to the branch that
returned 1 when amount <= basis_points/10_000, which is true for amount=0
and typical bps values—yielding a bogus minimum of 1. Short-circuit on
zero input so sell quotes stay correct.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 17:30:09 +08:00
0xfnzero 02d939b3cf fix(pumpswap): align remaining accounts with official SDK for buyback
- Append pool-v2 in buy/sell remaining accounts only when pool coin_creator
  is not default, matching @pump-fun/pump-swap-sdk. Always appending pool-v2
  shifted buyback pubkey/ATA and caused BuybackFeeRecipientNotAuthorized (6053).

- Add coin_creator to PumpSwapParams (filled from decoded Pool / from_trade).
  Update pumpswap_trading example to pass pool_data.coin_creator.

- Document buyback fee recipient constants against GlobalConfig.buyback_fee_recipients.

- Extend PumpSwap quote math to include cashback_fee_basis_points in creator-side fees.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 14:50:26 +08:00
28 changed files with 3173 additions and 2536 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.4"
version = "4.0.7"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
@@ -37,7 +37,7 @@ members = [
]
[lib]
crate-type = ["cdylib", "rlib"]
crate-type = ["rlib"]
[features]
default = []
+4
View File
@@ -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:
+5
View File
@@ -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)**。
#### PumpFunCreator Rewards Sharingcreator_vault
部分 PumpFun 代币启用了 **Creator Rewards Sharing**,链上 `creator_vault` 可能与默认推导结果不同。若在**卖出**时复用**买入**时缓存的 params,可能触发程序错误 **2006seeds constraint violated**。建议:
+208
View File
@@ -0,0 +1,208 @@
# Pump.funBonding 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. SPLToken **`initializeAccount3`**——`incorrect program id for instruction`
### 现象
- 内联指令里 **`Token Program: initializeAccount3`**(或 Token-2022 等价指令)报错 **`incorrect program id for instruction`**。
### 含义
**Mint** 创建用户 ATA 时,使用的 **token programLegacy 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/` 下按相同结构扩展。
+4
View File
@@ -162,7 +162,9 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
trade_info.base_token_program,
trade_info.quote_token_program,
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
trade_info.cashback_fee_basis_points,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
@@ -191,7 +193,9 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
trade_info.base_token_program,
trade_info.quote_token_program,
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
trade_info.cashback_fee_basis_points,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
+1222
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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),
+2
View File
@@ -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;
Executable → Regular
+81 -94
View File
@@ -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},
@@ -7,12 +9,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},
@@ -23,33 +28,41 @@ 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<Vec<Instruction>> {
// ========================================
// Parameter validation and basic data preparation
// ========================================
let protocol_params = params
.protocol_params
.as_any()
.downcast_ref::<PumpFunParams>()
.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(
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,
&params.output_mint,
protocol_params.fee_sharing_creator_vault_if_active,
)
.ok_or_else(|| {
anyhow!(
@@ -58,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(
@@ -68,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(&params.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
@@ -110,14 +114,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
let user_volume_accumulator = get_user_volume_accumulator_pda(&params.payer.pubkey())
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
// ========================================
// Build instructions
// ========================================
// Hot path: no RPC here (latency). For legacy curves &lt;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(
@@ -130,35 +128,24 @@ 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 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(&params.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);
let buy_data = if params.use_exact_sol_amount.unwrap_or(true) {
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
encode_pumpfun_buy_exact_sol_in_ix_data(
lamports_in,
min_tokens_out,
TRACK_VOLUME_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 =
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
let bonding_curve_v2 = get_bonding_curve_v2_pda(&params.output_mint).ok_or_else(|| {
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint)
})?;
let mut accounts: Vec<AccountMeta> = vec![
let mut metas: Vec<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META,
fee_recipient_meta,
AccountMeta::new_readonly(params.output_mint, false),
@@ -168,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,
@@ -176,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<Vec<Instruction>> {
// ========================================
// Parameter validation and basic data preparation
// ========================================
let protocol_params = params
.protocol_params
.as_any()
@@ -204,12 +194,15 @@ 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(
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,
&params.input_mint,
protocol_params.fee_sharing_creator_vault_if_active,
)
.ok_or_else(|| {
anyhow!(
@@ -218,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,
@@ -230,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(&params.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
@@ -264,20 +250,12 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
params.open_seed_optimize,
);
// ========================================
// Build instructions
// ========================================
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);
let mut accounts: Vec<AccountMeta> = vec![
let mut metas: Vec<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META,
fee_recipient_meta,
AccountMeta::new_readonly(params.input_mint, false),
@@ -286,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,
@@ -294,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(&params.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(&params.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
{
@@ -327,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<Instruction> {
const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?;
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,
))
}
+47
View File
@@ -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
}
+54 -52
View File
@@ -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,
@@ -76,6 +79,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
creator = params_coin_creator_vault_authority;
}
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
let (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc {
let result = buy_quote_input_internal(
@@ -84,6 +88,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
pool_base_token_reserves,
pool_quote_token_reserves,
&creator,
cashback_fee_bps,
)
.unwrap();
// base_amount_out, max_quote_amount_in
@@ -95,6 +100,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
pool_base_token_reserves,
pool_quote_token_reserves,
&creator,
cashback_fee_bps,
)
.unwrap();
// min_quote_amount_out, base_amount_in
@@ -202,11 +208,15 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
accounts.push(AccountMeta::new(wsol_ata, false));
}
}
// remainingAccounts: @pump-fun/pump-swap-sdk 要求末尾传 poolV2Pda(baseMint),勿删
let pool_v2 = get_pool_v2_pda(&base_mint)
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
accounts.push(AccountMeta::new_readonly(pool_v2, false));
// Apr 2026: protocol fee recipient + quote ATA (after pool-v2)
// `pool-v2` only when coin_creator ≠ default (@pump-fun/pump-swap-sdk remainingAccounts)
// 否则多出的一格会把 buyback pubkey 错位,触发 BuybackFeeRecipientNotAuthorized6053)。
if protocol_params.coin_creator != Pubkey::default() {
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
})?;
accounts.push(AccountMeta::new_readonly(pool_v2, false));
}
// Trailing accounts: GlobalConfig.buyback_fee_recipients 中任 pubkey + quote ATA(与 pump-swap-sdk 静态池对齐;轮换时需查链上)。
let protocol_extra = get_protocol_extra_fee_recipient_random();
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
accounts.push(AccountMeta::new(
@@ -214,35 +224,35 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
false,
));
// Create instruction databuy/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<u8> = 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(&params.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(&params.payer.pubkey()));
@@ -299,6 +309,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
creator = params_coin_creator_vault_authority;
}
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
let (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc {
let result = sell_base_input_internal(
@@ -307,6 +318,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
pool_base_token_reserves,
pool_quote_token_reserves,
&creator,
cashback_fee_bps,
)
.unwrap();
// base_amount_in, min_quote_amount_out
@@ -318,6 +330,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
pool_base_token_reserves,
pool_quote_token_reserves,
&creator,
cashback_fee_bps,
)
.unwrap();
// max_quote_amount_in, base_amount_out
@@ -410,10 +423,12 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
accounts.push(AccountMeta::new(accumulator, false));
}
}
// remainingAccounts: @pump-fun/pump-swap-sdk sell 要求末尾传 poolV2Pda(baseMint),勿删
let pool_v2 = get_pool_v2_pda(&base_mint)
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
accounts.push(AccountMeta::new_readonly(pool_v2, false));
if protocol_params.coin_creator != Pubkey::default() {
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
})?;
accounts.push(AccountMeta::new_readonly(pool_v2, false));
}
let protocol_extra = get_protocol_extra_fee_recipient_random();
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
accounts.push(AccountMeta::new(
@@ -421,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(&params.payer.pubkey()));
+51
View File
@@ -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
}
+430 -271
View File
@@ -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,225 +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: <https://github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md>
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];
/// Check if a pubkey is one of the Mayhem fee recipients
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];
pub(crate) const SHARING_CONFIG_STATUS_ACTIVE: u8 = 1;
// --- Fee recipient pools -----------------------------------------------
#[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
@@ -237,18 +197,58 @@ pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
}
/// 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 is_standard_bonding_fee_recipient(pubkey: &Pubkey) -> bool {
*pubkey == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(pubkey)
}
#[inline]
pub fn reconcile_mayhem_mode_for_trade(
mayhem_from_event: Option<bool>,
fee_recipient: &Pubkey,
) -> bool {
if *fee_recipient == Pubkey::default() {
return mayhem_from_event.unwrap_or(false);
}
let fee_m = is_mayhem_fee_recipient(fee_recipient);
let fee_s = is_standard_bonding_fee_recipient(fee_recipient);
match mayhem_from_event {
Some(log_m) => {
if fee_m && !log_m {
true
} else if fee_s && log_m && !fee_m {
false
} else {
log_m
}
}
None => fee_m,
}
}
#[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)
}
}
#[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] = &[
@@ -271,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
@@ -279,12 +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`);未提供时按 mayhem 从静态池随机。
#[inline]
pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
if from_stream != Pubkey::default() {
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,
}
@@ -295,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<Pubkey> {
crate::common::fast_fn::get_cached_pda(
@@ -308,13 +325,11 @@ pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|| {
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<Pubkey> {
crate::common::fast_fn::get_cached_pda(
@@ -322,8 +337,7 @@ pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
|| {
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)
},
)
}
@@ -333,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<Option<Pubkey>> =
std::sync::LazyLock::new(|| get_creator_vault_pda(&Pubkey::default()));
match DEFAULT_CREATOR_VAULT.as_ref() {
@@ -350,24 +363,25 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option<Pubkey> {
|| {
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> {
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")
@@ -378,52 +392,47 @@ 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)` 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).
#[inline]
pub fn resolve_creator_vault_for_ix(
creator: &Pubkey,
creator_vault_from_event: Pubkey,
mint: &Pubkey,
) -> Option<Pubkey> {
resolve_creator_vault_for_ix_with_fee_sharing(creator, creator_vault_from_event, mint, None)
}
/// 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,
fee_sharing_creator_vault_if_active: Option<Pubkey>,
) -> Option<Pubkey> {
let phantom = phantom_default_creator_vault();
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;
}
// Real creator: poisoned cache may hold phantom vault — always remap to PDA(creator).
if creator_vault_from_event == phantom {
return get_creator_vault_pda(creator);
}
let v_derived = get_creator_vault_pda(creator)?;
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);
}
}
Some(v_derived)
get_creator_vault_pda(creator)
}
#[inline]
@@ -431,34 +440,13 @@ pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
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 seed: &[&[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)
Pubkey::try_find_program_address(seed, program_id).map(|pubkey| pubkey.0)
},
)
}
#[inline]
pub async fn fetch_bonding_curve_account(
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
let bonding_curve_pda: Pubkey =
get_bonding_curve_pda(mint).ok_or(anyhow!("Bonding curve not found"))?;
let account = rpc.get_account(&bonding_curve_pda).await?;
if account.data.is_empty() {
return Err(anyhow!("Bonding curve not found"));
}
let bonding_curve =
solana_sdk::borsh1::try_from_slice_unchecked::<BondingCurveAccount>(&account.data[8..])
.map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?;
Ok((Arc::new(bonding_curve), bonding_curve_pda))
}
#[inline]
pub fn get_buy_price(
amount: u64,
@@ -479,9 +467,72 @@ pub fn get_buy_price(
s_u64.min(real_token_reserves)
}
// --- RPC -------------------------------------------------------------
#[inline]
pub async fn fetch_fee_sharing_creator_vault_if_active(
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Option<Pubkey>, anyhow::Error> {
let Some(config_pda) = get_fee_sharing_config_pda(mint) else {
return Ok(None);
};
let acc = match rpc.get_account(&config_pda).await {
Ok(a) => a,
Err(_) => return Ok(None),
};
if acc.owner != accounts::FEE_PROGRAM {
return Ok(None);
}
let d = acc.data.as_slice();
if d.len() < 43 || d[..8] != SHARING_CONFIG_ACCOUNT_DISCRIMINATOR {
return Ok(None);
}
if d[10] != SHARING_CONFIG_STATUS_ACTIVE {
return Ok(None);
}
let mint_on_chain = Pubkey::new_from_array(
d[11..43]
.try_into()
.map_err(|_| anyhow!("SharingConfig mint slice"))?,
);
if mint_on_chain != *mint {
return Ok(None);
}
Ok(get_creator_vault_pda(&config_pda))
}
#[inline]
pub async fn fetch_bonding_curve_account(
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
let bonding_curve_pda: Pubkey =
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() {
return Err(anyhow!("Bonding curve not found"));
}
let bonding_curve =
solana_sdk::borsh1::try_from_slice_unchecked::<BondingCurveAccount>(&account.data[8..])
.map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?;
Ok((Arc::new(bonding_curve), bonding_curve_pda))
}
#[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]
@@ -518,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]
@@ -542,11 +597,53 @@ 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_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();
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(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]
fn resolve_remaps_phantom_vault_when_creator_known() {
let creator = Pubkey::new_unique();
@@ -557,4 +654,66 @@ mod tests {
Some(expected)
);
}
#[test]
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();
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(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]
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
);
}
}
+3 -1
View File
@@ -91,7 +91,9 @@ pub mod accounts {
/// Default Mayhem fee recipient (first of MAYHEM_FEE_RECIPIENTS)
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
/// Protocol extra fee recipients (Apr 2026 breaking upgrade). After `pool-v2`: recipient (readonly), then quote ATA (writable).
/// Buyback trailing fee recipients (`GlobalConfig.buyback_fee_recipients` on Pump AMM).
/// Must match one of these for the pubkey passed after optional `pool-v2` (`@pump-fun/pump-swap-sdk` `getBuybackFeeRecipient`).
/// Static mirror of pump-public-docs; if protocol rotates configs, decode global_config from RPC.
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
+6 -1220
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -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<T>(ptr: *const T) {
pub unsafe fn prefetch_read_data<T>(_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<T>(ptr: *const T) {
pub unsafe fn prefetch_write_data<T>(_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);
}
}
}
+1 -1
View File
@@ -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.
-879
View File
@@ -1,879 +0,0 @@
use crate::common::bonding_curve::BondingCurveAccount;
use crate::common::nonce_cache::DurableNonceInfo;
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
use crate::common::{GasFeeStrategy, SolanaRpcClient};
use core_affinity::CoreId;
use crate::instruction::utils::pumpfun::is_mayhem_fee_recipient;
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
#[derive(Clone)]
pub struct SenderConcurrencyConfig {
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
pub effective_core_ids: Arc<Vec<CoreId>>,
pub max_sender_concurrency: usize,
}
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
use crate::swqos::{SwqosClient, TradeType};
use crate::trading::common::get_multi_token_balances;
use crate::trading::MiddlewareManager;
use solana_hash::Hash;
use solana_message::AddressLookupTableAccount;
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use std::sync::Arc;
/// DEX 参数枚举 - 零开销抽象替代 Box<dyn ProtocolParams>
#[derive(Clone)]
pub enum DexParamEnum {
PumpFun(PumpFunParams),
PumpSwap(PumpSwapParams),
Bonk(BonkParams),
RaydiumCpmm(RaydiumCpmmParams),
RaydiumAmmV4(RaydiumAmmV4Params),
MeteoraDammV2(MeteoraDammV2Params),
}
impl DexParamEnum {
/// 获取内部参数的 Any 引用,用于向后兼容的类型检查
#[inline]
pub fn as_any(&self) -> &dyn std::any::Any {
match self {
DexParamEnum::PumpFun(p) => p,
DexParamEnum::PumpSwap(p) => p,
DexParamEnum::Bonk(p) => p,
DexParamEnum::RaydiumCpmm(p) => p,
DexParamEnum::RaydiumAmmV4(p) => p,
DexParamEnum::MeteoraDammV2(p) => p,
}
}
}
/// Swap parameters
#[derive(Clone)]
pub struct SwapParams {
pub rpc: Option<Arc<SolanaRpcClient>>,
pub payer: Arc<Keypair>,
pub trade_type: TradeType,
pub input_mint: Pubkey,
pub input_token_program: Option<Pubkey>,
pub output_mint: Pubkey,
pub output_token_program: Option<Pubkey>,
pub input_amount: Option<u64>,
pub slippage_basis_points: Option<u64>,
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
pub recent_blockhash: Option<Hash>,
pub wait_tx_confirmed: bool,
pub protocol_params: DexParamEnum,
pub open_seed_optimize: bool,
/// Arc<Vec<..>> so cloning from infrastructure is a single Arc clone.
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
pub middleware_manager: Option<Arc<MiddlewareManager>>,
pub durable_nonce: Option<DurableNonceInfo>,
pub with_tip: bool,
pub create_input_mint_ata: bool,
pub close_input_mint_ata: bool,
pub create_output_mint_ata: bool,
pub close_output_mint_ata: bool,
pub fixed_output_amount: Option<u64>,
pub gas_fee_strategy: GasFeeStrategy,
pub simulate: bool,
/// Whether to output SDK logs (from TradeConfig.log_enabled).
pub log_enabled: bool,
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
pub use_dedicated_sender_threads: bool,
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
/// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path.
pub max_sender_concurrency: usize,
/// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only.
pub effective_core_ids: Arc<Vec<CoreId>>,
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
pub check_min_tip: bool,
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
pub grpc_recv_us: Option<i64>,
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
/// 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<bool>,
}
impl SwapParams {
/// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only.
#[inline]
pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig {
SenderConcurrencyConfig {
sender_thread_cores: self.sender_thread_cores.clone(),
effective_core_ids: self.effective_core_ids.clone(),
max_sender_concurrency: self.max_sender_concurrency,
}
}
}
impl std::fmt::Debug for SwapParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SwapParams: ...")
}
}
/// PumpFun protocol specific parameters
/// Configuration parameters specific to PumpFun trading protocol.
///
/// **Creator vault**: Pump buy/sell instructions always pass `creator_vault` =
/// `PDA(["creator-vault", bonding_curve.creator])` derived from [`BondingCurveAccount::creator`].
/// Keep `bonding_curve.creator` in sync with chain (gRPC / RPC); stale `creator_vault` in this struct
/// does not affect ix building.
#[derive(Clone)]
pub struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>,
pub associated_bonding_curve: Pubkey,
/// Resolved by [`resolve_creator_vault_for_ix`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix): use ix vault when it matches `PDA(creator)` or fee-sharing vault; else `PDA(creator)`.
pub creator_vault: Pubkey,
pub token_program: Pubkey,
/// Whether to close token account when selling, only effective during sell operations
pub close_token_account_when_sell: Option<bool>,
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。
pub fee_recipient: Pubkey,
}
impl PumpFunParams {
pub fn immediate_sell(
creator_vault: Pubkey,
token_program: Pubkey,
close_token_account_when_sell: bool,
) -> Self {
Self {
bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }),
associated_bonding_curve: Pubkey::default(),
creator_vault: creator_vault,
token_program: token_program,
close_token_account_when_sell: Some(close_token_account_when_sell),
fee_recipient: Pubkey::default(),
}
}
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
/// so that sell instructions include the correct remaining accounts for cashback.
/// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`).
/// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts).
pub fn from_dev_trade(
mint: Pubkey,
token_amount: u64,
max_sol_cost: u64,
creator: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
creator_vault: Pubkey,
close_token_account_when_sell: Option<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
) -> Self {
let is_mayhem_mode =
mayhem_mode.unwrap_or_else(|| is_mayhem_fee_recipient(&fee_recipient));
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
bonding_curve,
&mint,
token_amount,
max_sol_cost,
creator,
is_mayhem_mode,
is_cashback_coin,
);
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
&bonding_curve_account.creator,
creator_vault,
&mint,
)
.or_else(|| {
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
})
.unwrap_or_default();
Self {
bonding_curve: Arc::new(bonding_curve_account),
associated_bonding_curve: associated_bonding_curve,
creator_vault: creator_vault_resolved,
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
}
}
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
/// so that sell instructions include the correct remaining accounts for cashback.
///
/// `mayhem_mode`:
/// - **`Some(v)`**(推荐):显式使用链上事件中的值。gRPC 日志解析对应 Explorer 的 `tradeEvent.mayhemMode`
/// **不会**再用 `fee_recipient` 覆盖。
/// - **`None`**:无该字段时(例如 ShredStream 仅解外层指令、或冷路径),才用 `fee_recipient` 是否落在 Mayhem 静态列表上推断。
pub fn from_trade(
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
mint: Pubkey,
creator: Pubkey,
creator_vault: Pubkey,
virtual_token_reserves: u64,
virtual_sol_reserves: u64,
real_token_reserves: u64,
real_sol_reserves: u64,
close_token_account_when_sell: Option<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
) -> Self {
let is_mayhem_mode = match mayhem_mode {
Some(v) => v,
None => is_mayhem_fee_recipient(&fee_recipient),
};
let bonding_curve = BondingCurveAccount::from_trade(
bonding_curve,
mint,
creator,
virtual_token_reserves,
virtual_sol_reserves,
real_token_reserves,
real_sol_reserves,
is_mayhem_mode,
is_cashback_coin,
);
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
&bonding_curve.creator,
creator_vault,
&mint,
)
.or_else(|| {
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
})
.unwrap_or_default();
Self {
bonding_curve: Arc::new(bonding_curve),
associated_bonding_curve: associated_bonding_curve,
creator_vault: creator_vault_resolved,
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
}
}
pub async fn from_mint_by_rpc(
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Self, anyhow::Error> {
let account =
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?;
let mint_account = rpc.get_account(&mint).await?;
let bonding_curve = BondingCurveAccount {
discriminator: 0,
account: account.1,
virtual_token_reserves: account.0.virtual_token_reserves,
virtual_sol_reserves: account.0.virtual_sol_reserves,
real_token_reserves: account.0.real_token_reserves,
real_sol_reserves: account.0.real_sol_reserves,
token_total_supply: account.0.token_total_supply,
complete: account.0.complete,
creator: account.0.creator,
is_mayhem_mode: account.0.is_mayhem_mode,
is_cashback_coin: account.0.is_cashback_coin,
};
let associated_bonding_curve = get_associated_token_address_with_program_id(
&bonding_curve.account,
mint,
&mint_account.owner,
);
let creator_vault =
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator);
Ok(Self {
bonding_curve: Arc::new(bonding_curve),
associated_bonding_curve: associated_bonding_curve,
creator_vault: creator_vault.unwrap(),
close_token_account_when_sell: None,
token_program: mint_account.owner,
fee_recipient: Pubkey::default(),
})
}
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`].
#[inline]
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
self.creator_vault = creator_vault;
self
}
}
/// PumpSwap Protocol Specific Parameters
///
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
/// token configuration, and transaction amounts.
///
/// **Performance Note**: If these parameters are not provided, the system will attempt to
/// retrieve the relevant information from RPC, which will increase transaction time.
/// For optimal performance, it is recommended to provide all necessary parameters in advance.
#[derive(Clone)]
pub struct PumpSwapParams {
/// Liquidity pool address
pub pool: Pubkey,
/// Base token mint address
/// The mint account address of the base token in the trading pair
pub base_mint: Pubkey,
/// Quote token mint address
/// The mint account address of the quote token in the trading pair, usually SOL or USDC
pub quote_mint: Pubkey,
/// Pool base token account
pub pool_base_token_account: Pubkey,
/// Pool quote token account
pub pool_quote_token_account: Pubkey,
/// Base token reserves in the pool
pub pool_base_token_reserves: u64,
/// Quote token reserves in the pool
pub pool_quote_token_reserves: u64,
/// Coin creator vault ATA
pub coin_creator_vault_ata: Pubkey,
/// Coin creator vault authority
pub coin_creator_vault_authority: Pubkey,
/// Token program ID
pub base_token_program: Pubkey,
/// Quote token program ID
pub quote_token_program: Pubkey,
/// Whether the pool is in mayhem mode
pub is_mayhem_mode: bool,
/// Whether the pool's coin has cashback enabled
pub is_cashback_coin: bool,
}
impl PumpSwapParams {
pub fn new(
pool: Pubkey,
base_mint: Pubkey,
quote_mint: Pubkey,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
pool_base_token_reserves: u64,
pool_quote_token_reserves: u64,
coin_creator_vault_ata: Pubkey,
coin_creator_vault_authority: Pubkey,
base_token_program: Pubkey,
quote_token_program: Pubkey,
fee_recipient: Pubkey,
is_cashback_coin: bool,
) -> Self {
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
Self {
pool,
base_mint,
quote_mint,
pool_base_token_account,
pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program,
quote_token_program,
is_mayhem_mode,
is_cashback_coin,
}
}
/// Fast-path constructor for building PumpSwap parameters directly from decoded
/// trade/event data and the accompanying instruction accounts, avoiding RPC
/// lookups and associated latency. Token program IDs should be sourced from
/// the instruction accounts themselves to respect Token Program vs Token-2022
/// differences.
///
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
/// from the event so that buy/sell instructions include the correct remaining
/// accounts for cashback.
pub fn from_trade(
pool: Pubkey,
base_mint: Pubkey,
quote_mint: Pubkey,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
pool_base_token_reserves: u64,
pool_quote_token_reserves: u64,
coin_creator_vault_ata: Pubkey,
coin_creator_vault_authority: Pubkey,
base_token_program: Pubkey,
quote_token_program: Pubkey,
fee_recipient: Pubkey,
is_cashback_coin: bool,
) -> Self {
Self::new(
pool,
base_mint,
quote_mint,
pool_base_token_account,
pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program,
quote_token_program,
fee_recipient,
is_cashback_coin,
)
}
pub async fn from_mint_by_rpc(
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Self, anyhow::Error> {
if let Ok((pool_address, _)) =
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
{
Self::from_pool_address_by_rpc(rpc, &pool_address).await
} else if let Ok((pool_address, _)) =
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
{
Self::from_pool_address_by_rpc(rpc, &pool_address).await
} else {
return Err(anyhow::anyhow!("No pool found for mint"));
}
}
pub async fn from_pool_address_by_rpc(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
) -> Result<Self, anyhow::Error> {
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
Self::from_pool_data(rpc, pool_address, &pool_data).await
}
/// Build params from an already-decoded Pool, only fetching token balances.
///
/// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data
/// is already available (e.g. from `pumpswap::find_by_mint` which returns the
/// decoded Pool).
pub async fn from_pool_data(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
) -> Result<Self, anyhow::Error> {
let (pool_base_token_reserves, pool_quote_token_reserves) =
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
let creator = pool_data.coin_creator;
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
creator,
pool_data.quote_mint,
);
let coin_creator_vault_authority =
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
let base_token_program_ata = get_associated_token_address_with_program_id(
pool_address,
&pool_data.base_mint,
&crate::constants::TOKEN_PROGRAM,
);
let quote_token_program_ata = get_associated_token_address_with_program_id(
pool_address,
&pool_data.quote_mint,
&crate::constants::TOKEN_PROGRAM,
);
Ok(Self {
pool: *pool_address,
base_mint: pool_data.base_mint,
quote_mint: pool_data.quote_mint,
pool_base_token_account: pool_data.pool_base_token_account,
pool_quote_token_account: pool_data.pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
crate::constants::TOKEN_PROGRAM
} else {
crate::constants::TOKEN_PROGRAM_2022
},
is_cashback_coin: pool_data.is_cashback_coin,
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
crate::constants::TOKEN_PROGRAM
} else {
crate::constants::TOKEN_PROGRAM_2022
},
is_mayhem_mode: pool_data.is_mayhem_mode,
})
}
}
/// Bonk protocol specific parameters
/// Configuration parameters specific to Bonk trading protocol
#[derive(Clone, Default)]
pub struct BonkParams {
pub virtual_base: u128,
pub virtual_quote: u128,
pub real_base: u128,
pub real_quote: u128,
pub pool_state: Pubkey,
pub base_vault: Pubkey,
pub quote_vault: Pubkey,
/// Token program ID
pub mint_token_program: Pubkey,
pub platform_config: Pubkey,
pub platform_associated_account: Pubkey,
pub creator_associated_account: Pubkey,
pub global_config: Pubkey,
}
impl BonkParams {
pub fn immediate_sell(
mint_token_program: Pubkey,
platform_config: Pubkey,
platform_associated_account: Pubkey,
creator_associated_account: Pubkey,
global_config: Pubkey,
) -> Self {
Self {
mint_token_program,
platform_config,
platform_associated_account,
creator_associated_account,
global_config,
..Default::default()
}
}
pub fn from_trade(
virtual_base: u64,
virtual_quote: u64,
real_base_after: u64,
real_quote_after: u64,
pool_state: Pubkey,
base_vault: Pubkey,
quote_vault: Pubkey,
base_token_program: Pubkey,
platform_config: Pubkey,
platform_associated_account: Pubkey,
creator_associated_account: Pubkey,
global_config: Pubkey,
) -> Self {
Self {
virtual_base: virtual_base as u128,
virtual_quote: virtual_quote as u128,
real_base: real_base_after as u128,
real_quote: real_quote_after as u128,
pool_state: pool_state,
base_vault: base_vault,
quote_vault: quote_vault,
mint_token_program: base_token_program,
platform_config: platform_config,
platform_associated_account: platform_associated_account,
creator_associated_account: creator_associated_account,
global_config: global_config,
}
}
pub fn from_dev_trade(
is_exact_in: bool,
amount_in: u64,
amount_out: u64,
pool_state: Pubkey,
base_vault: Pubkey,
quote_vault: Pubkey,
base_token_program: Pubkey,
platform_config: Pubkey,
platform_associated_account: Pubkey,
creator_associated_account: Pubkey,
global_config: Pubkey,
) -> Self {
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
let _amount_in = if is_exact_in {
amount_in
} else {
crate::instruction::utils::bonk::get_amount_in(
amount_out,
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
DEFAULT_VIRTUAL_BASE,
DEFAULT_VIRTUAL_QUOTE,
0,
0,
0,
)
};
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
amount_in,
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
) as u128;
let _amount_out = if is_exact_in {
crate::instruction::utils::bonk::get_amount_out(
amount_in,
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
DEFAULT_VIRTUAL_BASE,
DEFAULT_VIRTUAL_QUOTE,
0,
0,
0,
) as u128
} else {
amount_out as u128
};
let real_base = _amount_out;
Self {
virtual_base: DEFAULT_VIRTUAL_BASE,
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
real_base: real_base,
real_quote: real_quote,
pool_state: pool_state,
base_vault: base_vault,
quote_vault: quote_vault,
mint_token_program: base_token_program,
platform_config: platform_config,
platform_associated_account: platform_associated_account,
creator_associated_account: creator_associated_account,
global_config: global_config,
}
}
pub async fn from_mint_by_rpc(
rpc: &SolanaRpcClient,
mint: &Pubkey,
usd1_pool: bool,
) -> Result<Self, anyhow::Error> {
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
mint,
if usd1_pool {
&crate::constants::USD1_TOKEN_ACCOUNT
} else {
&crate::constants::WSOL_TOKEN_ACCOUNT
},
)
.unwrap();
let pool_data =
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
let token_account = rpc.get_account(&pool_data.base_mint).await?;
let platform_associated_account =
crate::instruction::utils::bonk::get_platform_associated_account(
&pool_data.platform_config,
);
let creator_associated_account =
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
let platform_associated_account = platform_associated_account.unwrap();
let creator_associated_account = creator_associated_account.unwrap();
Ok(Self {
virtual_base: pool_data.virtual_base as u128,
virtual_quote: pool_data.virtual_quote as u128,
real_base: pool_data.real_base as u128,
real_quote: pool_data.real_quote as u128,
pool_state: pool_address,
base_vault: pool_data.base_vault,
quote_vault: pool_data.quote_vault,
mint_token_program: token_account.owner,
platform_config: pool_data.platform_config,
platform_associated_account,
creator_associated_account,
global_config: pool_data.global_config,
})
}
}
/// RaydiumCpmm protocol specific parameters
/// Configuration parameters specific to Raydium CPMM trading protocol
#[derive(Clone)]
pub struct RaydiumCpmmParams {
/// Pool address
pub pool_state: Pubkey,
/// Amm config address
pub amm_config: Pubkey,
/// Base token mint address
pub base_mint: Pubkey,
/// Quote token mint address
pub quote_mint: Pubkey,
/// Base token reserve amount in the pool
pub base_reserve: u64,
/// Quote token reserve amount in the pool
pub quote_reserve: u64,
/// Base token vault address
pub base_vault: Pubkey,
/// Quote token vault address
pub quote_vault: Pubkey,
/// Base token program ID
pub base_token_program: Pubkey,
/// Quote token program ID
pub quote_token_program: Pubkey,
/// Observation state account
pub observation_state: Pubkey,
}
impl RaydiumCpmmParams {
pub fn from_trade(
pool_state: Pubkey,
amm_config: Pubkey,
input_token_mint: Pubkey,
output_token_mint: Pubkey,
input_vault: Pubkey,
output_vault: Pubkey,
input_token_program: Pubkey,
output_token_program: Pubkey,
observation_state: Pubkey,
base_reserve: u64,
quote_reserve: u64,
) -> Self {
Self {
pool_state: pool_state,
amm_config: amm_config,
base_mint: input_token_mint,
quote_mint: output_token_mint,
base_reserve: base_reserve,
quote_reserve: quote_reserve,
base_vault: input_vault,
quote_vault: output_vault,
base_token_program: input_token_program,
quote_token_program: output_token_program,
observation_state: observation_state,
}
}
pub async fn from_pool_address_by_rpc(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
) -> Result<Self, anyhow::Error> {
let pool =
crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?;
let (token0_balance, token1_balance) =
crate::instruction::utils::raydium_cpmm::get_pool_token_balances(
rpc,
pool_address,
&pool.token0_mint,
&pool.token1_mint,
)
.await?;
Ok(Self {
pool_state: *pool_address,
amm_config: pool.amm_config,
base_mint: pool.token0_mint,
quote_mint: pool.token1_mint,
base_reserve: token0_balance,
quote_reserve: token1_balance,
base_vault: pool.token0_vault,
quote_vault: pool.token1_vault,
base_token_program: pool.token0_program,
quote_token_program: pool.token1_program,
observation_state: pool.observation_key,
})
}
}
/// RaydiumCpmm protocol specific parameters
/// Configuration parameters specific to Raydium CPMM trading protocol
#[derive(Clone)]
pub struct RaydiumAmmV4Params {
/// AMM pool address
pub amm: Pubkey,
/// Base token (coin) mint address
pub coin_mint: Pubkey,
/// Quote token (pc) mint address
pub pc_mint: Pubkey,
/// Pool's coin token account address
pub token_coin: Pubkey,
/// Pool's pc token account address
pub token_pc: Pubkey,
/// Current coin reserve amount in the pool
pub coin_reserve: u64,
/// Current pc reserve amount in the pool
pub pc_reserve: u64,
}
impl RaydiumAmmV4Params {
pub fn new(
amm: Pubkey,
coin_mint: Pubkey,
pc_mint: Pubkey,
token_coin: Pubkey,
token_pc: Pubkey,
coin_reserve: u64,
pc_reserve: u64,
) -> Self {
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
}
pub async fn from_amm_address_by_rpc(
rpc: &SolanaRpcClient,
amm: Pubkey,
) -> Result<Self, anyhow::Error> {
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
let (coin_reserve, pc_reserve) =
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
Ok(Self {
amm,
coin_mint: amm_info.coin_mint,
pc_mint: amm_info.pc_mint,
token_coin: amm_info.token_coin,
token_pc: amm_info.token_pc,
coin_reserve,
pc_reserve,
})
}
}
/// MeteoraDammV2 protocol specific parameters
/// Configuration parameters specific to Meteora Damm V2 trading protocol
#[derive(Clone)]
pub struct MeteoraDammV2Params {
pub pool: Pubkey,
pub token_a_vault: Pubkey,
pub token_b_vault: Pubkey,
pub token_a_mint: Pubkey,
pub token_b_mint: Pubkey,
pub token_a_program: Pubkey,
pub token_b_program: Pubkey,
}
impl MeteoraDammV2Params {
pub fn new(
pool: Pubkey,
token_a_vault: Pubkey,
token_b_vault: Pubkey,
token_a_mint: Pubkey,
token_b_mint: Pubkey,
token_a_program: Pubkey,
token_b_program: Pubkey,
) -> Self {
Self {
pool,
token_a_vault,
token_b_vault,
token_a_mint,
token_b_mint,
token_a_program,
token_b_program,
}
}
pub async fn from_pool_address_by_rpc(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
) -> Result<Self, anyhow::Error> {
let pool_data =
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
let mint_accounts = rpc
.get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint])
.await?;
let token_a_program = mint_accounts
.get(0)
.and_then(|a| a.as_ref())
.map(|a| a.owner)
.ok_or_else(|| anyhow::anyhow!("Token A mint account not found"))?;
let token_b_program = mint_accounts
.get(1)
.and_then(|a| a.as_ref())
.map(|a| a.owner)
.ok_or_else(|| anyhow::anyhow!("Token B mint account not found"))?;
Ok(Self {
pool: *pool_address,
token_a_vault: pool_data.token_a_vault,
token_b_vault: pool_data.token_b_vault,
token_a_mint: pool_data.token_a_mint,
token_b_mint: pool_data.token_b_mint,
token_a_program,
token_b_program,
})
}
}
+178
View File
@@ -0,0 +1,178 @@
use crate::common::SolanaRpcClient;
use solana_sdk::pubkey::Pubkey;
/// Bonk protocol specific parameters
/// Configuration parameters specific to Bonk trading protocol
#[derive(Clone, Default)]
pub struct BonkParams {
pub virtual_base: u128,
pub virtual_quote: u128,
pub real_base: u128,
pub real_quote: u128,
pub pool_state: Pubkey,
pub base_vault: Pubkey,
pub quote_vault: Pubkey,
/// Token program ID
pub mint_token_program: Pubkey,
pub platform_config: Pubkey,
pub platform_associated_account: Pubkey,
pub creator_associated_account: Pubkey,
pub global_config: Pubkey,
}
impl BonkParams {
pub fn immediate_sell(
mint_token_program: Pubkey,
platform_config: Pubkey,
platform_associated_account: Pubkey,
creator_associated_account: Pubkey,
global_config: Pubkey,
) -> Self {
Self {
mint_token_program,
platform_config,
platform_associated_account,
creator_associated_account,
global_config,
..Default::default()
}
}
pub fn from_trade(
virtual_base: u64,
virtual_quote: u64,
real_base_after: u64,
real_quote_after: u64,
pool_state: Pubkey,
base_vault: Pubkey,
quote_vault: Pubkey,
base_token_program: Pubkey,
platform_config: Pubkey,
platform_associated_account: Pubkey,
creator_associated_account: Pubkey,
global_config: Pubkey,
) -> Self {
Self {
virtual_base: virtual_base as u128,
virtual_quote: virtual_quote as u128,
real_base: real_base_after as u128,
real_quote: real_quote_after as u128,
pool_state: pool_state,
base_vault: base_vault,
quote_vault: quote_vault,
mint_token_program: base_token_program,
platform_config: platform_config,
platform_associated_account: platform_associated_account,
creator_associated_account: creator_associated_account,
global_config: global_config,
}
}
pub fn from_dev_trade(
is_exact_in: bool,
amount_in: u64,
amount_out: u64,
pool_state: Pubkey,
base_vault: Pubkey,
quote_vault: Pubkey,
base_token_program: Pubkey,
platform_config: Pubkey,
platform_associated_account: Pubkey,
creator_associated_account: Pubkey,
global_config: Pubkey,
) -> Self {
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
let _amount_in = if is_exact_in {
amount_in
} else {
crate::instruction::utils::bonk::get_amount_in(
amount_out,
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
DEFAULT_VIRTUAL_BASE,
DEFAULT_VIRTUAL_QUOTE,
0,
0,
0,
)
};
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
amount_in,
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
) as u128;
let _amount_out = if is_exact_in {
crate::instruction::utils::bonk::get_amount_out(
amount_in,
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
DEFAULT_VIRTUAL_BASE,
DEFAULT_VIRTUAL_QUOTE,
0,
0,
0,
) as u128
} else {
amount_out as u128
};
let real_base = _amount_out;
Self {
virtual_base: DEFAULT_VIRTUAL_BASE,
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
real_base: real_base,
real_quote: real_quote,
pool_state: pool_state,
base_vault: base_vault,
quote_vault: quote_vault,
mint_token_program: base_token_program,
platform_config: platform_config,
platform_associated_account: platform_associated_account,
creator_associated_account: creator_associated_account,
global_config: global_config,
}
}
pub async fn from_mint_by_rpc(
rpc: &SolanaRpcClient,
mint: &Pubkey,
usd1_pool: bool,
) -> Result<Self, anyhow::Error> {
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
mint,
if usd1_pool {
&crate::constants::USD1_TOKEN_ACCOUNT
} else {
&crate::constants::WSOL_TOKEN_ACCOUNT
},
)
.unwrap();
let pool_data =
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
let token_account = rpc.get_account(&pool_data.base_mint).await?;
let platform_associated_account =
crate::instruction::utils::bonk::get_platform_associated_account(
&pool_data.platform_config,
);
let creator_associated_account =
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
let platform_associated_account = platform_associated_account.unwrap();
let creator_associated_account = creator_associated_account.unwrap();
Ok(Self {
virtual_base: pool_data.virtual_base as u128,
virtual_quote: pool_data.virtual_quote as u128,
real_base: pool_data.real_base as u128,
real_quote: pool_data.real_quote as u128,
pool_state: pool_address,
base_vault: pool_data.base_vault,
quote_vault: pool_data.quote_vault,
mint_token_program: token_account.owner,
platform_config: pool_data.platform_config,
platform_associated_account,
creator_associated_account,
global_config: pool_data.global_config,
})
}
}
+118
View File
@@ -0,0 +1,118 @@
use crate::common::nonce_cache::DurableNonceInfo;
use crate::common::{GasFeeStrategy, SolanaRpcClient};
use crate::swqos::{SwqosClient, TradeType};
use crate::trading::MiddlewareManager;
use core_affinity::CoreId;
use solana_hash::Hash;
use solana_message::AddressLookupTableAccount;
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use std::sync::Arc;
use super::bonk::BonkParams;
use super::meteora_damm_v2::MeteoraDammV2Params;
use super::pumpfun::PumpFunParams;
use super::pumpswap::PumpSwapParams;
use super::raydium_amm_v4::RaydiumAmmV4Params;
use super::raydium_cpmm::RaydiumCpmmParams;
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
#[derive(Clone)]
pub struct SenderConcurrencyConfig {
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
pub effective_core_ids: Arc<Vec<CoreId>>,
pub max_sender_concurrency: usize,
}
/// DEX 参数枚举 - 零开销抽象替代 Box<dyn ProtocolParams>
#[derive(Clone)]
pub enum DexParamEnum {
PumpFun(PumpFunParams),
PumpSwap(PumpSwapParams),
Bonk(BonkParams),
RaydiumCpmm(RaydiumCpmmParams),
RaydiumAmmV4(RaydiumAmmV4Params),
MeteoraDammV2(MeteoraDammV2Params),
}
impl DexParamEnum {
/// 获取内部参数的 Any 引用,用于向后兼容的类型检查
#[inline]
pub fn as_any(&self) -> &dyn std::any::Any {
match self {
DexParamEnum::PumpFun(p) => p,
DexParamEnum::PumpSwap(p) => p,
DexParamEnum::Bonk(p) => p,
DexParamEnum::RaydiumCpmm(p) => p,
DexParamEnum::RaydiumAmmV4(p) => p,
DexParamEnum::MeteoraDammV2(p) => p,
}
}
}
/// Swap parameters
#[derive(Clone)]
pub struct SwapParams {
pub rpc: Option<Arc<SolanaRpcClient>>,
pub payer: Arc<Keypair>,
pub trade_type: TradeType,
pub input_mint: Pubkey,
pub input_token_program: Option<Pubkey>,
pub output_mint: Pubkey,
pub output_token_program: Option<Pubkey>,
pub input_amount: Option<u64>,
pub slippage_basis_points: Option<u64>,
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
pub recent_blockhash: Option<Hash>,
pub wait_tx_confirmed: bool,
pub protocol_params: DexParamEnum,
pub open_seed_optimize: bool,
/// Arc<Vec<..>> so cloning from infrastructure is a single Arc clone.
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
pub middleware_manager: Option<Arc<MiddlewareManager>>,
pub durable_nonce: Option<DurableNonceInfo>,
pub with_tip: bool,
pub create_input_mint_ata: bool,
pub close_input_mint_ata: bool,
pub create_output_mint_ata: bool,
pub close_output_mint_ata: bool,
pub fixed_output_amount: Option<u64>,
pub gas_fee_strategy: GasFeeStrategy,
pub simulate: bool,
/// Whether to output SDK logs (from TradeConfig.log_enabled).
pub log_enabled: bool,
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
pub use_dedicated_sender_threads: bool,
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
/// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path.
pub max_sender_concurrency: usize,
/// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only.
pub effective_core_ids: Arc<Vec<CoreId>>,
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
pub check_min_tip: bool,
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
pub grpc_recv_us: Option<i64>,
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
/// 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<bool>,
}
impl SwapParams {
/// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only.
#[inline]
pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig {
SenderConcurrencyConfig {
sender_thread_cores: self.sender_thread_cores.clone(),
effective_core_ids: self.effective_core_ids.clone(),
max_sender_concurrency: self.max_sender_concurrency,
}
}
}
impl std::fmt::Debug for SwapParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SwapParams: ...")
}
}
@@ -0,0 +1,67 @@
use crate::common::SolanaRpcClient;
use solana_sdk::pubkey::Pubkey;
/// MeteoraDammV2 protocol specific parameters
/// Configuration parameters specific to Meteora Damm V2 trading protocol
#[derive(Clone)]
pub struct MeteoraDammV2Params {
pub pool: Pubkey,
pub token_a_vault: Pubkey,
pub token_b_vault: Pubkey,
pub token_a_mint: Pubkey,
pub token_b_mint: Pubkey,
pub token_a_program: Pubkey,
pub token_b_program: Pubkey,
}
impl MeteoraDammV2Params {
pub fn new(
pool: Pubkey,
token_a_vault: Pubkey,
token_b_vault: Pubkey,
token_a_mint: Pubkey,
token_b_mint: Pubkey,
token_a_program: Pubkey,
token_b_program: Pubkey,
) -> Self {
Self {
pool,
token_a_vault,
token_b_vault,
token_a_mint,
token_b_mint,
token_a_program,
token_b_program,
}
}
pub async fn from_pool_address_by_rpc(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
) -> Result<Self, anyhow::Error> {
let pool_data =
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
let mint_accounts = rpc
.get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint])
.await?;
let token_a_program = mint_accounts
.get(0)
.and_then(|a| a.as_ref())
.map(|a| a.owner)
.ok_or_else(|| anyhow::anyhow!("Token A mint account not found"))?;
let token_b_program = mint_accounts
.get(1)
.and_then(|a| a.as_ref())
.map(|a| a.owner)
.ok_or_else(|| anyhow::anyhow!("Token B mint account not found"))?;
Ok(Self {
pool: *pool_address,
token_a_vault: pool_data.token_a_vault,
token_b_vault: pool_data.token_b_vault,
token_a_mint: pool_data.token_a_mint,
token_b_mint: pool_data.token_b_mint,
token_a_program,
token_b_program,
})
}
}
+17
View File
@@ -0,0 +1,17 @@
//! DEX protocol parameter types and [`SwapParams`].
mod bonk;
mod dex_swap;
mod meteora_damm_v2;
mod pumpfun;
mod pumpswap;
mod raydium_amm_v4;
mod raydium_cpmm;
pub use bonk::BonkParams;
pub use dex_swap::{DexParamEnum, SenderConcurrencyConfig, SwapParams};
pub use meteora_damm_v2::MeteoraDammV2Params;
pub use pumpfun::PumpFunParams;
pub use pumpswap::PumpSwapParams;
pub use raydium_amm_v4::RaydiumAmmV4Params;
pub use raydium_cpmm::RaydiumCpmmParams;
+270
View File
@@ -0,0 +1,270 @@
use crate::common::bonding_curve::BondingCurveAccount;
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
use crate::common::SolanaRpcClient;
use crate::instruction::utils::pumpfun::reconcile_mayhem_mode_for_trade;
use solana_sdk::pubkey::Pubkey;
use std::sync::Arc;
/// PumpFun protocol specific parameters
/// Configuration parameters specific to PumpFun trading protocol.
///
/// **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)).
/// **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<BondingCurveAccount>,
pub associated_bonding_curve: Pubkey,
/// 最新一笔可观测 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<Pubkey>,
/// 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<Pubkey>,
/// 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<bool>,
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。
pub fee_recipient: Pubkey,
}
impl PumpFunParams {
pub fn immediate_sell(
creator_vault: Pubkey,
token_program: Pubkey,
close_token_account_when_sell: bool,
) -> Self {
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,
close_token_account_when_sell: Some(close_token_account_when_sell),
fee_recipient: Pubkey::default(),
}
}
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
/// so that sell instructions include the correct remaining accounts for cashback.
/// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`).
/// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts).
pub fn from_dev_trade(
mint: Pubkey,
token_amount: u64,
max_sol_cost: u64,
creator: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
creator_vault: Pubkey,
close_token_account_when_sell: Option<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
) -> Self {
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
bonding_curve,
&mint,
token_amount,
max_sol_cost,
creator,
is_mayhem_mode,
is_cashback_coin,
);
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
&bonding_curve_account.creator,
creator_vault,
&mint,
None,
)
.or_else(|| {
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
})
.unwrap_or_default();
Self {
bonding_curve: Arc::new(bonding_curve_account),
associated_bonding_curve: associated_bonding_curve,
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,
token_program: token_program,
fee_recipient,
}
}
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
/// so that sell instructions include the correct remaining accounts for cashback.
///
/// `mayhem_mode`:
/// - **`Some(v)`**:优先采用 gRPC / `tradeEvent`,但与 **`fee_recipient` 所属池**Mayhem vs 普通,见 pump-public-docs)不一致时,以 fee 地址为准纠偏,避免链上 `NotAuthorized`。
/// - **`None`**:用 `fee_recipient` 是否落在 Mayhem 静态列表推断。
pub fn from_trade(
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
mint: Pubkey,
creator: Pubkey,
creator_vault: Pubkey,
virtual_token_reserves: u64,
virtual_sol_reserves: u64,
real_token_reserves: u64,
real_sol_reserves: u64,
close_token_account_when_sell: Option<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
) -> Self {
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
let bonding_curve = BondingCurveAccount::from_trade(
bonding_curve,
mint,
creator,
virtual_token_reserves,
virtual_sol_reserves,
real_token_reserves,
real_sol_reserves,
is_mayhem_mode,
is_cashback_coin,
);
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
&bonding_curve.creator,
creator_vault,
&mint,
None,
)
.or_else(|| {
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
})
.unwrap_or_default();
Self {
bonding_curve: Arc::new(bonding_curve),
associated_bonding_curve: associated_bonding_curve,
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,
token_program: token_program,
fee_recipient,
}
}
/// 仅 RPC 读取曲线快照;[`Self::observed_trade_creator`] 为 `None`,便于 bot 缓存合并时用粘性的 trade 日志 creator 覆盖陈旧曲线推导。
pub async fn from_mint_by_rpc(
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Self, anyhow::Error> {
let account =
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?;
let mint_account = rpc.get_account(&mint).await?;
let bonding_curve = BondingCurveAccount {
discriminator: 0,
account: account.1,
virtual_token_reserves: account.0.virtual_token_reserves,
virtual_sol_reserves: account.0.virtual_sol_reserves,
real_token_reserves: account.0.real_token_reserves,
real_sol_reserves: account.0.real_sol_reserves,
token_total_supply: account.0.token_total_supply,
complete: account.0.complete,
creator: account.0.creator,
is_mayhem_mode: account.0.is_mayhem_mode,
is_cashback_coin: account.0.is_cashback_coin,
};
let associated_bonding_curve = get_associated_token_address_with_program_id(
&bonding_curve.account,
mint,
&mint_account.owner,
);
let fee_sharing_creator_vault_if_active =
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint)
.await?;
let creator_vault = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
&bonding_curve.creator,
Pubkey::default(),
mint,
fee_sharing_creator_vault_if_active,
)
.or_else(|| crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator))
.unwrap_or_default();
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,
token_program: mint_account.owner,
fee_recipient: Pubkey::default(),
})
}
/// 链下公式与 **`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(
mut self,
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Self, anyhow::Error> {
self.fee_sharing_creator_vault_if_active =
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint)
.await?;
let c = self.effective_creator_for_trade();
if let Some(v) =
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
&c,
self.creator_vault,
mint,
self.fee_sharing_creator_vault_if_active,
)
{
self.creator_vault = v;
}
Ok(self)
}
/// 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<Pubkey>) -> 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(
mut self,
fee_sharing_creator_vault_if_active: Option<Pubkey>,
) -> Self {
self.fee_sharing_creator_vault_if_active = fee_sharing_creator_vault_if_active;
self
}
}
+220
View File
@@ -0,0 +1,220 @@
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
use crate::common::SolanaRpcClient;
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
use solana_sdk::pubkey::Pubkey;
/// PumpSwap Protocol Specific Parameters
///
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
/// token configuration, and transaction amounts.
///
/// **Performance Note**: If these parameters are not provided, the system will attempt to
/// retrieve the relevant information from RPC, which will increase transaction time.
/// For optimal performance, it is recommended to provide all necessary parameters in advance.
#[derive(Clone)]
pub struct PumpSwapParams {
/// Liquidity pool address
pub pool: Pubkey,
/// Base token mint address
/// The mint account address of the base token in the trading pair
pub base_mint: Pubkey,
/// Quote token mint address
/// The mint account address of the quote token in the trading pair, usually SOL or USDC
pub quote_mint: Pubkey,
/// Pool base token account
pub pool_base_token_account: Pubkey,
/// Pool quote token account
pub pool_quote_token_account: Pubkey,
/// Base token reserves in the pool
pub pool_base_token_reserves: u64,
/// Quote token reserves in the pool
pub pool_quote_token_reserves: u64,
/// Coin creator vault ATA
pub coin_creator_vault_ata: Pubkey,
/// Coin creator vault authority
pub coin_creator_vault_authority: Pubkey,
/// Token program ID
pub base_token_program: Pubkey,
/// Quote token program ID
pub quote_token_program: Pubkey,
/// Whether the pool is in mayhem mode
pub is_mayhem_mode: bool,
/// Pool [`Pool::coin_creator`](crate::instruction::utils::pumpswap_types::Pool). Used for PumpSwap
/// `remaining_accounts`: **`pool-v2` is appended only when this is not `Pubkey::default()`
/// (matches `@pump-fun/pump-swap-sdk`); wrong flag causes buys to revert with buyback recipient errors (e.g. 6053).
pub coin_creator: Pubkey,
/// Whether the pool's coin has cashback enabled
pub is_cashback_coin: bool,
/// Cashback fee in basis points (from trade events / sol-parser-sdk). For quote-in buy and base-in sell
/// math, this is summed with [`COIN_CREATOR_FEE_BASIS_POINTS`](crate::instruction::utils::pumpswap::accounts::COIN_CREATOR_FEE_BASIS_POINTS)
/// when a creator vault applies — matching on-chain treating creator + cashback as one fee bucket.
/// Use `0` when unknown (e.g. RPC-only pool decode has no per-mint cashback bps).
pub cashback_fee_basis_points: u64,
}
impl PumpSwapParams {
pub fn new(
pool: Pubkey,
base_mint: Pubkey,
quote_mint: Pubkey,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
pool_base_token_reserves: u64,
pool_quote_token_reserves: u64,
coin_creator_vault_ata: Pubkey,
coin_creator_vault_authority: Pubkey,
base_token_program: Pubkey,
quote_token_program: Pubkey,
fee_recipient: Pubkey,
coin_creator: Pubkey,
is_cashback_coin: bool,
cashback_fee_basis_points: u64,
) -> Self {
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
Self {
pool,
base_mint,
quote_mint,
pool_base_token_account,
pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program,
quote_token_program,
is_mayhem_mode,
coin_creator,
is_cashback_coin,
cashback_fee_basis_points,
}
}
/// Fast-path constructor for building PumpSwap parameters directly from decoded
/// trade/event data and the accompanying instruction accounts, avoiding RPC
/// lookups and associated latency. Token program IDs should be sourced from
/// the instruction accounts themselves to respect Token Program vs Token-2022
/// differences.
///
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
/// from the event so that buy/sell instructions include the correct remaining
/// accounts for cashback.
pub fn from_trade(
pool: Pubkey,
base_mint: Pubkey,
quote_mint: Pubkey,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
pool_base_token_reserves: u64,
pool_quote_token_reserves: u64,
coin_creator_vault_ata: Pubkey,
coin_creator_vault_authority: Pubkey,
base_token_program: Pubkey,
quote_token_program: Pubkey,
fee_recipient: Pubkey,
coin_creator: Pubkey,
is_cashback_coin: bool,
cashback_fee_basis_points: u64,
) -> Self {
Self::new(
pool,
base_mint,
quote_mint,
pool_base_token_account,
pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program,
quote_token_program,
fee_recipient,
coin_creator,
is_cashback_coin,
cashback_fee_basis_points,
)
}
pub async fn from_mint_by_rpc(
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Self, anyhow::Error> {
if let Ok((pool_address, _)) =
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
{
Self::from_pool_address_by_rpc(rpc, &pool_address).await
} else if let Ok((pool_address, _)) =
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
{
Self::from_pool_address_by_rpc(rpc, &pool_address).await
} else {
return Err(anyhow::anyhow!("No pool found for mint"));
}
}
pub async fn from_pool_address_by_rpc(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
) -> Result<Self, anyhow::Error> {
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
Self::from_pool_data(rpc, pool_address, &pool_data).await
}
/// Build params from an already-decoded Pool, only fetching token balances.
///
/// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data
/// is already available (e.g. from `pumpswap::find_by_mint` which returns the
/// decoded Pool).
pub async fn from_pool_data(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
) -> Result<Self, anyhow::Error> {
let (pool_base_token_reserves, pool_quote_token_reserves) =
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
let creator = pool_data.coin_creator;
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
creator,
pool_data.quote_mint,
);
let coin_creator_vault_authority =
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
let base_token_program_ata = get_associated_token_address_with_program_id(
pool_address,
&pool_data.base_mint,
&crate::constants::TOKEN_PROGRAM,
);
let quote_token_program_ata = get_associated_token_address_with_program_id(
pool_address,
&pool_data.quote_mint,
&crate::constants::TOKEN_PROGRAM,
);
Ok(Self {
pool: *pool_address,
base_mint: pool_data.base_mint,
quote_mint: pool_data.quote_mint,
pool_base_token_account: pool_data.pool_base_token_account,
pool_quote_token_account: pool_data.pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
crate::constants::TOKEN_PROGRAM
} else {
crate::constants::TOKEN_PROGRAM_2022
},
is_cashback_coin: pool_data.is_cashback_coin,
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
crate::constants::TOKEN_PROGRAM
} else {
crate::constants::TOKEN_PROGRAM_2022
},
is_mayhem_mode: pool_data.is_mayhem_mode,
coin_creator: pool_data.coin_creator,
cashback_fee_basis_points: 0,
})
}
}
+54
View File
@@ -0,0 +1,54 @@
use crate::common::SolanaRpcClient;
use crate::trading::common::get_multi_token_balances;
use solana_sdk::pubkey::Pubkey;
/// RaydiumCpmm protocol specific parameters
/// Configuration parameters specific to Raydium CPMM trading protocol
#[derive(Clone)]
pub struct RaydiumAmmV4Params {
/// AMM pool address
pub amm: Pubkey,
/// Base token (coin) mint address
pub coin_mint: Pubkey,
/// Quote token (pc) mint address
pub pc_mint: Pubkey,
/// Pool's coin token account address
pub token_coin: Pubkey,
/// Pool's pc token account address
pub token_pc: Pubkey,
/// Current coin reserve amount in the pool
pub coin_reserve: u64,
/// Current pc reserve amount in the pool
pub pc_reserve: u64,
}
impl RaydiumAmmV4Params {
pub fn new(
amm: Pubkey,
coin_mint: Pubkey,
pc_mint: Pubkey,
token_coin: Pubkey,
token_pc: Pubkey,
coin_reserve: u64,
pc_reserve: u64,
) -> Self {
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
}
pub async fn from_amm_address_by_rpc(
rpc: &SolanaRpcClient,
amm: Pubkey,
) -> Result<Self, anyhow::Error> {
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
let (coin_reserve, pc_reserve) =
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
Ok(Self {
amm,
coin_mint: amm_info.coin_mint,
pc_mint: amm_info.pc_mint,
token_coin: amm_info.token_coin,
token_pc: amm_info.token_pc,
coin_reserve,
pc_reserve,
})
}
}
+89
View File
@@ -0,0 +1,89 @@
use crate::common::SolanaRpcClient;
use solana_sdk::pubkey::Pubkey;
/// RaydiumCpmm protocol specific parameters
/// Configuration parameters specific to Raydium CPMM trading protocol
#[derive(Clone)]
pub struct RaydiumCpmmParams {
/// Pool address
pub pool_state: Pubkey,
/// Amm config address
pub amm_config: Pubkey,
/// Base token mint address
pub base_mint: Pubkey,
/// Quote token mint address
pub quote_mint: Pubkey,
/// Base token reserve amount in the pool
pub base_reserve: u64,
/// Quote token reserve amount in the pool
pub quote_reserve: u64,
/// Base token vault address
pub base_vault: Pubkey,
/// Quote token vault address
pub quote_vault: Pubkey,
/// Base token program ID
pub base_token_program: Pubkey,
/// Quote token program ID
pub quote_token_program: Pubkey,
/// Observation state account
pub observation_state: Pubkey,
}
impl RaydiumCpmmParams {
pub fn from_trade(
pool_state: Pubkey,
amm_config: Pubkey,
input_token_mint: Pubkey,
output_token_mint: Pubkey,
input_vault: Pubkey,
output_vault: Pubkey,
input_token_program: Pubkey,
output_token_program: Pubkey,
observation_state: Pubkey,
base_reserve: u64,
quote_reserve: u64,
) -> Self {
Self {
pool_state: pool_state,
amm_config: amm_config,
base_mint: input_token_mint,
quote_mint: output_token_mint,
base_reserve: base_reserve,
quote_reserve: quote_reserve,
base_vault: input_vault,
quote_vault: output_vault,
base_token_program: input_token_program,
quote_token_program: output_token_program,
observation_state: observation_state,
}
}
pub async fn from_pool_address_by_rpc(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
) -> Result<Self, anyhow::Error> {
let pool =
crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?;
let (token0_balance, token1_balance) =
crate::instruction::utils::raydium_cpmm::get_pool_token_balances(
rpc,
pool_address,
&pool.token0_mint,
&pool.token1_mint,
)
.await?;
Ok(Self {
pool_state: *pool_address,
amm_config: pool.amm_config,
base_mint: pool.token0_mint,
quote_mint: pool.token1_mint,
base_reserve: token0_balance,
quote_reserve: token1_balance,
base_vault: pool.token0_vault,
quote_vault: pool.token1_vault,
base_token_program: pool.token0_program,
quote_token_program: pool.token1_program,
observation_state: pool.observation_key,
})
}
}
+3
View File
@@ -72,6 +72,9 @@ pub const fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64
/// * basis_points = 500 -> 5% slippage
#[inline(always)]
pub const fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 {
if amount == 0 {
return 0;
}
if amount <= basis_points / 10000 {
1
} else {
+31 -12
View File
@@ -6,6 +6,21 @@ use crate::instruction::utils::pumpswap::accounts::{
};
use solana_sdk::pubkey::Pubkey;
/// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional
/// cashback fee bps for cashback-enabled coins (see Pump AMM / parser event field).
#[inline]
pub(crate) fn creator_side_fee_basis_points(
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> u64 {
let creator_bps = if *coin_creator == Pubkey::default() {
0
} else {
COIN_CREATOR_FEE_BASIS_POINTS
};
creator_bps.saturating_add(cashback_fee_basis_points)
}
/// Result for buying base tokens with base amount input
#[derive(Clone, Debug)]
pub struct BuyBaseInputResult {
@@ -58,6 +73,7 @@ pub struct SellQuoteInputResult {
/// * `base_reserve` - Base token reserves in the pool
/// * `quote_reserve` - Quote token reserves in the pool
/// * `coin_creator` - Token creator address
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins (from on-chain / events); use `0` if unknown
///
/// # Returns
/// * `BuyBaseInputResult` containing quote amounts and slippage calculations
@@ -67,6 +83,7 @@ pub fn buy_base_input_internal(
base_reserve: u64,
quote_reserve: u64,
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> Result<BuyBaseInputResult, String> {
if base_reserve == 0 || quote_reserve == 0 {
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
@@ -89,11 +106,9 @@ pub fn buy_base_input_internal(
let lp_fee = compute_fee(quote_amount_in as u128, LP_FEE_BASIS_POINTS as u128) as u64;
let protocol_fee =
compute_fee(quote_amount_in as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
let coin_creator_fee = if *coin_creator == Pubkey::default() {
0
} else {
compute_fee(quote_amount_in as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64
};
let creator_bps =
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
let coin_creator_fee = compute_fee(quote_amount_in as u128, creator_bps) as u64;
let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee;
// Calculate max quote with slippage
@@ -114,6 +129,7 @@ pub fn buy_base_input_internal(
/// * `base_reserve` - Base token reserves in the pool
/// * `quote_reserve` - Quote token reserves in the pool
/// * `coin_creator` - Token creator address
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
///
/// # Returns
/// * `BuyQuoteInputResult` containing base amount and slippage calculations
@@ -123,6 +139,7 @@ pub fn buy_quote_input_internal(
base_reserve: u64,
quote_reserve: u64,
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> Result<BuyQuoteInputResult, String> {
if base_reserve == 0 || quote_reserve == 0 {
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
@@ -131,7 +148,7 @@ pub fn buy_quote_input_internal(
// Calculate total fee basis points
let total_fee_bps = LP_FEE_BASIS_POINTS
+ PROTOCOL_FEE_BASIS_POINTS
+ if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS };
+ creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points);
let denominator = 10_000 + total_fee_bps;
// Calculate effective quote amount after fees
@@ -165,6 +182,7 @@ pub fn buy_quote_input_internal(
/// * `base_reserve` - Base token reserves in the pool
/// * `quote_reserve` - Quote token reserves in the pool
/// * `coin_creator` - Token creator address
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
///
/// # Returns
/// * `SellBaseInputResult` containing quote amounts and slippage calculations
@@ -174,6 +192,7 @@ pub fn sell_base_input_internal(
base_reserve: u64,
quote_reserve: u64,
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> Result<SellBaseInputResult, String> {
if base_reserve == 0 || quote_reserve == 0 {
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
@@ -187,11 +206,9 @@ pub fn sell_base_input_internal(
let lp_fee = compute_fee(quote_amount_out as u128, LP_FEE_BASIS_POINTS as u128) as u64;
let protocol_fee =
compute_fee(quote_amount_out as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
let coin_creator_fee = if *coin_creator == Pubkey::default() {
0
} else {
compute_fee(quote_amount_out as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64
};
let creator_bps =
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
let coin_creator_fee = compute_fee(quote_amount_out as u128, creator_bps) as u64;
// Calculate final quote after fees
let total_fees = lp_fee + protocol_fee + coin_creator_fee;
@@ -234,6 +251,7 @@ fn calculate_quote_amount_out(
/// * `base_reserve` - Base token reserves in the pool
/// * `quote_reserve` - Quote token reserves in the pool
/// * `coin_creator` - Token creator address
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
///
/// # Returns
/// * `SellQuoteInputResult` containing base amount and slippage calculations
@@ -243,6 +261,7 @@ pub fn sell_quote_input_internal(
base_reserve: u64,
quote_reserve: u64,
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> Result<SellQuoteInputResult, String> {
if base_reserve == 0 || quote_reserve == 0 {
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
@@ -256,7 +275,7 @@ pub fn sell_quote_input_internal(
quote,
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS },
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
);
// Calculate base amount needed using inverse constant product formula