Compare commits

...

5 Commits

Author SHA1 Message Date
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
22 changed files with 2625 additions and 2199 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.4"
version = "4.0.6"
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
@@ -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
@@ -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;
+18 -23
View File
@@ -7,12 +7,15 @@ use crate::{
},
};
use crate::{
instruction::pumpfun_ix_data::{
encode_pumpfun_buy_exact_sol_in_ix_data, encode_pumpfun_buy_ix_data,
encode_pumpfun_sell_ix_data, TRACK_VOLUME_TRUE,
},
instruction::utils::pumpfun::{
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda,
get_protocol_extra_fee_recipient_random, get_user_volume_accumulator_pda,
pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix,
pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix_with_fee_sharing,
global_constants::{self},
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
},
utils::calc::{
common::{calculate_with_slippage_buy, calculate_with_slippage_sell},
@@ -46,10 +49,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
// creator_vault must be PDA(creator) per bonding curve. Event vault: use only if == derived;
// if stream sends a mismatched vault (wrong token / stale), fall back to derived.
let creator = bonding_curve.creator;
let creator_vault_pda = resolve_creator_vault_for_ix(
let creator_vault_pda = resolve_creator_vault_for_ix_with_fee_sharing(
&creator,
protocol_params.creator_vault,
&params.output_mint,
protocol_params.fee_sharing_creator_vault_if_active,
)
.ok_or_else(|| {
anyhow!(
@@ -130,26 +134,19 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
);
}
// IDL: buy/buy_exact_sol_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true)
let track_volume = if bonding_curve.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false)
let mut buy_data = [0u8; 26];
if params.use_exact_sol_amount.unwrap_or(true) {
// buy_exact_sol_in(spendable_sol_in: u64, min_tokens_out: u64, track_volume)
let buy_data = if params.use_exact_sol_amount.unwrap_or(true) {
let min_tokens_out = calculate_with_slippage_sell(
buy_token_amount,
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
buy_data[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR);
buy_data[8..16].copy_from_slice(&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);
encode_pumpfun_buy_exact_sol_in_ix_data(
params.input_amount.unwrap_or(0),
min_tokens_out,
TRACK_VOLUME_TRUE,
)
} else {
// buy(token_amount: u64, max_sol_cost: u64, track_volume)
buy_data[..8].copy_from_slice(&BUY_DISCRIMINATOR);
buy_data[8..16].copy_from_slice(&buy_token_amount.to_le_bytes());
buy_data[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
buy_data[24..26].copy_from_slice(&track_volume);
}
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, TRACK_VOLUME_TRUE)
};
// Fee recipient: gRPC/ShredStream 填入的 `PumpFunParams.fee_recipient`(同笔 create_v2+buy 或 trade 日志)优先;热路径无 RPC。
let fee_recipient_meta =
@@ -206,10 +203,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
let bonding_curve = &protocol_params.bonding_curve;
let creator = bonding_curve.creator;
let creator_vault_pda = resolve_creator_vault_for_ix(
let creator_vault_pda = resolve_creator_vault_for_ix_with_fee_sharing(
&creator,
protocol_params.creator_vault,
&params.input_mint,
protocol_params.fee_sharing_creator_vault_if_active,
)
.ok_or_else(|| {
anyhow!(
@@ -269,10 +267,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
// ========================================
let mut instructions = Vec::with_capacity(2);
let mut sell_data = [0u8; 24];
sell_data[..8].copy_from_slice(&SELL_DISCRIMINATOR);
sell_data[8..16].copy_from_slice(&token_amount.to_le_bytes());
sell_data[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
let sell_data = encode_pumpfun_sell_ix_data(token_amount, min_sol_output);
let fee_recipient_meta =
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
+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
}
+199 -10
View File
@@ -219,6 +219,12 @@ pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
pub const BUY_EXACT_SOL_IN_DISCRIMINATOR: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
/// Anchor account discriminator for `SharingConfig` on `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` (`pump_fees.json`).
pub const SHARING_CONFIG_ACCOUNT_DISCRIMINATOR: [u8; 8] = [216, 74, 9, 0, 56, 140, 93, 75];
/// `ConfigStatus::Active` as serialized by Anchor (enum variant index; `Paused` = 0).
const SHARING_CONFIG_STATUS_ACTIVE: u8 = 1;
/// Check if a pubkey is one of the Mayhem fee recipients
#[inline]
pub fn is_mayhem_fee_recipient(pubkey: &Pubkey) -> bool {
@@ -237,6 +243,50 @@ pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
}
/// Non-mayhem bonding-curve fee pool: primary fee recipient + AMM protocol fee slots (`getFeeRecipient` when `mayhemMode === false`).
#[inline]
pub fn is_standard_bonding_fee_recipient(pubkey: &Pubkey) -> bool {
*pubkey == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(pubkey)
}
/// Bonding-curve trade: `mayhemMode` 与账户 #2 fee recipient 必须同属 Mayhem 池或同属普通池,否则会 Anchor `NotAuthorized`fee_recipient 校验)。
/// gRPC 偶尔只带对一侧的字段时,用 fee 地址与静态池对齐。
#[inline]
pub fn reconcile_mayhem_mode_for_trade(
mayhem_from_event: Option<bool>,
fee_recipient: &Pubkey,
) -> bool {
if *fee_recipient == Pubkey::default() {
return mayhem_from_event.unwrap_or(false);
}
let fee_m = is_mayhem_fee_recipient(fee_recipient);
let fee_s = is_standard_bonding_fee_recipient(fee_recipient);
match mayhem_from_event {
Some(log_m) => {
if fee_m && !log_m {
true
} else if fee_s && log_m && !fee_m {
false
} else {
log_m
}
}
None => fee_m,
}
}
/// Whether `pk` is allowed as account #2 for this bonding-curve mode. Unknown pubkeys (not in static pools) are accepted — chain is authoritative.
#[inline]
pub fn fee_recipient_ok_for_bonding_curve_mode(pk: &Pubkey, is_mayhem_mode: bool) -> bool {
let is_m = is_mayhem_fee_recipient(pk);
let is_s = is_standard_bonding_fee_recipient(pk);
if is_mayhem_mode {
!(is_s && !is_m)
} else {
!(is_m && !is_s)
}
}
/// Mayhem: random among `Global.reservedFeeRecipient` + `Global.reservedFeeRecipients` (`fees.ts` `getFeeRecipient` when `mayhemMode === true`).
/// Uses hardcoded `MAYHEM_FEE_RECIPIENTS`; prefer gRPC/event `PumpFunParams.fee_recipient` when set.
#[inline]
@@ -279,10 +329,12 @@ pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
.unwrap_or(&global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
}
/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供时按 mayhem 从静态池随机
/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供或与 `is_mayhem_mode` 池不一致时按 mayhem 从静态池随机(避免 NotAuthorized
#[inline]
pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
if from_stream != Pubkey::default() {
let trust_stream = from_stream != Pubkey::default()
&& fee_recipient_ok_for_bonding_curve_mode(&from_stream, is_mayhem_mode);
if trust_stream {
AccountMeta {
pubkey: from_stream,
is_signer: false,
@@ -384,17 +436,42 @@ pub fn is_phantom_default_creator_vault(pk: &Pubkey) -> bool {
/// `creator_vault` was filled from **instruction accounts** (e.g. `fill_trade_accounts` index 9),
/// **trust that vault** — unless it equals [`phantom_default_creator_vault`] (bad derivation / cache).
/// - If event `creator_vault` is **missing** → [`get_creator_vault_pda`]`(creator)` (never `PDA(default)`).
/// - If it **matches** `PDA(creator)` or `PDA(fee_sharing_config(mint))` → use it (fast path, matches ix).
/// - If it **does not match** either (e.g. stale vault but `creator` from tradeEvent is correct) → use
/// [`get_creator_vault_pda`]`(creator)` so seeds match on-chain bonding curve (fixes 2006 Left≠Right).
/// - If it **matches** `PDA(creator)` → use it (fast path).
/// - If **Creator Rewards Sharing is active** (`fee_sharing_creator_vault_if_active` / RPC) and the event
/// vault matches `PDA(["creator-vault", fee_sharing_config_pda])` → use it.
/// - If `fee_sharing_creator_vault_if_active` is **None**, do **not** trust a stale event vault that merely
/// equals the theoretical sharing-layout PDA — fall back to `PDA(creator)` (sharing may have migrated off).
/// - If the event vault is some other stale value but `creator` is correct → `PDA(creator)` (fixes 2006 Left≠Right).
///
/// For **Creator Rewards Sharing** (`fee_sharing_creator_vault_if_active`), prefer
/// [`resolve_creator_vault_for_ix_with_fee_sharing`] or [`fetch_fee_sharing_creator_vault_if_active`].
#[inline]
pub fn resolve_creator_vault_for_ix(
creator: &Pubkey,
creator_vault_from_event: Pubkey,
mint: &Pubkey,
) -> Option<Pubkey> {
resolve_creator_vault_for_ix_with_fee_sharing(creator, creator_vault_from_event, mint, None)
}
/// Same as [`resolve_creator_vault_for_ix`], but when `fee_sharing_creator_vault_if_active` is
/// `Some(PDA(["creator-vault", fee_sharing_config_pda(mint)]))` from an RPC hint, overrides stale
/// `PDA(creator)` so sell/buy match on-chain seeds (Anchor 2006).
#[inline]
pub fn resolve_creator_vault_for_ix_with_fee_sharing(
creator: &Pubkey,
creator_vault_from_event: Pubkey,
mint: &Pubkey,
fee_sharing_creator_vault_if_active: Option<Pubkey>,
) -> Option<Pubkey> {
let phantom = phantom_default_creator_vault();
let fee_sharing_vault: Option<Pubkey> = fee_sharing_creator_vault_if_active.and_then(|v| {
let sharing_pk = get_fee_sharing_config_pda(mint)?;
let expected = get_creator_vault_pda(&sharing_pk)?;
if v == expected { Some(v) } else { None }
});
if *creator == Pubkey::default() {
if creator_vault_from_event == Pubkey::default() {
return None;
@@ -405,27 +482,83 @@ pub fn resolve_creator_vault_for_ix(
return Some(creator_vault_from_event);
}
// Real creator: poisoned cache may hold phantom vault — always remap to PDA(creator).
if creator_vault_from_event == phantom {
if let Some(vs) = fee_sharing_vault {
let v_derived = get_creator_vault_pda(creator)?;
if vs != v_derived {
return Some(vs);
}
}
return get_creator_vault_pda(creator);
}
let v_derived = get_creator_vault_pda(creator)?;
if let Some(vs) = fee_sharing_vault {
if vs != v_derived
&& (creator_vault_from_event == Pubkey::default()
|| creator_vault_from_event == v_derived
|| creator_vault_from_event != vs)
{
return Some(vs);
}
}
if creator_vault_from_event == Pubkey::default() {
return Some(v_derived);
}
if creator_vault_from_event == v_derived {
return Some(creator_vault_from_event);
}
if let Some(sharing) = get_fee_sharing_config_pda(mint) {
let v_sharing = get_creator_vault_pda(&sharing)?;
if creator_vault_from_event == v_sharing {
return Some(creator_vault_from_event);
// 仅当 RPC / 调用方已确认 Creator Rewards Sharing **仍 Active**hint 为 Some)时,才信任
// `creator_vault == PDA(["creator-vault", fee_sharing_config])`。hint 为 None 时可能是「已停用」或从未拉取:
// 此时旧缓存里的 fee-sharing vault 必须回退到 `PDA(creator)`,否则易与链上 seeds 不一致(2006)。
if fee_sharing_creator_vault_if_active.is_some() {
if let Some(sharing) = get_fee_sharing_config_pda(mint) {
let v_sharing = get_creator_vault_pda(&sharing)?;
if creator_vault_from_event == v_sharing {
return Some(creator_vault_from_event);
}
}
}
Some(v_derived)
}
/// Read pump-fees `SharingConfig` for `mint`; if **Active** and `mint` matches, return
/// `PDA(["creator-vault", fee_sharing_config_pda])` on Pump program (same as npm `pump-sdk`).
#[inline]
pub async fn fetch_fee_sharing_creator_vault_if_active(
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Option<Pubkey>, anyhow::Error> {
let Some(config_pda) = get_fee_sharing_config_pda(mint) else {
return Ok(None);
};
let acc = match rpc.get_account(&config_pda).await {
Ok(a) => a,
Err(_) => return Ok(None),
};
if acc.owner != accounts::FEE_PROGRAM {
return Ok(None);
}
let d = acc.data.as_slice();
if d.len() < 43 || d[..8] != SHARING_CONFIG_ACCOUNT_DISCRIMINATOR {
return Ok(None);
}
if d[10] != SHARING_CONFIG_STATUS_ACTIVE {
return Ok(None);
}
let mint_on_chain = Pubkey::new_from_array(
d[11..43]
.try_into()
.map_err(|_| anyhow::anyhow!("SharingConfig mint slice"))?,
);
if mint_on_chain != *mint {
return Ok(None);
}
Ok(get_creator_vault_pda(&config_pda))
}
#[inline]
pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
crate::common::fast_fn::get_cached_pda(
@@ -547,6 +680,23 @@ mod tests {
);
}
#[test]
fn resolve_prefers_fee_sharing_vault_when_rpc_hint_and_differs_from_creator_pda() {
let creator = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let v_derived = get_creator_vault_pda(&creator).unwrap();
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
assert_ne!(v_derived, vs);
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(
&creator,
v_derived,
&mint,
Some(vs),
);
assert_eq!(resolved, Some(vs));
}
#[test]
fn resolve_remaps_phantom_vault_when_creator_known() {
let creator = Pubkey::new_unique();
@@ -557,4 +707,43 @@ mod tests {
Some(expected)
);
}
#[test]
fn resolve_stale_fee_sharing_vault_falls_back_to_creator_pda_when_hint_inactive() {
let creator = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let v_derived = get_creator_vault_pda(&creator).unwrap();
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
assert_ne!(v_derived, vs);
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, None);
assert_eq!(
resolved,
Some(v_derived),
"hint None: stale sharing-layout vault in cache must not win over PDA(creator)"
);
}
#[test]
fn reconcile_mayhem_prefers_fee_when_log_says_false_but_fee_is_mayhem_pool() {
let fee = global_constants::MAYHEM_FEE_RECIPIENTS[0];
assert!(reconcile_mayhem_mode_for_trade(Some(false), &fee));
}
#[test]
fn reconcile_mayhem_prefers_fee_when_log_says_true_but_fee_is_standard_pool() {
let fee = global_constants::PUMPFUN_AMM_FEE_4;
assert!(!reconcile_mayhem_mode_for_trade(Some(true), &fee));
}
#[test]
fn pump_fee_meta_rejects_standard_fee_when_building_mayhem_ix() {
let fee = global_constants::PUMPFUN_AMM_FEE_4;
let m = pump_fun_fee_recipient_meta(fee, true);
assert!(
global_constants::MAYHEM_FEE_RECIPIENTS.contains(&m.pubkey),
"expected fallback to mayhem pool, got {}",
m.pubkey
);
}
}
+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
-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;
+240
View File
@@ -0,0 +1,240 @@
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)).
/// Keep `bonding_curve.creator` in sync with chain; ix building uses [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing).
#[derive(Clone)]
pub struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>,
pub associated_bonding_curve: Pubkey,
/// Resolved by [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing): ix vault when it matches `PDA(creator)`, fee-sharing vault, or RPC hint.
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>,
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,
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,
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,
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,
}
}
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,
creator_vault,
fee_sharing_creator_vault_if_active,
close_token_account_when_sell: None,
token_program: mint_account.owner,
fee_recipient: Pubkey::default(),
})
}
/// 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.bonding_curve.creator;
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 [`BondingCurveAccount::creator`].
#[inline]
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
self.creator_vault = creator_vault;
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