diff --git a/src/common/seed.rs b/src/common/seed.rs index b9481c5..cecc8d7 100644 --- a/src/common/seed.rs +++ b/src/common/seed.rs @@ -25,6 +25,15 @@ pub async fn update_rents(client: &SolanaRpcClient) -> Result<(), anyhow::Error> Ok(()) } +/// 165 字节 Token 账户的典型租金(lamports),RPC 超时时用作回退 +const DEFAULT_TOKEN_ACCOUNT_RENT: u64 = 2_039_280; + +/// 当 RPC 超时或不可用时设置默认租金,避免客户端创建卡死 +pub fn set_default_rents() { + SPL_TOKEN_RENT.store(DEFAULT_TOKEN_ACCOUNT_RENT, Ordering::Release); + SPL_TOKEN_2022_RENT.store(DEFAULT_TOKEN_ACCOUNT_RENT, Ordering::Release); +} + pub fn start_rent_updater(client: Arc) { tokio::spawn(async move { loop { diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index c60b3bc..dfba40d 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -331,3 +331,22 @@ pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires mini pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002; /// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed). pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005; + +/// Fastlane SWQoS: v2 API POST /v2/sendTransaction, body = binary bincode. ny / fra endpoints. +pub const SWQOS_ENDPOINTS_FASTLANE: [&str; 8] = [ + "http://64.130.37.195:8080", // NewYork (ny) + "http://70.40.184.37:8080", // Frankfurt (fra) + "http://70.40.184.37:8080", // Amsterdam -> fra + "http://64.130.37.195:8080", // SLC -> ny + "http://64.130.37.195:8080", // Tokyo -> ny + "http://70.40.184.37:8080", // London -> fra + "http://64.130.37.195:8080", // LosAngeles -> ny + "http://64.130.37.195:8080", // Default -> ny +]; + +/// Fastlane: server parses tip from tx; this is optional default tip wallet for SDK. +pub const FASTLANE_TIP_ACCOUNTS: &[Pubkey] = &[ + pubkey!("DYQ3ATDe9fBNeMYXDfrUtAQXQ41xzaH1hJ1CXktnL3fG"), +]; + +pub const SWQOS_MIN_TIP_FASTLANE: f64 = 0.00001; diff --git a/src/instruction/utils/pumpswap.rs b/src/instruction/utils/pumpswap.rs index 352be58..3bb1acd 100644 --- a/src/instruction/utils/pumpswap.rs +++ b/src/instruction/utils/pumpswap.rs @@ -2,13 +2,17 @@ use crate::{ common::{ spl_associated_token_account::get_associated_token_address_with_program_id, SolanaRpcClient, }, - constants::TOKEN_PROGRAM, + constants::{TOKEN_PROGRAM, WSOL_TOKEN_ACCOUNT}, instruction::utils::pumpswap_types::{pool_decode, Pool}, }; use anyhow::anyhow; use solana_account_decoder::UiAccountEncoding; use solana_sdk::pubkey::Pubkey; +/// PumpSwap 池账户总长度(见 pump-public-docs Breaking Change):8 字节 discriminator + 244 字节 Pool。 +/// 官方文档:pool structure needs to be 244 bytes (was 243),含 is_mayhem_mode。DataSize 必须与此一致,否则 getProgramAccounts 会返回 0。 +const POOL_ACCOUNT_DATA_LEN: u64 = 8 + 244; + /// Constants used as seeds for deriving PDAs (Program Derived Addresses) pub mod seeds { /// Seed for the global state PDA @@ -29,6 +33,10 @@ pub mod seeds { /// Seed for pool v2 PDA (required by program upgrade, readonly at end of account list) pub const POOL_V2_SEED: &[u8] = b"pool-v2"; + /// Legacy pool PDA seed (used with index, creator, base_mint, quote_mint) + pub const POOL_SEED: &[u8] = b"pool"; + /// Pump program: pool-authority PDA seed (creator for canonical pool) + pub const POOL_AUTHORITY_SEED: &[u8] = b"pool-authority"; } /// Constants related to program accounts and authorities @@ -53,6 +61,8 @@ pub mod accounts { pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); pub const AMM_PROGRAM: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"); + /// Pump Bonding Curve program(canonical pool 的 creator 来自此程序的 pool-authority PDA) + pub const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); pub const LP_FEE_BASIS_POINTS: u64 = 25; pub const PROTOCOL_FEE_BASIS_POINTS: u64 = 5; @@ -152,6 +162,35 @@ pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option { Some(pda) } +/// Pump 程序上的 pool-authority PDA(canonical pool 的 creator),与 @pump-fun/pump-swap-sdk 一致。 +#[inline] +pub fn get_pump_pool_authority_pda(mint: &Pubkey) -> Pubkey { + Pubkey::find_program_address( + &[seeds::POOL_AUTHORITY_SEED, mint.as_ref()], + &accounts::PUMP_PROGRAM_ID, + ) + .0 +} + +/// Canonical Pump 池 PDA:index=0,creator=pumpPoolAuthorityPda(mint),base_mint=mint,quote_mint=WSOL。 +/// 与 @pump-fun/pump-swap-sdk 的 canonicalPumpPoolPda(mint) 一致,用于从 bonding curve 迁移后的标准池查找。 +#[inline] +pub fn get_canonical_pool_pda(mint: &Pubkey) -> Pubkey { + const CANONICAL_POOL_INDEX: u16 = 0; + let authority = get_pump_pool_authority_pda(mint); + let (pda, _) = Pubkey::find_program_address( + &[ + seeds::POOL_SEED, + &CANONICAL_POOL_INDEX.to_le_bytes(), + authority.as_ref(), + mint.as_ref(), + WSOL_TOKEN_ACCOUNT.as_ref(), + ], + &accounts::AMM_PROGRAM, + ); + pda +} + // Find a pool for a specific mint pub async fn find_pool(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result { let (pool_address, _) = find_by_mint(rpc, mint).await?; @@ -231,11 +270,12 @@ pub async fn find_by_base_mint( rpc: &SolanaRpcClient, base_mint: &Pubkey, ) -> Result<(Pubkey, Pool), anyhow::Error> { - // Use getProgramAccounts to find pools for the given mint + // Use getProgramAccounts to find pools for the given mint. + // base_mint 在账户布局中的偏移:8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43 let filters = vec![ - // solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool account size + solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN), solana_rpc_client_api::filter::RpcFilterType::Memcmp( - solana_client::rpc_filter::Memcmp::new_base58_encoded(43, &base_mint.to_bytes()), + solana_client::rpc_filter::Memcmp::new_base58_encoded(43, base_mint.as_ref()), ), ]; let config = solana_rpc_client_api::config::RpcProgramAccountsConfig { @@ -282,11 +322,12 @@ pub async fn find_by_quote_mint( rpc: &SolanaRpcClient, quote_mint: &Pubkey, ) -> Result<(Pubkey, Pool), anyhow::Error> { - // Use getProgramAccounts to find pools for the given mint + // Use getProgramAccounts to find pools for the given mint. + // quote_mint 在账户布局中的偏移:8 + 1 + 2 + 32 + 32 = 75 let filters = vec![ - // solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool account size + solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN), solana_rpc_client_api::filter::RpcFilterType::Memcmp( - solana_client::rpc_filter::Memcmp::new_base58_encoded(75, "e_mint.to_bytes()), + solana_client::rpc_filter::Memcmp::new_base58_encoded(75, quote_mint.as_ref()), ), ]; let config = solana_rpc_client_api::config::RpcProgramAccountsConfig { @@ -329,17 +370,52 @@ pub async fn find_by_quote_mint( Ok((address, pool)) } +/// 按 mint 查找 PumpSwap 池(本函数仅用于 PumpSwap,其他 DEX 勿用)。 +/// +/// 查找顺序(与 @pump-fun/pump-swap-sdk 一致): +/// 1. Pool v2 PDA ["pool-v2", base_mint] — 一次 getAccount +/// 2. Canonical pool PDA ["pool", 0, pumpPoolAuthority(mint), mint, WSOL] — 迁移后的标准池 +/// 3. getProgramAccounts 按 base_mint / quote_mint 过滤 pub async fn find_by_mint( rpc: &SolanaRpcClient, mint: &Pubkey, ) -> Result<(Pubkey, Pool), anyhow::Error> { - if let Ok((address, pool)) = find_by_base_mint(rpc, mint).await { - return Ok((address, pool)); + let mut diag = Vec::::new(); + + // 1. PumpSwap v2 PDA(seeds: ["pool-v2", base_mint]) + if let Some(pool_address) = get_pool_v2_pda(mint) { + diag.push(format!("PDA(v2)={}", pool_address)); + match fetch_pool(rpc, &pool_address).await { + Ok(pool) if pool.base_mint == *mint => return Ok((pool_address, pool)), + Ok(_) => diag.push("PDA(v2) 账户存在但 base_mint 不匹配".into()), + Err(e) => diag.push(format!("PDA(v2) get_account/decode 失败: {}", e)), + } } - if let Ok((address, pool)) = find_by_quote_mint(rpc, mint).await { - return Ok((address, pool)); + + // 2. Canonical pool PDA(与 pump-swap-sdk canonicalPumpPoolPda(mint) 一致) + let canonical_address = get_canonical_pool_pda(mint); + diag.push(format!("canonical={}", canonical_address)); + match fetch_pool(rpc, &canonical_address).await { + Ok(pool) if pool.base_mint == *mint => return Ok((canonical_address, pool)), + Ok(_) => diag.push("canonical 账户存在但 base_mint 不匹配".into()), + Err(e) => diag.push(format!("canonical get_account/decode 失败: {}", e)), } - Err(anyhow!("No pool found for mint {}", mint)) + + // 3. 回退:getProgramAccounts 按 base_mint / quote_mint + match find_by_base_mint(rpc, mint).await { + Ok((address, pool)) => return Ok((address, pool)), + Err(e) => diag.push(format!("getProgramAccounts(base_mint): {}", e)), + } + match find_by_quote_mint(rpc, mint).await { + Ok((address, pool)) => return Ok((address, pool)), + Err(e) => diag.push(format!("getProgramAccounts(quote_mint): {}", e)), + } + + Err(anyhow!( + "No pool found for mint {}. 诊断: {}。若使用自建 RPC 请确认已开启 getProgramAccounts 或换用公共 RPC 重试;若代币未在 PumpSwap 建池请先在 pump.fun/DEX 上确认", + mint, + diag.join("; ") + )) } pub async fn get_token_balances( diff --git a/src/instruction/utils/pumpswap_types.rs b/src/instruction/utils/pumpswap_types.rs index 9410820..99f624b 100644 --- a/src/instruction/utils/pumpswap_types.rs +++ b/src/instruction/utils/pumpswap_types.rs @@ -19,6 +19,7 @@ pub struct Pool { pub is_cashback_coin: bool, } +/// Borsh 解码用的 Pool 字段长度。链上池账户数据为 244 字节(pump-public-docs),末尾可有 reserved 字节,解码只取前 POOL_SIZE。 pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1; pub fn pool_decode(data: &[u8]) -> Option { diff --git a/src/lib.rs b/src/lib.rs index c229118..547c280 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,25 @@ fn validate_protocol_params(dex_type: DexType, params: &DexParamEnum) -> bool { } } +/// 按 mint 查找池地址(通用入口,根据 DEX 类型分发,仅 PumpSwap 等已实现的类型会走优化路径)。 +/// +/// * `dex_type`:PumpSwap 时先走 PDA 再回退 getProgramAccounts,其他类型返回未实现错误。 +pub async fn find_pool_by_mint( + rpc: &SolanaRpcClient, + mint: &Pubkey, + dex_type: DexType, +) -> Result { + match dex_type { + DexType::PumpSwap => { + crate::instruction::utils::pumpswap::find_pool(rpc, mint).await + } + _ => Err(anyhow::anyhow!( + "find_pool_by_mint not implemented for {:?}", + dex_type + )), + } +} + /// Type of the token to buy #[derive(Clone, PartialEq)] pub enum TradeTokenType { @@ -97,27 +116,47 @@ impl TradingInfrastructure { config.commitment.clone(), )); - // Initialize rent cache and start background updater - common::seed::update_rents(&rpc).await.unwrap(); + // Initialize rent cache (with timeout so slow RPC doesn't block forever) + const RENT_UPDATE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15); + match tokio::time::timeout(RENT_UPDATE_TIMEOUT, common::seed::update_rents(&rpc)).await { + Ok(Ok(())) => {} + Ok(Err(e)) => { + if sdk_log::sdk_log_enabled() { + warn!(target: "sol_trade_sdk", "rent update failed: {}, using defaults", e); + } + common::seed::set_default_rents(); + } + Err(_) => { + if sdk_log::sdk_log_enabled() { + warn!(target: "sol_trade_sdk", "rent update timed out ({}s), using defaults; check RPC", RENT_UPDATE_TIMEOUT.as_secs()); + } + common::seed::set_default_rents(); + } + } common::seed::start_rent_updater(rpc.clone()); - // Create SWQOS clients with blacklist checking + // Create SWQOS clients with blacklist checking(单节点超时 5s,避免某一家卡死整段初始化) + const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); let mut swqos_clients: Vec> = vec![]; for swqos in &config.swqos_configs { - // Check blacklist, skip disabled providers if swqos.is_blacklisted() { if sdk_log::sdk_log_enabled() { warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type()); } continue; } - match SwqosConfig::get_swqos_client( - config.rpc_url.clone(), - config.commitment.clone(), - swqos.clone(), - ).await { - Ok(swqos_client) => swqos_clients.push(swqos_client), - Err(err) => { + match tokio::time::timeout( + SWQOS_CLIENT_TIMEOUT, + SwqosConfig::get_swqos_client( + config.rpc_url.clone(), + config.commitment.clone(), + swqos.clone(), + ), + ) + .await + { + Ok(Ok(swqos_client)) => swqos_clients.push(swqos_client), + Ok(Err(err)) => { if sdk_log::sdk_log_enabled() { warn!( target: "sol_trade_sdk", @@ -126,6 +165,16 @@ impl TradingInfrastructure { ); } } + Err(_) => { + if sdk_log::sdk_log_enabled() { + warn!( + target: "sol_trade_sdk", + "swqos {:?} init timed out ({}s), skipping", + swqos.swqos_type(), + SWQOS_CLIENT_TIMEOUT.as_secs() + ); + } + } } } @@ -348,7 +397,7 @@ impl TradingClient { } } - /// Helper to ensure WSOL ATA exists for a wallet + /// 确保钱包存在 WSOL ATA;不存在则发交易创建(会花费租金 + 手续费,初始化阶段唯一会扣钱的逻辑) async fn ensure_wsol_ata(payer: &Arc, rpc: &Arc) { let wsol_ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast( @@ -496,9 +545,32 @@ impl TradingClient { // Initialize wallet-specific caches crate::common::fast_fn::fast_init(&payer.pubkey()); - // Handle WSOL ATA creation if configured + // ═══════════════════════════════════════════════════════════════════════════════ + // 初始化阶段会花费租金/手续费的唯一路径:创建 WSOL ATA(ensure_wsol_ata) + // - 触发条件:create_wsol_ata_on_startup == true 且钱包 SOL >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS + // - 花费:ATA 租金(约 0.00203928 SOL)+ 交易手续费;钱包不足时已跳过 + // - 其它初始化(TradingInfrastructure::new、update_rents、get_swqos_client)仅 RPC/HTTP,不发送交易 + // ═══════════════════════════════════════════════════════════════════════════════ if trade_config.create_wsol_ata_on_startup { - Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await; + const MIN_SOL_FOR_WSOL_ATA_LAMPORTS: u64 = 500_000; // 约 0.0005 SOL,用于 ATA 租金 + 手续费 + const BALANCE_CHECK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); + let balance = tokio::time::timeout( + BALANCE_CHECK_TIMEOUT, + infrastructure.rpc.get_balance(&payer.pubkey()), + ) + .await + .unwrap_or(Ok(0)) + .unwrap_or(0); + if balance >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS { + Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await; + } else if sdk_log::sdk_log_enabled() { + info!( + target: "sol_trade_sdk", + "⏭️ 跳过创建 WSOL ATA:钱包 SOL 不足(当前 {} lamports,需要至少 {})", + balance, + MIN_SOL_FOR_WSOL_ATA_LAMPORTS + ); + } } let instance = Self { diff --git a/src/swqos/fastlane.rs b/src/swqos/fastlane.rs new file mode 100644 index 0000000..2a866a0 --- /dev/null +++ b/src/swqos/fastlane.rs @@ -0,0 +1,118 @@ +//! Fastlane SWQoS client: v2 API only (POST /v2/sendTransaction, body = binary bincode). +//! Endpoints: ny http://64.130.37.195:8080, fra http://70.40.184.37:8080. + +use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation}; +use anyhow::Result; +use bincode::serialize as bincode_serialize; +use rand::seq::IndexedRandom; +use reqwest::Client; +use solana_client::rpc_client::SerializableTransaction; +use solana_sdk::transaction::VersionedTransaction; +use std::sync::Arc; +use std::time::Instant; + +use crate::swqos::{SwqosClientTrait, SwqosType, TradeType}; +use crate::{common::SolanaRpcClient, constants::swqos::FASTLANE_TIP_ACCOUNTS}; + +/// Fastlane v2 submit path (binary bincode, no Base64). +const FASTLANE_V2_PATH: &str = "/v2/sendTransaction"; + +#[derive(Clone)] +pub struct FastlaneClient { + /// Base URL including port, e.g. http://64.130.37.195:8080 + pub base_url: String, + /// Optional API key for request header api-key (empty = no auth). + pub api_key: String, + pub rpc_client: Arc, + pub http_client: Client, +} + +#[async_trait::async_trait] +impl SwqosClientTrait for FastlaneClient { + async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { + self.send_transaction_impl(trade_type, transaction, wait_confirmation).await + } + + async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec, wait_confirmation: bool) -> Result<()> { + for tx in transactions { + self.send_transaction_impl(trade_type, tx, wait_confirmation).await?; + } + Ok(()) + } + + fn get_tip_account(&self) -> Result { + let tip_account = *FASTLANE_TIP_ACCOUNTS + .choose(&mut rand::rng()) + .or_else(|| FASTLANE_TIP_ACCOUNTS.first()) + .unwrap(); + Ok(tip_account.to_string()) + } + + fn get_swqos_type(&self) -> SwqosType { + SwqosType::Fastlane + } +} + +impl FastlaneClient { + pub fn new(rpc_url: String, base_url: String, api_key: String) -> Self { + let rpc_client = SolanaRpcClient::new(rpc_url); + let http_client = default_http_client_builder().build().unwrap(); + Self { + base_url, + api_key, + rpc_client: Arc::new(rpc_client), + http_client, + } + } + + fn v2_url(&self) -> String { + let base = self.base_url.trim_end_matches('/'); + format!("{}{}", base, FASTLANE_V2_PATH) + } + + /// Send transaction via Fastlane v2 API: POST /v2/sendTransaction, body = bincode. + pub async fn send_transaction_impl( + &self, + trade_type: TradeType, + transaction: &VersionedTransaction, + wait_confirmation: bool, + ) -> Result<()> { + let start_time = Instant::now(); + let signature = transaction.get_signature(); + + let body_bytes = bincode_serialize(transaction) + .map_err(|e| anyhow::anyhow!("Fastlane bincode serialize failed: {}", e))?; + + let url = self.v2_url(); + let mut req = self.http_client.post(&url).header("Content-Type", "application/octet-stream").body(body_bytes); + if !self.api_key.is_empty() { + req = req.header("api-key", self.api_key.as_str()); + } + + let response = req.send().await?; + let status = response.status(); + let _ = response.bytes().await; + if status.is_success() { + println!(" [fastlane] {} submitted: {:?}", trade_type, start_time.elapsed()); + } else { + eprintln!(" [fastlane] {} submission failed: status {}", trade_type, status); + return Err(anyhow::anyhow!("Fastlane sendTransaction failed: {}", status)); + } + + let start_confirm = Instant::now(); + match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await { + Ok(_) => (), + Err(e) => { + println!(" signature: {:?}", signature); + println!(" [fastlane] {} confirmation failed: {:?}", trade_type, start_confirm.elapsed()); + return Err(e); + } + } + if wait_confirmation { + println!(" signature: {:?}", signature); + println!(" [fastlane] {} confirmed: {:?}", trade_type, start_confirm.elapsed()); + } + + Ok(()) + } +} diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index b47a44f..96713f3 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -10,6 +10,7 @@ pub mod node1; pub mod flashblock; pub mod blockrazor; pub mod astralane; +pub mod fastlane; pub mod stellium; pub mod lightspeed; pub mod soyas; @@ -36,6 +37,7 @@ use crate::{ SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_ASTRALANE, + SWQOS_ENDPOINTS_FASTLANE, SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING, @@ -50,6 +52,7 @@ use crate::{ SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_ASTRALANE, + SWQOS_MIN_TIP_FASTLANE, SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_SOYAS, @@ -67,6 +70,7 @@ use crate::{ flashblock::FlashBlockClient, blockrazor::BlockRazorClient, astralane::AstralaneClient, + fastlane::FastlaneClient, stellium::StelliumClient, lightspeed::LightspeedClient, soyas::SoyasClient, @@ -117,6 +121,7 @@ pub enum SwqosType { FlashBlock, BlockRazor, Astralane, + Fastlane, Stellium, Lightspeed, Soyas, @@ -137,6 +142,7 @@ impl SwqosType { Self::FlashBlock, Self::BlockRazor, Self::Astralane, + Self::Fastlane, Self::Stellium, Self::Lightspeed, Self::Soyas, @@ -168,6 +174,7 @@ pub trait SwqosClientTrait { SwqosType::FlashBlock => SWQOS_MIN_TIP_FLASHBLOCK, SwqosType::BlockRazor => SWQOS_MIN_TIP_BLOCKRAZOR, SwqosType::Astralane => SWQOS_MIN_TIP_ASTRALANE, + SwqosType::Fastlane => SWQOS_MIN_TIP_FASTLANE, SwqosType::Stellium => SWQOS_MIN_TIP_STELLIUM, SwqosType::Lightspeed => SWQOS_MIN_TIP_LIGHTSPEED, SwqosType::Soyas => SWQOS_MIN_TIP_SOYAS, @@ -211,6 +218,8 @@ pub enum SwqosConfig { BlockRazor(String, SwqosRegion, Option), /// Astralane(api_token, region, custom_url) Astralane(String, SwqosRegion, Option), + /// Fastlane(api_key_optional, region, custom_url). v2 API only: POST /v2/sendTransaction, body = bincode. + Fastlane(String, SwqosRegion, Option), /// Stellium(api_token, region, custom_url) Stellium(String, SwqosRegion, Option), /// Lightspeed(api_key, region, custom_url) - Solana Vibe Station @@ -240,6 +249,7 @@ impl SwqosConfig { SwqosConfig::FlashBlock(_, _, _) => SwqosType::FlashBlock, SwqosConfig::BlockRazor(_, _, _) => SwqosType::BlockRazor, SwqosConfig::Astralane(_, _, _) => SwqosType::Astralane, + SwqosConfig::Fastlane(_, _, _) => SwqosType::Fastlane, SwqosConfig::Stellium(_, _, _) => SwqosType::Stellium, SwqosConfig::Lightspeed(_, _, _) => SwqosType::Lightspeed, SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas, @@ -268,6 +278,7 @@ impl SwqosConfig { SwqosType::FlashBlock => SWQOS_ENDPOINTS_FLASHBLOCK[region as usize].to_string(), SwqosType::BlockRazor => SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string(), SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string(), + SwqosType::Fastlane => SWQOS_ENDPOINTS_FASTLANE[region as usize].to_string(), SwqosType::Stellium => SWQOS_ENDPOINTS_STELLIUM[region as usize].to_string(), SwqosType::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(), @@ -360,6 +371,15 @@ impl SwqosConfig { ); Ok(Arc::new(astralane_client)) }, + SwqosConfig::Fastlane(api_key, region, url) => { + let base_url = SwqosConfig::get_endpoint(SwqosType::Fastlane, region, url); + let fastlane_client = FastlaneClient::new( + rpc_url.clone(), + base_url, + api_key, + ); + Ok(Arc::new(fastlane_client)) + }, SwqosConfig::Stellium(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Stellium, region, url); let stellium_client = StelliumClient::new( diff --git a/src/trading/common/utils.rs b/src/trading/common/utils.rs index 68399c1..a36ab8e 100644 --- a/src/trading/common/utils.rs +++ b/src/trading/common/utils.rs @@ -2,7 +2,11 @@ use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction use solana_system_interface::instruction::transfer; use crate::common::{ - fast_fn::get_associated_token_address_with_program_id_fast, spl_token::close_account, + fast_fn::{ + get_associated_token_address_with_program_id_fast, + get_associated_token_address_with_program_id_fast_use_seed, + }, + spl_token::close_account, SolanaRpcClient, }; use anyhow::anyhow; @@ -36,10 +40,30 @@ pub async fn get_token_balance( payer: &Pubkey, mint: &Pubkey, ) -> Result { - let ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + get_token_balance_with_options( + rpc, payer, mint, &crate::constants::TOKEN_PROGRAM, + false, + ) + .await +} + +/// 使用与交易指令一致的 ATA 推导(可选 seed)查询余额;卖出/余额查询应与买入使用同一 ATA 地址。 +#[inline] +pub async fn get_token_balance_with_options( + rpc: &SolanaRpcClient, + payer: &Pubkey, + mint: &Pubkey, + token_program: &Pubkey, + use_seed: bool, +) -> Result { + let ata = get_associated_token_address_with_program_id_fast_use_seed( + payer, + mint, + token_program, + use_seed, ); let balance = rpc.get_token_account_balance(&ata).await?; let balance_u64 = diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 27005fd..b11e2e3 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -31,6 +31,23 @@ impl TradingClient { trading::common::utils::get_token_balance(&self.infrastructure.rpc, &self.payer.pubkey(), mint).await } + /// 使用与交易一致的 ATA 推导(含 seed 优化)查询 payer 某 mint 的余额;卖出前查余额应使用此接口并传入池的 base_token_program,否则若使用 seed ATA 会查错账户。 + #[inline] + pub async fn get_payer_token_balance_with_program( + &self, + mint: &Pubkey, + token_program: &Pubkey, + ) -> Result { + trading::common::utils::get_token_balance_with_options( + &self.infrastructure.rpc, + &self.payer.pubkey(), + mint, + token_program, + self.use_seed_optimize, + ) + .await + } + #[inline] pub fn get_payer_pubkey(&self) -> Pubkey { self.payer.pubkey()