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:
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user