├── fix_parser.sh ├── src ├── dex │ ├── mod.rs │ └── raydium_launchpad.rs ├── core │ ├── mod.rs │ ├── tx.rs │ └── token.rs ├── common │ ├── mod.rs │ ├── constants.rs │ ├── logger.rs │ ├── cache.rs │ └── config.rs ├── lib.rs ├── services │ ├── mod.rs │ ├── cache_maintenance.rs │ ├── blockhash_processor.rs │ ├── rpc_client.rs │ ├── zeroslot.rs │ ├── health_check.rs │ └── jupiter_api.rs ├── engine │ ├── mod.rs │ ├── swap.rs │ ├── monitor.rs │ ├── transaction_parser.rs │ ├── transaction_retry.rs │ └── risk_management.rs ├── env.example ├── error │ └── mod.rs └── main.rs ├── .gitignore ├── Makefile ├── Cargo.toml └── README.md /fix_parser.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/dex/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod raydium_launchpad; 2 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token; 2 | pub mod tx; 3 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod constants; 3 | pub mod logger; 4 | pub mod cache; 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | pub mod core; 3 | pub mod dex; 4 | pub mod engine; 5 | pub mod error; 6 | pub mod services; 7 | -------------------------------------------------------------------------------- /src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blockhash_processor; 2 | pub mod cache_maintenance; 3 | pub mod rpc_client; 4 | pub mod zeroslot; 5 | pub mod jupiter_api; 6 | -------------------------------------------------------------------------------- /src/engine/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sniper_bot; 2 | pub mod monitor; 3 | pub mod risk_management; 4 | pub mod selling_strategy; 5 | pub mod swap; 6 | pub mod transaction_parser; 7 | pub mod transaction_retry; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | buy-grpc 2 | .env 3 | target 4 | blacklist.txt 5 | # Rust build artifacts 6 | /target 7 | **/*.rs.bk 8 | source 9 | Cargo.lock 10 | solana-rpc-client-2.1.14 11 | # IDE files 12 | .idea/ 13 | .vscode/ 14 | *.iml 15 | backend 16 | # Environment files 17 | .env 18 | .env.* 19 | 20 | # Transaction records 21 | /record/ 22 | /record/pumpfun/ 23 | /record/pumpswap/ 24 | /record/raydium/ 25 | 26 | # Logs 27 | *.log -------------------------------------------------------------------------------- /src/common/constants.rs: -------------------------------------------------------------------------------- 1 | pub const INIT_MSG: &str = "Made by DEVDUDES"; 2 | 3 | // Whale selling detection threshold - emergency sell all when detecting 10+ SOL whale selling transaction 4 | pub const WHALE_SELLING_AMOUNT_FOR_SELLING_TRIGGER: f64 = 10.0; // SOL 5 | 6 | pub const RUN_MSG: &str = " 7 | RUNNING BOT..... 8 | "; 9 | -------------------------------------------------------------------------------- /src/engine/swap.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use serde::Deserialize; 3 | 4 | #[derive(ValueEnum, Debug, Clone, Deserialize, PartialEq)] 5 | pub enum SwapDirection { 6 | #[serde(rename = "buy")] 7 | Buy, 8 | #[serde(rename = "sell")] 9 | Sell, 10 | } 11 | impl From for u8 { 12 | fn from(value: SwapDirection) -> Self { 13 | match value { 14 | SwapDirection::Buy => 0, 15 | SwapDirection::Sell => 1, 16 | } 17 | } 18 | } 19 | 20 | #[derive(ValueEnum, Debug, Clone, Deserialize, PartialEq)] 21 | pub enum SwapInType { 22 | /// Quantity 23 | #[serde(rename = "qty")] 24 | Qty, 25 | /// Percentage 26 | #[serde(rename = "pct")] 27 | Pct, 28 | } 29 | 30 | #[derive(ValueEnum, Debug, Clone, Deserialize, PartialEq)] 31 | pub enum SwapProtocol { 32 | #[serde(rename = "raydium")] 33 | RaydiumLaunchpad, 34 | #[serde(rename = "auto")] 35 | Auto, 36 | #[serde(rename = "unknown")] 37 | Unknown, 38 | } 39 | 40 | impl Default for SwapProtocol { 41 | fn default() -> Self { 42 | SwapProtocol::Auto 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/engine/monitor.rs: -------------------------------------------------------------------------------- 1 | use anchor_client::solana_sdk::pubkey::Pubkey; 2 | use std::{collections::HashSet, time::Instant}; 3 | 4 | #[derive(Clone, Debug, PartialEq, Eq, Copy)] 5 | pub enum InstructionType { 6 | PumpMint, 7 | RaydiumBuy, 8 | RaydiumSell 9 | } 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct BondingCurveInfo { 13 | pub bonding_curve: Pubkey, 14 | pub new_virtual_sol_reserve: u64, 15 | pub new_virtual_token_reserve: u64, 16 | } 17 | 18 | #[derive(Clone, Debug)] 19 | pub struct PoolInfo { 20 | pub pool_id: Pubkey, 21 | pub base_mint: Pubkey, 22 | pub quote_mint: Pubkey, 23 | pub base_reserve: u64, 24 | pub quote_reserve: u64, 25 | pub coin_creator: Pubkey, 26 | } 27 | 28 | 29 | #[derive(Debug, Clone, Copy)] 30 | pub struct RetracementLevel { 31 | pub percentage: u64, 32 | pub threshold: u64, 33 | pub sell_amount: u64, 34 | } 35 | 36 | #[derive(Clone, Debug)] 37 | pub struct TokenTrackingInfo { 38 | pub top_pnl: f64, 39 | pub last_sell_time: Instant, 40 | pub completed_intervals: HashSet, 41 | pub sell_attempts: usize, 42 | pub sell_success: usize, 43 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Variables 2 | TARGET_X86_64 = x86_64-pc-windows-gnu 3 | TARGET_I686 = i686-pc-windows-gnu 4 | PROJECT_NAME = solana-vntr-sniper # Change this to your project name 5 | CARGO = cargo 6 | 7 | # Target to install prerequisites 8 | .PHONY: install 9 | install: 10 | sudo apt update 11 | sudo apt install -y mingw-w64 12 | rustup target add $(TARGET_X86_64) 13 | rustup target add $(TARGET_I686) 14 | 15 | # pm2 to install prerequisites 16 | .PHONY: pm2 17 | pm2: 18 | pm2 start target/release/solana-vntr-sniper 19 | 20 | # Target to build for x86_64 Windows 21 | .PHONY: build-x86_64 22 | build-x86_64: 23 | $(CARGO) build --target=$(TARGET_X86_64) --release 24 | 25 | # Target to build for i686 Windows 26 | .PHONY: build-i686 27 | build-i686: 28 | $(CARGO) build --target=$(TARGET_I686) --release 29 | 30 | # Target to clean the project 31 | .PHONY: clean 32 | clean: 33 | $(CARGO) clean 34 | 35 | # Start the server 36 | .PHONY: start 37 | start: 38 | pm2 start 0 39 | 40 | # Stop the server 41 | .PHONY: stop 42 | stop: 43 | pm2 stop 0 44 | 45 | # Stop the server 46 | .PHONY: build 47 | build: 48 | $(CARGO) clean 49 | $(CARGO) build -r 50 | 51 | # Target to display help 52 | .PHONY: help 53 | help: 54 | @echo "Makefile commands:" 55 | @echo " install - Install necessary packages and configure Rust targets" 56 | @echo " build-x86_64 - Build for 64-bit Windows" 57 | @echo " build-i686 - Build for 32-bit Windows" 58 | @echo " clean - Clean the target directory" 59 | @echo " help - Display this help message" 60 | @echo " start - Start the server" 61 | @echo " stop - Stop the server" 62 | @echo " build - Build the server" 63 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana-vntr-sniper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | solana-client = { version = "2.1.14" } 8 | solana-account-decoder = "2.1.14" 9 | solana-program-pack = "2.1.14" 10 | solana-sdk = { version = "2.1.14" } 11 | solana-transaction-status = { version = "2.1.14" } 12 | dotenv = "0.15" 13 | chrono = "0.4.26" 14 | clap = { version = "4.5.7", features = ["derive"] } 15 | anyhow = "1.0.62" 16 | serde = "1.0.145" 17 | serde_json = "1.0.86" 18 | tokio = { version = "1.21.2", features = ["full"] } 19 | tokio-util = "0.7" 20 | tokio-tungstenite = { version = "0.23.1", features = ["native-tls"] } 21 | tokio-stream = "0.1.11" 22 | anchor-client = { version = "0.31.0", features = ["async"] } 23 | anchor-lang = "=0.31.0" 24 | yellowstone-grpc-client = "4.1.0" 25 | yellowstone-grpc-proto = "4.1.1" 26 | spl-token = { version = "4.0.0", features = ["no-entrypoint"] } 27 | spl-token-2022 = { version = "6.0.0", features = ["no-entrypoint"] } 28 | spl-associated-token-account = { version = "6.0.0", features = [ 29 | "no-entrypoint", 30 | ] } 31 | spl-token-client = "0.13.0" 32 | base64 = "0.13" 33 | rand = "0.8.5" 34 | borsh = { version = "1.5.3"} 35 | borsh-derive = "1.5.3" 36 | colored = "3.0.0" 37 | reqwest = { version = "0.11.27", features = ["json", "socks", "native-tls"] } 38 | lazy_static = "1.5.0" 39 | bs58 = "0.4" 40 | bs64 = "0.1.2" 41 | bincode = "1.3.3" 42 | tokio-js-set-interval = "1.3.0" 43 | bytemuck = "1.21.0" 44 | indicatif = "0.17.8" 45 | tracing = "0.1.40" 46 | futures-util = "0.3.30" 47 | maplit = "1.0.2" 48 | futures = "0.3.31" 49 | teloxide = { version = "0.12", features = ["macros"] } 50 | dashmap = "5.5.3" 51 | lru = "0.10.0" 52 | once_cell = "1.21.3" 53 | -------------------------------------------------------------------------------- /src/common/logger.rs: -------------------------------------------------------------------------------- 1 | use chrono::Local; 2 | use colored::*; 3 | 4 | const LOG_LEVEL: &str = "LOG"; 5 | 6 | #[derive(Clone)] 7 | pub struct Logger { 8 | prefix: String, 9 | date_format: String, 10 | } 11 | 12 | impl Logger { 13 | // Constructor function to create a new Logger instance 14 | pub fn new(prefix: String) -> Self { 15 | Logger { 16 | prefix, 17 | date_format: String::from("%Y-%m-%d %H:%M:%S"), 18 | } 19 | } 20 | 21 | // Method to log a message with a prefix 22 | pub fn log(&self, message: String) -> String { 23 | let log = format!("{} {}", self.prefix_with_date(), message); 24 | println!("{}", log); 25 | log 26 | } 27 | 28 | pub fn debug(&self, message: String) -> String { 29 | let log = format!("{} [{}] {}", self.prefix_with_date(), "DEBUG", message); 30 | if LogLevel::new().is_debug() { 31 | println!("{}", log); 32 | } 33 | log 34 | } 35 | pub fn error(&self, message: String) -> String { 36 | let log = format!("{} [{}] {}", self.prefix_with_date(), "ERROR", message); 37 | println!("{}", log); 38 | 39 | log 40 | } 41 | 42 | // Add success method to fix compilation errors in monitor.rs 43 | pub fn success(&self, message: String) -> String { 44 | let log = format!("{} [{}] {}", self.prefix_with_date(), "SUCCESS".green().bold(), message); 45 | println!("{}", log); 46 | log 47 | } 48 | 49 | // Add a new method for performance-critical paths 50 | pub fn log_critical(&self, message: String) -> String { 51 | // Only log if not in a performance-critical section 52 | let log = format!("{} {}", self.prefix_with_date(), message); 53 | // Skip println for critical paths 54 | log 55 | } 56 | 57 | fn prefix_with_date(&self) -> String { 58 | let date = Local::now(); 59 | format!( 60 | "[{}] {}", 61 | date.format(&self.date_format.as_str().blue().bold()), 62 | self.prefix 63 | ) 64 | } 65 | } 66 | 67 | struct LogLevel<'a> { 68 | level: &'a str, 69 | } 70 | impl LogLevel<'_> { 71 | fn new() -> Self { 72 | let level = LOG_LEVEL; 73 | LogLevel { level } 74 | } 75 | fn is_debug(&self) -> bool { 76 | self.level.to_lowercase().eq("debug") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/services/cache_maintenance.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use tokio::time; 3 | use colored::Colorize; 4 | 5 | use crate::common::logger::Logger; 6 | use crate::common::cache::{TOKEN_ACCOUNT_CACHE, TOKEN_MINT_CACHE}; 7 | 8 | /// CacheMaintenanceService handles periodic cleanup of expired cache entries 9 | pub struct CacheMaintenanceService { 10 | logger: Logger, 11 | cleanup_interval: Duration, 12 | } 13 | 14 | impl CacheMaintenanceService { 15 | pub fn new(cleanup_interval_seconds: u64) -> Self { 16 | Self { 17 | logger: Logger::new("[CACHE-MAINTENANCE] => ".magenta().to_string()), 18 | cleanup_interval: Duration::from_secs(cleanup_interval_seconds), 19 | } 20 | } 21 | 22 | /// Start the cache maintenance service 23 | pub async fn start(self) { 24 | self.logger.log("Starting cache maintenance service".to_string()); 25 | 26 | let mut interval = time::interval(self.cleanup_interval); 27 | 28 | loop { 29 | interval.tick().await; 30 | self.cleanup_expired_entries().await; 31 | } 32 | } 33 | 34 | /// Clean up expired cache entries 35 | async fn cleanup_expired_entries(&self) { 36 | self.logger.log("Running cache cleanup".to_string()); 37 | 38 | // Clean up token account cache 39 | let token_account_count_before = TOKEN_ACCOUNT_CACHE.size(); 40 | TOKEN_ACCOUNT_CACHE.clear_expired(); 41 | let token_account_count_after = TOKEN_ACCOUNT_CACHE.size(); 42 | 43 | // Clean up token mint cache 44 | let token_mint_count_before = TOKEN_MINT_CACHE.size(); 45 | TOKEN_MINT_CACHE.clear_expired(); 46 | let token_mint_count_after = TOKEN_MINT_CACHE.size(); 47 | 48 | // Log cleanup results 49 | self.logger.log(format!( 50 | "Cache cleanup complete - Token accounts: {} -> {}, Token mints: {} -> {}", 51 | token_account_count_before, token_account_count_after, 52 | token_mint_count_before, token_mint_count_after 53 | )); 54 | } 55 | } 56 | 57 | /// Start the cache maintenance service in a background task 58 | pub async fn start_cache_maintenance(cleanup_interval_seconds: u64) { 59 | let service = CacheMaintenanceService::new(cleanup_interval_seconds); 60 | 61 | // Spawn a background task for cache maintenance 62 | tokio::spawn(async move { 63 | service.start().await; 64 | }); 65 | } -------------------------------------------------------------------------------- /src/env.example: -------------------------------------------------------------------------------- 1 | # Target Wallet Monitoring Configuration 2 | COPY_TRADING_TARGET_ADDRESS=suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK 3 | IS_MULTI_COPY_TRADING=true 4 | COUNTER_LIMIT=10 # Increase this to allow monitoring more tokens simultaneously 5 | 6 | # Trading Configuration 7 | TOKEN_AMOUNT=0.001 # token amount to purchase 8 | BUY_IN_SELL=0.02 9 | BUY_IN_SELL_LIMIT=30 # if target sell token for more than 10 sol, buy BUY_IN_SELL amount of token 10 | 11 | # Performance Settings 12 | UNIT_PRICE=2000000 13 | UNIT_LIMIT=2000000 14 | SELLING_UNIT_PRICE=4000000 15 | SELLING_UNIT_LIMIT=2000000 16 | ZERO_SLOT_TIP_VALUE=0.0025 17 | 18 | # Sniper Bot Focus Token Settings 19 | # If a focus token's price drops by this fraction from its initial price, mark as dropped 20 | # e.g. 0.15 means 15% drop 21 | FOCUS_DROP_THRESHOLD_PCT=0.15 22 | # After drop, trigger buy if an observed buy is at least this many SOL 23 | FOCUS_TRIGGER_SOL=1.0 24 | 25 | # Selling Strategy 26 | COPY_SELLING_LIMIT=1.5 27 | TAKE_PROFIT=8.0 # Take profit percentage 28 | STOP_LOSS=-2 # Stop loss percentage 29 | MAX_HOLD_TIME=3600 # Maximum hold time in seconds 30 | DYNAMIC_RETRACEMENT_PERCENTAGE=15 31 | RETRACEMENT_PNL_THRESHOLD=15 32 | RETRACEMENT_THRESHOLD=15 33 | MIN_LIQUIDITY=4 34 | 35 | # Dynamic Trailing Stop Configuration 36 | TRAILING_STOP_ACTIVATION_PERCENTAGE=20.0 # Minimum PnL to activate trailing stop 37 | TRAILING_STOP_TRAIL_PERCENTAGE=10.0 # Fallback trailing stop percentage 38 | # Format: "pnl_threshold:trail_percentage,pnl_threshold:trail_percentage,..." 39 | # Default: 20:5,50:10,100:30,200:100,500:100,1000:100 40 | DYNAMIC_TRAILING_STOP_THRESHOLDS=20:5,50:10,100:30,200:100,500:100,1000:100 41 | 42 | # Transaction Settings 43 | TRANSACTION_LANDING_SERVICE=0 44 | SLIPPAGE=3000 45 | MAX_WAIT_TIME=650000 # 600 seconds 46 | TIME_EXCEED=10 # seconds; time limit for volume non-increasing 47 | 48 | # Excluded Addresses 49 | EXCLUDED_ADDRESSES=675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8,CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C 50 | 51 | # RPC Configuration 52 | RPC_HTTP=https://rpc.shyft.to?api_key=YOUR_API_KEY 53 | RPC_WSS=wss://mainnet-fra.fountainhead.land/ 54 | YELLOWSTONE_GRPC_HTTP=https://grpc.ny.shyft.to 55 | YELLOWSTONE_GRPC_TOKEN=YOUR_GRPC_TOKEN 56 | 57 | # ZeroSlot Configuration 58 | ZERO_SLOT_URL=http://ny1.0slot.trade/?api-key=YOUR_API_KEY 59 | ZERO_SLOT_HEALTH=https://ny1.0slot.trade/health 60 | ZERO_SLOT_TIP_VALUE=0.00015 61 | JITO_TIP_VALUE=0.001 62 | 63 | # Wallet Configuration 64 | PRIVATE_KEY=YOUR_PRIVATE_KEY_HERE 65 | WRAP_AMOUNT=0.5 # 0.12 sol -------------------------------------------------------------------------------- /src/services/blockhash_processor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::time::{Duration, Instant}; 3 | use tokio::sync::RwLock; 4 | use solana_sdk::hash::Hash; 5 | use solana_client::rpc_client::RpcClient; 6 | use anyhow::{Result, anyhow}; 7 | use colored::Colorize; 8 | use lazy_static::lazy_static; 9 | use crate::common::logger::Logger; 10 | 11 | // Global state for latest blockhash and timestamp 12 | lazy_static! { 13 | static ref LATEST_BLOCKHASH: Arc>> = Arc::new(RwLock::new(None)); 14 | static ref BLOCKHASH_LAST_UPDATED: Arc>> = Arc::new(RwLock::new(None)); 15 | } 16 | 17 | const BLOCKHASH_STALENESS_THRESHOLD: Duration = Duration::from_secs(10); 18 | const UPDATE_INTERVAL: Duration = Duration::from_millis(300); 19 | 20 | pub struct BlockhashProcessor { 21 | rpc_client: Arc, 22 | logger: Logger, 23 | } 24 | 25 | impl BlockhashProcessor { 26 | pub async fn new(rpc_client: Arc) -> Result { 27 | let logger = Logger::new("[BLOCKHASH-PROCESSOR] => ".cyan().to_string()); 28 | 29 | Ok(Self { 30 | rpc_client, 31 | logger, 32 | }) 33 | } 34 | 35 | pub async fn start(&self) -> Result<()> { 36 | self.logger.log("Starting blockhash processor...".green().to_string()); 37 | 38 | // Clone necessary components for the background task 39 | let rpc_client = self.rpc_client.clone(); 40 | let logger = self.logger.clone(); 41 | 42 | tokio::spawn(async move { 43 | loop { 44 | match Self::update_blockhash_from_rpc(&rpc_client).await { 45 | Ok(blockhash) => { 46 | // Update global blockhash 47 | let mut latest = LATEST_BLOCKHASH.write().await; 48 | *latest = Some(blockhash); 49 | 50 | // Update timestamp 51 | let mut last_updated = BLOCKHASH_LAST_UPDATED.write().await; 52 | *last_updated = Some(Instant::now()); 53 | 54 | // logger.log(format!("Updated latest blockhash: {}", blockhash)); 55 | } 56 | Err(e) => { 57 | logger.log(format!("Error getting latest blockhash: {}", e).red().to_string()); 58 | } 59 | } 60 | 61 | tokio::time::sleep(UPDATE_INTERVAL).await; 62 | } 63 | }); 64 | 65 | Ok(()) 66 | } 67 | 68 | async fn update_blockhash_from_rpc(rpc_client: &RpcClient) -> Result { 69 | rpc_client.get_latest_blockhash() 70 | .map_err(|e| anyhow!("Failed to get blockhash from RPC: {}", e)) 71 | } 72 | 73 | /// Update the latest blockhash and its timestamp 74 | async fn update_blockhash(hash: Hash) { 75 | let mut latest = LATEST_BLOCKHASH.write().await; 76 | *latest = Some(hash); 77 | 78 | let mut last_updated = BLOCKHASH_LAST_UPDATED.write().await; 79 | *last_updated = Some(Instant::now()); 80 | } 81 | 82 | /// Get the latest cached blockhash with freshness check 83 | pub async fn get_latest_blockhash() -> Option { 84 | // Check if blockhash is stale 85 | let last_updated = BLOCKHASH_LAST_UPDATED.read().await; 86 | if let Some(instant) = *last_updated { 87 | if instant.elapsed() > BLOCKHASH_STALENESS_THRESHOLD { 88 | return None; 89 | } 90 | } 91 | 92 | let latest = LATEST_BLOCKHASH.read().await; 93 | *latest 94 | } 95 | 96 | /// Get a fresh blockhash, falling back to RPC if necessary 97 | pub async fn get_fresh_blockhash(&self) -> Result { 98 | if let Some(hash) = Self::get_latest_blockhash().await { 99 | return Ok(hash); 100 | } 101 | 102 | // Fallback to RPC if cached blockhash is stale or missing 103 | self.logger.log("Cached blockhash is stale or missing, falling back to RPC...".yellow().to_string()); 104 | let new_hash = self.rpc_client.get_latest_blockhash() 105 | .map_err(|e| anyhow!("Failed to get blockhash from RPC: {}", e))?; 106 | 107 | Self::update_blockhash(new_hash).await; 108 | Ok(new_hash) 109 | } 110 | } -------------------------------------------------------------------------------- /src/common/cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::sync::RwLock; 3 | use std::time::{Duration, Instant}; 4 | use anchor_client::solana_sdk::pubkey::Pubkey; 5 | use spl_token_2022::state::{Account, Mint}; 6 | use spl_token_2022::extension::StateWithExtensionsOwned; 7 | use lazy_static::lazy_static; 8 | 9 | /// TTL Cache entry that stores a value with an expiration time 10 | pub struct CacheEntry { 11 | pub value: T, 12 | pub expires_at: Instant, 13 | } 14 | 15 | impl CacheEntry { 16 | pub fn new(value: T, ttl_seconds: u64) -> Self { 17 | Self { 18 | value, 19 | expires_at: Instant::now() + Duration::from_secs(ttl_seconds), 20 | } 21 | } 22 | 23 | pub fn is_expired(&self) -> bool { 24 | Instant::now() > self.expires_at 25 | } 26 | } 27 | 28 | /// Token account cache 29 | pub struct TokenAccountCache { 30 | accounts: RwLock>>>, 31 | default_ttl: u64, 32 | } 33 | 34 | impl TokenAccountCache { 35 | pub fn new(default_ttl: u64) -> Self { 36 | Self { 37 | accounts: RwLock::new(HashMap::new()), 38 | default_ttl, 39 | } 40 | } 41 | 42 | pub fn get(&self, key: &Pubkey) -> Option> { 43 | let accounts = self.accounts.read().unwrap(); 44 | if let Some(entry) = accounts.get(key) { 45 | if !entry.is_expired() { 46 | return Some(entry.value.clone()); 47 | } 48 | } 49 | None 50 | } 51 | 52 | pub fn insert(&self, key: Pubkey, value: StateWithExtensionsOwned, ttl: Option) { 53 | let ttl = ttl.unwrap_or(self.default_ttl); 54 | let mut accounts = self.accounts.write().unwrap(); 55 | accounts.insert(key, CacheEntry::new(value, ttl)); 56 | } 57 | 58 | pub fn remove(&self, key: &Pubkey) { 59 | let mut accounts = self.accounts.write().unwrap(); 60 | accounts.remove(key); 61 | } 62 | 63 | pub fn clear_expired(&self) { 64 | let mut accounts = self.accounts.write().unwrap(); 65 | accounts.retain(|_, entry| !entry.is_expired()); 66 | } 67 | 68 | // Get the current size of the cache 69 | pub fn size(&self) -> usize { 70 | let accounts = self.accounts.read().unwrap(); 71 | accounts.len() 72 | } 73 | } 74 | 75 | /// Token mint cache 76 | pub struct TokenMintCache { 77 | mints: RwLock>>>, 78 | default_ttl: u64, 79 | } 80 | 81 | impl TokenMintCache { 82 | pub fn new(default_ttl: u64) -> Self { 83 | Self { 84 | mints: RwLock::new(HashMap::new()), 85 | default_ttl, 86 | } 87 | } 88 | 89 | pub fn get(&self, key: &Pubkey) -> Option> { 90 | let mints = self.mints.read().unwrap(); 91 | if let Some(entry) = mints.get(key) { 92 | if !entry.is_expired() { 93 | return Some(entry.value.clone()); 94 | } 95 | } 96 | None 97 | } 98 | 99 | pub fn insert(&self, key: Pubkey, value: StateWithExtensionsOwned, ttl: Option) { 100 | let ttl = ttl.unwrap_or(self.default_ttl); 101 | let mut mints = self.mints.write().unwrap(); 102 | mints.insert(key, CacheEntry::new(value, ttl)); 103 | } 104 | 105 | pub fn remove(&self, key: &Pubkey) { 106 | let mut mints = self.mints.write().unwrap(); 107 | mints.remove(key); 108 | } 109 | 110 | pub fn clear_expired(&self) { 111 | let mut mints = self.mints.write().unwrap(); 112 | mints.retain(|_, entry| !entry.is_expired()); 113 | } 114 | 115 | pub fn size(&self) -> usize { 116 | let mints = self.mints.read().unwrap(); 117 | mints.len() 118 | } 119 | } 120 | 121 | /// Simple wallet token account tracker 122 | pub struct WalletTokenAccounts { 123 | accounts: RwLock>, 124 | } 125 | 126 | impl WalletTokenAccounts { 127 | pub fn new() -> Self { 128 | Self { 129 | accounts: RwLock::new(HashSet::new()), 130 | } 131 | } 132 | 133 | pub fn contains(&self, account: &Pubkey) -> bool { 134 | let accounts = self.accounts.read().unwrap(); 135 | accounts.contains(account) 136 | } 137 | 138 | pub fn insert(&self, account: Pubkey) -> bool { 139 | let mut accounts = self.accounts.write().unwrap(); 140 | accounts.insert(account) 141 | } 142 | 143 | pub fn remove(&self, account: &Pubkey) -> bool { 144 | let mut accounts = self.accounts.write().unwrap(); 145 | accounts.remove(account) 146 | } 147 | 148 | pub fn get_all(&self) -> HashSet { 149 | let accounts = self.accounts.read().unwrap(); 150 | accounts.clone() 151 | } 152 | 153 | pub fn clear(&self) { 154 | let mut accounts = self.accounts.write().unwrap(); 155 | accounts.clear(); 156 | } 157 | 158 | pub fn size(&self) -> usize { 159 | let accounts = self.accounts.read().unwrap(); 160 | accounts.len() 161 | } 162 | } 163 | 164 | // Global cache instances with reasonable TTL values 165 | lazy_static! { 166 | pub static ref TOKEN_ACCOUNT_CACHE: TokenAccountCache = TokenAccountCache::new(60); // 60 seconds TTL 167 | pub static ref TOKEN_MINT_CACHE: TokenMintCache = TokenMintCache::new(300); // 5 minutes TTL 168 | pub static ref WALLET_TOKEN_ACCOUNTS: WalletTokenAccounts = WalletTokenAccounts::new(); 169 | } -------------------------------------------------------------------------------- /src/services/rpc_client.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | use anchor_client::solana_client::nonblocking::rpc_client::RpcClient; 4 | use anchor_client::solana_sdk::pubkey::Pubkey; 5 | use spl_token_2022::extension::StateWithExtensionsOwned; 6 | use spl_token_2022::state::{Account, Mint}; 7 | use anyhow::Result; 8 | use colored::Colorize; 9 | use tokio::sync::RwLock; 10 | 11 | use crate::common::logger::Logger; 12 | use crate::common::cache::{TOKEN_ACCOUNT_CACHE, TOKEN_MINT_CACHE}; 13 | 14 | /// BatchRpcClient provides optimized methods for fetching multiple accounts in a single RPC call 15 | pub struct BatchRpcClient { 16 | rpc_client: Arc, 17 | connection_pool: Arc>>>, 18 | logger: Logger, 19 | } 20 | 21 | impl BatchRpcClient { 22 | pub fn new(rpc_client: Arc) -> Self { 23 | // Create a connection pool with the initial client 24 | let mut pool = Vec::with_capacity(5); 25 | pool.push(rpc_client.clone()); 26 | 27 | Self { 28 | rpc_client, 29 | connection_pool: Arc::new(RwLock::new(pool)), 30 | logger: Logger::new("[BATCH-RPC] => ".cyan().to_string()), 31 | } 32 | } 33 | 34 | /// Get a client from the connection pool 35 | pub async fn get_client(&self) -> Arc { 36 | let pool = self.connection_pool.read().await; 37 | if pool.is_empty() { 38 | self.rpc_client.clone() 39 | } else { 40 | // Simple round-robin selection 41 | let index = std::time::SystemTime::now() 42 | .duration_since(std::time::UNIX_EPOCH) 43 | .unwrap_or_default() 44 | .as_millis() as usize % pool.len(); 45 | pool[index].clone() 46 | } 47 | } 48 | 49 | /// Add a new client to the connection pool 50 | pub async fn add_client(&self, client: Arc) { 51 | let mut pool = self.connection_pool.write().await; 52 | pool.push(client); 53 | } 54 | 55 | /// Get multiple token accounts in a single RPC call 56 | pub async fn get_multiple_token_accounts( 57 | &self, 58 | mint: &Pubkey, 59 | accounts: &[Pubkey] 60 | ) -> Result>> { 61 | let mut result = HashMap::new(); 62 | let mut accounts_to_fetch = Vec::new(); 63 | 64 | // Check cache first 65 | for account in accounts { 66 | if let Some(cached_account) = TOKEN_ACCOUNT_CACHE.get(account) { 67 | result.insert(*account, cached_account); 68 | } else { 69 | accounts_to_fetch.push(*account); 70 | } 71 | } 72 | 73 | if accounts_to_fetch.is_empty() { 74 | return Ok(result); 75 | } 76 | 77 | self.logger.log(format!("Fetching {} token accounts in batch", accounts_to_fetch.len())); 78 | 79 | // Get accounts that weren't in cache 80 | let client = self.get_client().await; 81 | let fetched_accounts = client.get_multiple_accounts(&accounts_to_fetch).await?; 82 | 83 | for (i, maybe_account) in fetched_accounts.iter().enumerate() { 84 | if let Some(account_data) = maybe_account { 85 | if account_data.owner == spl_token::ID { 86 | match StateWithExtensionsOwned::::unpack(account_data.data.clone()) { 87 | Ok(token_account) => { 88 | if token_account.base.mint == *mint { 89 | // Cache the result 90 | TOKEN_ACCOUNT_CACHE.insert(accounts_to_fetch[i], token_account.clone(), None); 91 | result.insert(accounts_to_fetch[i], token_account); 92 | } 93 | }, 94 | Err(_) => continue, 95 | } 96 | } 97 | } 98 | } 99 | 100 | Ok(result) 101 | } 102 | 103 | /// Get multiple mint accounts in a single RPC call 104 | pub async fn get_multiple_mints( 105 | &self, 106 | mints: &[Pubkey] 107 | ) -> Result>> { 108 | let mut result = HashMap::new(); 109 | let mut mints_to_fetch = Vec::new(); 110 | 111 | // Check cache first 112 | for mint in mints { 113 | if let Some(cached_mint) = TOKEN_MINT_CACHE.get(mint) { 114 | result.insert(*mint, cached_mint); 115 | } else { 116 | mints_to_fetch.push(*mint); 117 | } 118 | } 119 | 120 | if mints_to_fetch.is_empty() { 121 | return Ok(result); 122 | } 123 | 124 | self.logger.log(format!("Fetching {} mints in batch", mints_to_fetch.len())); 125 | 126 | // Get mints that weren't in cache 127 | let client = self.get_client().await; 128 | let fetched_mints = client.get_multiple_accounts(&mints_to_fetch).await?; 129 | 130 | for (i, maybe_mint) in fetched_mints.iter().enumerate() { 131 | if let Some(mint_data) = maybe_mint { 132 | if mint_data.owner == spl_token::ID { 133 | match StateWithExtensionsOwned::::unpack(mint_data.data.clone()) { 134 | Ok(mint) => { 135 | // Cache the result 136 | TOKEN_MINT_CACHE.insert(mints_to_fetch[i], mint.clone(), None); 137 | result.insert(mints_to_fetch[i], mint); 138 | }, 139 | Err(_) => continue, 140 | } 141 | } 142 | } 143 | } 144 | 145 | Ok(result) 146 | } 147 | 148 | /// Check if multiple token accounts exist in a single RPC call 149 | pub async fn check_multiple_accounts_exist( 150 | &self, 151 | accounts: &[Pubkey] 152 | ) -> Result> { 153 | let mut result = HashMap::new(); 154 | 155 | // Get accounts 156 | let client = self.get_client().await; 157 | let fetched_accounts = client.get_multiple_accounts(accounts).await?; 158 | 159 | for (i, maybe_account) in fetched_accounts.iter().enumerate() { 160 | result.insert(accounts[i], maybe_account.is_some()); 161 | } 162 | 163 | Ok(result) 164 | } 165 | } 166 | 167 | /// Create a batch RPC client from an existing RPC client 168 | pub fn create_batch_client(rpc_client: Arc) -> BatchRpcClient { 169 | BatchRpcClient::new(rpc_client) 170 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lets Bonk Dot Fun Sniper Bot 2 | 3 | This is a high-performance Rust-based sniper trading bot that monitors and executes trades on the Let's Bonk Dot Fun platform using Raydium Launchpad. The bot uses advanced transaction monitoring to detect and execute trades in real-time, giving you an edge in the market. 4 | 5 | The bot specifically tracks `buy` and `sell` transactions on Raydium Launchpad for Let's Bonk Dot Fun tokens, providing fast execution and automated trading capabilities. 6 | 7 | ## Features: 8 | 9 | - **Real-time Transaction Monitoring** - Uses Yellowstone gRPC to monitor transactions with minimal latency and high reliability 10 | - **Raydium Launchpad Integration** - Optimized for Let's Bonk Dot Fun platform trading 11 | - **Automated Copy Trading** - Instantly replicates buy and sell transactions from monitored wallets 12 | - **Smart Transaction Parsing** - Advanced transaction analysis to accurately identify and process trading activities 13 | - **Configurable Trading Parameters** - Customizable settings for trade amounts, timing, and risk management 14 | - **Built-in Selling Strategy** - Intelligent profit-taking mechanisms with customizable exit conditions 15 | - **Performance Optimization** - Efficient async processing with tokio for high-throughput transaction handling 16 | - **Reliable Error Recovery** - Automatic reconnection and retry mechanisms for uninterrupted operation 17 | 18 | ## Architecture Diagram 19 | 20 | ```mermaid 21 | graph TB 22 | subgraph "External Services" 23 | YG[Yellowstone gRPC
Transaction Feed] 24 | RL[Raydium Launchpad
DEX] 25 | TG[Telegram Bot
Notifications] 26 | end 27 | 28 | subgraph "Sniper Bot Core" 29 | subgraph "Monitoring Layer" 30 | TM[Transaction Monitor
Real-time Detection] 31 | TP[Transaction Parser
Decode & Analyze] 32 | end 33 | 34 | subgraph "Trading Engine" 35 | CT[Copy Trading
Execute Trades] 36 | SS[Selling Strategy
Profit/Loss Management] 37 | TR[Transaction Retry
Error Recovery] 38 | end 39 | 40 | subgraph "DEX Integration" 41 | RLH[Raydium Launchpad Handler
Buy/Sell Operations] 42 | end 43 | end 44 | 45 | subgraph "Configuration" 46 | ENV[Environment Variables
Trading Parameters] 47 | TARGET[Target Wallets
Monitor Addresses] 48 | end 49 | 50 | subgraph "Data Flow" 51 | YG -->|Transaction Stream| TM 52 | TM -->|Raw Transaction Data| TP 53 | TP -->|Parsed Trade Info| CT 54 | CT -->|Trade Execution| RLH 55 | RLH -->|Buy/Sell Orders| RL 56 | CT -->|Sell Triggers| SS 57 | SS -->|Sell Orders| RLH 58 | TR -->|Retry Failed Trades| RLH 59 | CT -->|Notifications| TG 60 | ENV -->|Configuration| CT 61 | TARGET -->|Wallet Addresses| TM 62 | end 63 | 64 | subgraph "Trading Flow" 65 | START([Bot Starts]) --> CONNECT[Connect to Yellowstone gRPC] 66 | CONNECT --> MONITOR[Monitor Target Wallets] 67 | MONITOR --> DETECT{Transaction Detected?} 68 | DETECT -->|Yes| PARSE[Parse Transaction Data] 69 | DETECT -->|No| MONITOR 70 | PARSE --> VALIDATE{Valid Trade?} 71 | VALIDATE -->|Yes| EXECUTE[Execute Copy Trade] 72 | VALIDATE -->|No| MONITOR 73 | EXECUTE --> SUCCESS{Trade Successful?} 74 | SUCCESS -->|Yes| NOTIFY[Send Telegram Notification] 75 | SUCCESS -->|No| RETRY[Retry with Error Recovery] 76 | RETRY --> EXECUTE 77 | NOTIFY --> MONITOR 78 | end 79 | 80 | style YG fill:#e1f5fe 81 | style RL fill:#f3e5f5 82 | style TG fill:#e8f5e8 83 | style TM fill:#fff3e0 84 | style TP fill:#fff3e0 85 | style CT fill:#fce4ec 86 | style SS fill:#fce4ec 87 | style RLH fill:#f1f8e9 88 | ``` 89 | 90 | ## Who is it for? 91 | 92 | - Bot users looking for the fastest transaction feed possible for Let's Bonk Dot Fun trading 93 | - Traders who want automated execution on Raydium Launchpad 94 | - Users who want to copy trade from successful wallets 95 | 96 | ## Setting up 97 | 98 | ### Environment Variables 99 | 100 | Before run, you will need to add the following environment variables to your `.env` file: 101 | 102 | - `GRPC_ENDPOINT` - Your Geyser RPC endpoint url. 103 | - `GRPC_X_TOKEN` - Leave it set to `None` if your Geyser RPC does not require a token for authentication. 104 | - `GRPC_SERVER_ENDPOINT` - The address of its gRPC server. By default is set at `0.0.0.0:50051`. 105 | - `COPY_TRADING_TARGET_ADDRESS` - Wallet address(es) to monitor for trades (comma-separated for multiple addresses) 106 | 107 | ### Telegram Notifications 108 | 109 | To enable Telegram notifications: 110 | 111 | - `TELEGRAM_BOT_TOKEN` - Your Telegram bot token 112 | - `TELEGRAM_CHAT_ID` - Your chat ID for receiving notifications 113 | 114 | ### Optional Variables 115 | 116 | - `IS_MULTI_COPY_TRADING` - Set to `true` to monitor multiple addresses (default: `false`) 117 | - `PROTOCOL_PREFERENCE` - Set to `raydium` for Raydium Launchpad (default: `auto`) 118 | - `COUNTER_LIMIT` - Maximum number of trades to execute 119 | - `SELLING_TIME` - Time in seconds before selling (default: 600) 120 | - `PROFIT_PERCENTAGE` - Profit percentage for selling (default: 20.0) 121 | - `STOP_LOSS_PERCENTAGE` - Stop loss percentage (default: 10.0) 122 | 123 | ## Run Command 124 | 125 | ```bash 126 | # Build the project 127 | cargo build --release 128 | 129 | # Run the bot 130 | RUSTFLAGS="-C target-cpu=native" RUST_LOG=info cargo run --release 131 | ``` 132 | 133 | ## Project Structure 134 | 135 | The codebase is organized into several modules: 136 | 137 | - **engine/** - Core trading logic including copy trading, selling strategies, and transaction parsing 138 | - **dex/** - Protocol-specific implementations for Raydium Launchpad 139 | - **common/** - Shared utilities, configuration, and constants 140 | - **core/** - Core system functionality 141 | - **error/** - Error handling and definitions 142 | - **services/** - External service integrations (RPC, blockhash processing, etc.) 143 | 144 | ## Usage 145 | 146 | Once started, the bot will: 147 | 148 | 1. Connect to the Yellowstone gRPC endpoint 149 | 2. Monitor transactions from the specified wallet address(es) 150 | 3. Automatically copy buy and sell transactions as they occur 151 | 4. Send notifications via Telegram for detected transactions and executed trades 152 | 5. Execute trades on Raydium Launchpad for Let's Bonk Dot Fun tokens 153 | 154 | ## Recent Updates 155 | 156 | - Removed PumpFun and PumpSwap support 157 | - Focused on Raydium Launchpad integration for Let's Bonk Dot Fun 158 | - Implemented concurrent transaction processing using tokio tasks 159 | - Enhanced error handling and reporting 160 | - Improved selling strategy implementation 161 | 162 | ## Contact 163 | 164 | For questions or support, please contact the developer. 165 | 166 | ## Disclaimer 167 | 168 | This bot is for educational purposes only. Trading cryptocurrencies involves substantial risk of loss and is not suitable for all investors. The past performance of any trading system or methodology is not necessarily indicative of future results. -------------------------------------------------------------------------------- /src/services/zeroslot.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ClientError; 2 | use anyhow::{anyhow, Result}; 3 | use rand::{seq::IteratorRandom, thread_rng}; 4 | use serde_json::{json, Value}; 5 | use anchor_client::solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction}; 6 | use std::{str::FromStr, sync::LazyLock}; 7 | use bs64; 8 | 9 | use crate::common::config::import_env_var; 10 | 11 | pub static ZERO_SLOT_URL: LazyLock = LazyLock::new(|| import_env_var("ZERO_SLOT_URL")); 12 | 13 | pub fn get_tip_account() -> Result { 14 | let accounts = [ 15 | "6fQaVhYZA4w3MBSXjJ81Vf6W1EDYeUPXpgVQ6UQyU1Av".to_string(), 16 | "4HiwLEP2Bzqj3hM2ENxJuzhcPCdsafwiet3oGkMkuQY4".to_string(), 17 | "7toBU3inhmrARGngC7z6SjyP85HgGMmCTEwGNRAcYnEK".to_string(), 18 | "8mR3wB1nh4D6J9RUCugxUpc6ya8w38LPxZ3ZjcBhgzws".to_string(), 19 | "6SiVU5WEwqfFapRuYCndomztEwDjvS5xgtEof3PLEGm9".to_string(), 20 | "TpdxgNJBWZRL8UXF5mrEsyWxDWx9HQexA9P1eTWQ42p".to_string(), 21 | "D8f3WkQu6dCF33cZxuAsrKHrGsqGP2yvAHf8mX6RXnwf".to_string(), 22 | "GQPFicsy3P3NXxB5piJohoxACqTvWE9fKpLgdsMduoHE".to_string(), 23 | "Ey2JEr8hDkgN8qKJGrLf2yFjRhW7rab99HVxwi5rcvJE".to_string(), 24 | "4iUgjMT8q2hNZnLuhpqZ1QtiV8deFPy2ajvvjEpKKgsS".to_string(), 25 | "3Rz8uD83QsU8wKvZbgWAPvCNDU6Fy8TSZTMcPm3RB6zt".to_string(), 26 | "DiTmWENJsHQdawVUUKnUXkconcpW4Jv52TnMWhkncF6t".to_string(), 27 | "HRyRhQ86t3H4aAtgvHVpUJmw64BDrb61gRiKcdKUXs5c".to_string(), 28 | "7y4whZmw388w1ggjToDLSBLv47drw5SUXcLk6jtmwixd".to_string(), 29 | "J9BMEWFbCBEjtQ1fG5Lo9kouX1HfrKQxeUxetwXrifBw".to_string(), 30 | "8U1JPQh3mVQ4F5jwRdFTBzvNRQaYFQppHQYoH38DJGSQ".to_string(), 31 | "Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3".to_string(), 32 | "FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe".to_string(), 33 | "ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13".to_string(), 34 | "6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK".to_string(), 35 | "Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr".to_string(), 36 | ]; 37 | let mut rng = thread_rng(); 38 | let tip_account = match accounts.iter().choose(&mut rng) { 39 | Some(acc) => Ok(Pubkey::from_str(acc).inspect_err(|err| { 40 | println!("zeroslot: failed to parse Pubkey: {:?}", err); 41 | })?), 42 | None => Err(anyhow!("zeroslot: no tip accounts available")), 43 | }; 44 | 45 | let tip_account = tip_account?; 46 | Ok(tip_account) 47 | } 48 | 49 | pub async fn get_tip_value() -> Result { 50 | // If ZERO_SLOT_TIP_VALUE is set, use it 51 | if let Ok(tip_value) = std::env::var("ZERO_SLOT_TIP_VALUE") { 52 | match f64::from_str(&tip_value) { 53 | Ok(value) => Ok(value), 54 | Err(_) => { 55 | println!( 56 | "Invalid ZERO_SLOT_TIP_VALUE in environment variable: '{}'. Falling back to percentile calculation.", 57 | tip_value 58 | ); 59 | Err(anyhow!("Invalid TIP_VALUE in environment variable")) 60 | } 61 | } 62 | } else { 63 | Err(anyhow!("ZERO_SLOT_TIP_VALUE environment variable not set")) 64 | } 65 | } 66 | 67 | pub const MAX_RETRIES: u8 = 3; 68 | 69 | #[derive(Debug, Clone)] 70 | pub struct TransactionConfig { 71 | pub skip_preflight: bool, 72 | pub encoding: String, 73 | } 74 | 75 | impl Default for TransactionConfig { 76 | fn default() -> Self { 77 | Self { 78 | skip_preflight: true, 79 | encoding: "base64".to_string(), 80 | } 81 | } 82 | } 83 | 84 | #[derive(Clone, Debug)] 85 | pub struct ZeroSlotClient { 86 | endpoint: String, 87 | client: reqwest::Client, 88 | config: TransactionConfig, 89 | } 90 | 91 | impl ZeroSlotClient { 92 | pub fn new(endpoint: &str) -> Self { 93 | Self { 94 | endpoint: endpoint.to_string(), 95 | client: reqwest::Client::new(), 96 | config: TransactionConfig::default(), 97 | } 98 | } 99 | 100 | pub async fn send_transaction( 101 | &self, 102 | transaction: &Transaction, 103 | ) -> Result { 104 | let wire_transaction = bincode::serialize(transaction).map_err(|e| { 105 | ClientError::Parse( 106 | "Transaction serialization failed".to_string(), 107 | e.to_string(), 108 | ) 109 | })?; 110 | 111 | let encoded_tx = &bs64::encode(&wire_transaction); 112 | 113 | for retry in 0..MAX_RETRIES { 114 | match self.try_send_transaction(encoded_tx).await { 115 | Ok(signature) => { 116 | return Signature::from_str(&signature).map_err(|e| { 117 | ClientError::Parse("Invalid signature".to_string(), e.to_string()) 118 | }); 119 | } 120 | Err(e) => { 121 | println!("Retry {} failed: {:?}", retry, e); 122 | if retry == MAX_RETRIES - 1 { 123 | return Err(e); 124 | } 125 | // tokio::time::sleep(RETRY_DELAY).await; 126 | } 127 | } 128 | } 129 | 130 | Err(ClientError::Other("Max retries exceeded".to_string())) 131 | } 132 | 133 | async fn try_send_transaction(&self, encoded_tx: &str) -> Result { 134 | let params = json!([ 135 | encoded_tx, 136 | { 137 | "skipPreflight": self.config.skip_preflight, 138 | "encoding": self.config.encoding, 139 | } 140 | ]); 141 | 142 | let response = self.send_request("sendTransaction", params).await?; 143 | 144 | response["result"] 145 | .as_str() 146 | .map(|s| s.to_string()) 147 | .ok_or_else(|| { 148 | ClientError::Parse( 149 | "Invalid response format".to_string(), 150 | "Missing result field".to_string(), 151 | ) 152 | }) 153 | } 154 | 155 | async fn send_request(&self, method: &str, params: Value) -> Result { 156 | let request_body = json!({ 157 | "jsonrpc": "2.0", 158 | "id": 1, 159 | "method": method, 160 | "params": params 161 | }); 162 | 163 | let response = self 164 | .client 165 | .post(&self.endpoint) 166 | .header("Content-Type", "application/json") 167 | .json(&request_body) 168 | .send() 169 | .await 170 | .map_err(|e| ClientError::Solana("Request failed".to_string(), e.to_string()))?; 171 | 172 | let response_data: Value = response 173 | .json() 174 | .await 175 | .map_err(|e| ClientError::Parse("Invalid JSON response".to_string(), e.to_string()))?; 176 | 177 | if let Some(error) = response_data.get("error") { 178 | return Err(ClientError::Solana( 179 | "RPC error".to_string(), 180 | error.to_string(), 181 | )); 182 | } 183 | 184 | Ok(response_data) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/services/health_check.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::time::{Duration, Instant}; 3 | use tokio::time::{interval, sleep}; 4 | use reqwest::Client; 5 | use anyhow::Result; 6 | use colored::Colorize; 7 | use lazy_static::lazy_static; 8 | use dashmap::DashMap; 9 | use crate::common::logger::Logger; 10 | use crate::common::config::TransactionLandingMode; 11 | 12 | // Global state for health checks 13 | lazy_static! { 14 | static ref HEALTH_STATUS: Arc> = Arc::new(DashMap::new()); 15 | } 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct HealthStatus { 19 | pub service_name: String, 20 | pub is_healthy: bool, 21 | pub last_check: Instant, 22 | pub response_time_ms: u64, 23 | pub error_count: u32, 24 | pub consecutive_failures: u32, 25 | } 26 | 27 | impl HealthStatus { 28 | pub fn new(service_name: String) -> Self { 29 | Self { 30 | service_name, 31 | is_healthy: true, 32 | last_check: Instant::now(), 33 | response_time_ms: 0, 34 | error_count: 0, 35 | consecutive_failures: 0, 36 | } 37 | } 38 | } 39 | 40 | pub struct HealthCheckManager { 41 | logger: Logger, 42 | zeroslot_client: Arc, 43 | } 44 | 45 | impl HealthCheckManager { 46 | pub fn new( 47 | zeroslot_client: Arc, 48 | ) -> Self { 49 | Self { 50 | logger: Logger::new("[HEALTH-CHECK] => ".blue().to_string()), 51 | zeroslot_client, 52 | } 53 | } 54 | 55 | /// Start health check monitoring for Zeroslot service 56 | pub async fn start_monitoring(&self) -> Result<()> { 57 | self.logger.log("Starting health check monitoring for Zeroslot RPC service".green().to_string()); 58 | 59 | // Initialize health status for Zeroslot service 60 | HEALTH_STATUS.insert("zeroslot".to_string(), HealthStatus::new("zeroslot".to_string())); 61 | 62 | // Start monitoring task 63 | self.start_zeroslot_monitoring().await; 64 | 65 | Ok(()) 66 | } 67 | 68 | /// Start Zeroslot monitoring with 65-second keepalive 69 | async fn start_zeroslot_monitoring(&self) { 70 | let client = self.zeroslot_client.clone(); 71 | let logger = self.logger.clone(); 72 | 73 | tokio::spawn(async move { 74 | let mut interval = interval(Duration::from_secs(60)); // 60 seconds to be safe 75 | 76 | loop { 77 | interval.tick().await; 78 | 79 | let start_time = Instant::now(); 80 | let result = Self::check_zeroslot_health(&client).await; 81 | let response_time = start_time.elapsed().as_millis() as u64; 82 | 83 | Self::update_health_status("zeroslot", result.is_ok(), response_time, &logger).await; 84 | 85 | if let Err(e) = result { 86 | logger.log(format!("Zeroslot health check failed: {}", e).red().to_string()); 87 | } else { 88 | logger.log(format!("Zeroslot keepalive successful ({}ms)", response_time).green().to_string()); 89 | } 90 | } 91 | }); 92 | } 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | /// Check Zeroslot health with keepalive 103 | async fn check_zeroslot_health(client: &Client) -> Result<()> { 104 | let health_url = crate::common::config::get_zero_slot_health_url(); 105 | let response = client 106 | .get(&health_url) 107 | .timeout(Duration::from_secs(10)) 108 | .send() 109 | .await?; 110 | 111 | if response.status().is_success() { 112 | Ok(()) 113 | } else { 114 | Err(anyhow::anyhow!("Zeroslot health check failed with status: {}", response.status())) 115 | } 116 | } 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | /// Update health status for a service 127 | async fn update_health_status(service_name: &str, is_healthy: bool, response_time_ms: u64, logger: &Logger) { 128 | let mut status = HEALTH_STATUS.entry(service_name.to_string()) 129 | .or_insert_with(|| HealthStatus::new(service_name.to_string())); 130 | 131 | status.last_check = Instant::now(); 132 | status.response_time_ms = response_time_ms; 133 | 134 | if is_healthy { 135 | status.is_healthy = true; 136 | status.consecutive_failures = 0; 137 | } else { 138 | status.is_healthy = false; 139 | status.error_count += 1; 140 | status.consecutive_failures += 1; 141 | 142 | if status.consecutive_failures >= 3 { 143 | logger.log(format!("Service {} is unhealthy after {} consecutive failures", 144 | service_name, status.consecutive_failures).red().bold().to_string()); 145 | } 146 | } 147 | } 148 | 149 | /// Get health status for a service 150 | pub fn get_health_status(service_name: &str) -> Option { 151 | HEALTH_STATUS.get(service_name).map(|status| status.clone()) 152 | } 153 | 154 | /// Get the healthiest service for a given transaction landing mode 155 | pub fn get_healthiest_service(landing_mode: &TransactionLandingMode) -> Option { 156 | match landing_mode { 157 | TransactionLandingMode::Zeroslot => { 158 | if Self::is_service_healthy("zeroslot") { Some("zeroslot".to_string()) } else { None } 159 | }, 160 | _ => { 161 | // Default to Zeroslot for any other mode 162 | if Self::is_service_healthy("zeroslot") { Some("zeroslot".to_string()) } else { None } 163 | } 164 | } 165 | } 166 | 167 | 168 | 169 | /// Check if a service is healthy 170 | fn is_service_healthy(service_name: &str) -> bool { 171 | HEALTH_STATUS.get(service_name) 172 | .map(|status| status.is_healthy) 173 | .unwrap_or(false) 174 | } 175 | 176 | /// Log health status for Zeroslot service 177 | pub fn log_all_health_status(logger: &Logger) { 178 | logger.log("=== Zeroslot RPC Service Health Status ===".blue().bold().to_string()); 179 | 180 | for entry in HEALTH_STATUS.iter() { 181 | let service = entry.key(); 182 | let status = entry.value(); 183 | 184 | let health_indicator = if status.is_healthy { "✅" } else { "❌" }; 185 | let color = if status.is_healthy { "green" } else { "red" }; 186 | 187 | logger.log(format!( 188 | "{} {}: {} ({}ms, {} failures)", 189 | health_indicator, 190 | service, 191 | if status.is_healthy { "HEALTHY" } else { "UNHEALTHY" }, 192 | status.response_time_ms, 193 | status.consecutive_failures 194 | ).color(color).to_string()); 195 | } 196 | } 197 | 198 | /// Wait for service to become healthy 199 | pub async fn wait_for_service_health(service_name: &str, timeout_secs: u64) -> bool { 200 | let start_time = Instant::now(); 201 | let timeout = Duration::from_secs(timeout_secs); 202 | 203 | while start_time.elapsed() < timeout { 204 | if Self::is_service_healthy(service_name) { 205 | return true; 206 | } 207 | 208 | sleep(Duration::from_secs(1)).await; 209 | } 210 | 211 | false 212 | } 213 | } -------------------------------------------------------------------------------- /src/error/mod.rs: -------------------------------------------------------------------------------- 1 | //! Error types for the Pump.fun SDK. 2 | //! 3 | //! This module defines the `ClientError` enum, which encompasses various error types that can occur when interacting with the Pump.fun program. 4 | //! It includes specific error cases for bonding curve operations, metadata uploads, Solana client errors, and more. 5 | //! 6 | //! The `ClientError` enum provides a comprehensive set of error types to help developers handle and debug issues that may arise during interactions with the Pump.fun program. 7 | //! 8 | //! # Error Types 9 | //! 10 | //! - `BondingCurveNotFound`: The bonding curve account was not found. 11 | //! - `BondingCurveError`: An error occurred while interacting with the bonding curve. 12 | //! - `BorshError`: An error occurred while serializing or deserializing data using Borsh. 13 | //! - `SolanaClientError`: An error occurred while interacting with the Solana RPC client. 14 | //! - `UploadMetadataError`: An error occurred while uploading metadata to IPFS. 15 | //! - `InvalidInput`: Invalid input parameters were provided. 16 | //! - `InsufficientFunds`: Insufficient funds for a transaction. 17 | //! - `SimulationError`: Transaction simulation failed. 18 | //! - `RateLimitExceeded`: Rate limit exceeded. 19 | 20 | use serde_json::Error; 21 | use anchor_client::solana_client::{ 22 | client_error::ClientError as SolanaClientError, pubsub_client::PubsubClientError, 23 | }; 24 | use anchor_client::solana_sdk::pubkey::ParsePubkeyError; 25 | 26 | // #[derive(Debug)] 27 | // #[allow(dead_code)] 28 | // pub struct AppError(anyhow::Error); 29 | 30 | // impl From for AppError 31 | // where 32 | // E: Into, 33 | // { 34 | // fn from(err: E) -> Self { 35 | // Self(err.into()) 36 | // } 37 | // } 38 | 39 | #[derive(Debug)] 40 | pub enum ClientError { 41 | /// Bonding curve account was not found 42 | BondingCurveNotFound, 43 | /// Error related to bonding curve operations 44 | BondingCurveError(&'static str), 45 | /// Error deserializing data using Borsh 46 | BorshError(std::io::Error), 47 | /// Error from Solana RPC client 48 | SolanaClientError(anchor_client::solana_client::client_error::ClientError), 49 | /// Error uploading metadata 50 | UploadMetadataError(Box), 51 | /// Invalid input parameters 52 | InvalidInput(&'static str), 53 | /// Insufficient funds for transaction 54 | InsufficientFunds, 55 | /// Transaction simulation failed 56 | SimulationError(String), 57 | /// Rate limit exceeded 58 | RateLimitExceeded, 59 | 60 | OrderLimitExceeded, 61 | 62 | ExternalService(String), 63 | 64 | Redis(String, String), 65 | 66 | Solana(String, String), 67 | 68 | Parse(String, String), 69 | 70 | Pubkey(String, String), 71 | 72 | Jito(String, String), 73 | 74 | Join(String), 75 | 76 | Subscribe(String, String), 77 | 78 | Send(String, String), 79 | 80 | Other(String), 81 | 82 | InvalidData(String), 83 | 84 | RaydiumBuy(String), 85 | 86 | RaydiumSell(String), 87 | 88 | Timeout(String, String), 89 | 90 | Duplicate(String), 91 | 92 | InvalidEventType, 93 | 94 | ChannelClosed, 95 | } 96 | 97 | impl std::fmt::Display for ClientError { 98 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 99 | match self { 100 | Self::BondingCurveNotFound => write!(f, "Bonding curve not found"), 101 | Self::BondingCurveError(msg) => write!(f, "Bonding curve error: {}", msg), 102 | Self::BorshError(err) => write!(f, "Borsh serialization error: {}", err), 103 | Self::SolanaClientError(err) => write!(f, "Solana client error: {}", err), 104 | Self::UploadMetadataError(err) => write!(f, "Metadata upload error: {}", err), 105 | Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), 106 | Self::InsufficientFunds => write!(f, "Insufficient funds for transaction"), 107 | Self::SimulationError(msg) => write!(f, "Transaction simulation failed: {}", msg), 108 | Self::ExternalService(msg) => write!(f, "External service error: {}", msg), 109 | Self::RateLimitExceeded => write!(f, "Rate limit exceeded"), 110 | Self::OrderLimitExceeded => write!(f, "Order limit exceeded"), 111 | Self::Solana(msg, details) => write!(f, "Solana error: {}, details: {}", msg, details), 112 | Self::Parse(msg, details) => write!(f, "Parse error: {}, details: {}", msg, details), 113 | Self::Jito(msg, details) => write!(f, "Jito error: {}, details: {}", msg, details), 114 | Self::Redis(msg, details) => write!(f, "Redis error: {}, details: {}", msg, details), 115 | Self::Join(msg) => write!(f, "Task join error: {}", msg), 116 | Self::Pubkey(msg, details) => write!(f, "Pubkey error: {}, details: {}", msg, details), 117 | Self::Subscribe(msg, details) => { 118 | write!(f, "Subscribe error: {}, details: {}", msg, details) 119 | } 120 | Self::Send(msg, details) => write!(f, "Send error: {}, details: {}", msg, details), 121 | Self::Other(msg) => write!(f, "Other error: {}", msg), 122 | Self::RaydiumBuy(msg) => write!(f, "Raydium buy error: {}", msg), 123 | Self::RaydiumSell(msg) => write!(f, "Raydium sell error: {}", msg), 124 | Self::InvalidData(msg) => write!(f, "Invalid data: {}", msg), 125 | Self::Timeout(msg, details) => { 126 | write!(f, "Operation timed out: {}, details: {}", msg, details) 127 | } 128 | Self::Duplicate(msg) => write!(f, "Duplicate event: {}", msg), 129 | Self::InvalidEventType => write!(f, "Invalid event type"), 130 | Self::ChannelClosed => write!(f, "Channel closed"), 131 | } 132 | } 133 | } 134 | impl std::error::Error for ClientError { 135 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 136 | match self { 137 | Self::BorshError(err) => Some(err), 138 | Self::SolanaClientError(err) => Some(err), 139 | Self::UploadMetadataError(err) => Some(err.as_ref()), 140 | Self::ExternalService(_) => None, 141 | Self::Redis(_, _) => None, 142 | Self::Solana(_, _) => None, 143 | Self::Parse(_, _) => None, 144 | Self::Jito(_, _) => None, 145 | Self::Join(_) => None, 146 | Self::Pubkey(_, _) => None, 147 | Self::Subscribe(_, _) => None, 148 | Self::Send(_, _) => None, 149 | Self::Other(_) => None, 150 | Self::RaydiumBuy(_) => None, 151 | Self::RaydiumSell(_) => None, 152 | Self::Timeout(_, _) => None, 153 | Self::Duplicate(_) => None, 154 | Self::InvalidEventType => None, 155 | Self::ChannelClosed => None, 156 | _ => None, 157 | } 158 | } 159 | } 160 | 161 | impl From for ClientError { 162 | fn from(error: SolanaClientError) -> Self { 163 | ClientError::Solana("Solana client error".to_string(), error.to_string()) 164 | } 165 | } 166 | 167 | impl From for ClientError { 168 | fn from(error: PubsubClientError) -> Self { 169 | ClientError::Solana("PubSub client error".to_string(), error.to_string()) 170 | } 171 | } 172 | 173 | impl From for ClientError { 174 | fn from(error: ParsePubkeyError) -> Self { 175 | ClientError::Pubkey("Pubkey error".to_string(), error.to_string()) 176 | } 177 | } 178 | 179 | impl From for ClientError { 180 | fn from(err: Error) -> Self { 181 | ClientError::Parse("JSON serialization error".to_string(), err.to_string()) 182 | } 183 | } 184 | 185 | pub type ClientResult = Result; 186 | -------------------------------------------------------------------------------- /src/engine/transaction_parser.rs: -------------------------------------------------------------------------------- 1 | use bs58; 2 | use std::str::FromStr; 3 | use solana_sdk::pubkey::Pubkey; 4 | use colored::Colorize; 5 | use crate::common::logger::Logger; 6 | use lazy_static; 7 | use yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction; 8 | use std::time::Instant; 9 | // Import RAYDIUM_LAUNCHPAD_PROGRAM 10 | use crate::dex::raydium_launchpad::RAYDIUM_LAUNCHPAD_PROGRAM; 11 | // Create a static logger for this module 12 | lazy_static::lazy_static! { 13 | static ref LOGGER: Logger = Logger::new("[PARSER] => ".blue().to_string()); 14 | } 15 | 16 | // Quiet parser logs; sniper logic will log only for focus tokens 17 | #[inline] 18 | fn dex_log(_msg: String) {} 19 | 20 | #[derive(Clone, Debug, PartialEq)] 21 | pub enum DexType { 22 | RaydiumLaunchpad, 23 | Unknown, 24 | } 25 | 26 | #[derive(Clone, Debug)] 27 | pub struct TradeInfoFromToken { 28 | // Common fields 29 | pub dex_type: DexType, 30 | pub slot: u64, 31 | pub signature: String, 32 | pub pool_id: String, 33 | pub mint: String, 34 | pub timestamp: u64, 35 | pub is_buy: bool, 36 | pub price: u64, 37 | pub is_reverse: bool, 38 | pub coin_creator: Option, 39 | pub sol_change: f64, 40 | pub token_change: f64, 41 | pub liquidity: f64, // this is for filtering out small trades 42 | pub virtual_sol_reserves: u64, 43 | pub virtual_token_reserves: u64, 44 | } 45 | 46 | /// Helper function to check if transaction contains Buy instruction 47 | fn has_buy_instruction(txn: &SubscribeUpdateTransaction) -> bool { 48 | if let Some(tx_inner) = &txn.transaction { 49 | if let Some(meta) = &tx_inner.meta { 50 | return meta.log_messages.iter().any(|log| { 51 | log.contains("Program log: Instruction: Buy") || 52 | log.contains("Program log: Instruction: Swap") 53 | }); 54 | } 55 | } 56 | false 57 | } 58 | 59 | /// Helper function to check if transaction contains Sell instruction 60 | fn has_sell_instruction(txn: &SubscribeUpdateTransaction) -> bool { 61 | if let Some(tx_inner) = &txn.transaction { 62 | if let Some(meta) = &tx_inner.meta { 63 | return meta.log_messages.iter().any(|log| { 64 | log.contains("Program log: Instruction: Sell") || 65 | log.contains("Program log: Instruction: Swap") 66 | }); 67 | } 68 | } 69 | false 70 | } 71 | 72 | /// Parses the transaction data buffer into a TradeInfoFromToken struct 73 | pub fn parse_transaction_data(txn: &SubscribeUpdateTransaction, buffer: &[u8]) -> Option { 74 | fn parse_public_key(buffer: &[u8], offset: usize) -> Option { 75 | if offset + 32 > buffer.len() { 76 | return None; 77 | } 78 | Some(bs58::encode(&buffer[offset..offset+32]).into_string()) 79 | } 80 | 81 | fn parse_u64(buffer: &[u8], offset: usize) -> Option { 82 | if offset + 8 > buffer.len() { 83 | return None; 84 | } 85 | let mut bytes = [0u8; 8]; 86 | bytes.copy_from_slice(&buffer[offset..offset+8]); 87 | Some(u64::from_le_bytes(bytes)) 88 | } 89 | 90 | fn parse_u8(buffer: &[u8], offset: usize) -> Option { 91 | if offset >= buffer.len() { 92 | return None; 93 | } 94 | Some(buffer[offset]) 95 | } 96 | 97 | // Helper function to extract token mint from token balances 98 | fn extract_token_info( 99 | txn: &SubscribeUpdateTransaction, 100 | ) -> String { 101 | 102 | let mut mint = String::new(); 103 | 104 | // Try to extract from token balances if txn is available 105 | if let Some(tx_inner) = &txn.transaction { 106 | if let Some(meta) = &tx_inner.meta { 107 | // Check post token balances 108 | if !meta.post_token_balances.is_empty() { 109 | mint = meta.post_token_balances[0].mint.clone(); 110 | 111 | // Skip WSOL and look for the actual token 112 | if mint == "So11111111111111111111111111111111111111112" { 113 | if meta.post_token_balances.len() > 1 { 114 | mint = meta.post_token_balances[1].mint.clone(); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | // If we couldn't extract from token balances, use default 122 | if mint.is_empty() { 123 | mint = "2ivzYvjnKqA4X3dVvPKr7bctGpbxwrXbbxm44TJCpump".to_string(); 124 | } 125 | 126 | mint 127 | } 128 | 129 | let start_time = Instant::now(); 130 | 131 | // Extract token mint 132 | let mint = extract_token_info(&txn); 133 | let timestamp = std::time::SystemTime::now() 134 | .duration_since(std::time::UNIX_EPOCH) 135 | .unwrap_or_default() 136 | .as_secs(); 137 | 138 | // Determine if this is a buy or sell based on instruction logs 139 | let is_buy = has_buy_instruction(txn); 140 | 141 | // For Raydium Launchpad, we'll use simplified parsing 142 | // In a real implementation, you'd parse the specific Raydium instruction data 143 | let price = 1000000000; // Default price in lamports 144 | let sol_change = if is_buy { -0.1 } else { 0.1 }; // Example values 145 | let token_change = if is_buy { 1000000.0 } else { -1000000.0 }; // Example values 146 | let liquidity = 1000.0; // Example liquidity 147 | let virtual_sol_reserves = 30000000000; // 30 SOL in lamports 148 | let virtual_token_reserves = 1000000000000000; // 1B tokens 149 | 150 | dex_log(format!("RaydiumLaunchpad {}: {} SOL (Price: {})", 151 | if is_buy { "BUY" } else { "SELL" }, 152 | sol_change.abs(), 153 | price as f64 / 1_000_000_000.0 154 | ).green().to_string()); 155 | 156 | Some(TradeInfoFromToken { 157 | dex_type: DexType::RaydiumLaunchpad, 158 | slot: 0, // Will be set from transaction data 159 | signature: String::new(), // Will be set from transaction data 160 | pool_id: String::new(), // Will be set from transaction data 161 | mint: mint.clone(), 162 | timestamp, 163 | is_buy, 164 | price, 165 | is_reverse: false, // Raydium Launchpad doesn't use reverse logic 166 | coin_creator: None, // Will be extracted from metadata if available 167 | sol_change, 168 | token_change, 169 | liquidity, 170 | virtual_sol_reserves, 171 | virtual_token_reserves, 172 | }) 173 | } 174 | 175 | /// Main function to process transaction and extract trade information 176 | pub fn process_transaction(txn: &SubscribeUpdateTransaction) -> Option { 177 | // Check if this transaction involves the Raydium Launchpad program 178 | if let Some(tx_inner) = &txn.transaction { 179 | if let Some(transaction) = &tx_inner.transaction { 180 | if let Some(message) = &transaction.message { 181 | // Check if any of the account keys match the Raydium Launchpad program 182 | let raydium_program_id = match Pubkey::from_str(&RAYDIUM_LAUNCHPAD_PROGRAM) { 183 | Ok(pubkey) => pubkey, 184 | Err(_) => return None, 185 | }; 186 | 187 | if message.account_keys.contains(&raydium_program_id) { 188 | // Extract instruction data if available 189 | if let Some(meta) = &tx_inner.meta { 190 | if let Some(inner_instructions) = &meta.inner_instructions { 191 | for inner_instruction in inner_instructions { 192 | for instruction in &inner_instruction.instructions { 193 | if let Some(data) = &instruction.data { 194 | if let Some(trade_info) = parse_transaction_data(txn, data) { 195 | return Some(trade_info); 196 | } 197 | } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | 207 | None 208 | } -------------------------------------------------------------------------------- /src/core/tx.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::str::FromStr; 3 | use anyhow::{Result, anyhow}; 4 | use colored::Colorize; 5 | use anchor_client::solana_client::nonblocking::rpc_client::RpcClient; 6 | use anchor_client::solana_sdk::{ 7 | instruction::Instruction, 8 | signature::Keypair, 9 | system_instruction, 10 | transaction::Transaction, 11 | }; 12 | use std::env; 13 | use anchor_client::solana_sdk::pubkey::Pubkey; 14 | use spl_token::ui_amount_to_amount; 15 | use solana_sdk::signature::Signer; 16 | use tokio::time::{Instant, sleep}; 17 | use tokio::sync::Mutex; 18 | use once_cell::sync::Lazy; 19 | use reqwest::{Client, ClientBuilder}; 20 | use base64; 21 | use bs58; 22 | use std::time::Duration; 23 | use crate::{ 24 | common::{ 25 | logger::Logger, 26 | config::TransactionLandingMode, 27 | }, 28 | services::{ 29 | zeroslot::{self, ZeroSlotClient}, 30 | }, 31 | }; 32 | use dotenv::dotenv; 33 | 34 | // prioritization fee = UNIT_PRICE * UNIT_LIMIT 35 | fn get_unit_price() -> u64 { 36 | env::var("UNIT_PRICE") 37 | .ok() 38 | .and_then(|v| u64::from_str(&v).ok()) 39 | .unwrap_or(20000) 40 | } 41 | 42 | fn get_unit_limit() -> u32 { 43 | env::var("UNIT_LIMIT") 44 | .ok() 45 | .and_then(|v| u32::from_str(&v).ok()) 46 | .unwrap_or(200_000) 47 | } 48 | 49 | 50 | // Cache the FlashBlock API key 51 | static FLASHBLOCK_API_KEY: Lazy = Lazy::new(|| { 52 | std::env::var("FLASHBLOCK_API_KEY") 53 | .ok() 54 | .unwrap_or_else(|| "da07907679634859".to_string()) 55 | }); 56 | 57 | // Create a static HTTP client with optimized configuration for FlashBlock API 58 | static HTTP_CLIENT: Lazy = Lazy::new(|| { 59 | let client = reqwest::Client::new(); 60 | client 61 | }); 62 | 63 | pub async fn new_signed_and_send_zeroslot( 64 | zeroslot_rpc_client: Arc, 65 | recent_blockhash: solana_sdk::hash::Hash, 66 | keypair: &Keypair, 67 | mut instructions: Vec, 68 | logger: &Logger, 69 | ) -> Result> { 70 | let tip_account = zeroslot::get_tip_account()?; 71 | let start_time = Instant::now(); 72 | let mut txs: Vec = vec![]; 73 | 74 | // zeroslot tip, the upper limit is 0.1 75 | let tip = zeroslot::get_tip_value().await?; 76 | let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); 77 | 78 | let zeroslot_tip_instruction = 79 | system_instruction::transfer(&keypair.pubkey(), &tip_account, tip_lamports); 80 | 81 | let unit_limit = get_unit_limit(); // TODO: update in mev boost 82 | let unit_price = get_unit_price(); // TODO: update in mev boost 83 | let modify_compute_units = 84 | solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit(unit_limit); 85 | let add_priority_fee = 86 | solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price(unit_price); 87 | instructions.insert(1, modify_compute_units); 88 | instructions.insert(2, add_priority_fee); 89 | 90 | instructions.push(zeroslot_tip_instruction); // zeroslot is different with others. 91 | // send init tx 92 | let txn = Transaction::new_signed_with_payer( 93 | &instructions, 94 | Some(&keypair.pubkey()), 95 | &vec![keypair], 96 | recent_blockhash, 97 | ); 98 | 99 | let tx_result = zeroslot_rpc_client.send_transaction(&txn).await; 100 | 101 | match tx_result { 102 | Ok(signature) => { 103 | txs.push(signature.to_string()); 104 | logger.log( 105 | format!("[TXN-ELAPSED(ZEROSLOT)]: {:?}", start_time.elapsed()) 106 | .yellow() 107 | .to_string(), 108 | ); 109 | } 110 | Err(_) => { 111 | // Convert the error to a Send-compatible form 112 | return Err(anyhow::anyhow!("zeroslot send_transaction failed")); 113 | } 114 | }; 115 | 116 | Ok(txs) 117 | } 118 | 119 | 120 | pub async fn new_signed_and_send_zeroslot_fast( 121 | compute_unit_limit: u32, 122 | compute_unit_price: u64, 123 | tip_lamports: u64, 124 | zeroslot_rpc_client: Arc, 125 | recent_blockhash: solana_sdk::hash::Hash, 126 | keypair: &Keypair, 127 | mut instructions: Vec, 128 | logger: &Logger, 129 | ) -> Result> { 130 | let tip_account = zeroslot::get_tip_account()?; 131 | let start_time = Instant::now(); 132 | let mut txs: Vec = vec![]; 133 | 134 | // zeroslot tip, the upper limit is 0.1 135 | let tip = zeroslot::get_tip_value().await?; 136 | let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); 137 | 138 | let zeroslot_tip_instruction = 139 | system_instruction::transfer(&keypair.pubkey(), &tip_account, tip_lamports); 140 | 141 | let unit_limit = get_unit_limit(); // TODO: update in mev boost 142 | let unit_price = get_unit_price(); // TODO: update in mev boost 143 | let modify_compute_units = 144 | solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit(unit_limit); 145 | let add_priority_fee = 146 | solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price(unit_price); 147 | instructions.insert(1, modify_compute_units); 148 | instructions.insert(2, add_priority_fee); 149 | 150 | instructions.push(zeroslot_tip_instruction); // zeroslot is different with others. 151 | // send init tx 152 | let txn = Transaction::new_signed_with_payer( 153 | &instructions, 154 | Some(&keypair.pubkey()), 155 | &vec![keypair], 156 | recent_blockhash, 157 | ); 158 | 159 | let tx_result = zeroslot_rpc_client.send_transaction(&txn).await; 160 | 161 | match tx_result { 162 | Ok(signature) => { 163 | txs.push(signature.to_string()); 164 | logger.log( 165 | format!("[TXN-ELAPSED(ZEROSLOT)]: {:?}", start_time.elapsed()) 166 | .yellow() 167 | .to_string(), 168 | ); 169 | } 170 | Err(_) => { 171 | // Convert the error to a Send-compatible form 172 | return Err(anyhow::anyhow!("zeroslot send_transaction failed")); 173 | } 174 | }; 175 | 176 | Ok(txs) 177 | } 178 | 179 | /// Send transaction using normal RPC without any service or tips 180 | pub async fn new_signed_and_send_normal( 181 | rpc_client: Arc, 182 | recent_blockhash: anchor_client::solana_sdk::hash::Hash, 183 | keypair: &Keypair, 184 | mut instructions: Vec, 185 | logger: &Logger, 186 | ) -> Result> { 187 | let start_time = Instant::now(); 188 | 189 | // Add compute budget instructions for priority fee 190 | // let unit_limit = 200000; 191 | // let unit_price = 20000; 192 | // let modify_compute_units = 193 | // solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit(unit_limit); 194 | // let add_priority_fee = 195 | // solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price(unit_price); 196 | // instructions.insert(0, modify_compute_units); 197 | // instructions.insert(1, add_priority_fee); 198 | 199 | // Create and send transaction 200 | let txn = Transaction::new_signed_with_payer( 201 | &instructions, 202 | Some(&keypair.pubkey()), 203 | &vec![keypair], 204 | recent_blockhash, 205 | ); 206 | 207 | match rpc_client.send_transaction(&txn).await { 208 | Ok(signature) => { 209 | logger.log( 210 | format!("[TXN-ELAPSED(NORMAL)]: {:?}", start_time.elapsed()) 211 | .yellow() 212 | .to_string(), 213 | ); 214 | Ok(vec![signature.to_string()]) 215 | } 216 | Err(e) => Err(anyhow!("Failed to send normal transaction: {}", e)) 217 | } 218 | } 219 | 220 | /// Universal transaction landing function that routes to the appropriate service 221 | pub async fn new_signed_and_send_with_landing_mode( 222 | transaction_landing_mode: TransactionLandingMode, 223 | app_state: &crate::common::config::AppState, 224 | recent_blockhash: anchor_client::solana_sdk::hash::Hash, 225 | keypair: &Keypair, 226 | instructions: Vec, 227 | logger: &Logger, 228 | ) -> Result> { 229 | // Route to the appropriate service 230 | match transaction_landing_mode { 231 | TransactionLandingMode::Zeroslot => { 232 | logger.log("Using Zeroslot for transaction landing".green().to_string()); 233 | new_signed_and_send_zeroslot( 234 | app_state.zeroslot_rpc_client.clone(), 235 | recent_blockhash, 236 | keypair, 237 | instructions, 238 | logger, 239 | ).await 240 | }, 241 | TransactionLandingMode::Normal => { 242 | logger.log("Using Normal RPC for transaction landing".green().to_string()); 243 | new_signed_and_send_normal( 244 | app_state.rpc_nonblocking_client.clone(), 245 | recent_blockhash, 246 | keypair, 247 | instructions, 248 | logger, 249 | ).await 250 | }, 251 | } 252 | } 253 | 254 | -------------------------------------------------------------------------------- /src/core/token.rs: -------------------------------------------------------------------------------- 1 | use anchor_client::solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, instruction::Instruction, rent::Rent, system_instruction}; 2 | use solana_program_pack::Pack; 3 | use spl_token_2022::{ 4 | extension::StateWithExtensionsOwned, 5 | state::{Account, Mint}, 6 | }; 7 | use spl_token_client::{ 8 | client::{ProgramClient, ProgramRpcClient, ProgramRpcClientSendTransaction}, 9 | token::{Token, TokenError, TokenResult}, 10 | }; 11 | use std::sync::Arc; 12 | use anyhow::{Result, anyhow}; 13 | use spl_associated_token_account::instruction::create_associated_token_account_idempotent; 14 | 15 | use crate::common::cache::{TOKEN_ACCOUNT_CACHE, TOKEN_MINT_CACHE}; 16 | 17 | pub fn get_token_address( 18 | client: Arc, 19 | keypair: Arc, 20 | address: &Pubkey, 21 | owner: &Pubkey, 22 | ) -> Pubkey { 23 | let token_client = Token::new( 24 | Arc::new(ProgramRpcClient::new( 25 | client.clone(), 26 | ProgramRpcClientSendTransaction, 27 | )), 28 | &spl_token::ID, 29 | address, 30 | None, 31 | Arc::new(Keypair::from_bytes(&keypair.to_bytes()).expect("failed to copy keypair")), 32 | ); 33 | token_client.get_associated_token_address(owner) 34 | } 35 | 36 | pub async fn get_account_info( 37 | client: Arc, 38 | address: Pubkey, 39 | account: Pubkey, 40 | ) -> TokenResult> { 41 | // Check cache first 42 | if let Some(cached_account) = TOKEN_ACCOUNT_CACHE.get(&account) { 43 | return Ok(cached_account); 44 | } 45 | 46 | // If not in cache, fetch from RPC 47 | let program_client = Arc::new(ProgramRpcClient::new( 48 | client.clone(), 49 | ProgramRpcClientSendTransaction, 50 | )); 51 | let account_data = program_client 52 | .get_account(account) 53 | .await 54 | .map_err(TokenError::Client)? 55 | .ok_or(TokenError::AccountNotFound) 56 | .inspect_err(|_err| { 57 | // logger.log(format!( 58 | // "get_account_info: {} {}: mint {}", 59 | // account, err, address 60 | // )); 61 | })?; 62 | 63 | if account_data.owner != spl_token::ID { 64 | return Err(TokenError::AccountInvalidOwner); 65 | } 66 | let account_info = StateWithExtensionsOwned::::unpack(account_data.data)?; 67 | if account_info.base.mint != address { 68 | return Err(TokenError::AccountInvalidMint); 69 | } 70 | 71 | // Cache the result 72 | TOKEN_ACCOUNT_CACHE.insert(account, account_info.clone(), None); 73 | 74 | Ok(account_info) 75 | } 76 | 77 | pub async fn get_mint_info( 78 | client: Arc, 79 | _keypair: Arc, 80 | address: Pubkey, 81 | ) -> TokenResult> { 82 | // Check cache first 83 | if let Some(cached_mint) = TOKEN_MINT_CACHE.get(&address) { 84 | return Ok(cached_mint); 85 | } 86 | 87 | // If not in cache, fetch from RPC 88 | let program_client = Arc::new(ProgramRpcClient::new( 89 | client.clone(), 90 | ProgramRpcClientSendTransaction, 91 | )); 92 | let account = program_client 93 | .get_account(address) 94 | .await 95 | .map_err(TokenError::Client)? 96 | .ok_or(TokenError::AccountNotFound) 97 | .inspect_err(|err| println!("{} {}: mint {}", address, err, address))?; 98 | 99 | if account.owner != spl_token::ID { 100 | return Err(TokenError::AccountInvalidOwner); 101 | } 102 | 103 | let mint_result = StateWithExtensionsOwned::::unpack(account.data).map_err(Into::into); 104 | let decimals: Option = None; 105 | if let (Ok(mint), Some(decimals)) = (&mint_result, decimals) { 106 | if decimals != mint.base.decimals { 107 | return Err(TokenError::InvalidDecimals); 108 | } 109 | } 110 | 111 | // Cache the result if successful 112 | if let Ok(mint_info) = &mint_result { 113 | TOKEN_MINT_CACHE.insert(address, mint_info.clone(), None); 114 | } 115 | 116 | mint_result 117 | } 118 | 119 | /// Check if a token account exists 120 | pub async fn account_exists( 121 | rpc_client: Arc, 122 | account: &Pubkey, 123 | ) -> Result { 124 | // Check cache first to avoid RPC call 125 | if TOKEN_ACCOUNT_CACHE.get(account).is_some() { 126 | return Ok(true); 127 | } 128 | 129 | // Just check if the account exists without validating the mint 130 | match rpc_client.get_account_with_commitment(account, rpc_client.commitment()).await { 131 | Ok(response) => { 132 | match response.value { 133 | Some(acc) => { 134 | // Check if the account is owned by the token program 135 | if acc.owner == spl_token::ID { 136 | // Try to parse the account to cache it for future use 137 | if let Ok(token_account) = StateWithExtensionsOwned::::unpack(acc.data.clone()) { 138 | TOKEN_ACCOUNT_CACHE.insert(*account, token_account, None); 139 | } 140 | Ok(true) 141 | } else { 142 | Ok(false) 143 | } 144 | }, 145 | None => Ok(false), 146 | } 147 | }, 148 | Err(e) => Err(anyhow!("Error checking account: {}, account: {}", e, account)), 149 | } 150 | } 151 | 152 | /// Check if a specific token account exists and validates the mint 153 | pub async fn verify_token_account( 154 | rpc_client: Arc, 155 | mint: &Pubkey, 156 | account: &Pubkey, 157 | ) -> Result { 158 | // Check cache first 159 | if let Some(cached_account) = TOKEN_ACCOUNT_CACHE.get(account) { 160 | return Ok(cached_account.base.mint == *mint); 161 | } 162 | 163 | match get_account_info(rpc_client, *mint, *account).await { 164 | Ok(_) => Ok(true), 165 | Err(TokenError::AccountNotFound) => Ok(false), 166 | Err(TokenError::AccountInvalidMint) => Ok(false), 167 | Err(TokenError::AccountInvalidOwner) => Ok(false), 168 | Err(e) => Err(anyhow!("Error checking account: {} , account: {}", e, account)), 169 | } 170 | } 171 | 172 | /// Get multiple token accounts in a single RPC call 173 | pub async fn get_multiple_token_accounts( 174 | rpc_client: Arc, 175 | accounts: &[Pubkey], 176 | ) -> Result>>, anyhow::Error> { 177 | let mut result = Vec::with_capacity(accounts.len()); 178 | let mut accounts_to_fetch = Vec::new(); 179 | let mut indices = Vec::new(); 180 | 181 | // Check cache first 182 | for (i, account) in accounts.iter().enumerate() { 183 | if let Some(cached_account) = TOKEN_ACCOUNT_CACHE.get(account) { 184 | result.push(Some(cached_account)); 185 | } else { 186 | result.push(None); 187 | accounts_to_fetch.push(*account); 188 | indices.push(i); 189 | } 190 | } 191 | 192 | if !accounts_to_fetch.is_empty() { 193 | // Fetch accounts not in cache 194 | let fetched_accounts = rpc_client.get_multiple_accounts(&accounts_to_fetch).await?; 195 | 196 | for (i, maybe_account) in fetched_accounts.iter().enumerate() { 197 | if let Some(account_data) = maybe_account { 198 | if account_data.owner == spl_token::ID { 199 | if let Ok(token_account) = StateWithExtensionsOwned::::unpack(account_data.data.clone()) { 200 | // Cache the account 201 | TOKEN_ACCOUNT_CACHE.insert(accounts_to_fetch[i], token_account.clone(), None); 202 | result[indices[i]] = Some(token_account); 203 | } 204 | } 205 | } 206 | } 207 | } 208 | 209 | Ok(result) 210 | } 211 | 212 | /// Create a wrapped SOL account with a specific amount 213 | pub fn create_wsol_account_with_amount( 214 | owner: Pubkey, 215 | amount: u64, 216 | ) -> Result<(Pubkey, Vec), anyhow::Error> { 217 | let wsol_account = Keypair::new(); 218 | let wsol_account_pubkey = wsol_account.pubkey(); 219 | 220 | let instructions = vec![ 221 | // Create account 222 | system_instruction::create_account( 223 | &owner, 224 | &wsol_account_pubkey, 225 | amount + Rent::default().minimum_balance(Account::LEN), 226 | Account::LEN as u64, 227 | &spl_token::id(), 228 | ), 229 | // Initialize as token account 230 | spl_token::instruction::initialize_account( 231 | &spl_token::id(), 232 | &wsol_account_pubkey, 233 | &spl_token::native_mint::id(), 234 | &owner, 235 | )?, 236 | ]; 237 | 238 | Ok((wsol_account_pubkey, instructions)) 239 | } 240 | 241 | /// Create a wrapped SOL account (without funding) 242 | pub fn create_wsol_account( 243 | owner: Pubkey, 244 | ) -> Result<(Pubkey, Vec), anyhow::Error> { 245 | let mut instructions = Vec::new(); 246 | 247 | // Create the associated token account for WSOL 248 | instructions.push( 249 | create_associated_token_account_idempotent( 250 | &owner, 251 | &owner, 252 | &spl_token::native_mint::id(), 253 | &spl_token::ID, 254 | ) 255 | ); 256 | 257 | // Get the WSOL ATA address using the SPL token function directly 258 | let wsol_account = spl_associated_token_account::get_associated_token_address( 259 | &owner, 260 | &spl_token::native_mint::id() 261 | ); 262 | 263 | Ok((wsol_account, instructions)) 264 | } 265 | 266 | /// Close a token account 267 | pub fn close_account( 268 | _owner: Pubkey, 269 | token_account: Pubkey, 270 | destination: Pubkey, 271 | authority: Pubkey, 272 | signers: &[&Pubkey], 273 | ) -> Result { 274 | Ok(spl_token::instruction::close_account( 275 | &spl_token::id(), 276 | &token_account, 277 | &destination, 278 | &authority, 279 | signers, 280 | )?) 281 | } 282 | -------------------------------------------------------------------------------- /src/engine/transaction_retry.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::time::{Duration, Instant}; 3 | use anyhow::{anyhow, Result}; 4 | use anchor_client::solana_sdk::{ 5 | pubkey::Pubkey, 6 | signature::{Signature, Keypair}, 7 | instruction::Instruction, 8 | transaction::{VersionedTransaction, Transaction}, 9 | signer::Signer, 10 | hash::Hash, 11 | }; 12 | use spl_associated_token_account::get_associated_token_address; 13 | use colored::Colorize; 14 | use tokio::time::sleep; 15 | use base64; 16 | 17 | use crate::common::{ 18 | config::{AppState, SwapConfig}, 19 | logger::Logger, 20 | }; 21 | use crate::engine::swap::SwapDirection; 22 | use crate::services::jupiter_api::JupiterClient; 23 | use crate::engine::transaction_parser::TradeInfoFromToken; 24 | use crate::core::tx; 25 | 26 | /// Maximum number of retry attempts for selling transactions 27 | const MAX_RETRIES: u32 = 3; 28 | 29 | /// Delay between retry attempts 30 | const RETRY_DELAY: Duration = Duration::from_secs(2); 31 | 32 | /// Timeout for transaction verification 33 | const VERIFICATION_TIMEOUT: Duration = Duration::from_secs(30); 34 | 35 | /// Result of a selling transaction attempt 36 | #[derive(Debug)] 37 | pub struct SellTransactionResult { 38 | pub success: bool, 39 | pub signature: Option, 40 | pub error: Option, 41 | pub used_jupiter_fallback: bool, 42 | pub attempt_count: u32, 43 | } 44 | 45 | /// Enhanced transaction verification with retry logic 46 | pub async fn verify_transaction_with_retry( 47 | signature: &Signature, 48 | app_state: Arc, 49 | logger: &Logger, 50 | max_retries: u32, 51 | ) -> Result { 52 | let mut retry_count = 0; 53 | 54 | while retry_count < max_retries { 55 | match app_state.rpc_client.get_signature_status(signature).await { 56 | Ok(Some(status)) => { 57 | if status.confirmation_status == Some(solana_transaction_status::TransactionConfirmationStatus::Confirmed) { 58 | return Ok(true); 59 | } else if status.confirmation_status == Some(solana_transaction_status::TransactionConfirmationStatus::Finalized) { 60 | return Ok(true); 61 | } else { 62 | logger.log(format!("Transaction not confirmed yet, retry {}/{}", retry_count + 1, max_retries)); 63 | retry_count += 1; 64 | sleep(RETRY_DELAY).await; 65 | } 66 | } 67 | Ok(None) => { 68 | logger.log(format!("Transaction not found, retry {}/{}", retry_count + 1, max_retries)); 69 | retry_count += 1; 70 | sleep(RETRY_DELAY).await; 71 | } 72 | Err(e) => { 73 | logger.log(format!("Error verifying transaction: {}, retry {}/{}", e, retry_count + 1, max_retries)); 74 | retry_count += 1; 75 | sleep(RETRY_DELAY).await; 76 | } 77 | } 78 | } 79 | 80 | Err(anyhow!("Transaction verification failed after {} retries", max_retries)) 81 | } 82 | 83 | /// Execute sell transaction with comprehensive retry logic 84 | pub async fn execute_sell_with_retry( 85 | trade_info: &TradeInfoFromToken, 86 | sell_config: SwapConfig, 87 | app_state: Arc, 88 | logger: &Logger, 89 | ) -> Result { 90 | let mut attempt_count = 0; 91 | let mut last_error = None; 92 | 93 | // Try Raydium Launchpad first 94 | while attempt_count < MAX_RETRIES { 95 | attempt_count += 1; 96 | logger.log(format!("Sell attempt {}/{} for token {}", attempt_count, MAX_RETRIES, trade_info.mint).yellow().to_string()); 97 | 98 | match execute_raydium_sell_attempt(trade_info, sell_config.clone(), app_state.clone(), logger).await { 99 | Ok(signature) => { 100 | logger.log(format!("Raydium sell transaction sent: {}", signature).green().to_string()); 101 | 102 | // Verify the transaction 103 | match verify_transaction_with_retry(&signature, app_state.clone(), logger, 3).await { 104 | Ok(verified) => { 105 | if verified { 106 | logger.log("Raydium sell transaction verified successfully".green().to_string()); 107 | return Ok(SellTransactionResult { 108 | success: true, 109 | signature: Some(signature), 110 | error: None, 111 | used_jupiter_fallback: false, 112 | attempt_count, 113 | }); 114 | } else { 115 | last_error = Some("Transaction verification failed".to_string()); 116 | } 117 | } 118 | Err(e) => { 119 | last_error = Some(format!("Transaction verification error: {}", e)); 120 | } 121 | } 122 | } 123 | Err(e) => { 124 | last_error = Some(format!("Raydium sell failed: {}", e)); 125 | logger.log(format!("Raydium sell attempt {} failed: {}", attempt_count, e).red().to_string()); 126 | } 127 | } 128 | 129 | if attempt_count < MAX_RETRIES { 130 | sleep(RETRY_DELAY).await; 131 | } 132 | } 133 | 134 | // If Raydium failed, try Jupiter as fallback 135 | logger.log("Raydium sell failed, trying Jupiter fallback...".yellow().to_string()); 136 | 137 | match execute_jupiter_sell_attempt(trade_info, sell_config, app_state.clone(), logger).await { 138 | Ok(signature) => { 139 | logger.log(format!("Jupiter sell transaction sent: {}", signature).green().to_string()); 140 | 141 | // Verify the transaction 142 | match verify_transaction_with_retry(&signature, app_state.clone(), logger, 3).await { 143 | Ok(verified) => { 144 | if verified { 145 | logger.log("Jupiter sell transaction verified successfully".green().to_string()); 146 | return Ok(SellTransactionResult { 147 | success: true, 148 | signature: Some(signature), 149 | error: None, 150 | used_jupiter_fallback: true, 151 | attempt_count, 152 | }); 153 | } else { 154 | last_error = Some("Jupiter transaction verification failed".to_string()); 155 | } 156 | } 157 | Err(e) => { 158 | last_error = Some(format!("Jupiter transaction verification error: {}", e)); 159 | } 160 | } 161 | } 162 | Err(e) => { 163 | last_error = Some(format!("Jupiter sell failed: {}", e)); 164 | logger.log(format!("Jupiter sell failed: {}", e).red().to_string()); 165 | } 166 | } 167 | 168 | Ok(SellTransactionResult { 169 | success: false, 170 | signature: None, 171 | error: last_error, 172 | used_jupiter_fallback: true, 173 | attempt_count, 174 | }) 175 | } 176 | 177 | /// Execute Raydium sell attempt 178 | async fn execute_raydium_sell_attempt( 179 | trade_info: &TradeInfoFromToken, 180 | sell_config: SwapConfig, 181 | app_state: Arc, 182 | logger: &Logger, 183 | ) -> Result { 184 | let raydium = crate::dex::raydium_launchpad::RaydiumLaunchpad::new( 185 | app_state.wallet.clone(), 186 | Some(app_state.rpc_client.clone()), 187 | Some(app_state.rpc_nonblocking_client.clone()), 188 | ); 189 | 190 | let (keypair, instructions, _price) = raydium.build_swap_from_parsed_data(trade_info, sell_config).await 191 | .map_err(|e| anyhow!("Failed to build Raydium swap: {}", e))?; 192 | 193 | let recent_blockhash = crate::services::blockhash_processor::BlockhashProcessor::get_latest_blockhash().await 194 | .ok_or_else(|| anyhow!("Failed to get recent blockhash"))?; 195 | 196 | let signature = crate::core::tx::new_signed_and_send_zeroslot( 197 | app_state.zeroslot_rpc_client.clone(), 198 | recent_blockhash, 199 | &keypair, 200 | instructions, 201 | logger, 202 | ).await 203 | .map_err(|e| anyhow!("Failed to send Raydium transaction: {}", e))?; 204 | 205 | if signature.is_empty() { 206 | return Err(anyhow!("No signature returned from Raydium transaction")); 207 | } 208 | 209 | let signature = Signature::from_str(&signature[0]) 210 | .map_err(|e| anyhow!("Failed to parse signature: {}", e))?; 211 | Ok(signature) 212 | } 213 | 214 | /// Execute Jupiter sell attempt as fallback 215 | async fn execute_jupiter_sell_attempt( 216 | trade_info: &TradeInfoFromToken, 217 | sell_config: SwapConfig, 218 | app_state: Arc, 219 | logger: &Logger, 220 | ) -> Result { 221 | let jupiter_client = JupiterClient::new(); 222 | 223 | // Get wallet public key 224 | let wallet_pubkey = app_state.wallet.try_pubkey() 225 | .map_err(|_| anyhow!("Failed to get wallet public key"))?; 226 | 227 | // Get token mint 228 | let token_mint = Pubkey::from_str(&trade_info.mint) 229 | .map_err(|_| anyhow!("Invalid token mint"))?; 230 | 231 | // Get associated token account 232 | let token_account = get_associated_token_address(&wallet_pubkey, &token_mint); 233 | 234 | // Get SOL mint for WSOL 235 | let sol_mint = Pubkey::from_str("So11111111111111111111111111111111111111112") 236 | .map_err(|_| anyhow!("Invalid SOL mint"))?; 237 | 238 | // Get WSOL account 239 | let wsol_account = get_associated_token_address(&wallet_pubkey, &sol_mint); 240 | 241 | // Calculate amount to sell (use a percentage of the token amount) 242 | let amount_to_sell = (trade_info.token_change * 0.5) as u64; // Sell 50% of tokens 243 | 244 | // Get quote from Jupiter 245 | let quote = jupiter_client.get_quote( 246 | &token_mint, 247 | &sol_mint, 248 | amount_to_sell, 249 | sell_config.slippage_bps, 250 | ).await 251 | .map_err(|e| anyhow!("Failed to get Jupiter quote: {}", e))?; 252 | 253 | // Get swap transaction from Jupiter 254 | let swap_transaction = jupiter_client.get_swap_transaction( 255 | "e, 256 | &wallet_pubkey, 257 | &token_account, 258 | &wsol_account, 259 | ).await 260 | .map_err(|e| anyhow!("Failed to get Jupiter swap transaction: {}", e))?; 261 | 262 | // Send the transaction 263 | let signature = app_state.rpc_client.send_and_confirm_transaction(&swap_transaction) 264 | .await 265 | .map_err(|e| anyhow!("Failed to send Jupiter transaction: {}", e))?; 266 | 267 | Ok(signature) 268 | } -------------------------------------------------------------------------------- /src/common/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bs58; 3 | use colored::Colorize; 4 | use dotenv::dotenv; 5 | use reqwest::Error; 6 | use serde::Deserialize; 7 | use anchor_client::solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair, signer::Signer}; 8 | use tokio::sync::{Mutex, OnceCell}; 9 | use tokio_tungstenite::tungstenite::http::request; 10 | use std::{env, sync::Arc}; 11 | use crate::engine::swap::SwapProtocol; 12 | use crate::{ 13 | common::{constants::INIT_MSG, logger::Logger}, 14 | engine::swap::{SwapDirection, SwapInType}, 15 | }; 16 | use std::time::Duration; 17 | 18 | static GLOBAL_CONFIG: OnceCell> = OnceCell::const_new(); 19 | 20 | #[derive(Clone, Debug)] 21 | pub enum TransactionLandingMode { 22 | Zeroslot, 23 | Normal, 24 | } 25 | 26 | impl Default for TransactionLandingMode { 27 | fn default() -> Self { 28 | TransactionLandingMode::Normal 29 | } 30 | } 31 | 32 | impl FromStr for TransactionLandingMode { 33 | type Err = String; 34 | 35 | fn from_str(s: &str) -> Result { 36 | match s { 37 | "0" | "zeroslot" => Ok(TransactionLandingMode::Zeroslot), 38 | "1" | "normal" => Ok(TransactionLandingMode::Normal), 39 | _ => Err(format!("Invalid transaction landing mode: {}. Use 'zeroslot' or 'normal'", s)), 40 | } 41 | } 42 | } 43 | 44 | use std::str::FromStr; 45 | 46 | pub struct Config { 47 | pub yellowstone_grpc_http: String, 48 | pub yellowstone_grpc_token: String, 49 | pub app_state: AppState, 50 | pub swap_config: SwapConfig, 51 | pub counter_limit: u32, 52 | pub transaction_landing_mode: TransactionLandingMode, 53 | pub copy_selling_limit: f64, // Add this field 54 | pub selling_unit_price: u64, // New: Priority fee for selling transactions 55 | pub selling_unit_limit: u32, // New: Compute units for selling transactions 56 | pub zero_slot_tip_value: f64, // New: Tip value for zeroslot selling 57 | // Sniper configuration 58 | pub focus_drop_threshold_pct: f64, // percentage drop from initial to flag "dropped" 59 | pub focus_trigger_sol: f64, // SOL size to trigger buy after drop 60 | } 61 | 62 | impl Config { 63 | pub async fn new() -> &'static Mutex { 64 | GLOBAL_CONFIG 65 | .get_or_init(|| async { 66 | let init_msg = INIT_MSG; 67 | println!("{}", init_msg); 68 | 69 | dotenv().ok(); // Load .env file 70 | 71 | let logger = Logger::new("[INIT] => ".blue().bold().to_string()); 72 | 73 | let yellowstone_grpc_http = import_env_var("YELLOWSTONE_GRPC_HTTP"); 74 | let yellowstone_grpc_token = import_env_var("YELLOWSTONE_GRPC_TOKEN"); 75 | let slippage_input = import_env_var("SLIPPAGE").parse::().unwrap_or(5000); 76 | let counter_limit = import_env_var("COUNTER_LIMIT").parse::().unwrap_or(0_u32); 77 | let transaction_landing_mode = import_env_var("TRANSACTION_LANDING_SERVICE") 78 | .parse::() 79 | .unwrap_or(TransactionLandingMode::default()); 80 | // Read COPY_SELLING_LIMIT from env (default 1.5) 81 | let copy_selling_limit = import_env_var("COPY_SELLING_LIMIT").parse::().unwrap_or(1.5); 82 | 83 | // Read selling configuration for front-running 84 | let selling_unit_price = import_env_var("SELLING_UNIT_PRICE").parse::().unwrap_or(4000000); 85 | let selling_unit_limit = import_env_var("SELLING_UNIT_LIMIT").parse::().unwrap_or(2000000); 86 | let zero_slot_tip_value = import_env_var("ZERO_SLOT_TIP_VALUE").parse::().unwrap_or(0.0025); 87 | // Sniper thresholds 88 | let focus_drop_threshold_pct = import_env_var("FOCUS_DROP_THRESHOLD_PCT").parse::().unwrap_or(0.15); 89 | let focus_trigger_sol = import_env_var("FOCUS_TRIGGER_SOL").parse::().unwrap_or(1.0); 90 | 91 | let max_slippage: u64 = 10000 ; 92 | let slippage = if slippage_input > max_slippage { 93 | max_slippage 94 | } else { 95 | slippage_input 96 | }; 97 | let solana_price = create_coingecko_proxy().await.unwrap_or(200_f64); 98 | let rpc_client = create_rpc_client().unwrap(); 99 | let rpc_nonblocking_client = create_nonblocking_rpc_client().await.unwrap(); 100 | let zeroslot_rpc_client = create_zeroslot_rpc_client().await.unwrap(); 101 | let wallet: std::sync::Arc = import_wallet().unwrap(); 102 | let balance = match rpc_nonblocking_client 103 | .get_account(&wallet.pubkey()) 104 | .await { 105 | Ok(account) => account.lamports, 106 | Err(err) => { 107 | logger.log(format!("Failed to get wallet balance: {}", err).red().to_string()); 108 | 0 // Default to zero if we can't get the balance 109 | } 110 | }; 111 | 112 | let wallet_cloned = wallet.clone(); 113 | let swap_direction = SwapDirection::Buy; //SwapDirection::Sell 114 | let in_type = SwapInType::Qty; //SwapInType::Pct 115 | let amount_in = import_env_var("TOKEN_AMOUNT") 116 | .parse::() 117 | .unwrap_or(0.001_f64); //quantity 118 | // let in_type = "pct"; //percentage 119 | // let amount_in = 0.5; //percentage 120 | 121 | let swap_config = SwapConfig { 122 | swap_direction, 123 | in_type, 124 | amount_in, 125 | slippage, 126 | }; 127 | 128 | let rpc_client = create_rpc_client().unwrap(); 129 | let app_state = AppState { 130 | rpc_client, 131 | rpc_nonblocking_client, 132 | zeroslot_rpc_client, 133 | wallet, 134 | protocol_preference: SwapProtocol::default(), 135 | }; 136 | logger.log( 137 | format!( 138 | "[SNIPER ENVIRONMENT]: \n\t\t\t\t [Yellowstone gRpc]: {}, 139 | \n\t\t\t\t * [Wallet]: {:?}, * [Balance]: {} Sol, 140 | \n\t\t\t\t * [Slippage]: {}, * [Solana]: {}, * [Amount]: {}", 141 | yellowstone_grpc_http, 142 | wallet_cloned.pubkey(), 143 | balance as f64 / 1_000_000_000_f64, 144 | slippage_input, 145 | solana_price, 146 | amount_in, 147 | ) 148 | .purple() 149 | .italic() 150 | .to_string(), 151 | ); 152 | Mutex::new(Config { 153 | yellowstone_grpc_http, 154 | yellowstone_grpc_token, 155 | app_state, 156 | swap_config, 157 | counter_limit, 158 | transaction_landing_mode, 159 | copy_selling_limit, // Set the field 160 | selling_unit_price, 161 | selling_unit_limit, 162 | zero_slot_tip_value, 163 | focus_drop_threshold_pct, 164 | focus_trigger_sol, 165 | }) 166 | }) 167 | .await 168 | } 169 | pub async fn get() -> tokio::sync::MutexGuard<'static, Config> { 170 | GLOBAL_CONFIG 171 | .get() 172 | .expect("Config not initialized") 173 | .lock() 174 | .await 175 | } 176 | } 177 | 178 | //TODO: raydium launchpad 179 | pub const RAYDIUM_LAUNCHPAD_LOG_INSTRUCTION: &str = "MintTo"; 180 | pub const RAYDIUM_LAUNCHPAD_PROGRAM_DATA_PREFIX: &str = "Program data: G3KpTd7rY3Y"; 181 | pub const RAYDIUM_LAUNCHPAD_BUY_LOG_INSTRUCTION: &str = "Buy"; 182 | pub const RAYDIUM_LAUNCHPAD_BUY_OR_SELL_PROGRAM_DATA_PREFIX: &str = "Program data: vdt/007mYe"; 183 | pub const RAYDIUM_LAUNCHPAD_SELL_LOG_INSTRUCTION: &str = "Sell"; 184 | 185 | 186 | use std::cmp::Eq; 187 | use std::hash::{Hash, Hasher}; 188 | 189 | #[derive(Debug, PartialEq, Clone)] 190 | pub struct LiquidityPool { 191 | pub mint: String, 192 | pub buy_price: f64, 193 | pub sell_price: f64, 194 | pub status: Status, 195 | pub timestamp: Option, 196 | } 197 | 198 | impl Eq for LiquidityPool {} 199 | impl Hash for LiquidityPool { 200 | fn hash(&self, state: &mut H) { 201 | self.mint.hash(state); 202 | self.buy_price.to_bits().hash(state); // Convert f64 to bits for hashing 203 | self.sell_price.to_bits().hash(state); 204 | self.status.hash(state); 205 | } 206 | } 207 | 208 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 209 | pub enum Status { 210 | Bought, 211 | Buying, 212 | Checking, 213 | Sold, 214 | Selling, 215 | Failure, 216 | } 217 | 218 | #[derive(Deserialize)] 219 | struct CoinGeckoResponse { 220 | solana: SolanaData, 221 | } 222 | #[derive(Deserialize)] 223 | struct SolanaData { 224 | usd: f64, 225 | } 226 | 227 | #[derive(Clone)] 228 | pub struct AppState { 229 | pub rpc_client: Arc, 230 | pub rpc_nonblocking_client: Arc, 231 | pub zeroslot_rpc_client: Arc, 232 | pub wallet: Arc, 233 | pub protocol_preference: SwapProtocol, 234 | } 235 | 236 | #[derive(Clone, Debug)] 237 | pub struct SwapConfig { 238 | pub swap_direction: SwapDirection, 239 | pub in_type: SwapInType, 240 | pub amount_in: f64, 241 | pub slippage: u64, 242 | } 243 | 244 | pub fn import_env_var(key: &str) -> String { 245 | match env::var(key){ 246 | Ok(res) => res, 247 | Err(e) => { 248 | println!("{}", format!("{}: {}", e, key).red().to_string()); 249 | loop{} 250 | } 251 | } 252 | } 253 | 254 | // Zero slot health check URL 255 | pub fn get_zero_slot_health_url() -> String { 256 | std::env::var("ZERO_SLOT_HEALTH").unwrap_or_else(|_| { 257 | eprintln!("ZERO_SLOT_HEALTH environment variable not set, using default"); 258 | "https://ny1.0slot.trade/health".to_string() 259 | }) 260 | } 261 | 262 | pub fn create_rpc_client() -> Result> { 263 | let rpc_http = import_env_var("RPC_HTTP"); 264 | let timeout = Duration::from_secs(30); // 30 second timeout 265 | let rpc_client = anchor_client::solana_client::rpc_client::RpcClient::new_with_timeout_and_commitment( 266 | rpc_http, 267 | timeout, 268 | CommitmentConfig::processed(), 269 | ); 270 | Ok(Arc::new(rpc_client)) 271 | } 272 | 273 | pub async fn create_nonblocking_rpc_client( 274 | ) -> Result> { 275 | let rpc_http = import_env_var("RPC_HTTP"); 276 | let timeout = Duration::from_secs(30); // 30 second timeout 277 | let rpc_client = anchor_client::solana_client::nonblocking::rpc_client::RpcClient::new_with_timeout_and_commitment( 278 | rpc_http, 279 | timeout, 280 | CommitmentConfig::processed(), 281 | ); 282 | Ok(Arc::new(rpc_client)) 283 | } 284 | 285 | pub async fn create_zeroslot_rpc_client() -> Result> { 286 | let client = crate::services::zeroslot::ZeroSlotClient::new( 287 | crate::services::zeroslot::ZERO_SLOT_URL.as_str() 288 | ); 289 | Ok(Arc::new(client)) 290 | } 291 | 292 | 293 | pub async fn create_coingecko_proxy() -> Result { 294 | 295 | let url = "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd"; 296 | 297 | let response = reqwest::get(url).await?; 298 | 299 | let body = response.json::().await?; 300 | // Get SOL price in USD 301 | let sol_price = body.solana.usd; 302 | Ok(sol_price) 303 | } 304 | 305 | pub fn import_wallet() -> Result> { 306 | let priv_key = import_env_var("PRIVATE_KEY"); 307 | if priv_key.len() < 85 { 308 | println!("{}", format!("Please check wallet priv key: Invalid length => {}", priv_key.len()).red().to_string()); 309 | loop{} 310 | } 311 | let wallet: Keypair = Keypair::from_base58_string(priv_key.as_str()); 312 | 313 | Ok(Arc::new(wallet)) 314 | } -------------------------------------------------------------------------------- /src/engine/risk_management.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # Risk Management Module 3 | 4 | This module provides automated risk management for copy trading operations by monitoring target wallet token balances. 5 | 6 | ## Environment Variables 7 | 8 | The following environment variables control the risk management system: 9 | 10 | - `RISK_MINIMUM_TARGET_BALANCE`: Minimum token balance threshold below which to trigger emergency sells (default: `1000.0`) 11 | - `RISK_CHECK_INTERVAL_MINUTES`: Interval in minutes between balance checks (default: `10`) 12 | 13 | ## How It Works 14 | 15 | 1. **Monitoring**: Every 10 minutes (configurable), the service automatically checks target wallet balances for all currently held tokens 16 | 2. **Risk Detection**: If a target wallet's balance for any held token drops below the configured threshold, it triggers an emergency sell 17 | 3. **Emergency Selling**: Uses the existing enhanced sell mechanism to immediately sell all of the token 18 | 4. **Cleanup**: Removes the sold token from the tracking system 19 | 20 | ## Example Configuration 21 | 22 | ```env 23 | RISK_MINIMUM_TARGET_BALANCE=1000.0 24 | RISK_CHECK_INTERVAL_MINUTES=10 25 | ``` 26 | 27 | With this configuration, the system will automatically check target wallet balances every 10 minutes and sell any held token 28 | where the target wallet's balance falls below 1000 tokens. 29 | */ 30 | 31 | use std::time::{Duration, Instant}; 32 | use solana_program_pack::Pack; 33 | use std::collections::HashMap; 34 | use std::sync::Arc; 35 | use tokio::time; 36 | use colored::Colorize; 37 | use anchor_client::solana_sdk::pubkey::Pubkey; 38 | use std::str::FromStr; 39 | use spl_token::state::Account as TokenAccount; 40 | use spl_token_2022::extension::StateWithExtensionsOwned; 41 | 42 | use crate::common::logger::Logger; 43 | use crate::common::config::{AppState, SwapConfig, import_env_var}; 44 | use crate::engine::sniper_bot::{BOUGHT_TOKEN_LIST, BoughtTokenInfo}; 45 | 46 | /// Risk management configuration 47 | pub struct RiskManagementConfig { 48 | pub target_addresses: Vec, 49 | pub minimum_target_balance: f64, // Minimum token balance threshold (default: 1000) 50 | pub check_interval_minutes: u64, // Check interval in minutes (default: 10) 51 | pub app_state: Arc, 52 | pub swap_config: Arc, 53 | } 54 | 55 | impl RiskManagementConfig { 56 | pub fn new( 57 | target_addresses: Vec, 58 | app_state: Arc, 59 | swap_config: Arc, 60 | ) -> Self { 61 | let minimum_target_balance = import_env_var("RISK_MINIMUM_TARGET_BALANCE") 62 | .parse::() 63 | .unwrap_or(1000.0); 64 | 65 | let check_interval_minutes = import_env_var("RISK_CHECK_INTERVAL_MINUTES") 66 | .parse::() 67 | .unwrap_or(10); 68 | 69 | Self { 70 | target_addresses, 71 | minimum_target_balance, 72 | check_interval_minutes, 73 | app_state, 74 | swap_config, 75 | } 76 | } 77 | } 78 | 79 | /// Risk management service that monitors target wallet balances 80 | pub struct RiskManagementService { 81 | config: RiskManagementConfig, 82 | logger: Logger, 83 | } 84 | 85 | impl RiskManagementService { 86 | pub fn new(config: RiskManagementConfig) -> Self { 87 | Self { 88 | config, 89 | logger: Logger::new("[RISK-MANAGEMENT] => ".red().bold().to_string()), 90 | } 91 | } 92 | 93 | /// Start the risk management monitoring service 94 | pub async fn start(&self) -> Result<(), String> { 95 | self.logger.log(format!( 96 | "🚨 Starting risk management service - checking every {} minutes for target balance < {}", 97 | self.config.check_interval_minutes, 98 | self.config.minimum_target_balance 99 | ).yellow().to_string()); 100 | 101 | let mut interval = time::interval(Duration::from_secs(self.config.check_interval_minutes * 60)); 102 | 103 | loop { 104 | interval.tick().await; 105 | 106 | if let Err(e) = self.check_target_balances().await { 107 | self.logger.log(format!("Error during balance check: {}", e).red().to_string()); 108 | } 109 | } 110 | } 111 | 112 | /// Check target wallet balances and trigger sells if needed 113 | async fn check_target_balances(&self) -> Result<(), String> { 114 | self.logger.log("🔍 Checking target wallet balances...".cyan().to_string()); 115 | 116 | // Get all currently held tokens 117 | let held_tokens: Vec<(String, BoughtTokenInfo)> = BOUGHT_TOKEN_LIST 118 | .iter() 119 | .map(|entry| (entry.key().clone(), entry.value().clone())) 120 | .collect(); 121 | 122 | if held_tokens.is_empty() { 123 | self.logger.log("No tokens currently held, skipping balance check".yellow().to_string()); 124 | return Ok(()); 125 | } 126 | 127 | self.logger.log(format!("Found {} held tokens to check", held_tokens.len()).cyan().to_string()); 128 | 129 | // Check each target wallet for each held token 130 | for target_address in &self.config.target_addresses { 131 | if let Err(e) = self.check_target_wallet_balances(target_address, &held_tokens).await { 132 | self.logger.log(format!("Error checking balances for target {}: {}", target_address, e).red().to_string()); 133 | continue; 134 | } 135 | } 136 | 137 | Ok(()) 138 | } 139 | 140 | /// Check balances for a specific target wallet 141 | async fn check_target_wallet_balances( 142 | &self, 143 | target_address: &str, 144 | held_tokens: &[(String, BoughtTokenInfo)], 145 | ) -> Result<(), String> { 146 | let target_pubkey = Pubkey::from_str(target_address) 147 | .map_err(|e| format!("Invalid target address {}: {}", target_address, e))?; 148 | 149 | // Get target wallet's token balances for our held tokens 150 | let target_balances = self.get_target_token_balances(&target_pubkey, held_tokens).await?; 151 | 152 | // Check each token balance and trigger sells if needed 153 | for (token_mint, token_info) in held_tokens { 154 | if let Some(target_balance) = target_balances.get(token_mint) { 155 | self.logger.log(format!( 156 | "Token {} - Target balance: {:.2}, Threshold: {:.2}", 157 | token_mint, 158 | target_balance, 159 | self.config.minimum_target_balance 160 | ).cyan().to_string()); 161 | 162 | // If target balance is below threshold, trigger emergency sell 163 | if *target_balance < self.config.minimum_target_balance { 164 | self.logger.log(format!( 165 | "🚨 RISK TRIGGER: Target {} balance ({:.2}) below threshold ({:.2}) for token {}", 166 | target_address, 167 | target_balance, 168 | self.config.minimum_target_balance, 169 | token_mint 170 | ).red().bold().to_string()); 171 | 172 | // Trigger emergency sell 173 | if let Err(e) = self.trigger_emergency_sell(token_mint, token_info).await { 174 | self.logger.log(format!( 175 | "❌ Failed to execute emergency sell for {}: {}", 176 | token_mint, e 177 | ).red().to_string()); 178 | } else { 179 | self.logger.log(format!( 180 | "✅ Successfully triggered emergency sell for {}", 181 | token_mint 182 | ).green().to_string()); 183 | } 184 | } 185 | } else { 186 | self.logger.log(format!( 187 | "⚠️ Target {} has no balance for token {} - triggering emergency sell", 188 | target_address, token_mint 189 | ).yellow().to_string()); 190 | 191 | // If no balance found, also trigger emergency sell 192 | if let Err(e) = self.trigger_emergency_sell(token_mint, token_info).await { 193 | self.logger.log(format!( 194 | "❌ Failed to execute emergency sell for {}: {}", 195 | token_mint, e 196 | ).red().to_string()); 197 | } 198 | } 199 | } 200 | 201 | Ok(()) 202 | } 203 | 204 | /// Get token balances for target wallet for specific tokens 205 | async fn get_target_token_balances( 206 | &self, 207 | target_pubkey: &Pubkey, 208 | held_tokens: &[(String, BoughtTokenInfo)], 209 | ) -> Result, String> { 210 | let mut balances = HashMap::new(); 211 | 212 | // Get all token accounts for the target wallet 213 | let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") 214 | .map_err(|e| format!("Invalid token program ID: {}", e))?; 215 | 216 | let accounts = self.config.app_state 217 | .rpc_client 218 | .get_token_accounts_by_owner( 219 | target_pubkey, 220 | anchor_client::solana_client::rpc_request::TokenAccountsFilter::ProgramId(token_program), 221 | ) 222 | .map_err(|e| format!("Failed to get token accounts: {}", e))?; 223 | 224 | // Parse each account and match with our held tokens 225 | for account_info in accounts { 226 | if let Ok(account_pubkey) = Pubkey::from_str(&account_info.pubkey) { 227 | if let Ok(account_data) = self.config.app_state.rpc_client.get_account(&account_pubkey) { 228 | if let Ok(parsed_account) = TokenAccount::unpack(&account_data.data) { 229 | let mint_str = parsed_account.mint.to_string(); 230 | 231 | // Check if this mint is one of our held tokens 232 | if held_tokens.iter().any(|(token_mint, _)| token_mint == &mint_str) { 233 | // Get mint info to calculate actual balance 234 | if let Ok(mint_data) = self.config.app_state.rpc_client.get_account(&parsed_account.mint) { 235 | if let Ok(mint_info) = spl_token::state::Mint::unpack(&mint_data.data) { 236 | let raw_balance = parsed_account.amount; 237 | let decimals = mint_info.decimals; 238 | let actual_balance = raw_balance as f64 / 10_f64.powi(decimals as i32); 239 | 240 | balances.insert(mint_str.clone(), actual_balance); 241 | 242 | self.logger.log(format!( 243 | "Found target balance for {}: {:.2} tokens", 244 | mint_str, actual_balance 245 | ).blue().to_string()); 246 | } 247 | } 248 | } 249 | } 250 | } 251 | } 252 | } 253 | 254 | Ok(balances) 255 | } 256 | 257 | /// Trigger emergency sell for a specific token 258 | async fn trigger_emergency_sell( 259 | &self, 260 | token_mint: &str, 261 | token_info: &BoughtTokenInfo, 262 | ) -> Result<(), String> { 263 | self.logger.log(format!( 264 | "🔥 Executing emergency sell for token {} due to risk management trigger", 265 | token_mint 266 | ).red().bold().to_string()); 267 | 268 | // Use the existing emergency sell mechanism 269 | crate::engine::sniper_bot::execute_enhanced_sell( 270 | token_mint.to_string(), 271 | self.config.app_state.clone(), 272 | self.config.swap_config.clone(), 273 | ).await?; 274 | 275 | // Remove from bought tokens list 276 | BOUGHT_TOKEN_LIST.remove(token_mint); 277 | 278 | // Check if all tokens are sold and stop streaming if needed 279 | crate::engine::sniper_bot::check_and_stop_streaming_if_all_sold(&self.logger).await; 280 | 281 | self.logger.log(format!( 282 | "✅ Emergency sell completed and token {} removed from tracking", 283 | token_mint 284 | ).green().to_string()); 285 | 286 | Ok(()) 287 | } 288 | } 289 | 290 | /// Start the risk management service 291 | pub async fn start_risk_management_service( 292 | target_addresses: Vec, 293 | app_state: Arc, 294 | swap_config: Arc, 295 | ) -> Result<(), String> { 296 | let logger = Logger::new("[RISK-MANAGEMENT] => ".red().bold().to_string()); 297 | logger.log("Starting risk management service...".green().to_string()); 298 | 299 | let config = RiskManagementConfig::new(target_addresses, app_state, swap_config); 300 | let service = RiskManagementService::new(config); 301 | 302 | // Start the service in a background task 303 | tokio::spawn(async move { 304 | if let Err(e) = service.start().await { 305 | eprintln!("Risk management service error: {}", e); 306 | } 307 | }); 308 | 309 | Ok(()) 310 | } -------------------------------------------------------------------------------- /src/services/jupiter_api.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::str::FromStr; 3 | use anyhow::{anyhow, Result}; 4 | use colored::Colorize; 5 | use reqwest::Client; 6 | use serde::{Deserialize, Serialize}; 7 | use anchor_client::solana_sdk::{ 8 | signature::{Keypair, Signer}, // Add Signer trait import 9 | pubkey::Pubkey, 10 | transaction::VersionedTransaction, 11 | }; 12 | use anchor_client::solana_client::nonblocking::rpc_client::RpcClient; 13 | use tokio::time::Duration; 14 | 15 | use crate::common::logger::Logger; 16 | 17 | const JUPITER_API_URL: &str = "https://lite-api.jup.ag/swap/v1"; 18 | const JUPITER_SWAP_API_URL: &str = "https://lite-api.jup.ag/swap/v1"; 19 | const SOL_MINT: &str = "So11111111111111111111111111111111111111112"; 20 | 21 | #[derive(Debug, Serialize)] 22 | struct QuoteRequest { 23 | #[serde(rename = "inputMint")] 24 | input_mint: String, 25 | #[serde(rename = "outputMint")] 26 | output_mint: String, 27 | amount: String, 28 | #[serde(rename = "slippageBps")] 29 | slippage_bps: u64, 30 | } 31 | 32 | #[derive(Debug, Deserialize, Serialize)] // Add Serialize derive 33 | pub struct QuoteResponse { 34 | #[serde(rename = "inputMint")] 35 | pub input_mint: String, 36 | #[serde(rename = "inAmount")] 37 | pub in_amount: String, 38 | #[serde(rename = "outputMint")] 39 | pub output_mint: String, 40 | #[serde(rename = "outAmount")] 41 | pub out_amount: String, 42 | #[serde(rename = "otherAmountThreshold")] 43 | pub other_amount_threshold: String, 44 | #[serde(rename = "swapMode")] 45 | pub swap_mode: String, 46 | #[serde(rename = "slippageBps")] 47 | pub slippage_bps: u64, 48 | #[serde(rename = "platformFee")] 49 | pub platform_fee: Option, 50 | #[serde(rename = "priceImpactPct")] 51 | pub price_impact_pct: String, 52 | #[serde(rename = "routePlan")] 53 | pub route_plan: Vec, 54 | #[serde(rename = "contextSlot")] 55 | pub context_slot: u64, 56 | } 57 | 58 | #[derive(Debug, Clone, Serialize, Deserialize)] 59 | pub struct PlatformFee { 60 | pub amount: String, 61 | #[serde(rename = "feeBps")] 62 | pub fee_bps: u64, 63 | } 64 | 65 | #[derive(Debug, Clone, Serialize, Deserialize)] 66 | pub struct RoutePlanInfo { 67 | #[serde(rename = "swapInfo")] 68 | pub swap_info: SwapInfo, 69 | pub percent: u8, 70 | } 71 | 72 | #[derive(Debug, Clone, Serialize, Deserialize)] 73 | pub struct SwapInfo { 74 | pub label: String, 75 | #[serde(rename = "ammKey")] 76 | pub amm_key: String, 77 | #[serde(rename = "inputMint")] 78 | pub input_mint: String, 79 | #[serde(rename = "outputMint")] 80 | pub output_mint: String, 81 | #[serde(rename = "inAmount")] 82 | pub in_amount: String, 83 | #[serde(rename = "outAmount")] 84 | pub out_amount: String, 85 | #[serde(rename = "feeAmount")] 86 | pub fee_amount: String, 87 | #[serde(rename = "feeMint")] 88 | pub fee_mint: String, 89 | } 90 | 91 | #[derive(Debug, Serialize)] 92 | struct SwapRequest { 93 | #[serde(rename = "quoteResponse")] 94 | quote_response: QuoteResponse, 95 | #[serde(rename = "userPublicKey")] 96 | user_public_key: String, 97 | #[serde(rename = "wrapAndUnwrapSol")] 98 | wrap_and_unwrap_sol: bool, 99 | #[serde(rename = "dynamicComputeUnitLimit")] 100 | dynamic_compute_unit_limit: bool, 101 | #[serde(rename = "prioritizationFeeLamports")] 102 | prioritization_fee_lamports: PrioritizationFee, 103 | } 104 | 105 | #[derive(Debug, Serialize)] 106 | struct PrioritizationFee { 107 | #[serde(rename = "priorityLevelWithMaxLamports")] 108 | priority_level_with_max_lamports: PriorityLevel, 109 | } 110 | 111 | #[derive(Debug, Serialize)] 112 | struct PriorityLevel { 113 | #[serde(rename = "maxLamports")] 114 | max_lamports: u64, 115 | #[serde(rename = "priorityLevel")] 116 | priority_level: String, 117 | } 118 | 119 | #[derive(Debug, Deserialize)] 120 | struct SwapResponse { 121 | #[serde(rename = "swapTransaction")] 122 | pub swap_transaction: String, 123 | } 124 | 125 | #[derive(Clone)] 126 | pub struct JupiterClient { 127 | client: Client, 128 | rpc_client: Arc, 129 | logger: Logger, 130 | } 131 | 132 | impl JupiterClient { 133 | pub fn new(rpc_client: Arc) -> Self { 134 | let client = Client::builder() 135 | .timeout(Duration::from_secs(30)) 136 | .build() 137 | .expect("Failed to create HTTP client"); 138 | 139 | Self { 140 | client, 141 | rpc_client, 142 | logger: Logger::new("[JUPITER] => ".magenta().to_string()), 143 | } 144 | } 145 | 146 | /// Get a quote for swapping tokens 147 | pub async fn get_quote( 148 | &self, 149 | input_mint: &str, 150 | output_mint: &str, 151 | amount: u64, 152 | slippage_bps: u64, 153 | ) -> Result { 154 | self.logger.log(format!("Getting Jupiter quote: {} -> {} (amount: {}, slippage: {}bps)", 155 | input_mint, output_mint, amount, slippage_bps)); 156 | 157 | let quote_request = QuoteRequest { 158 | input_mint: input_mint.to_string(), 159 | output_mint: output_mint.to_string(), 160 | amount: amount.to_string(), 161 | slippage_bps, 162 | }; 163 | 164 | let url = format!("{}/quote", JUPITER_API_URL); 165 | let response = self.client 166 | .get(&url) 167 | .query(&[ 168 | ("inputMint", "e_request.input_mint), 169 | ("outputMint", "e_request.output_mint), 170 | ("amount", "e_request.amount), 171 | ("slippageBps", "e_request.slippage_bps.to_string()), 172 | ]) 173 | .send() 174 | .await?; 175 | 176 | if !response.status().is_success() { 177 | let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string()); 178 | return Err(anyhow!("Jupiter quote API error: {}", error_text)); 179 | } 180 | 181 | // Log the raw response for debugging 182 | let response_text = response.text().await?; 183 | self.logger.log(format!("Raw quote response: {}", &response_text[..std::cmp::min(500, response_text.len())])); 184 | 185 | let quote: QuoteResponse = serde_json::from_str(&response_text) 186 | .map_err(|e| anyhow!("Failed to parse quote response: {}. Response: {}", e, &response_text[..std::cmp::min(200, response_text.len())]))?; 187 | 188 | self.logger.log(format!("Jupiter quote received: {} {} -> {} {} (price impact: {}%)", 189 | quote.in_amount, input_mint, quote.out_amount, output_mint, quote.price_impact_pct)); 190 | 191 | Ok(quote) 192 | } 193 | 194 | /// Get swap transaction from Jupiter 195 | pub async fn get_swap_transaction( 196 | &self, 197 | quote: QuoteResponse, 198 | user_public_key: &Pubkey, 199 | ) -> Result { 200 | self.logger.log(format!("Getting Jupiter swap transaction for user: {}", user_public_key)); 201 | 202 | let swap_request = SwapRequest { 203 | quote_response: quote, 204 | user_public_key: user_public_key.to_string(), 205 | wrap_and_unwrap_sol: true, 206 | dynamic_compute_unit_limit: true, 207 | prioritization_fee_lamports: PrioritizationFee { 208 | priority_level_with_max_lamports: PriorityLevel { 209 | max_lamports: 1_000_000, // 0.001 SOL max priority fee 210 | priority_level: "high".to_string(), 211 | }, 212 | }, 213 | }; 214 | 215 | let url = format!("{}/swap", JUPITER_SWAP_API_URL); 216 | 217 | // Log the request for debugging 218 | self.logger.log(format!("Sending swap request to: {}", url)); 219 | self.logger.log(format!("Request payload: {}", serde_json::to_string_pretty(&swap_request).unwrap_or_else(|_| "Failed to serialize".to_string()))); 220 | 221 | let response = self.client 222 | .post(&url) 223 | .json(&swap_request) 224 | .send() 225 | .await?; 226 | 227 | if !response.status().is_success() { 228 | let status = response.status(); 229 | let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string()); 230 | self.logger.log(format!("Jupiter swap API error: Status {}, Response: {}", status, error_text).red().to_string()); 231 | return Err(anyhow!("Swap API returned status: {} - {}", status, error_text)); 232 | } 233 | 234 | let swap_response: SwapResponse = response.json().await?; 235 | 236 | // Decode the base64 transaction 237 | let transaction_bytes = base64::decode(&swap_response.swap_transaction)?; 238 | let transaction: VersionedTransaction = bincode::deserialize(&transaction_bytes)?; 239 | 240 | self.logger.log("Jupiter swap transaction received and decoded successfully".to_string()); 241 | 242 | Ok(transaction) 243 | } 244 | 245 | /// Execute a token sell using Jupiter (complete flow) 246 | pub async fn sell_token_with_jupiter( 247 | &self, 248 | token_mint: &str, 249 | token_amount: u64, 250 | slippage_bps: u64, 251 | keypair: &Keypair, 252 | ) -> Result { 253 | self.logger.log(format!("Starting Jupiter sell for token {} (amount: {}, slippage: {}bps)", 254 | token_mint, token_amount, slippage_bps)); 255 | 256 | // Get quote 257 | self.logger.log("Getting Jupiter quote...".to_string()); 258 | let quote = self.get_quote( 259 | token_mint, 260 | SOL_MINT, 261 | token_amount, 262 | slippage_bps, 263 | ).await?; 264 | 265 | self.logger.log(format!("Quote received, getting swap transaction...")); 266 | 267 | // Get swap transaction 268 | let mut transaction = self.get_swap_transaction(quote, &keypair.pubkey()).await?; 269 | 270 | // Get recent blockhash 271 | let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; 272 | transaction.message.set_recent_blockhash(recent_blockhash); 273 | 274 | // For VersionedTransaction, we need to manually create the signature 275 | use anchor_client::solana_sdk::signer::Signer; 276 | let message_data = transaction.message.serialize(); 277 | let signature = keypair.sign_message(&message_data); 278 | 279 | // Find the position of the keypair in the account keys to place the signature 280 | let account_keys = transaction.message.static_account_keys(); 281 | if let Some(signer_index) = account_keys.iter().position(|key| *key == keypair.pubkey()) { 282 | // Ensure we have enough signatures 283 | if transaction.signatures.len() <= signer_index { 284 | transaction.signatures.resize(signer_index + 1, anchor_client::solana_sdk::signature::Signature::default()); 285 | } 286 | transaction.signatures[signer_index] = signature; 287 | } else { 288 | return Err(anyhow!("Keypair not found in transaction account keys")); 289 | } 290 | 291 | // Send the transaction 292 | let signature = self.rpc_client.send_transaction(&transaction).await?; 293 | 294 | self.logger.log(format!("Jupiter sell transaction sent: {}", signature).green().to_string()); 295 | 296 | Ok(signature.to_string()) 297 | } 298 | 299 | /// Verify if a transaction was successful 300 | pub async fn verify_transaction(&self, signature: &str) -> Result { 301 | let signature = anchor_client::solana_sdk::signature::Signature::from_str(signature)?; 302 | 303 | // Wait a bit for transaction to settle 304 | tokio::time::sleep(Duration::from_secs(2)).await; 305 | 306 | match self.rpc_client.get_signature_status(&signature).await? { 307 | Some(result) => { 308 | match result { 309 | Ok(_) => { 310 | self.logger.log(format!("Transaction {} confirmed successfully", signature).green().to_string()); 311 | Ok(true) 312 | }, 313 | Err(e) => { 314 | self.logger.log(format!("Transaction {} failed: {:?}", signature, e).red().to_string()); 315 | Ok(false) 316 | } 317 | } 318 | }, 319 | None => { 320 | self.logger.log(format!("Transaction {} not found or still pending", signature).yellow().to_string()); 321 | Ok(false) 322 | } 323 | } 324 | } 325 | 326 | /// High-level function to sell a token using Jupiter API 327 | pub async fn sell_token( 328 | &self, 329 | input_mint: &str, 330 | amount: u64, 331 | slippage_bps: u64, 332 | user_public_key: &Pubkey, 333 | ) -> Result<(String, f64)> { // Returns (signature, expected_sol_amount) 334 | let sol_mint = "So11111111111111111111111111111111111111112"; 335 | 336 | // Skip if it's already SOL 337 | if input_mint == sol_mint { 338 | return Ok(("skip".to_string(), 0.0)); 339 | } 340 | 341 | // Get quote 342 | let quote = self.get_quote(input_mint, sol_mint, amount, slippage_bps).await?; 343 | 344 | // Calculate expected SOL output 345 | let expected_sol_raw = quote.out_amount.parse::() 346 | .map_err(|e| anyhow!("Failed to parse output amount: {}", e))?; 347 | let expected_sol = expected_sol_raw as f64 / 1e9; 348 | 349 | // Skip if expected output is too small 350 | if expected_sol < 0.0001 { 351 | return Err(anyhow!("Expected SOL output too small: {} SOL", expected_sol)); 352 | } 353 | 354 | self.logger.log(format!("Expected SOL output for {}: {:.6}", input_mint, expected_sol)); 355 | 356 | // Get swap transaction 357 | let versioned_transaction = self.get_swap_transaction(quote, user_public_key).await?; 358 | 359 | // Send transaction using the RPC client 360 | let signature = self.rpc_client.send_transaction(&versioned_transaction).await 361 | .map_err(|e| anyhow!("Failed to send transaction: {}", e))?; 362 | 363 | self.logger.log(format!("Token sell transaction sent: {}", signature)); 364 | 365 | Ok((signature.to_string(), expected_sol)) 366 | } 367 | } -------------------------------------------------------------------------------- /src/dex/raydium_launchpad.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Arc, time::Instant}; 2 | use solana_program_pack::Pack; 3 | use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}; 4 | use solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}; 5 | use solana_account_decoder::UiAccountEncoding; 6 | use anyhow::{anyhow, Result}; 7 | use colored::Colorize; 8 | use solana_sdk::{ 9 | instruction::{AccountMeta, Instruction}, 10 | pubkey::Pubkey, 11 | signature::Keypair, 12 | system_program, 13 | signer::Signer, 14 | }; 15 | use crate::engine::transaction_parser::DexType; 16 | use spl_associated_token_account::{ 17 | get_associated_token_address, 18 | instruction::create_associated_token_account_idempotent 19 | }; 20 | use spl_token::ui_amount_to_amount; 21 | 22 | 23 | use crate::{ 24 | common::{config::SwapConfig, logger::Logger, cache::WALLET_TOKEN_ACCOUNTS}, 25 | core::token, 26 | engine::swap::{SwapDirection, SwapInType}, 27 | }; 28 | 29 | pub const TOKEN_PROGRAM: Pubkey = solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); 30 | pub const TOKEN_2022_PROGRAM: Pubkey = solana_sdk::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); 31 | pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = solana_sdk::pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); 32 | pub const RAYDIUM_LAUNCHPAD_PROGRAM: Pubkey = solana_sdk::pubkey!("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"); 33 | pub const RAYDIUM_LAUNCHPAD_AUTHORITY: Pubkey = solana_sdk::pubkey!("WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh"); 34 | pub const RAYDIUM_GLOBAL_CONFIG: Pubkey = solana_sdk::pubkey!("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX"); 35 | pub const RAYDIUM_PLATFORM_CONFIG: Pubkey = solana_sdk::pubkey!("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1"); 36 | pub const EVENT_AUTHORITY: Pubkey = solana_sdk::pubkey!("2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr"); 37 | pub const SOL_MINT: Pubkey = solana_sdk::pubkey!("So11111111111111111111111111111111111111112"); 38 | pub const BUY_DISCRIMINATOR: [u8; 8] = [250, 234, 13, 123, 213, 156, 19, 236]; // buy_exact_in discriminator 39 | pub const SELL_DISCRIMINATOR: [u8; 8] = [149, 39, 222, 155, 211, 124, 152, 26]; // sell_exact_in discriminator 40 | 41 | const TEN_THOUSAND: u64 = 10000; 42 | const POOL_VAULT_SEED: &[u8] = b"pool_vault"; 43 | 44 | 45 | 46 | /// A struct to represent the Raydium pool which uses constant product AMM 47 | #[derive(Debug, Clone)] 48 | pub struct RaydiumPool { 49 | pub pool_id: Pubkey, 50 | pub base_mint: Pubkey, 51 | pub quote_mint: Pubkey, 52 | pub pool_base_account: Pubkey, 53 | pub pool_quote_account: Pubkey, 54 | } 55 | 56 | pub struct Raydium { 57 | pub keypair: Arc, 58 | pub rpc_client: Option>, 59 | pub rpc_nonblocking_client: Option>, 60 | } 61 | 62 | impl Raydium { 63 | pub fn new( 64 | keypair: Arc, 65 | rpc_client: Option>, 66 | rpc_nonblocking_client: Option>, 67 | ) -> Self { 68 | Self { 69 | keypair, 70 | rpc_client, 71 | rpc_nonblocking_client, 72 | } 73 | } 74 | 75 | pub async fn get_raydium_pool( 76 | &self, 77 | mint_str: &str, 78 | ) -> Result { 79 | let mint = Pubkey::from_str(mint_str).map_err(|_| anyhow!("Invalid mint address"))?; 80 | let rpc_client = self.rpc_client.clone() 81 | .ok_or_else(|| anyhow!("RPC client not initialized"))?; 82 | get_pool_info(rpc_client, mint).await 83 | } 84 | 85 | pub async fn get_token_price(&self, mint_str: &str) -> Result { 86 | // For Raydium Launchpad, this method is mainly used for standalone price queries 87 | // Since we're now using trade_info.price directly in the main flow, 88 | // this fallback method returns a placeholder value 89 | let _pool = self.get_raydium_pool(mint_str).await?; 90 | 91 | // Return a placeholder price since the real price should come from trade_info 92 | // This method is rarely used in the main trading flow 93 | // Note: The correct Raydium Launchpad price formula is: 94 | // Price = (virtual_quote_reserve - real_quote_after) / (virtual_base_reserve - real_base_after) 95 | Ok(0.0001) // Placeholder price in SOL 96 | } 97 | 98 | async fn get_or_fetch_pool_info( 99 | &self, 100 | trade_info: &crate::engine::transaction_parser::TradeInfoFromToken, 101 | mint: Pubkey 102 | ) -> Result { 103 | // Use pool_id from trade_info instead of fetching it dynamically 104 | let pool_id = Pubkey::from_str(&trade_info.pool_id) 105 | .map_err(|e| anyhow!("Invalid pool_id in trade_info: {}", e))?; 106 | 107 | // For Raydium Launchpad, derive pool vault addresses using PDA (Program Derived Address) 108 | let pump_program = RAYDIUM_LAUNCHPAD_PROGRAM; 109 | let sol_mint = SOL_MINT; 110 | 111 | // Derive pool vault addresses using PDA with specific seeds 112 | let base_seeds = [POOL_VAULT_SEED, pool_id.as_ref(), mint.as_ref()]; 113 | let (pool_base_account, _) = Pubkey::find_program_address(&base_seeds, &pump_program); 114 | 115 | let quote_seeds = [POOL_VAULT_SEED, pool_id.as_ref(), sol_mint.as_ref()]; 116 | let (pool_quote_account, _) = Pubkey::find_program_address("e_seeds, &pump_program); 117 | 118 | let pool_info = RaydiumPool { 119 | pool_id, 120 | base_mint: mint, 121 | quote_mint: sol_mint, 122 | pool_base_account, 123 | pool_quote_account, 124 | }; 125 | 126 | 127 | 128 | Ok(pool_info) 129 | } 130 | 131 | // Helper method to determine the correct token program for a mint 132 | async fn get_token_program(&self, mint: &Pubkey) -> Result { 133 | if let Some(rpc_client) = &self.rpc_client { 134 | match rpc_client.get_account(mint) { 135 | Ok(account) => { 136 | if account.owner == TOKEN_2022_PROGRAM { 137 | Ok(TOKEN_2022_PROGRAM) 138 | } else { 139 | Ok(TOKEN_PROGRAM) 140 | } 141 | }, 142 | Err(_) => { 143 | // Default to TOKEN_PROGRAM if we can't fetch the account 144 | Ok(TOKEN_PROGRAM) 145 | } 146 | } 147 | } else { 148 | // Default to TOKEN_PROGRAM if no RPC client 149 | Ok(TOKEN_PROGRAM) 150 | } 151 | } 152 | 153 | // Highly optimized build_swap_from_parsed_data 154 | pub async fn build_swap_from_parsed_data( 155 | &self, 156 | trade_info: &crate::engine::transaction_parser::TradeInfoFromToken, 157 | swap_config: SwapConfig, 158 | ) -> Result<(Arc, Vec, f64)> { 159 | let owner = self.keypair.pubkey(); 160 | let mint = Pubkey::from_str(&trade_info.mint)?; 161 | 162 | // Get token program for the mint 163 | let token_program = self.get_token_program(&mint).await?; 164 | 165 | // Prepare swap parameters 166 | let (_token_in, _token_out, discriminator) = match swap_config.swap_direction { 167 | SwapDirection::Buy => (SOL_MINT, mint, BUY_DISCRIMINATOR), 168 | SwapDirection::Sell => (mint, SOL_MINT, SELL_DISCRIMINATOR), 169 | }; 170 | 171 | let mut instructions = Vec::with_capacity(3); // Pre-allocate for typical case 172 | 173 | // Check and create token accounts if needed 174 | let token_ata = get_associated_token_address(&owner, &mint); 175 | let wsol_ata = get_associated_token_address(&owner, &SOL_MINT); 176 | 177 | // Check if token account exists and create if needed 178 | if !WALLET_TOKEN_ACCOUNTS.contains(&token_ata) { 179 | // Double-check with RPC to see if the account actually exists 180 | let account_exists = if let Some(rpc_client) = &self.rpc_client { 181 | match rpc_client.get_account(&token_ata) { 182 | Ok(_) => { 183 | // Account exists, add to cache 184 | WALLET_TOKEN_ACCOUNTS.insert(token_ata); 185 | true 186 | }, 187 | Err(_) => false 188 | } 189 | } else { 190 | false // No RPC client, assume account doesn't exist 191 | }; 192 | 193 | if !account_exists { 194 | let logger = Logger::new("[RAYDIUM-ATA-CREATE] => ".yellow().to_string()); 195 | logger.log(format!("Creating token ATA for mint {} at address {}", mint, token_ata)); 196 | 197 | instructions.push(create_associated_token_account_idempotent( 198 | &owner, 199 | &owner, 200 | &mint, 201 | &TOKEN_PROGRAM, // Always use legacy token program for ATA creation 202 | )); 203 | 204 | // Cache the account since we're creating it 205 | WALLET_TOKEN_ACCOUNTS.insert(token_ata); 206 | } 207 | } 208 | 209 | // Check if WSOL account exists and create if needed 210 | if !WALLET_TOKEN_ACCOUNTS.contains(&wsol_ata) { 211 | // Double-check with RPC to see if the account actually exists 212 | let account_exists = if let Some(rpc_client) = &self.rpc_client { 213 | match rpc_client.get_account(&wsol_ata) { 214 | Ok(_) => { 215 | // Account exists, add to cache 216 | WALLET_TOKEN_ACCOUNTS.insert(wsol_ata); 217 | true 218 | }, 219 | Err(_) => false 220 | } 221 | } else { 222 | false // No RPC client, assume account doesn't exist 223 | }; 224 | 225 | if !account_exists { 226 | let logger = Logger::new("[RAYDIUM-WSOL-CREATE] => ".yellow().to_string()); 227 | logger.log(format!("Creating WSOL ATA at address {}", wsol_ata)); 228 | 229 | instructions.push(create_associated_token_account_idempotent( 230 | &owner, 231 | &owner, 232 | &SOL_MINT, 233 | &TOKEN_PROGRAM, // WSOL always uses legacy token program 234 | )); 235 | 236 | // Cache the account since we're creating it 237 | WALLET_TOKEN_ACCOUNTS.insert(wsol_ata); 238 | } 239 | } 240 | 241 | // Convert amount_in to lamports/token units 242 | // For Raydium Launchpad: 243 | // - Buy: amount_in is SOL amount (convert to lamports) 244 | // - Sell: amount_in is token amount (fetch actual balance and apply qty/pct logic) 245 | let amount_in = match swap_config.swap_direction { 246 | SwapDirection::Buy => { 247 | // For buy: amount_in is SOL amount, convert to lamports 248 | ui_amount_to_amount(swap_config.amount_in, 9) 249 | }, 250 | SwapDirection::Sell => { 251 | // For sell: need to get actual token balance first 252 | let token_ata = get_associated_token_address(&owner, &mint); 253 | 254 | // Get actual token balance from blockchain 255 | let actual_token_balance = if let Some(client) = &self.rpc_nonblocking_client { 256 | match client.get_token_account(&token_ata).await { 257 | Ok(Some(account)) => { 258 | // Parse the amount string to get actual balance 259 | match account.token_amount.amount.parse::() { 260 | Ok(balance) => balance, 261 | Err(_) => { 262 | return Err(anyhow!("Failed to parse token balance for mint {}", mint)); 263 | } 264 | } 265 | }, 266 | Ok(None) => { 267 | return Err(anyhow!("Token account does not exist for mint {}", mint)); 268 | }, 269 | Err(e) => { 270 | return Err(anyhow!("Failed to get token account balance: {}", e)); 271 | } 272 | } 273 | } else if let Some(client) = &self.rpc_client { 274 | // Fallback to blocking client 275 | match client.get_token_account(&token_ata) { 276 | Ok(Some(account)) => { 277 | match account.token_amount.amount.parse::() { 278 | Ok(balance) => balance, 279 | Err(_) => { 280 | return Err(anyhow!("Failed to parse token balance for mint {}", mint)); 281 | } 282 | } 283 | }, 284 | Ok(None) => { 285 | return Err(anyhow!("Token account does not exist for mint {}", mint)); 286 | }, 287 | Err(e) => { 288 | return Err(anyhow!("Failed to get token account balance: {}", e)); 289 | } 290 | } 291 | } else { 292 | return Err(anyhow!("No RPC client available to fetch token balance")); 293 | }; 294 | 295 | // Apply swap logic based on in_type 296 | match swap_config.in_type { 297 | SwapInType::Qty => { 298 | // Use specified quantity (convert from UI amount to token units) 299 | let decimals = 6; // All tokens are 6 decimals 300 | ui_amount_to_amount(swap_config.amount_in, decimals) 301 | }, 302 | SwapInType::Pct => { 303 | // Use percentage of actual balance 304 | let percentage = swap_config.amount_in.min(1.0); // Cap at 100% 305 | ((percentage * actual_token_balance as f64) as u64).max(1) // Ensure at least 1 token unit 306 | } 307 | } 308 | } 309 | }; 310 | 311 | // Calculate the actual quote amount using virtual reserves from trade_info 312 | let minimum_amount_out: u64 = 1; // to ignore slippage 313 | 314 | // Create accounts based on swap direction 315 | let accounts = match swap_config.swap_direction { 316 | SwapDirection::Buy => { 317 | // For buy, we need pool info for accounts 318 | let pool_info = self.get_or_fetch_pool_info(trade_info, mint).await?; 319 | create_buy_accounts( 320 | pool_info.pool_id, 321 | owner, 322 | mint, 323 | SOL_MINT, 324 | token_ata, 325 | wsol_ata, 326 | pool_info.pool_base_account, 327 | pool_info.pool_quote_account, 328 | &token_program, 329 | )? 330 | }, 331 | SwapDirection::Sell => { 332 | // For sell, we need pool info for accounts 333 | let pool_info = self.get_or_fetch_pool_info(trade_info, mint).await?; 334 | create_sell_accounts( 335 | pool_info.pool_id, 336 | owner, 337 | mint, 338 | SOL_MINT, 339 | token_ata, 340 | wsol_ata, 341 | pool_info.pool_base_account, 342 | pool_info.pool_quote_account, 343 | &token_program, 344 | )? 345 | } 346 | }; 347 | 348 | instructions.push(create_swap_instruction( 349 | RAYDIUM_LAUNCHPAD_PROGRAM, 350 | discriminator, 351 | amount_in, 352 | minimum_amount_out, 353 | accounts, 354 | )); 355 | 356 | // Return the actual price from trade_info (convert from lamports to SOL) 357 | let price_in_sol = trade_info.price as f64 / 1_000_000_000.0; 358 | 359 | Ok((self.keypair.clone(), instructions, price_in_sol)) 360 | } 361 | 362 | 363 | } 364 | 365 | /// Get the Raydium pool information for a specific token mint 366 | pub async fn get_pool_info( 367 | rpc_client: Arc, 368 | mint: Pubkey, 369 | ) -> Result { 370 | let logger = Logger::new("[RAYDIUM-GET-POOL-INFO] => ".blue().to_string()); 371 | 372 | // Initialize 373 | let sol_mint = SOL_MINT; 374 | let pump_program = RAYDIUM_LAUNCHPAD_PROGRAM; 375 | 376 | // Use getProgramAccounts with config for better efficiency 377 | let mut pool_id = Pubkey::default(); 378 | let mut retry_count = 0; 379 | let max_retries = 2; 380 | 381 | // Try to find the pool 382 | while retry_count < max_retries && pool_id == Pubkey::default() { 383 | match rpc_client.get_program_accounts_with_config( 384 | &pump_program, 385 | RpcProgramAccountsConfig { 386 | filters: Some(vec![ 387 | RpcFilterType::DataSize(300), 388 | RpcFilterType::Memcmp(Memcmp::new(43, MemcmpEncodedBytes::Base64(base64::encode(mint.to_bytes())))), 389 | ]), 390 | account_config: RpcAccountInfoConfig { 391 | encoding: Some(UiAccountEncoding::Base64), 392 | ..Default::default() 393 | }, 394 | ..Default::default() 395 | }, 396 | ) { 397 | Ok(accounts) => { 398 | for (pubkey, account) in accounts.iter() { 399 | if account.data.len() >= 75 { 400 | if let Ok(pubkey_from_data) = Pubkey::try_from(&account.data[43..75]) { 401 | if pubkey_from_data == mint { 402 | pool_id = *pubkey; 403 | break; 404 | } 405 | } 406 | } 407 | } 408 | 409 | if pool_id != Pubkey::default() { 410 | break; 411 | } else if retry_count + 1 < max_retries { 412 | logger.log("No pools found for the given mint, retrying...".to_string()); 413 | } 414 | } 415 | Err(err) => { 416 | logger.log(format!("Error getting program accounts (attempt {}/{}): {}", 417 | retry_count + 1, max_retries, err)); 418 | } 419 | } 420 | 421 | retry_count += 1; 422 | if retry_count < max_retries { 423 | std::thread::sleep(std::time::Duration::from_millis(500)); 424 | } 425 | } 426 | 427 | if pool_id == Pubkey::default() { 428 | return Err(anyhow!("Failed to find Raydium pool for mint {}", mint)); 429 | } 430 | 431 | // Derive pool vault addresses using PDA 432 | let base_seeds = [POOL_VAULT_SEED, pool_id.as_ref(), mint.as_ref()]; 433 | let (pool_base_account, _) = Pubkey::find_program_address(&base_seeds, &pump_program); 434 | 435 | let quote_seeds = [POOL_VAULT_SEED, pool_id.as_ref(), sol_mint.as_ref()]; 436 | let (pool_quote_account, _) = Pubkey::find_program_address("e_seeds, &pump_program); 437 | 438 | Ok(RaydiumPool { 439 | pool_id, 440 | base_mint: mint, 441 | quote_mint: sol_mint, 442 | pool_base_account, 443 | pool_quote_account, 444 | }) 445 | } 446 | 447 | // Optimized account creation with const pubkeys 448 | fn create_buy_accounts( 449 | pool_id: Pubkey, 450 | user: Pubkey, 451 | base_mint: Pubkey, 452 | quote_mint: Pubkey, 453 | user_base_token_account: Pubkey, 454 | wsol_account: Pubkey, 455 | pool_base_token_account: Pubkey, 456 | pool_quote_token_account: Pubkey, 457 | token_program: &Pubkey, 458 | ) -> Result> { 459 | 460 | Ok(vec![ 461 | AccountMeta::new(user, true), 462 | AccountMeta::new_readonly(RAYDIUM_LAUNCHPAD_AUTHORITY, false), 463 | AccountMeta::new_readonly(RAYDIUM_GLOBAL_CONFIG, false), 464 | AccountMeta::new_readonly(RAYDIUM_PLATFORM_CONFIG, false), 465 | AccountMeta::new(pool_id, false), 466 | AccountMeta::new(user_base_token_account, false), 467 | AccountMeta::new(wsol_account, false), 468 | AccountMeta::new(pool_base_token_account, false), 469 | AccountMeta::new(pool_quote_token_account, false), 470 | AccountMeta::new_readonly(base_mint, false), 471 | AccountMeta::new_readonly(quote_mint, false), 472 | AccountMeta::new_readonly(*token_program, false), // Use detected token program for base mint 473 | AccountMeta::new_readonly(TOKEN_PROGRAM, false), // Use legacy token program for WSOL 474 | AccountMeta::new_readonly(EVENT_AUTHORITY, false), 475 | AccountMeta::new_readonly(RAYDIUM_LAUNCHPAD_PROGRAM, false), 476 | ]) 477 | } 478 | 479 | // Similar optimization for sell accounts 480 | fn create_sell_accounts( 481 | pool_id: Pubkey, 482 | user: Pubkey, 483 | base_mint: Pubkey, 484 | quote_mint: Pubkey, 485 | user_base_token_account: Pubkey, 486 | wsol_account: Pubkey, 487 | pool_base_token_account: Pubkey, 488 | pool_quote_token_account: Pubkey, 489 | token_program: &Pubkey, 490 | ) -> Result> { 491 | 492 | 493 | Ok(vec![ 494 | AccountMeta::new(user, true), 495 | AccountMeta::new_readonly(RAYDIUM_LAUNCHPAD_AUTHORITY, false), 496 | AccountMeta::new_readonly(RAYDIUM_GLOBAL_CONFIG, false), 497 | AccountMeta::new_readonly(RAYDIUM_PLATFORM_CONFIG, false), 498 | AccountMeta::new(pool_id, false), 499 | AccountMeta::new(user_base_token_account, false), 500 | AccountMeta::new(wsol_account, false), 501 | AccountMeta::new(pool_base_token_account, false), 502 | AccountMeta::new(pool_quote_token_account, false), 503 | AccountMeta::new_readonly(base_mint, false), 504 | AccountMeta::new_readonly(quote_mint, false), 505 | AccountMeta::new_readonly(*token_program, false), // Use detected token program for base mint 506 | AccountMeta::new_readonly(TOKEN_PROGRAM, false), // Use legacy token program for WSOL 507 | AccountMeta::new_readonly(EVENT_AUTHORITY, false), 508 | AccountMeta::new_readonly(RAYDIUM_LAUNCHPAD_PROGRAM, false), 509 | ]) 510 | } 511 | 512 | #[inline] 513 | fn calculate_raydium_sell_amount_out( 514 | base_amount_in: u64, 515 | virtual_base_reserve: u64, 516 | virtual_quote_reserve: u64, 517 | real_base_reserve: u64, 518 | real_quote_reserve: u64 519 | ) -> u64 { 520 | if base_amount_in == 0 || virtual_base_reserve == 0 || virtual_quote_reserve == 0 { 521 | return 0; 522 | } 523 | 524 | // Raydium Launchpad constant product formula for selling: 525 | // input_reserve = virtual_base - real_base 526 | // output_reserve = virtual_quote + real_quote 527 | // amount_out = (amount_in * output_reserve) / (input_reserve + amount_in) 528 | 529 | let input_reserve = virtual_base_reserve.saturating_sub(real_base_reserve); 530 | let output_reserve = virtual_quote_reserve.saturating_add(real_quote_reserve); 531 | 532 | if input_reserve == 0 || input_reserve > virtual_base_reserve { 533 | return 0; 534 | } 535 | 536 | let numerator = (base_amount_in as u128).saturating_mul(output_reserve as u128); 537 | let denominator = (input_reserve as u128).saturating_add(base_amount_in as u128); 538 | 539 | if denominator == 0 { 540 | return 0; 541 | } 542 | 543 | numerator.checked_div(denominator).unwrap_or(0) as u64 544 | } 545 | 546 | // Optimized instruction creation 547 | fn create_swap_instruction( 548 | program_id: Pubkey, 549 | discriminator: [u8; 8], 550 | amount_in: u64, 551 | minimum_amount_out: u64, 552 | accounts: Vec, 553 | ) -> Instruction { 554 | let mut data = Vec::with_capacity(24); 555 | let share_fee_rate = 0_u64; 556 | data.extend_from_slice(&discriminator); 557 | data.extend_from_slice(&amount_in.to_le_bytes()); 558 | data.extend_from_slice(&minimum_amount_out.to_le_bytes()); 559 | data.extend_from_slice(&share_fee_rate.to_le_bytes()); 560 | 561 | Instruction { program_id, accounts, data } 562 | } 563 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Let's Bonk Dot Fun Sniper Bot 3 | * 4 | * A high-performance Rust-based sniper trading bot for Let's Bonk Dot Fun platform 5 | * using Raydium Launchpad integration. 6 | * 7 | * Features: 8 | * - Real-time transaction monitoring using Yellowstone gRPC 9 | * - Automated copy trading from target wallets 10 | * - Raydium Launchpad integration for Let's Bonk Dot Fun tokens 11 | * - Built-in selling strategies and risk management 12 | * - Performance optimized for high-frequency trading 13 | */ 14 | 15 | use anchor_client::solana_sdk::signature::Signer; 16 | use solana_vntr_sniper::{ 17 | common::{config::Config, constants::RUN_MSG, cache::WALLET_TOKEN_ACCOUNTS}, 18 | engine::{ 19 | sniper_bot::{start_target_wallet_monitoring, start_dex_monitoring, SniperConfig}, 20 | swap::SwapProtocol, 21 | }, 22 | services::{ 23 | cache_maintenance, 24 | blockhash_processor::BlockhashProcessor, 25 | jupiter_api::JupiterClient 26 | }, 27 | core::token, 28 | }; 29 | use std::sync::Arc; 30 | use solana_program_pack::Pack; 31 | use anchor_client::solana_sdk::pubkey::Pubkey; 32 | use anchor_client::solana_sdk::transaction::Transaction; 33 | use anchor_client::solana_sdk::system_instruction; 34 | use std::str::FromStr; 35 | use colored::Colorize; 36 | use spl_token::instruction::sync_native; 37 | use spl_token::ui_amount_to_amount; 38 | use spl_associated_token_account::get_associated_token_address; 39 | 40 | /// Initialize the wallet token account list by fetching all token accounts owned by the wallet 41 | async fn initialize_token_account_list(config: &Config) { 42 | let logger = solana_vntr_sniper::common::logger::Logger::new("[INIT-TOKEN-ACCOUNTS] => ".green().to_string()); 43 | 44 | if let Ok(wallet_pubkey) = config.app_state.wallet.try_pubkey() { 45 | logger.log(format!("Initializing token account list for wallet: {}", wallet_pubkey)); 46 | 47 | // Get the token program pubkey 48 | let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); 49 | 50 | // Query all token accounts owned by the wallet 51 | let accounts = config.app_state.rpc_client.get_token_accounts_by_owner( 52 | &wallet_pubkey, 53 | anchor_client::solana_client::rpc_request::TokenAccountsFilter::ProgramId(token_program) 54 | ); 55 | match accounts { 56 | Ok(accounts) => { 57 | logger.log(format!("Found {} existing token accounts", accounts.len())); 58 | 59 | // Add each token account to our global cache 60 | for account in accounts { 61 | let account_pubkey = Pubkey::from_str(&account.pubkey).unwrap(); 62 | WALLET_TOKEN_ACCOUNTS.insert(account_pubkey); 63 | logger.log(format!("✅ Cached token account: {}", account.pubkey )); 64 | } 65 | 66 | logger.log(format!("✅ Token account cache initialized with {} accounts", WALLET_TOKEN_ACCOUNTS.size())); 67 | }, 68 | Err(e) => { 69 | logger.log(format!("❌ Error fetching token accounts: {}", e).red().to_string()); 70 | logger.log("⚠️ Cache will be populated as new accounts are discovered".yellow().to_string()); 71 | } 72 | } 73 | } else { 74 | logger.log("❌ Failed to get wallet pubkey, can't initialize token account list".red().to_string()); 75 | } 76 | } 77 | 78 | /// Wrap SOL to Wrapped SOL (WSOL) 79 | async fn wrap_sol(config: &Config, amount: f64) -> Result<(), String> { 80 | let logger = solana_vntr_sniper::common::logger::Logger::new("[WRAP-SOL] => ".green().to_string()); 81 | 82 | // Get wallet pubkey 83 | let wallet_pubkey = match config.app_state.wallet.try_pubkey() { 84 | Ok(pk) => pk, 85 | Err(_) => return Err("Failed to get wallet pubkey".to_string()), 86 | }; 87 | 88 | // Create WSOL account instructions 89 | let (wsol_account, mut instructions) = match token::create_wsol_account(wallet_pubkey) { 90 | Ok(result) => result, 91 | Err(e) => return Err(format!("Failed to create WSOL account: {}", e)), 92 | }; 93 | 94 | logger.log(format!("WSOL account address: {}", wsol_account)); 95 | 96 | // Convert UI amount to lamports (1 SOL = 10^9 lamports) 97 | let lamports = ui_amount_to_amount(amount, 9); 98 | logger.log(format!("Wrapping {} SOL ({} lamports)", amount, lamports)); 99 | 100 | // Transfer SOL to the WSOL account 101 | instructions.push( 102 | system_instruction::transfer( 103 | &wallet_pubkey, 104 | &wsol_account, 105 | lamports, 106 | ) 107 | ); 108 | 109 | // Sync native instruction to update the token balance 110 | instructions.push( 111 | sync_native( 112 | &spl_token::id(), 113 | &wsol_account, 114 | ).map_err(|e| format!("Failed to create sync native instruction: {}", e))? 115 | ); 116 | 117 | // Send transaction 118 | let recent_blockhash = config.app_state.rpc_client.get_latest_blockhash() 119 | .map_err(|e| format!("Failed to get recent blockhash: {}", e))?; 120 | 121 | let transaction = Transaction::new_signed_with_payer( 122 | &instructions, 123 | Some(&wallet_pubkey), 124 | &[&config.app_state.wallet], 125 | recent_blockhash, 126 | ); 127 | 128 | match config.app_state.rpc_client.send_and_confirm_transaction(&transaction) { 129 | Ok(signature) => { 130 | logger.log(format!("SOL wrapped successfully, signature: {}", signature)); 131 | Ok(()) 132 | }, 133 | Err(e) => { 134 | Err(format!("Failed to wrap SOL: {}", e)) 135 | } 136 | } 137 | } 138 | 139 | /// Unwrap SOL from Wrapped SOL (WSOL) account 140 | async fn unwrap_sol(config: &Config) -> Result<(), String> { 141 | let logger = solana_vntr_sniper::common::logger::Logger::new("[UNWRAP-SOL] => ".green().to_string()); 142 | 143 | // Get wallet pubkey 144 | let wallet_pubkey = match config.app_state.wallet.try_pubkey() { 145 | Ok(pk) => pk, 146 | Err(_) => return Err("Failed to get wallet pubkey".to_string()), 147 | }; 148 | 149 | // Get the WSOL ATA address 150 | let wsol_account = get_associated_token_address( 151 | &wallet_pubkey, 152 | &spl_token::native_mint::id() 153 | ); 154 | 155 | logger.log(format!("WSOL account address: {}", wsol_account)); 156 | 157 | // Check if WSOL account exists 158 | match config.app_state.rpc_client.get_account(&wsol_account) { 159 | Ok(_) => { 160 | logger.log(format!("Found WSOL account: {}", wsol_account)); 161 | }, 162 | Err(_) => { 163 | return Err(format!("WSOL account does not exist: {}", wsol_account)); 164 | } 165 | } 166 | 167 | // Close the WSOL account to recover SOL 168 | let close_instruction = token::close_account( 169 | wallet_pubkey, 170 | wsol_account, 171 | wallet_pubkey, 172 | wallet_pubkey, 173 | &[&wallet_pubkey], 174 | ).map_err(|e| format!("Failed to create close account instruction: {}", e))?; 175 | 176 | // Send transaction 177 | let recent_blockhash = config.app_state.rpc_client.get_latest_blockhash() 178 | .map_err(|e| format!("Failed to get recent blockhash: {}", e))?; 179 | 180 | let transaction = Transaction::new_signed_with_payer( 181 | &[close_instruction], 182 | Some(&wallet_pubkey), 183 | &[&config.app_state.wallet], 184 | recent_blockhash, 185 | ); 186 | 187 | match config.app_state.rpc_client.send_and_confirm_transaction(&transaction) { 188 | Ok(signature) => { 189 | logger.log(format!("WSOL unwrapped successfully, signature: {}", signature)); 190 | Ok(()) 191 | }, 192 | Err(e) => { 193 | Err(format!("Failed to unwrap WSOL: {}", e)) 194 | } 195 | } 196 | } 197 | 198 | /// Sell all tokens using Jupiter API 199 | async fn sell_all_tokens(config: &Config) -> Result<(), String> { 200 | let logger = solana_vntr_sniper::common::logger::Logger::new("[SELL-ALL-TOKENS] => ".green().to_string()); 201 | let quote_logger = solana_vntr_sniper::common::logger::Logger::new("[JUPITER-QUOTE] => ".blue().to_string()); 202 | let execute_logger = solana_vntr_sniper::common::logger::Logger::new("[EXECUTE-SWAP] => ".yellow().to_string()); 203 | let sell_logger = solana_vntr_sniper::common::logger::Logger::new("[SELL-TOKEN] ".cyan().to_string()); 204 | 205 | // Get wallet pubkey 206 | let wallet_pubkey = match config.app_state.wallet.try_pubkey() { 207 | Ok(pk) => pk, 208 | Err(_) => return Err("Failed to get wallet pubkey".to_string()), 209 | }; 210 | 211 | logger.log(format!("🔍 Scanning wallet {} for tokens to sell", wallet_pubkey)); 212 | 213 | // Get the token program pubkey 214 | let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); 215 | 216 | // Query all token accounts owned by the wallet 217 | let accounts = config.app_state.rpc_client.get_token_accounts_by_owner( 218 | &wallet_pubkey, 219 | anchor_client::solana_client::rpc_request::TokenAccountsFilter::ProgramId(token_program) 220 | ).map_err(|e| format!("Failed to get token accounts: {}", e))?; 221 | 222 | if accounts.is_empty() { 223 | logger.log("No token accounts found".to_string()); 224 | return Ok(()); 225 | } 226 | 227 | logger.log(format!("Found {} token accounts", accounts.len())); 228 | 229 | // Create Jupiter API client 230 | let jupiter_client = JupiterClient::new(config.app_state.rpc_nonblocking_client.clone()); 231 | 232 | // Filter and collect token information 233 | let mut tokens_to_sell = Vec::new(); 234 | let mut total_token_count = 0; 235 | let mut sold_count = 0; 236 | let mut failed_count = 0; 237 | let mut total_sol_received = 0u64; 238 | 239 | for account_info in accounts { 240 | let token_account = Pubkey::from_str(&account_info.pubkey) 241 | .map_err(|_| format!("Invalid token account pubkey: {}", account_info.pubkey))?; 242 | 243 | // Get account data 244 | let account_data = match config.app_state.rpc_client.get_account(&token_account) { 245 | Ok(data) => data, 246 | Err(e) => { 247 | logger.log(format!("Failed to get account data for {}: {}", token_account, e).red().to_string()); 248 | continue; 249 | } 250 | }; 251 | 252 | // Parse token account data 253 | if let Ok(token_data) = spl_token::state::Account::unpack(&account_data.data) { 254 | // Skip WSOL (wrapped SOL) and accounts with zero balance 255 | if token_data.mint == spl_token::native_mint::id() || token_data.amount == 0 { 256 | continue; 257 | } 258 | 259 | total_token_count += 1; 260 | 261 | // Get mint account to determine decimals 262 | let mint_data = match config.app_state.rpc_client.get_account(&token_data.mint) { 263 | Ok(data) => data, 264 | Err(e) => { 265 | logger.log(format!("Failed to get mint data for {}: {}", token_data.mint, e).yellow().to_string()); 266 | continue; 267 | } 268 | }; 269 | 270 | let mint_info = match spl_token::state::Mint::unpack(&mint_data.data) { 271 | Ok(info) => info, 272 | Err(e) => { 273 | logger.log(format!("Failed to parse mint data for {}: {}", token_data.mint, e).yellow().to_string()); 274 | continue; 275 | } 276 | }; 277 | 278 | let decimals = mint_info.decimals; 279 | let token_amount = token_data.amount as f64 / 10f64.powi(decimals as i32); 280 | 281 | logger.log(format!("📦 Found token: {} - Amount: {} (decimals: {})", 282 | token_data.mint, token_amount, decimals)); 283 | 284 | tokens_to_sell.push((token_data.mint.to_string(), token_data.amount, decimals)); 285 | } 286 | } 287 | 288 | if tokens_to_sell.is_empty() { 289 | logger.log("No tokens found to sell (excluding SOL/WSOL)".yellow().to_string()); 290 | return Ok(()); 291 | } 292 | 293 | logger.log(format!("💱 Starting to sell {} tokens", tokens_to_sell.len())); 294 | 295 | // Sell each token using Jupiter API 296 | for (mint, amount, decimals) in tokens_to_sell { 297 | logger.log(format!("💱 Selling token: {}", mint).cyan().to_string()); 298 | 299 | // First get the quote to show detailed information 300 | let sol_mint = "So11111111111111111111111111111111111111112"; 301 | quote_logger.log(format!("Getting quote: {} -> {} (amount: {})", mint, sol_mint, amount)); 302 | 303 | match jupiter_client.get_quote(&mint, sol_mint, amount, 100).await { 304 | Ok(quote) => { 305 | // Log quote details like in the example 306 | quote_logger.log(format!("Raw quote response (first 500 chars): {}", 307 | serde_json::to_string("e).unwrap_or_default().chars().take(500).collect::())); 308 | 309 | quote_logger.log(format!("Quote received: {} {} -> {} {}", 310 | quote.in_amount, mint, quote.out_amount, sol_mint)); 311 | 312 | // Now get the actual transaction using the enhanced Jupiter sell method 313 | match jupiter_client.sell_token_with_jupiter(&mint, amount, 500, &config.app_state.wallet).await { 314 | Ok(signature) => { 315 | execute_logger.log(format!("Jupiter sell transaction sent: {}", signature)); 316 | 317 | // Wait a moment for confirmation 318 | tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; 319 | execute_logger.log(format!("Jupiter sell transaction confirmed: {}", signature)); 320 | 321 | // Log the successful sell 322 | sell_logger.log(format!("{} => Token sold successfully! Signature: {}", mint, signature)); 323 | 324 | // Parse the expected SOL amount from quote 325 | if let Ok(sol_amount) = quote.out_amount.parse::() { 326 | total_sol_received += sol_amount; 327 | } 328 | 329 | logger.log(format!("✅ Successfully sold {}: {}", mint, signature).green().to_string()); 330 | sold_count += 1; 331 | }, 332 | Err(e) => { 333 | logger.log(format!("❌ Failed to get sell transaction for token {}: {}", mint, e).red().to_string()); 334 | failed_count += 1; 335 | } 336 | } 337 | }, 338 | Err(e) => { 339 | logger.log(format!("❌ Failed to get quote for token {}: {}", mint, e).red().to_string()); 340 | failed_count += 1; 341 | } 342 | } 343 | 344 | // Small delay between transactions to avoid rate limiting 345 | tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; 346 | } 347 | 348 | // Final summary 349 | let sol_received_display = total_sol_received as f64 / 1_000_000_000.0; // Convert lamports to SOL 350 | logger.log(format!("Selling completed! ✅ {} successful, ❌ {} failed, ~{:.6} SOL received", 351 | sold_count, failed_count, sol_received_display).cyan().bold().to_string()); 352 | 353 | if failed_count > 0 { 354 | Err(format!("Failed to sell {} out of {} tokens", failed_count, total_token_count)) 355 | } else { 356 | Ok(()) 357 | } 358 | } 359 | 360 | /// Close all token accounts owned by the wallet 361 | async fn close_all_token_accounts(config: &Config) -> Result<(), String> { 362 | let logger = solana_vntr_sniper::common::logger::Logger::new("[CLOSE-TOKEN-ACCOUNTS] => ".green().to_string()); 363 | 364 | // Get wallet pubkey 365 | let wallet_pubkey = match config.app_state.wallet.try_pubkey() { 366 | Ok(pk) => pk, 367 | Err(_) => return Err("Failed to get wallet pubkey".to_string()), 368 | }; 369 | 370 | // Get the token program pubkey 371 | let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); 372 | 373 | // Query all token accounts owned by the wallet 374 | let accounts = config.app_state.rpc_client.get_token_accounts_by_owner( 375 | &wallet_pubkey, 376 | anchor_client::solana_client::rpc_request::TokenAccountsFilter::ProgramId(token_program) 377 | ).map_err(|e| format!("Failed to get token accounts: {}", e))?; 378 | 379 | if accounts.is_empty() { 380 | logger.log("No token accounts found to close".to_string()); 381 | return Ok(()); 382 | } 383 | 384 | logger.log(format!("Found {} token accounts to close", accounts.len())); 385 | 386 | let mut closed_count = 0; 387 | let mut failed_count = 0; 388 | 389 | // Close each token account 390 | for account_info in accounts { 391 | let token_account = Pubkey::from_str(&account_info.pubkey) 392 | .map_err(|_| format!("Invalid token account pubkey: {}", account_info.pubkey))?; 393 | 394 | // Skip WSOL accounts with non-zero balance (these need to be unwrapped first) 395 | let account_data = match config.app_state.rpc_client.get_account(&token_account) { 396 | Ok(data) => data, 397 | Err(e) => { 398 | logger.log(format!("Failed to get account data for {}: {}", token_account, e).red().to_string()); 399 | failed_count += 1; 400 | continue; 401 | } 402 | }; 403 | 404 | // Check if this is a WSOL account with balance 405 | if let Ok(token_data) = spl_token::state::Account::unpack(&account_data.data) { 406 | if token_data.mint == spl_token::native_mint::id() && token_data.amount > 0 { 407 | logger.log(format!("Skipping WSOL account with non-zero balance: {} ({})", 408 | token_account, 409 | token_data.amount as f64 / 1_000_000_000.0)); 410 | continue; 411 | } 412 | } 413 | 414 | // Create close instruction 415 | let close_instruction = token::close_account( 416 | wallet_pubkey, 417 | token_account, 418 | wallet_pubkey, 419 | wallet_pubkey, 420 | &[&wallet_pubkey], 421 | ).map_err(|e| format!("Failed to create close instruction for {}: {}", token_account, e))?; 422 | 423 | // Send transaction 424 | let recent_blockhash = config.app_state.rpc_client.get_latest_blockhash() 425 | .map_err(|e| format!("Failed to get recent blockhash: {}", e))?; 426 | 427 | let transaction = Transaction::new_signed_with_payer( 428 | &[close_instruction], 429 | Some(&wallet_pubkey), 430 | &[&config.app_state.wallet], 431 | recent_blockhash, 432 | ); 433 | 434 | match config.app_state.rpc_client.send_and_confirm_transaction(&transaction) { 435 | Ok(signature) => { 436 | logger.log(format!("Closed token account {}, signature: {}", token_account, signature)); 437 | closed_count += 1; 438 | }, 439 | Err(e) => { 440 | logger.log(format!("Failed to close token account {}: {}", token_account, e).red().to_string()); 441 | failed_count += 1; 442 | } 443 | } 444 | } 445 | 446 | logger.log(format!("Closed {} token accounts, {} failed", closed_count, failed_count)); 447 | 448 | if failed_count > 0 { 449 | Err(format!("Failed to close {} token accounts", failed_count)) 450 | } else { 451 | Ok(()) 452 | } 453 | } 454 | 455 | 456 | 457 | #[tokio::main] 458 | async fn main() { 459 | /* Initial Settings */ 460 | let config = Config::new().await; 461 | let config = config.lock().await; 462 | 463 | /* Running Bot */ 464 | let run_msg = RUN_MSG; 465 | println!("{}", run_msg); 466 | 467 | // Initialize blockhash processor 468 | match BlockhashProcessor::new(config.app_state.rpc_client.clone()).await { 469 | Ok(processor) => { 470 | if let Err(e) = processor.start().await { 471 | eprintln!("Failed to start blockhash processor: {}", e); 472 | return; 473 | } 474 | println!("Blockhash processor started successfully"); 475 | }, 476 | Err(e) => { 477 | eprintln!("Failed to initialize blockhash processor: {}", e); 478 | return; 479 | } 480 | } 481 | 482 | // Parse command line arguments 483 | let args: Vec = std::env::args().collect(); 484 | if args.len() > 1 { 485 | // Check for command line arguments 486 | if args.contains(&"--wrap".to_string()) { 487 | println!("Wrapping SOL to WSOL..."); 488 | 489 | // Get wrap amount from .env 490 | let wrap_amount = std::env::var("WRAP_AMOUNT") 491 | .ok() 492 | .and_then(|v| v.parse::().ok()) 493 | .unwrap_or(0.1); 494 | 495 | match wrap_sol(&config, wrap_amount).await { 496 | Ok(_) => { 497 | println!("Successfully wrapped {} SOL to WSOL", wrap_amount); 498 | return; 499 | }, 500 | Err(e) => { 501 | eprintln!("Failed to wrap SOL: {}", e); 502 | return; 503 | } 504 | } 505 | } else if args.contains(&"--unwrap".to_string()) { 506 | println!("Unwrapping WSOL to SOL..."); 507 | 508 | match unwrap_sol(&config).await { 509 | Ok(_) => { 510 | println!("Successfully unwrapped WSOL to SOL"); 511 | return; 512 | }, 513 | Err(e) => { 514 | eprintln!("Failed to unwrap WSOL: {}", e); 515 | return; 516 | } 517 | } 518 | } else if args.contains(&"--sell".to_string()) { 519 | println!("Selling all tokens using Jupiter API..."); 520 | 521 | match sell_all_tokens(&config).await { 522 | Ok(_) => { 523 | println!("Successfully sold all tokens"); 524 | return; 525 | }, 526 | Err(e) => { 527 | eprintln!("Failed to sell all tokens: {}", e); 528 | return; 529 | } 530 | } 531 | } else if args.contains(&"--close".to_string()) { 532 | println!("Closing all token accounts..."); 533 | 534 | match close_all_token_accounts(&config).await { 535 | Ok(_) => { 536 | println!("Successfully closed all token accounts"); 537 | return; 538 | }, 539 | Err(e) => { 540 | eprintln!("Failed to close all token accounts: {}", e); 541 | return; 542 | } 543 | } 544 | } 545 | } 546 | 547 | // Initialize token account list 548 | initialize_token_account_list(&config).await; 549 | 550 | // Start cache maintenance service (clean up expired cache entries every 60 seconds) 551 | cache_maintenance::start_cache_maintenance(60).await; 552 | println!("Cache maintenance service started"); 553 | 554 | // Selling instruction cache removed - no maintenance needed 555 | 556 | // Initialize and log selling strategy parameters 557 | let selling_config = solana_vntr_sniper::engine::selling_strategy::SellingConfig::set_from_env(); 558 | let selling_engine = solana_vntr_sniper::engine::selling_strategy::SellingEngine::new( 559 | Arc::new(config.app_state.clone()), 560 | Arc::new(config.swap_config.clone()), 561 | selling_config, 562 | ); 563 | selling_engine.log_selling_parameters(); 564 | 565 | // Initialize copy selling for existing token balances 566 | match selling_engine.initialize_copy_selling_for_existing_tokens().await { 567 | Ok(count) => { 568 | if count > 0 { 569 | println!("✅ Copy selling initialized for {} existing tokens", count); 570 | } 571 | }, 572 | Err(e) => { 573 | eprintln!("⚠️ Failed to initialize copy selling for existing tokens: {}", e); 574 | } 575 | } 576 | 577 | // Get copy trading target addresses from environment 578 | let copy_trading_target_address = std::env::var("COPY_TRADING_TARGET_ADDRESS").ok(); 579 | let is_multi_copy_trading = std::env::var("IS_MULTI_COPY_TRADING") 580 | .ok() 581 | .and_then(|v| v.parse::().ok()) 582 | .unwrap_or(false); 583 | let excluded_addresses_str = std::env::var("EXCLUDED_ADDRESSES").ok(); 584 | 585 | // Prepare target addresses for monitoring 586 | let mut target_addresses = Vec::new(); 587 | let mut excluded_addresses = Vec::new(); 588 | 589 | // Handle multiple copy trading targets if enabled 590 | if is_multi_copy_trading { 591 | if let Some(address_str) = copy_trading_target_address { 592 | // Parse comma-separated addresses 593 | for addr in address_str.split(',') { 594 | let trimmed_addr = addr.trim(); 595 | if !trimmed_addr.is_empty() { 596 | target_addresses.push(trimmed_addr.to_string()); 597 | } 598 | } 599 | } 600 | } else if let Some(address) = copy_trading_target_address { 601 | // Single address mode 602 | if !address.is_empty() { 603 | target_addresses.push(address); 604 | } 605 | } 606 | 607 | if let Some(excluded_addresses_str) = excluded_addresses_str { 608 | for addr in excluded_addresses_str.split(',') { 609 | let trimmed_addr = addr.trim(); 610 | if !trimmed_addr.is_empty() { 611 | excluded_addresses.push(trimmed_addr.to_string()); 612 | } 613 | } 614 | } 615 | 616 | if target_addresses.is_empty() { 617 | eprintln!("No COPY_TRADING_TARGET_ADDRESS specified. Please set this environment variable."); 618 | return; 619 | } 620 | 621 | 622 | 623 | // Get protocol preference from environment 624 | let protocol_preference = std::env::var("PROTOCOL_PREFERENCE") 625 | .ok() 626 | .map(|p| match p.to_lowercase().as_str() { 627 | "raydium" => SwapProtocol::RaydiumLaunchpad, 628 | _ => SwapProtocol::Auto, 629 | }) 630 | .unwrap_or(SwapProtocol::Auto); 631 | 632 | // Start risk management service 633 | println!("Starting risk management service..."); 634 | if let Err(e) = solana_vntr_sniper::engine::risk_management::start_risk_management_service( 635 | target_addresses.clone(), 636 | Arc::new(config.app_state.clone()), 637 | Arc::new(config.swap_config.clone()), 638 | ).await { 639 | eprintln!("Failed to start risk management service: {}", e); 640 | } else { 641 | println!("Risk management service started successfully"); 642 | } 643 | 644 | // Create copy trading config 645 | let sniper_config = SniperConfig { 646 | yellowstone_grpc_http: config.yellowstone_grpc_http.clone(), 647 | yellowstone_grpc_token: config.yellowstone_grpc_token.clone(), 648 | app_state: config.app_state.clone(), 649 | swap_config: config.swap_config.clone(), 650 | counter_limit: config.counter_limit as u64, 651 | target_addresses, 652 | excluded_addresses, 653 | protocol_preference, 654 | }; 655 | 656 | // Run both monitoring functions simultaneously 657 | println!("🚀 Starting both monitoring systems simultaneously..."); 658 | 659 | // Spawn both monitoring tasks to run in parallel 660 | let target_monitoring_handle = tokio::spawn({ 661 | let config = sniper_config.clone(); 662 | async move { 663 | match start_target_wallet_monitoring(config).await { 664 | Ok(_) => println!("✅ Target wallet monitoring completed successfully"), 665 | Err(e) => eprintln!("❌ Target wallet monitoring error: {}", e), 666 | } 667 | } 668 | }); 669 | 670 | let dex_monitoring_handle = tokio::spawn({ 671 | let config = sniper_config; 672 | async move { 673 | match start_dex_monitoring(config).await { 674 | Ok(_) => println!("✅ DEX monitoring completed successfully"), 675 | Err(e) => eprintln!("❌ DEX monitoring error: {}", e), 676 | } 677 | } 678 | }); 679 | 680 | // Wait for both tasks to complete (or error) 681 | println!("⏳ Waiting for monitoring tasks to complete..."); 682 | tokio::try_join!(target_monitoring_handle, dex_monitoring_handle) 683 | .map(|_| println!("🎯 Both monitoring systems have completed")) 684 | .unwrap_or_else(|_| println!("⚠️ One or both monitoring systems encountered errors")); 685 | 686 | } 687 | --------------------------------------------------------------------------------