From 8ed2aef931763b5b65aed9a6f03855c1301bcbf5 Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Thu, 21 May 2026 17:50:06 +0800 Subject: [PATCH] Fix trade execution correctness and release 4.0.11 --- Cargo.toml | 2 +- src/common/fast_fn.rs | 89 ++++++++++++++------ src/common/seed.rs | 65 +++++++------- src/instruction/pumpfun.rs | 41 ++++----- src/trading/common/compute_budget_manager.rs | 22 ++++- src/trading/common/transaction_builder.rs | 11 +-- src/trading/core/async_executor.rs | 32 +++++-- src/trading/core/params/pumpfun.rs | 5 +- src/trading/core/transaction_pool.rs | 10 +-- src/utils/calc/pumpfun.rs | 41 +++------ 10 files changed, 186 insertions(+), 132 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6f34c7a..8223b5f 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "4.0.10" +version = "4.0.11" edition = "2021" authors = [ "William ", diff --git a/src/common/fast_fn.rs b/src/common/fast_fn.rs index 8330021..086c74f 100644 --- a/src/common/fast_fn.rs +++ b/src/common/fast_fn.rs @@ -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(cache: &DashMap, 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 = 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 } diff --git a/src/common/seed.rs b/src/common/seed.rs index e80fd94..71e7575 100644 --- a/src/common/seed.rs +++ b/src/common/seed.rs @@ -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 { - 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) } diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index 7222431..adaa3d0 100644 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -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> { })?; let is_mayhem_mode = bonding_curve.is_mayhem_mode; - let token_program = effective_pump_mint_token_program(¶ms.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> { })?; let is_mayhem_mode = bonding_curve.is_mayhem_mode; - let token_program = effective_pump_mint_token_program(¶ms.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> { })?; let is_mayhem_mode = bonding_curve.is_mayhem_mode; - let base_token_program = - effective_pump_mint_token_program(¶ms.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> { })?; let is_mayhem_mode = bonding_curve.is_mayhem_mode; - let base_token_program = effective_pump_mint_token_program(¶ms.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(¶ms).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); } diff --git a/src/trading/common/compute_budget_manager.rs b/src/trading/common/compute_budget_manager.rs index bb0c181..26b0fc8 100755 --- a/src/trading/common/compute_budget_manager.rs +++ b/src/trading/common/compute_budget_manager.rs @@ -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>>> = Lazy::new(|| DashMap::new()); +#[inline] +fn prune_cache(cache: &DashMap, 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 = 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 } diff --git a/src/trading/common/transaction_builder.rs b/src/trading/common/transaction_builder.rs index bc9ae19..e413b7b 100755 --- a/src/trading/common/transaction_builder.rs +++ b/src/trading/common/transaction_builder.rs @@ -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) } diff --git a/src/trading/core/async_executor.rs b/src/trading/core/async_executor.rs index 9f1ff79..1b86325 100644 --- a/src/trading/core/async_executor.rs +++ b/src/trading/core/async_executor.rs @@ -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, Option, Vec)> { + 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; diff --git a/src/trading/core/params/pumpfun.rs b/src/trading/core/params/pumpfun.rs index 5e58270..a5ee490 100644 --- a/src/trading/core/params/pumpfun.rs +++ b/src/trading/core/params/pumpfun.rs @@ -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, /// 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, /// 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`. diff --git a/src/trading/core/transaction_pool.rs b/src/trading/core/transaction_pool.rs index 527dad9..3f5b3cf 100644 --- a/src/trading/core/transaction_pool.rs +++ b/src/trading/core/transaction_pool.rs @@ -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 { 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)) } } } diff --git a/src/utils/calc/pumpfun.rs b/src/utils/calc/pumpfun.rs index 946ee7f..7b60be3 100644 --- a/src/utils/calc/pumpfun.rs +++ b/src/utils/calc/pumpfun.rs @@ -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 { - sol.parse::().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