Compare commits

..

2 Commits

Author SHA1 Message Date
0xfnzero 8ed2aef931 Fix trade execution correctness and release 4.0.11 2026-05-21 17:50:06 +08:00
0xfnzero 0cf0064e80 docs: clarify PumpFun V2 configuration 2026-05-21 14:48:43 +08:00
12 changed files with 205 additions and 136 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.10"
version = "4.0.11"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+7 -3
View File
@@ -152,7 +152,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .check_min_tip(false) // default: false - filter SWQOS below min tip
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
// .use_pumpfun_v2(false) // default: false - V1 (18 accounts); set true for V2 (27 accounts, quote_mint) when PumpFun deploys V2
// .use_pumpfun_v2(true) // PumpFun V2 (27 accounts, quote_mint); required for USDC-paired PumpFun coins
.build();
// Create TradingClient
@@ -376,7 +376,7 @@ PumpFun has two instruction sets for bonding-curve trading:
**How to enable V2:**
**Method 1 — Global runtime flag** (recommended when PumpFun officially deploys V2 on mainnet):
**Method 1 — Global runtime flag** (recommended for `TradingClient`; required for USDC-paired coins):
```rust
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
@@ -384,7 +384,9 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
.build();
```
**Method 2 — Per-trade via `quote_mint`** (for USDC-paired coins or mixed V1/V2 scenarios):
**Method 2 — Set the quote mint on `PumpFunParams`**:
When using the high-level `TradingClient`, keep `.use_pumpfun_v2(true)` enabled in `TradeConfig` and set `quote_mint` on the PumpFun params to select the pair:
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
@@ -399,6 +401,8 @@ let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(USDC_TOKEN_ACCOUNT);
```
`with_quote_mint(...)` also marks the params as V2-capable for lower-level instruction builders, but the high-level `TradingClient` uses the client-level `use_pumpfun_v2` runtime flag when choosing V1 vs V2.
> **Note**: V2 transactions with ATA creation + durable nonce may exceed `PACKET_DATA_SIZE`. Enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
#### PumpSwap: coin_creator_vault from events (no RPC)
+12 -1
View File
@@ -152,6 +152,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
// .mev_protection(false) // 默认: false - MEVAstralane QUIC :9000 或 HTTP mev-protect / BlockRazor
// .use_pumpfun_v2(true) // PumpFun V227 个账户,quote_mint);USDC 配对 PumpFun 币必须开启
.build();
// 创建 TradingClient
@@ -372,7 +373,15 @@ Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过
**使用方式:**
设置 `PumpFunParams``quote_mint` 即可,SDK 会自动切换到 v2 discriminator 和新账户布局
高层 `TradingClient` 需要先在 `TradeConfig` 开启 PumpFun V2USDC 配对币必须开启
```rust
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
.use_pumpfun_v2(true)
.build();
```
然后在 `PumpFunParams` 设置 `quote_mint` 来选择 SOL/USDC 配对:
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
@@ -391,6 +400,8 @@ client.buy(buy_params).await?;
client.sell(sell_params).await?;
```
`with_quote_mint(...)` 会把参数标记为可使用 V2,底层 instruction builder 可据此走 V2;但高层 `TradingClient` 选择 V1/V2 时使用的是 client 级别的 `use_pumpfun_v2` 运行时开关。
| quote_mint | use_v2_ix | 实际使用的指令 | 说明 |
|-----------|-------------|---------|------|
| 未设置(默认) | `false` | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
+64 -25
View File
@@ -4,7 +4,7 @@ use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use std::sync::Arc;
use std::{hash::Hash, sync::Arc};
use crate::common::{
spl_associated_token_account::get_associated_token_address_with_program_id,
@@ -21,6 +21,22 @@ const MAX_PDA_CACHE_SIZE: usize = 100_000;
const MAX_ATA_CACHE_SIZE: usize = 100_000;
const MAX_INSTRUCTION_CACHE_SIZE: usize = 100_000;
#[inline]
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
where
K: Eq + Hash + Clone,
{
let len = cache.len();
if len <= max_size {
return;
}
let remove_count = (len - max_size).max(max_size / 16).min(len);
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
for key in keys {
cache.remove(&key);
}
}
// --------------------- Instruction Cache ---------------------
/// Instruction cache key for uniquely identifying instruction types and parameters
@@ -66,7 +82,10 @@ where
};
// Lock-free cache lookup with entry API
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone()
let instructions =
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone();
prune_cache(&INSTRUCTION_CACHE, MAX_INSTRUCTION_CACHE_SIZE);
instructions
}
// --------------------- Associated Token Account ---------------------
@@ -106,8 +125,26 @@ pub fn _create_associated_token_account_idempotent_fast(
use_seed,
};
// Only use seed if the mint address is not wSOL or SOL
// 🔧 修复:Token-2022 也支持 seed 方式(白名单方式更安全)
let build_standard_create = || {
let associated_token_address =
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
vec![Instruction {
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*payer, true), // Payer (signer, writable)
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
crate::constants::SYSTEM_PROGRAM_META,
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
],
data: vec![1],
}]
};
// Only use seed if the mint address is not wSOL or SOL.
// Seed accounts are deterministic token accounts, not ATAs; callers must ensure they are not
// recreating an existing seeded account. Fall back to idempotent ATA if seed assembly fails.
let arc_instructions = if use_seed
&& !mint.eq(&crate::constants::WSOL_TOKEN_ACCOUNT)
&& !mint.eq(&crate::constants::SOL_TOKEN_ACCOUNT)
@@ -117,29 +154,18 @@ pub fn _create_associated_token_account_idempotent_fast(
// Use cache to get instruction
get_cached_instructions(cache_key, || {
super::seed::create_associated_token_account_use_seed(payer, owner, mint, token_program)
.unwrap()
.unwrap_or_else(|err| {
tracing::warn!(
"seed token account setup failed for mint {}: {}; fallback to idempotent ATA",
mint,
err
);
build_standard_create()
})
})
} else {
// Use cache to get instruction
get_cached_instructions(cache_key, || {
// Get Associated Token Address using cache
let associated_token_address =
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
// Create Associated Token Account instruction
// Reference implementation of spl_associated_token_account::instruction::create_associated_token_account
vec![Instruction {
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*payer, true), // Payer (signer, writable)
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
crate::constants::SYSTEM_PROGRAM_META,
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
],
data: vec![1],
}]
})
get_cached_instructions(cache_key, build_standard_create)
};
// 🚀 性能优化:尝试零开销解包 Arc,如果引用计数=1则直接移出,否则克隆
@@ -183,6 +209,7 @@ where
if let Some(pda) = pda_result {
PDA_CACHE.insert(cache_key, pda);
prune_cache(&PDA_CACHE, MAX_PDA_CACHE_SIZE);
}
pda_result
@@ -265,7 +292,18 @@ fn _get_associated_token_address_with_program_id_fast(
token_mint_address,
token_program_id,
)
.unwrap()
.unwrap_or_else(|err| {
tracing::warn!(
"seed token account address failed for mint {}: {}; fallback to ATA",
token_mint_address,
err
);
get_associated_token_address_with_program_id(
wallet_address,
token_mint_address,
token_program_id,
)
})
} else {
get_associated_token_address_with_program_id(
wallet_address,
@@ -276,6 +314,7 @@ fn _get_associated_token_address_with_program_id_fast(
// Store computation result in cache (lock-free)
ATA_CACHE.insert(cache_key, ata);
prune_cache(&ATA_CACHE, MAX_ATA_CACHE_SIZE);
ata
}
+31 -34
View File
@@ -1,5 +1,4 @@
use crate::common::SolanaRpcClient;
use anyhow::anyhow;
use fnv::FnvHasher;
use once_cell::sync::Lazy;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
@@ -50,6 +49,26 @@ async fn fetch_rent_for_token_account(
Ok(client.get_minimum_balance_for_rent_exemption(165).await?)
}
#[inline]
fn derive_seed_from_mint(mint: &Pubkey) -> String {
// Keep the legacy 8-hex seed stable. Changing this derivation changes the token account
// address and can strand balances created by earlier buys.
let mut hasher = FnvHasher::default();
hasher.write(mint.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
let mut seed = String::with_capacity(8);
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
let byte = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
seed.push(byte as char);
}
seed
}
pub fn create_associated_token_account_use_seed(
payer: &Pubkey,
owner: &Pubkey,
@@ -63,33 +82,23 @@ pub fn create_associated_token_account_use_seed(
let rent = if is_2022_token {
let v = SPL_TOKEN_2022_RENT.load(Ordering::Relaxed);
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
DEFAULT_TOKEN_ACCOUNT_RENT
} else {
v
}
v
} else {
let v = SPL_TOKEN_RENT.load(Ordering::Relaxed);
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
DEFAULT_TOKEN_ACCOUNT_RENT
} else {
v
}
v
};
let mut buf = [0u8; 8];
let mut hasher = FnvHasher::default();
hasher.write(mint.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
buf[i] = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
}
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
let seed = derive_seed_from_mint(mint);
// 🔧 修复:使用传入的 token_program 生成地址(支持 Token 和 Token-2022
// 买入和卖出只要都使用事件中的 token_program,地址自然一致
let ata_like = Pubkey::create_with_seed(payer, seed, token_program)?;
let ata_like = Pubkey::create_with_seed(payer, &seed, token_program)?;
let len = 165;
// 🔧 修复:create_account_with_seed 的第3个参数必须是 payer(与第92行生成地址时使用的 base 一致)
@@ -98,7 +107,7 @@ pub fn create_associated_token_account_use_seed(
payer,
&ata_like,
payer,
seed,
&seed,
rent,
len,
token_program,
@@ -118,21 +127,9 @@ pub fn get_associated_token_address_with_program_id_use_seed(
token_mint_address: &Pubkey,
token_program_id: &Pubkey,
) -> Result<Pubkey, anyhow::Error> {
let mut buf = [0u8; 8];
let mut hasher = FnvHasher::default();
hasher.write(token_mint_address.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
buf[i] = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
}
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
let seed = derive_seed_from_mint(token_mint_address);
// 🔧 修复:使用传入的 token_program_id 生成地址(支持 Token 和 Token-2022
// 买入和卖出只要都使用事件中的 token_program_id,地址自然一致
let ata_like = Pubkey::create_with_seed(wallet_address, seed, token_program_id)?;
let ata_like = Pubkey::create_with_seed(wallet_address, &seed, token_program_id)?;
Ok(ata_like)
}
+18 -23
View File
@@ -30,24 +30,20 @@ use crate::{
},
};
use anyhow::{anyhow, Result};
use solana_sdk::instruction::AccountMeta;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signer::Signer,
};
#[inline]
fn is_pump_suffix_mint(mint: &Pubkey) -> bool {
mint.to_string().ends_with("pump")
}
#[inline]
fn effective_pump_mint_token_program(mint: &Pubkey, protocol_params: &PumpFunParams) -> Pubkey {
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey {
let tp = protocol_params.token_program;
if tp == Pubkey::default() || tp == TOKEN_PROGRAM_2022 {
return TOKEN_PROGRAM_2022;
if tp == Pubkey::default() {
TOKEN_PROGRAM_2022
} else {
tp
}
if is_pump_suffix_mint(mint) {
return TOKEN_PROGRAM_2022;
}
tp
}
/// Resolve quote mint and its token program from PumpFunParams.
@@ -123,7 +119,7 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let token_program = effective_pump_mint_token_program(&params.output_mint, protocol_params);
let token_program = effective_pump_mint_token_program(protocol_params);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -277,7 +273,7 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let token_program = effective_pump_mint_token_program(&params.input_mint, protocol_params);
let token_program = effective_pump_mint_token_program(protocol_params);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -391,8 +387,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let base_token_program =
effective_pump_mint_token_program(&params.output_mint, protocol_params);
let base_token_program = effective_pump_mint_token_program(protocol_params);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -612,7 +607,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let base_token_program = effective_pump_mint_token_program(&params.input_mint, protocol_params);
let base_token_program = effective_pump_mint_token_program(protocol_params);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -773,7 +768,7 @@ mod tests {
use super::*;
use crate::{
common::{bonding_curve::BondingCurveAccount, GasFeeStrategy},
constants::{TOKEN_PROGRAM, TOKEN_PROGRAM_2022},
constants::TOKEN_PROGRAM,
trading::core::params::{DexParamEnum, PumpFunParams, SwapParams},
};
use solana_sdk::signature::Keypair;
@@ -857,14 +852,14 @@ mod tests {
}
#[test]
fn pump_suffix_buy_forces_token_2022_even_when_params_legacy() {
fn pump_suffix_buy_respects_explicit_legacy_token_program() {
crate::common::seed::set_default_rents();
let params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
let instructions = build_buy_v1(&params).unwrap();
assert_eq!(instructions.len(), 3);
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM_2022);
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM_2022);
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM);
assert_eq!(instructions[2].accounts.len(), 18);
assert_eq!(instructions[2].data.len(), 25);
}
+21 -1
View File
@@ -3,7 +3,9 @@ use once_cell::sync::Lazy;
use smallvec::SmallVec;
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_sdk::instruction::Instruction;
use std::sync::Arc;
use std::{hash::Hash, sync::Arc};
const MAX_COMPUTE_BUDGET_CACHE_SIZE: usize = 4_096;
/// Cache key containing all parameters for compute budget instructions
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -17,6 +19,22 @@ struct ComputeBudgetCacheKey {
static COMPUTE_BUDGET_CACHE: Lazy<DashMap<ComputeBudgetCacheKey, Arc<SmallVec<[Instruction; 2]>>>> =
Lazy::new(|| DashMap::new());
#[inline]
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
where
K: Eq + Hash + Clone,
{
let len = cache.len();
if len <= max_size {
return;
}
let remove_count = (len - max_size).max(max_size / 16).min(len);
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
for key in keys {
cache.remove(&key);
}
}
/// Extend `instructions` with compute budget instructions; on cache hit extends from cached Arc (no SmallVec clone).
#[inline(always)]
pub fn extend_compute_budget_instructions(
@@ -41,6 +59,7 @@ pub fn extend_compute_budget_instructions(
let arc = Arc::new(insts);
instructions.extend(arc.iter().cloned());
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
}
/// Returns compute budget instructions (allocates on cache hit; prefer `extend_compute_budget_instructions` on hot path).
@@ -59,5 +78,6 @@ pub fn compute_budget_instructions(unit_price: u64, unit_limit: u32) -> SmallVec
}
let arc = Arc::new(insts.clone());
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
insts
}
+6 -5
View File
@@ -1,3 +1,4 @@
use anyhow::anyhow;
use solana_hash::Hash;
use solana_message::AddressLookupTableAccount;
use solana_sdk::{
@@ -93,19 +94,19 @@ fn build_versioned_transaction(
// 使用预分配的交易构建器以降低延迟
let mut builder = acquire_builder();
let versioned_msg = builder.build_zero_alloc(
let build_result = builder.build_zero_alloc(
&payer.pubkey(),
&full_instructions,
address_lookup_table_account,
blockhash,
);
release_builder(builder);
let versioned_msg = build_result?;
let msg_bytes = versioned_msg.serialize();
let signature = payer.as_ref().try_sign_message(&msg_bytes).expect("sign failed");
let signature =
payer.as_ref().try_sign_message(&msg_bytes).map_err(|e| anyhow!("sign failed: {e}"))?;
let tx = VersionedTransaction { signatures: vec![signature], message: versioned_msg };
// 归还构建器到池
release_builder(builder);
Ok(tx)
}
+25 -7
View File
@@ -471,8 +471,31 @@ impl ResultCollector {
}
}
/// Fast submit mode for callers that do not wait for on-chain confirmation.
/// Return as soon as one route accepts, a landed failure consumes the nonce, all routes finish,
/// or a short submit window expires. Slow HTTP routes continue in worker tasks but no longer
/// block post-buy monitoring / sell scheduling.
async fn wait_for_first_submitted(
&self,
timeout: Duration,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let start = Instant::now();
let poll_interval = Duration::from_millis(1);
loop {
if self.success_flag.load(Ordering::Acquire)
|| self.landed_failed_flag.load(Ordering::Acquire)
|| self.completed_count.load(Ordering::Acquire) >= self.total_tasks
|| start.elapsed() >= timeout
{
return self.get_first();
}
tokio::time::sleep(poll_interval).await;
}
}
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
#[allow(dead_code)]
async fn wait_for_all_submitted(
&self,
timeout_secs: u64,
@@ -627,9 +650,6 @@ pub async fn execute_parallel(
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
let channel_count = selected_task_configs.len().max(1);
let collector = Arc::new(ResultCollector::new(channel_count));
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
let submit_timeout_secs: u64 =
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
let shared = Arc::new(SwqosSharedContext {
payer,
instructions,
@@ -714,12 +734,10 @@ pub async fn execute_parallel(
// All jobs enqueued (no spawn on hot path)
if !wait_transaction_confirmed {
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
let ret = collector.wait_for_all_submitted(submit_timeout_secs).await.unwrap_or((
let ret = collector.wait_for_first_submitted(Duration::from_millis(500)).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within grace window (primary {}s)", submit_timeout_secs)),
Some(anyhow!("No SWQOS result within fast submit window")),
vec![],
));
let (success, signatures, last_error, submit_timings) = ret;
+3 -2
View File
@@ -32,12 +32,13 @@ pub struct PumpFunParams {
/// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc).
pub fee_sharing_creator_vault_if_active: Option<Pubkey>,
/// SPL Token or Token-2022 program id owning the **mint** (from gRPC / parser / cache).
/// **`Pubkey::default()`**ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致);显式传入 Legacy 或 Token-2022 id 可覆盖该默认值
/// **`Pubkey::default()`**ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致)。
/// 显式传入 Legacy 或 Token-2022 id 时严格按该值组装,不再用 mint 字符串后缀猜测。
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)
/// `Pubkey::default()` 时只能使用 SDK 静态 fallback,可能落后于主网 Global;交易热路径应优先传入 gRPC / parser 观测值
pub fee_recipient: Pubkey,
/// Quote mint for v2 instructions (default: `So11111111111111111111111111111111111111112` for SOL-paired).
/// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
+5 -5
View File
@@ -17,6 +17,7 @@ const PARALLEL_SENDER_COUNT: usize = 18;
/// 启动时预填充数量,必须 >= PARALLEL_SENDER_COUNT,否则 18 路并发 build 会触发分配或争抢
const TX_BUILDER_POOL_PREFILL: usize = 64;
use anyhow::Result;
use crossbeam_queue::ArrayQueue;
use once_cell::sync::Lazy;
use solana_message::AddressLookupTableAccount;
@@ -82,7 +83,7 @@ impl PreallocatedTxBuilder {
instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
recent_blockhash: Hash,
) -> VersionedMessage {
) -> Result<VersionedMessage> {
self.reset();
self.instructions.extend_from_slice(instructions);
@@ -92,14 +93,13 @@ impl PreallocatedTxBuilder {
&self.instructions,
std::slice::from_ref(alt),
recent_blockhash,
)
.expect("v0 message compile failed");
VersionedMessage::V0(message)
)?;
Ok(VersionedMessage::V0(message))
} else {
// ✅ 没有查找表,使用 Legacy 消息(兼容所有 RPC
let message =
Message::new_with_blockhash(&self.instructions, Some(payer), &recent_blockhash);
VersionedMessage::Legacy(message)
Ok(VersionedMessage::Legacy(message))
}
}
}
+12 -29
View File
@@ -1,22 +1,10 @@
// Note: sol_to_lamports moved to solana_native_token crate in 3.x
// Using manual conversion: 1 SOL = 1_000_000_000 lamports
use solana_sdk::pubkey::Pubkey;
fn sol_to_lamports(sol: f64) -> u64 {
(sol * 1_000_000_000.0) as u64
}
use crate::{
instruction::utils::pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS},
utils::calc::common::compute_fee,
};
/// Converts SOL string to lamports (wrapper for sol_to_lamports)
#[inline]
fn sol_str_to_lamports(sol: &str) -> Option<u64> {
sol.parse::<f64>().ok().map(|s| sol_to_lamports(s))
}
/// Calculates the amount of tokens that can be purchased with a given SOL amount
/// using the bonding curve formula.
///
@@ -54,26 +42,21 @@ pub fn get_buy_token_amount_from_sol_amount(
let input_amount = amount_128
.checked_mul(10_000)
.unwrap()
.checked_div(total_fee_basis_points_128 + 10_000)
.unwrap();
.and_then(|v| v.checked_div(total_fee_basis_points_128 + 10_000))
.unwrap_or(0);
let denominator = virtual_sol_reserves + input_amount;
let mut tokens_received =
input_amount.checked_mul(virtual_token_reserves).unwrap().checked_div(denominator).unwrap();
tokens_received = tokens_received.min(real_token_reserves);
if tokens_received <= 100 * 1_000_000_u128 {
tokens_received = if amount > sol_str_to_lamports("0.01").unwrap_or(0) {
25547619 * 1_000_000_u128
} else {
255476 * 1_000_000_u128
};
let Some(denominator) = virtual_sol_reserves.checked_add(input_amount) else { return 0 };
if denominator == 0 {
return 0;
}
tokens_received as u64
let tokens_received = input_amount
.checked_mul(virtual_token_reserves)
.and_then(|v| v.checked_div(denominator))
.unwrap_or(0)
.min(real_token_reserves);
tokens_received.min(u64::MAX as u128) as u64
}
/// Calculates the amount of SOL that will be received when selling a given token amount