feat: Fastlane SWQoS, PumpSwap pool lookup and init robustness

- swqos: add Fastlane client and endpoint/tip constants
- pumpswap: pool 244-byte DataSize, canonical PDA lookup, find_by_mint diagnostics
- lib: find_pool_by_mint generic entry; rent/SWQoS init timeouts and default rent fallback
- lib: create WSOL ATA only when SOL balance is sufficient
- utils: get_token_balance_with_options and get_payer_token_balance_with_program (seed ATA aligned)

Made-with: Cursor
This commit is contained in:
Wood
2026-03-05 23:45:42 +08:00
parent d2ce193e2c
commit 1142829394
9 changed files with 384 additions and 28 deletions
+118
View File
@@ -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<SolanaRpcClient>,
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<VersionedTransaction>, 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<String> {
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(())
}
}
+20
View File
@@ -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<String>),
/// Astralane(api_token, region, custom_url)
Astralane(String, SwqosRegion, Option<String>),
/// Fastlane(api_key_optional, region, custom_url). v2 API only: POST /v2/sendTransaction, body = bincode.
Fastlane(String, SwqosRegion, Option<String>),
/// Stellium(api_token, region, custom_url)
Stellium(String, SwqosRegion, Option<String>),
/// 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(