├── src ├── core │ ├── mod.rs │ ├── tx.rs │ └── token.rs ├── dex │ ├── mod.rs │ ├── dex_manager.rs │ ├── raydium_launchpad.rs │ ├── raydium_cpmm.rs │ └── pump_fun.rs ├── lib.rs ├── services │ ├── mod.rs │ ├── cache_maintenance.rs │ ├── telegram.rs │ ├── rpc_client.rs │ └── blockhash_processor.rs ├── engine │ ├── mod.rs │ ├── monitor.rs │ ├── swap.rs │ ├── transaction_parser.rs │ ├── market_maker.rs │ └── random_trader.rs ├── common │ ├── mod.rs │ ├── constants.rs │ ├── price_monitor.rs │ ├── config.rs │ ├── logger.rs │ ├── volume_waves.rs │ ├── dynamic_ratios.rs │ ├── wallet_pool.rs │ ├── cache.rs │ └── guardian_mode.rs ├── error │ └── mod.rs └── main.rs ├── .gitignore ├── .env.example ├── example.env ├── Cargo.toml └── README.md /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token; 2 | pub mod tx; 3 | -------------------------------------------------------------------------------- /src/dex/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod raydium_cpmm; 2 | pub mod pump_fun; 3 | pub mod raydium_launchpad; 4 | pub mod dex_manager; 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 rpc_client; 3 | pub mod telegram; 4 | pub mod cache_maintenance; 5 | -------------------------------------------------------------------------------- /src/engine/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod market_maker; 2 | pub mod monitor; 3 | pub mod swap; 4 | pub mod transaction_parser; 5 | pub mod random_trader; 6 | -------------------------------------------------------------------------------- /src/services/cache_maintenance.rs: -------------------------------------------------------------------------------- 1 | pub async fn start_cache_maintenance(_interval_seconds: u64) { 2 | println!("Cache maintenance service started (placeholder)"); 3 | } -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod constants; 3 | pub mod logger; 4 | pub mod cache; 5 | pub mod wallet_pool; 6 | pub mod price_monitor; 7 | pub mod dynamic_ratios; 8 | pub mod volume_waves; 9 | pub mod guardian_mode; 10 | -------------------------------------------------------------------------------- /src/common/constants.rs: -------------------------------------------------------------------------------- 1 | pub const INIT_MSG: &str = "Made by DEVDUDES"; 2 | 3 | pub const RUN_MSG: &str = " 4 | RUNNING BOT..... 5 | "; 6 | -------------------------------------------------------------------------------- /src/services/telegram.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | pub async fn init() -> Result<()> { 4 | println!("Telegram service initialized (placeholder)"); 5 | Ok(()) 6 | } 7 | 8 | pub async fn send_trade_notification(_data: &T, _protocol: &str, _action: &str) -> Result<()> { 9 | // Placeholder implementation 10 | Ok(()) 11 | } 12 | 13 | pub async fn send_error_notification(_message: &str) -> Result<()> { 14 | // Placeholder implementation 15 | println!("Error notification: {}", _message); 16 | Ok(()) 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pumpfun-ideal-mev-bot.git 2 | .env 3 | target 4 | blacklist.txt 5 | yellowstone-grpc 6 | pumpfun 7 | pumpfun-sdk 8 | substreams-solana-idl-examples 9 | pumpswap-sdk 10 | pump-swap-sdk 11 | src_restore 12 | # Rust build artifacts 13 | /target 14 | **/*.rs.bk 15 | source 16 | Cargo.lock 17 | solana-rpc-client-2.1.14 18 | # IDE files 19 | .idea/ 20 | .vscode/ 21 | *.iml 22 | backend 23 | # Environment files 24 | .env 25 | .env.local 26 | .env.*.local 27 | 28 | # Transaction records 29 | /record/ 30 | /record/pumpfun/ 31 | /record/pumpswap/ 32 | /record/raydium/ 33 | 34 | # Logs 35 | *.log 36 | raydium_cpmm_sample_bot 37 | # Generated wallets directory -------------------------------------------------------------------------------- /src/common/price_monitor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::sync::Arc; 3 | use tokio::sync::Mutex; 4 | use tokio::time::{Duration, Instant}; 5 | use colored::Colorize; 6 | use crate::common::logger::Logger; 7 | 8 | /// Price data point for tracking price history 9 | #[derive(Debug, Clone)] 10 | pub struct PricePoint { 11 | pub price: f64, 12 | pub timestamp: Instant, 13 | pub volume_sol: f64, 14 | } 15 | 16 | /// Price monitoring system for detecting sharp price movements 17 | pub struct PriceMonitor { 18 | price_history: VecDeque, 19 | logger: Logger, 20 | max_history_size: usize, 21 | price_change_threshold: f64, 22 | throttle_duration: Duration, 23 | last_throttle_time: Option, 24 | is_throttling: bool, 25 | } 26 | -------------------------------------------------------------------------------- /src/dex/dex_manager.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use colored::Colorize; 3 | use std::sync::Arc; 4 | use anchor_client::solana_sdk::{ 5 | instruction::Instruction, 6 | pubkey::Pubkey, 7 | signature::Keypair, 8 | signer::Signer, 9 | }; 10 | 11 | use crate::{ 12 | common::{config::SwapConfig, logger::Logger}, 13 | engine::{swap::SwapDirection, transaction_parser::DexType}, 14 | dex::{ 15 | raydium_cpmm::RaydiumCPMM, 16 | pump_fun::Pump, 17 | raydium_launchpad::RaydiumLaunchpad, 18 | }, 19 | }; 20 | 21 | #[derive(Clone)] 22 | pub enum DexInstance { 23 | RaydiumCPMM(RaydiumCPMM), 24 | PumpFun(Pump), 25 | RaydiumLaunchpad(RaydiumLaunchpad), 26 | } 27 | 28 | #[derive(Clone)] 29 | pub struct DexManager { 30 | dex_instance: DexInstance, 31 | logger: Logger, 32 | mint: String, 33 | coin_creator: String, 34 | } 35 | 36 | -------------------------------------------------------------------------------- /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 | PumpBuy, 8 | PumpSell, 9 | PumpSwapBuy, 10 | PumpSwapSell 11 | } 12 | 13 | #[derive(Clone, Debug)] 14 | pub struct PoolInfo { 15 | pub pool_id: Pubkey, 16 | pub base_mint: Pubkey, 17 | pub quote_mint: Pubkey, 18 | pub base_reserve: u64, 19 | pub quote_reserve: u64, 20 | pub coin_creator: Pubkey, 21 | } 22 | 23 | 24 | #[derive(Debug, Clone, Copy)] 25 | pub struct RetracementLevel { 26 | pub percentage: u64, 27 | pub threshold: u64, 28 | pub sell_amount: u64, 29 | } 30 | 31 | #[derive(Clone, Debug)] 32 | pub struct TokenTrackingInfo { 33 | pub top_pnl: f64, 34 | pub last_sell_time: Instant, 35 | pub completed_intervals: HashSet, 36 | } -------------------------------------------------------------------------------- /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 | Raydium, 33 | Auto, 34 | } 35 | 36 | impl Default for SwapProtocol { 37 | fn default() -> Self { 38 | SwapProtocol::Auto 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Fast Trading Strategy Configuration 2 | SELLING_TIME_AFTER_BUYING=1 # sell all 1 second later after buying 3 | INTERVAL=10 # interval between buying operations in seconds 4 | 5 | # Trading Configuration 6 | MINIMAL_WSOL_BALANCE_FOR_TRADING=0.002 7 | MINIMAL_BALANCE_FOR_FEE=0.01 8 | MIN_SOL=0.01 9 | TOKEN_AMOUNT=0.001 # token amount to purchase 10 | MIN_BUY_AMOUNT=0.001 11 | MAX_BUY_AMOUNT=0.005 12 | 13 | # Wallet Configuration 14 | WALLET_COUNT=5 15 | WRAP_AMOUNT=0.03 16 | 17 | # Target Token (required) 18 | TARGET_TOKEN_MINT=CGrptxv4hSiNSCTufJzBMzarfrfjNhD9vMmhYQ8eVPsA 19 | 20 | # RPC Configuration (required) 21 | PRIVATE_KEY=your_wallet_private_key_here 22 | RPC_HTTP=https://mainnet.helius-rpc.com/?api-key=your_api_key_here 23 | YELLOWSTONE_GRPC_HTTP=https://grpc.fra.shyft.to 24 | YELLOWSTONE_GRPC_TOKEN=your_grpc_token_here 25 | 26 | # Trading Parameters 27 | SLIPPAGE=3000 28 | COUNTER_LIMIT=1 29 | 30 | # Telegram Bot Settings (optional) 31 | TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here 32 | TELEGRAM_CHAT_ID=your_telegram_chat_id_here 33 | 34 | # Nozomi RPC Configuration 35 | NOZOMI_URL=http://ewr1.nozomi.temporal.xyz/?c=your_nozomi_key_here 36 | -------------------------------------------------------------------------------- /src/core/tx.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::str::FromStr; 3 | use anyhow::Result; 4 | use colored::Colorize; 5 | use anchor_client::solana_sdk::{ 6 | instruction::Instruction, 7 | signature::Keypair, 8 | system_instruction, 9 | transaction::Transaction, 10 | hash::Hash, 11 | signature::Signature, 12 | }; 13 | use anchor_client::solana_sdk::pubkey::Pubkey; 14 | use spl_token::ui_amount_to_amount; 15 | use solana_sdk::signer::Signer; 16 | use tokio::time::{Instant, sleep}; 17 | use std::time::Duration; 18 | use std::env; 19 | use solana_client::rpc_client::SerializableTransaction; 20 | use anchor_client::solana_client::rpc_config::RpcSendTransactionConfig; 21 | use anchor_client::solana_sdk::commitment_config::CommitmentLevel; 22 | use solana_transaction_status; 23 | use crate::{ 24 | common::logger::Logger, 25 | }; 26 | use dotenv::dotenv; 27 | 28 | // prioritization fee = UNIT_PRICE * UNIT_LIMIT 29 | fn get_unit_price() -> u64 { 30 | env::var("UNIT_PRICE") 31 | .ok() 32 | .and_then(|v| v.parse::().ok()) 33 | .unwrap_or(20000) 34 | } 35 | 36 | fn get_unit_limit() -> u32 { 37 | env::var("UNIT_LIMIT") 38 | .ok() 39 | .and_then(|v| v.parse::().ok()) 40 | .unwrap_or(200_000) 41 | } 42 | -------------------------------------------------------------------------------- /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 | 35 | /// Create a batch RPC client from an existing RPC client 36 | pub fn create_batch_client(rpc_client: Arc) -> BatchRpcClient { 37 | BatchRpcClient::new(rpc_client) 38 | } -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | # Example .env file for the market maker bot 2 | # Copy this to .env and fill in your actual values 3 | 4 | # RPC Configuration 5 | RPC_HTTP=https://api.mainnet-beta.solana.com 6 | PRIVATE_KEY=your_private_key_here 7 | 8 | # Yellowstone gRPC Configuration 9 | YELLOWSTONE_GRPC_HTTP=https://grpc.yellowstone.com 10 | YELLOWSTONE_GRPC_TOKEN=your_yellowstone_token_here 11 | 12 | # Target Token Configuration 13 | TARGET_TOKEN_MINT=CGrptxv4hSiNSCTufJzBMzarfrfjNhD9vMmhYQ8eVPsA 14 | COIN_CREATOR= 15 | # Pool Configuration for Raydium CPMM 16 | POOL_ID=51WkKvB7zGPvPd8Hr57xv2rWevVa5CDwVhYQAfFMjTKG 17 | POOL_BASE_ACCOUNT=Gb3z5zsk3LPNYhXSBLdDjx6kpdxMMT6q6WsU1eKPqtCZ 18 | POOL_QUOTE_ACCOUNT=H2FkTkXdqjjLMPaAzcmF5FFVAVL1n41QHUUyWmHdmQRN 19 | 20 | # Trading Configuration 21 | SLIPPAGE=10000 22 | TOKEN_AMOUNT=0.001 23 | MIN_BUY_AMOUNT=0.2 24 | MAX_BUY_AMOUNT=0.005 25 | MIN_SOL=0.005 26 | MINIMAL_BALANCE_FOR_FEE=0.01 27 | MINIMAL_WSOL_BALANCE_FOR_TRADING=0.001 28 | 29 | # DEX Configuration (0=RaydiumCPMM, 1=PumpFun, 2=RaydiumLaunchpad) 30 | DEX=0 31 | 32 | # Advanced Features 33 | MIN_SELL_DELAY_HOURS=24 34 | MAX_SELL_DELAY_HOURS=72 35 | PRICE_CHANGE_THRESHOLD=0.15 36 | MIN_BUY_RATIO=0.67 37 | MAX_BUY_RATIO=0.73 38 | VOLUME_WAVE_ACTIVE_HOURS=2 39 | VOLUME_WAVE_SLOW_HOURS=6 40 | GUARDIAN_MODE_ENABLED=true 41 | GUARDIAN_DROP_THRESHOLD=0.10 42 | 43 | # Trading Strategy 44 | SELLING_TIME_AFTER_BUYING=1 45 | INTERVAL=10 46 | IS_PROGRESSIVE_SELL=false 47 | COUNTER_LIMIT=0 48 | 49 | # Wallet Management 50 | WALLET_COUNT=5 51 | WRAP_AMOUNT=0.1 52 | 53 | # Optional: Target wallet monitoring 54 | IS_CHECK_TARGET_WALLET_TOKEN_ACCOUNT=false -------------------------------------------------------------------------------- /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/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 std::{env, sync::Arc}; 10 | use crate::engine::swap::SwapProtocol; 11 | use crate::engine::transaction_parser::DexType; 12 | use crate::{ 13 | common::{constants::INIT_MSG, logger::Logger}, 14 | engine::swap::{SwapDirection, SwapInType}, 15 | }; 16 | 17 | static GLOBAL_CONFIG: OnceCell> = OnceCell::const_new(); 18 | 19 | pub struct Config { 20 | pub yellowstone_grpc_http: String, 21 | pub yellowstone_grpc_token: String, 22 | pub app_state: AppState, 23 | pub swap_config: SwapConfig, 24 | pub counter_limit: u32, 25 | pub is_progressive_sell: bool, 26 | pub target_token_mint: String, 27 | pub coin_creator: String, // New field for pump.fun coin creator 28 | pub min_buy_amount: f64, 29 | pub max_buy_amount: f64, 30 | pub min_sol: f64, 31 | pub minimal_balance_for_fee: f64, 32 | pub minimal_wsol_balance_for_trading: f64, 33 | // New fast trading strategy configuration 34 | pub selling_time_after_buying: u64, // seconds to wait before selling after buying 35 | pub interval: u64, // interval between buying operations in seconds 36 | // New advanced features configuration 37 | pub min_sell_delay_hours: u64, 38 | pub max_sell_delay_hours: u64, 39 | pub price_change_threshold: f64, 40 | pub min_buy_ratio: f64, 41 | pub max_buy_ratio: f64, 42 | pub volume_wave_active_hours: u64, 43 | pub volume_wave_slow_hours: u64, 44 | pub guardian_mode_enabled: bool, 45 | pub guardian_drop_threshold: f64, 46 | // DEX configuration 47 | pub dex_type: DexType, 48 | // Pool configuration for Raydium CPMM (only used when DEX != PumpFun) 49 | pub pool_id: String, 50 | pub pool_base_account: String, 51 | pub pool_quote_account: String, 52 | } 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/dex/raydium_launchpad.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Arc, time::Instant}; 2 | use solana_program_pack::Pack; 3 | use anchor_client::solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}; 4 | use anchor_client::solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}; 5 | use solana_account_decoder::UiAccountEncoding; 6 | use anyhow::{anyhow, Result}; 7 | use colored::Colorize; 8 | use anchor_client::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 | // Constants - moved to lazy_static for single initialization 30 | lazy_static::lazy_static! { 31 | static ref TOKEN_PROGRAM: Pubkey = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); 32 | static ref TOKEN_2022_PROGRAM: Pubkey = Pubkey::from_str("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb").unwrap(); 33 | static ref ASSOCIATED_TOKEN_PROGRAM: Pubkey = Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap(); 34 | static ref RAYDIUM_LAUNCHPAD_PROGRAM: Pubkey = Pubkey::from_str("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj").unwrap(); 35 | static ref RAYDIUM_LAUNCHPAD_AUTHORITY: Pubkey = Pubkey::from_str("WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh").unwrap(); 36 | static ref RAYDIUM_GLOBAL_CONFIG: Pubkey = Pubkey::from_str("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX").unwrap(); 37 | static ref RAYDIUM_PLATFORM_CONFIG: Pubkey = Pubkey::from_str("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1").unwrap(); 38 | static ref EVENT_AUTHORITY: Pubkey = Pubkey::from_str("2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr").unwrap(); 39 | static ref SOL_MINT: Pubkey = Pubkey::from_str("So11111111111111111111111111111111111111112").unwrap(); 40 | } 41 | 42 | const TEN_THOUSAND: u64 = 10000; 43 | const POOL_VAULT_SEED: &[u8] = b"pool_vault"; 44 | 45 | 46 | -------------------------------------------------------------------------------- /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/dex/raydium_cpmm.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Arc}; 2 | use solana_program_pack::Pack; 3 | use anchor_client::solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}; 4 | use anchor_client::solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}; 5 | use solana_account_decoder::UiAccountEncoding; 6 | use anyhow::{anyhow, Result}; 7 | use colored::Colorize; 8 | use anchor_client::solana_sdk::{ 9 | instruction::{AccountMeta, Instruction}, 10 | pubkey::Pubkey, 11 | signature::Keypair, 12 | system_program, 13 | signer::Signer, 14 | }; 15 | use spl_token_client::token::TokenError; 16 | 17 | use crate::engine::transaction_parser::DexType; 18 | use crate::services::rpc_client; 19 | use spl_associated_token_account::{ 20 | get_associated_token_address, 21 | instruction::create_associated_token_account_idempotent 22 | }; 23 | use spl_token::ui_amount_to_amount; 24 | use tokio::sync::OnceCell; 25 | use lru::LruCache; 26 | use std::num::NonZeroUsize; 27 | 28 | use crate::{ 29 | common::{config::SwapConfig, logger::Logger, cache::WALLET_TOKEN_ACCOUNTS}, 30 | core::token, 31 | engine::swap::{SwapDirection, SwapInType}, 32 | }; 33 | 34 | // Constants - moved to lazy_static for single initialization 35 | lazy_static::lazy_static! { 36 | static ref TOKEN_PROGRAM: Pubkey = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); 37 | static ref TOKEN_2022_PROGRAM: Pubkey = Pubkey::from_str("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb").unwrap(); 38 | static ref ASSOCIATED_TOKEN_PROGRAM: Pubkey = Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap(); 39 | static ref OBSERVATION_STATE: Pubkey = Pubkey::from_str("52z4oFKcZvJ3qcUxujZUhvC5FsWf5m8CGeqL2E9y8T3B").unwrap(); 40 | static ref RAYDIUM_VAULT_AUTHORITY: Pubkey = Pubkey::from_str("GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL").unwrap(); 41 | static ref RAYDIUM_CPMM_PROGRAM_ID: Pubkey = Pubkey::from_str("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C").unwrap(); 42 | } 43 | 44 | // Thread-safe cache with LRU eviction policy 45 | static TOKEN_ACCOUNT_CACHE: OnceCell> = OnceCell::const_new(); 46 | 47 | const TEN_THOUSAND: u64 = 10000; 48 | const CACHE_SIZE: usize = 1000; 49 | 50 | async fn init_caches() { 51 | TOKEN_ACCOUNT_CACHE.get_or_init(|| async { 52 | LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()) 53 | }).await; 54 | } 55 | -------------------------------------------------------------------------------- /src/engine/transaction_parser.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use anyhow::{anyhow, Result}; 3 | use anchor_client::solana_sdk::pubkey::Pubkey; 4 | use colored::Colorize; 5 | use yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction; 6 | use yellowstone_grpc_proto::prelude::{TransactionStatusMeta, TokenBalance}; 7 | use crate::common::logger::Logger; 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | pub enum DexType { 11 | RaydiumCPMM, 12 | PumpFun, 13 | RaydiumLaunchpad, 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct TransactionAnalysis { 18 | pub mint: String, 19 | pub is_buy: bool, 20 | pub amount_in: u64, 21 | pub amount_out: u64, 22 | pub user: String, 23 | pub volume_change: f64, 24 | pub dex_type: DexType, 25 | pub swap_event: Option, 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | pub struct SwapEventData { 30 | pub amount_in: u64, 31 | pub amount_out: u64, 32 | pub before_source_balance: u64, 33 | pub after_source_balance: u64, 34 | pub before_destination_balance: u64, 35 | pub after_destination_balance: u64, 36 | } 37 | 38 | #[derive(Debug, Clone)] 39 | pub struct BalanceChange { 40 | pub account_index: usize, 41 | pub pre_balance: u64, 42 | pub post_balance: u64, 43 | pub mint: Option, 44 | pub token_change: Option, 45 | } 46 | 47 | #[derive(Debug, Clone)] 48 | pub struct TokenBalanceChange { 49 | pub mint: String, 50 | pub pre_amount: u64, 51 | pub post_amount: u64, 52 | pub decimals: u8, 53 | } 54 | 55 | // Helper trait for TradeInfoFromToken compatibility 56 | #[derive(Debug, Clone)] 57 | pub struct TradeInfoFromToken { 58 | pub mint: String, 59 | pub is_buy: bool, 60 | pub dex_type: DexType, 61 | pub user: String, 62 | pub volume_change: f64, 63 | pub amount_in: u64, 64 | pub amount_out: u64, 65 | } 66 | 67 | /// Parse Raydium CPMM transaction logs and extract trading information 68 | pub fn parse_raydium_cpmm_transaction( 69 | txn: &SubscribeUpdateTransaction, 70 | target_mint: &str, 71 | ) -> Option { 72 | let logger = Logger::new("[TX-PARSER] => ".cyan().to_string()); 73 | 74 | let transaction = txn.transaction.as_ref()?; 75 | let meta = transaction.meta.as_ref()?; 76 | 77 | // Parse log messages for swap events 78 | let swap_event = parse_swap_event_from_logs(&meta.log_messages)?; 79 | 80 | // Parse balance changes 81 | let balance_changes = parse_balance_changes(meta, target_mint); 82 | 83 | // Determine if this is a buy or sell based on the swap event and balance changes 84 | let (is_buy, user, volume_change) = analyze_transaction_direction(&balance_changes, &swap_event, target_mint)?; 85 | 86 | logger.log(format!("Parsed transaction - Mint: {}, Is Buy: {}, Volume: {}", 87 | target_mint, is_buy, volume_change).green().to_string()); 88 | 89 | Some(TransactionAnalysis { 90 | mint: target_mint.to_string(), 91 | is_buy, 92 | amount_in: swap_event.amount_in, 93 | amount_out: swap_event.amount_out, 94 | user, 95 | volume_change, 96 | dex_type: DexType::RaydiumCPMM, 97 | swap_event: Some(swap_event), 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /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(100); 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/volume_waves.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use tokio::sync::Mutex; 3 | use tokio::time::{Duration, Instant}; 4 | use rand::Rng; 5 | use colored::Colorize; 6 | use crate::common::logger::Logger; 7 | 8 | /// Volume wave manager that creates realistic trading patterns 9 | pub struct VolumeWaveManager { 10 | current_phase: TradingPhase, 11 | phase_start_time: Instant, 12 | active_duration: Duration, 13 | slow_duration: Duration, 14 | logger: Logger, 15 | activity_multipliers: PhaseMultipliers, 16 | } 17 | 18 | impl VolumeWaveManager { 19 | /// Create a new volume wave manager 20 | pub fn new(active_hours: u64, slow_hours: u64) -> Self { 21 | let logger = Logger::new("[VOLUME-WAVES] => ".blue().bold().to_string()); 22 | 23 | // Start with a random phase 24 | let mut rng = rand::thread_rng(); 25 | let initial_phase = if rng.gen_bool(0.6) { 26 | TradingPhase::Active 27 | } else { 28 | TradingPhase::Slow 29 | }; 30 | 31 | logger.log(format!("🌊 Volume wave manager initialized in {:?} phase", initial_phase).blue().to_string()); 32 | 33 | Self { 34 | current_phase: initial_phase, 35 | phase_start_time: Instant::now(), 36 | active_duration: Duration::from_secs(active_hours * 3600), 37 | slow_duration: Duration::from_secs(slow_hours * 3600), 38 | logger, 39 | activity_multipliers: PhaseMultipliers::default(), 40 | } 41 | } 42 | 43 | /// Get the current trading phase, updating if necessary 44 | pub fn get_current_phase(&mut self) -> TradingPhase { 45 | let now = Instant::now(); 46 | let elapsed = now.duration_since(self.phase_start_time); 47 | 48 | let should_switch = match self.current_phase { 49 | TradingPhase::Active => elapsed >= self.active_duration, 50 | TradingPhase::Slow => elapsed >= self.slow_duration, 51 | TradingPhase::Burst => elapsed >= Duration::from_secs(15 * 60), // Burst lasts 15 minutes 52 | TradingPhase::Dormant => elapsed >= Duration::from_secs(60 * 60), // Dormant lasts 1 hour 53 | }; 54 | 55 | if should_switch { 56 | self.switch_phase(); 57 | } 58 | 59 | self.current_phase 60 | } 61 | 62 | /// Switch to the next trading phase 63 | fn switch_phase(&mut self) { 64 | let old_phase = self.current_phase; 65 | let mut rng = rand::thread_rng(); 66 | 67 | self.current_phase = match self.current_phase { 68 | TradingPhase::Active => { 69 | // After active, go to slow with occasional burst 70 | if rng.gen_bool(0.15) { // 15% chance of burst 71 | TradingPhase::Burst 72 | } else { 73 | TradingPhase::Slow 74 | } 75 | }, 76 | TradingPhase::Slow => { 77 | // After slow, go to active with occasional dormant 78 | if rng.gen_bool(0.1) { // 10% chance of dormant 79 | TradingPhase::Dormant 80 | } else { 81 | TradingPhase::Active 82 | } 83 | }, 84 | TradingPhase::Burst => { 85 | // After burst, always go to slow to cool down 86 | TradingPhase::Slow 87 | }, 88 | TradingPhase::Dormant => { 89 | // After dormant, always go to active 90 | TradingPhase::Active 91 | }, 92 | }; 93 | 94 | self.phase_start_time = Instant::now(); 95 | 96 | let duration_text = match self.current_phase { 97 | TradingPhase::Active => format!("{:.1} hours", self.active_duration.as_secs_f64() / 3600.0), 98 | TradingPhase::Slow => format!("{:.1} hours", self.slow_duration.as_secs_f64() / 3600.0), 99 | TradingPhase::Burst => "15 minutes".to_string(), 100 | TradingPhase::Dormant => "1 hour".to_string(), 101 | }; 102 | 103 | self.logger.log(format!( 104 | "🔄 Phase transition: {:?} -> {:?} (Duration: {})", 105 | old_phase, self.current_phase, duration_text 106 | ).blue().bold().to_string()); 107 | } 108 | 109 | /// Get the frequency multiplier for the current phase 110 | pub fn get_frequency_multiplier(&self) -> f64 { 111 | match self.current_phase { 112 | TradingPhase::Active => self.activity_multipliers.active_frequency, 113 | TradingPhase::Slow => self.activity_multipliers.slow_frequency, 114 | TradingPhase::Burst => self.activity_multipliers.burst_frequency, 115 | TradingPhase::Dormant => self.activity_multipliers.dormant_frequency, 116 | } 117 | } 118 | 119 | /// Get the amount multiplier for the current phase 120 | pub fn get_amount_multiplier(&self) -> f64 { 121 | match self.current_phase { 122 | TradingPhase::Active => self.activity_multipliers.active_amount, 123 | TradingPhase::Slow => self.activity_multipliers.slow_amount, 124 | TradingPhase::Burst => self.activity_multipliers.burst_amount, 125 | TradingPhase::Dormant => self.activity_multipliers.dormant_amount, 126 | } 127 | } 128 | 129 | /// Get comprehensive wave information 130 | pub fn get_wave_info(&self) -> VolumeWaveInfo { 131 | let elapsed = Instant::now().duration_since(self.phase_start_time); 132 | let remaining = match self.current_phase { 133 | TradingPhase::Active => self.active_duration.saturating_sub(elapsed), 134 | TradingPhase::Slow => self.slow_duration.saturating_sub(elapsed), 135 | TradingPhase::Burst => Duration::from_secs(15 * 60).saturating_sub(elapsed), 136 | TradingPhase::Dormant => Duration::from_secs(60 * 60).saturating_sub(elapsed), 137 | }; 138 | 139 | VolumeWaveInfo { 140 | current_phase: self.current_phase, 141 | time_in_phase: elapsed, 142 | time_remaining: remaining, 143 | frequency_multiplier: self.get_frequency_multiplier(), 144 | amount_multiplier: self.get_amount_multiplier(), 145 | } 146 | } 147 | 148 | } -------------------------------------------------------------------------------- /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 | //! 5 | //! 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. 6 | //! 7 | //! # Error Types 8 | //! 9 | //! - `SolanaClientError`: An error occurred while interacting with the Solana RPC client. 10 | //! - `UploadMetadataError`: An error occurred while uploading metadata to IPFS. 11 | //! - `InvalidInput`: Invalid input parameters were provided. 12 | //! - `InsufficientFunds`: Insufficient funds for a transaction. 13 | //! - `SimulationError`: Transaction simulation failed. 14 | //! - `RateLimitExceeded`: Rate limit exceeded. 15 | 16 | use serde_json::Error; 17 | use anchor_client::solana_client::{ 18 | client_error::ClientError as SolanaClientError, pubsub_client::PubsubClientError, 19 | }; 20 | use anchor_client::solana_sdk::pubkey::ParsePubkeyError; 21 | 22 | // #[derive(Debug)] 23 | // #[allow(dead_code)] 24 | // pub struct AppError(anyhow::Error); 25 | 26 | // impl From for AppError 27 | // where 28 | // E: Into, 29 | // { 30 | // fn from(err: E) -> Self { 31 | // Self(err.into()) 32 | // } 33 | // } 34 | 35 | #[derive(Debug)] 36 | pub enum ClientError { 37 | /// Error deserializing data using Borsh 38 | BorshError(std::io::Error), 39 | /// Error from Solana RPC client 40 | SolanaClientError(anchor_client::solana_client::client_error::ClientError), 41 | /// Error uploading metadata 42 | UploadMetadataError(Box), 43 | /// Invalid input parameters 44 | InvalidInput(&'static str), 45 | /// Insufficient funds for transaction 46 | InsufficientFunds, 47 | /// Transaction simulation failed 48 | SimulationError(String), 49 | /// Rate limit exceeded 50 | RateLimitExceeded, 51 | 52 | OrderLimitExceeded, 53 | 54 | ExternalService(String), 55 | 56 | Redis(String, String), 57 | 58 | Solana(String, String), 59 | 60 | Parse(String, String), 61 | 62 | Pubkey(String, String), 63 | 64 | 65 | Join(String), 66 | 67 | Subscribe(String, String), 68 | 69 | Send(String, String), 70 | 71 | Other(String), 72 | 73 | InvalidData(String), 74 | 75 | Timeout(String, String), 76 | 77 | Duplicate(String), 78 | 79 | InvalidEventType, 80 | 81 | ChannelClosed, 82 | } 83 | 84 | impl std::fmt::Display for ClientError { 85 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 86 | match self { 87 | Self::BorshError(err) => write!(f, "Borsh serialization error: {}", err), 88 | Self::SolanaClientError(err) => write!(f, "Solana client error: {}", err), 89 | Self::UploadMetadataError(err) => write!(f, "Metadata upload error: {}", err), 90 | Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), 91 | Self::InsufficientFunds => write!(f, "Insufficient funds for transaction"), 92 | Self::SimulationError(msg) => write!(f, "Transaction simulation failed: {}", msg), 93 | Self::ExternalService(msg) => write!(f, "External service error: {}", msg), 94 | Self::RateLimitExceeded => write!(f, "Rate limit exceeded"), 95 | Self::OrderLimitExceeded => write!(f, "Order limit exceeded"), 96 | Self::Solana(msg, details) => write!(f, "Solana error: {}, details: {}", msg, details), 97 | Self::Parse(msg, details) => write!(f, "Parse error: {}, details: {}", msg, details), 98 | Self::Redis(msg, details) => write!(f, "Redis error: {}, details: {}", msg, details), 99 | Self::Join(msg) => write!(f, "Task join error: {}", msg), 100 | Self::Pubkey(msg, details) => write!(f, "Pubkey error: {}, details: {}", msg, details), 101 | Self::Subscribe(msg, details) => { 102 | write!(f, "Subscribe error: {}, details: {}", msg, details) 103 | } 104 | Self::Send(msg, details) => write!(f, "Send error: {}, details: {}", msg, details), 105 | Self::Other(msg) => write!(f, "Other error: {}", msg), 106 | Self::InvalidData(msg) => write!(f, "Invalid data: {}", msg), 107 | Self::Timeout(msg, details) => { 108 | write!(f, "Operation timed out: {}, details: {}", msg, details) 109 | } 110 | Self::Duplicate(msg) => write!(f, "Duplicate event: {}", msg), 111 | Self::InvalidEventType => write!(f, "Invalid event type"), 112 | Self::ChannelClosed => write!(f, "Channel closed"), 113 | } 114 | } 115 | } 116 | impl std::error::Error for ClientError { 117 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 118 | match self { 119 | Self::BorshError(err) => Some(err), 120 | Self::SolanaClientError(err) => Some(err), 121 | Self::UploadMetadataError(err) => Some(err.as_ref()), 122 | Self::ExternalService(_) => None, 123 | Self::Redis(_, _) => None, 124 | Self::Solana(_, _) => None, 125 | Self::Parse(_, _) => None, 126 | Self::Join(_) => None, 127 | Self::Pubkey(_, _) => None, 128 | Self::Subscribe(_, _) => None, 129 | Self::Send(_, _) => None, 130 | Self::Other(_) => None, 131 | Self::Timeout(_, _) => None, 132 | Self::Duplicate(_) => None, 133 | Self::InvalidEventType => None, 134 | Self::ChannelClosed => None, 135 | _ => None, 136 | } 137 | } 138 | } 139 | 140 | impl From for ClientError { 141 | fn from(error: SolanaClientError) -> Self { 142 | ClientError::Solana("Solana client error".to_string(), error.to_string()) 143 | } 144 | } 145 | 146 | impl From for ClientError { 147 | fn from(error: PubsubClientError) -> Self { 148 | ClientError::Solana("PubSub client error".to_string(), error.to_string()) 149 | } 150 | } 151 | 152 | impl From for ClientError { 153 | fn from(error: ParsePubkeyError) -> Self { 154 | ClientError::Pubkey("Pubkey error".to_string(), error.to_string()) 155 | } 156 | } 157 | 158 | impl From for ClientError { 159 | fn from(err: Error) -> Self { 160 | ClientError::Parse("JSON serialization error".to_string(), err.to_string()) 161 | } 162 | } 163 | 164 | pub type ClientResult = Result; 165 | -------------------------------------------------------------------------------- /src/engine/market_maker.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::time::Duration; 3 | use std::collections::{HashMap, VecDeque}; 4 | use tokio::time::Instant; 5 | use anyhow::Result; 6 | use anchor_client::solana_sdk::signature::Signature; 7 | use anchor_client::solana_sdk::signer::Signer; 8 | use anchor_client::solana_sdk::pubkey::Pubkey; 9 | use anchor_client::solana_sdk::signature::Keypair; 10 | use anchor_client::solana_sdk::system_instruction; 11 | use anchor_client::solana_sdk::transaction::Transaction; 12 | use colored::Colorize; 13 | use solana_transaction_status; 14 | use tokio::time; 15 | use tokio::sync::Mutex; 16 | use futures_util::stream::StreamExt; 17 | use futures_util::{SinkExt, Sink}; 18 | use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient}; 19 | use yellowstone_grpc_proto::geyser::{ 20 | subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest, SubscribeRequestPing, 21 | SubscribeRequestFilterTransactions, SubscribeUpdate, 22 | }; 23 | use crate::engine::transaction_parser; 24 | use crate::common::{ 25 | config::{AppState, SwapConfig, JUPITER_PROGRAM, OKX_DEX_PROGRAM}, 26 | logger::Logger, 27 | wallet_pool::{WalletPool, RandomizationConfig, TradeType}, 28 | price_monitor::{GlobalPriceMonitor, create_global_price_monitor}, 29 | dynamic_ratios::{GlobalDynamicRatioManager, create_global_dynamic_ratio_manager}, 30 | volume_waves::{GlobalVolumeWaveManager, create_global_volume_wave_manager}, 31 | guardian_mode::{GlobalGuardianMode, create_global_guardian_mode}, 32 | }; 33 | use crate::dex::{raydium_cpmm::RaydiumCPMM, dex_manager::DexManager}; 34 | use crate::engine::swap::{SwapDirection, SwapInType}; 35 | use crate::core::token; 36 | use spl_token::instruction::sync_native; 37 | use spl_associated_token_account::{get_associated_token_address, instruction::create_associated_token_account_idempotent}; 38 | use solana_program_pack::Pack; 39 | use std::str::FromStr; 40 | use rand::Rng; 41 | use crate::engine::transaction_parser::{parse_target_token_transaction, TradeInfoFromToken, DexType}; 42 | 43 | // Activity tracking structures for token analysis 44 | #[derive(Debug, Clone)] 45 | pub struct TokenActivity { 46 | pub timestamp: Instant, 47 | pub is_buy: bool, 48 | pub volume_sol: f64, 49 | pub user: String, 50 | pub price: f64, 51 | } 52 | 53 | #[derive(Debug, Default)] 54 | pub struct TokenActivityReport { 55 | pub total_trades: u32, 56 | pub buy_trades: u32, 57 | pub sell_trades: u32, 58 | pub total_volume_sol: f64, 59 | pub buy_volume_sol: f64, 60 | pub sell_volume_sol: f64, 61 | pub average_price: f64, 62 | pub min_price: f64, 63 | pub max_price: f64, 64 | pub unique_traders: u32, 65 | pub report_period_minutes: u64, 66 | } 67 | 68 | /// Configuration for market maker bot with advanced multi-wallet support 69 | #[derive(Clone)] 70 | pub struct MarketMakerConfig { 71 | pub yellowstone_grpc_http: String, 72 | pub yellowstone_grpc_token: String, 73 | pub app_state: Arc, 74 | pub target_token_mint: String, 75 | pub coin_creator: String, 76 | pub slippage: u64, 77 | pub randomization_config: RandomizationConfig, 78 | pub enable_multi_wallet: bool, 79 | pub max_concurrent_trades: usize, 80 | pub enable_telegram_notifications: bool, 81 | pub dex_type: DexType, 82 | // Pool configuration for Raydium CPMM 83 | pub pool_id: String, 84 | pub pool_base_account: String, 85 | pub pool_quote_account: String, 86 | } 87 | 88 | impl MarketMakerConfig { 89 | /// Create a new MarketMakerConfig with stealth mode settings 90 | pub fn stealth_mode( 91 | yellowstone_grpc_http: String, 92 | yellowstone_grpc_token: String, 93 | app_state: Arc, 94 | target_token_mint: String, 95 | coin_creator: String, 96 | dex_type: DexType, 97 | pool_id: String, 98 | pool_base_account: String, 99 | pool_quote_account: String, 100 | ) -> Self { 101 | Self { 102 | yellowstone_grpc_http, 103 | yellowstone_grpc_token, 104 | app_state, 105 | target_token_mint, 106 | coin_creator, 107 | slippage: 1000, // 10% 108 | randomization_config: RandomizationConfig::stealth_mode(), 109 | enable_multi_wallet: true, 110 | max_concurrent_trades: 3, 111 | enable_telegram_notifications: true, 112 | dex_type, 113 | // Pool configuration from parameters 114 | pool_id, 115 | pool_base_account, 116 | pool_quote_account, 117 | } 118 | } 119 | 120 | /// Create a new MarketMakerConfig with conservative settings 121 | pub fn conservative_mode( 122 | yellowstone_grpc_http: String, 123 | yellowstone_grpc_token: String, 124 | app_state: Arc, 125 | target_token_mint: String, 126 | coin_creator: String, 127 | dex_type: DexType, 128 | pool_id: String, 129 | pool_base_account: String, 130 | pool_quote_account: String, 131 | ) -> Self { 132 | Self { 133 | yellowstone_grpc_http, 134 | yellowstone_grpc_token, 135 | app_state, 136 | target_token_mint, 137 | coin_creator, 138 | slippage: 1500, // 15% 139 | randomization_config: RandomizationConfig::conservative_mode(), 140 | enable_multi_wallet: true, 141 | max_concurrent_trades: 2, 142 | enable_telegram_notifications: true, 143 | dex_type, 144 | // Pool configuration from parameters 145 | pool_id, 146 | pool_base_account, 147 | pool_quote_account, 148 | } 149 | } 150 | 151 | /// Create a new MarketMakerConfig with default settings 152 | pub fn new( 153 | yellowstone_grpc_http: String, 154 | yellowstone_grpc_token: String, 155 | app_state: Arc, 156 | target_token_mint: String, 157 | coin_creator: String, 158 | dex_type: DexType, 159 | pool_id: String, 160 | pool_base_account: String, 161 | pool_quote_account: String, 162 | ) -> Self { 163 | Self { 164 | yellowstone_grpc_http, 165 | yellowstone_grpc_token, 166 | app_state, 167 | target_token_mint, 168 | coin_creator, 169 | slippage: 1000, // 10% 170 | randomization_config: RandomizationConfig::default(), 171 | enable_multi_wallet: true, 172 | max_concurrent_trades: 2, 173 | enable_telegram_notifications: true, 174 | dex_type, 175 | // Pool configuration from parameters 176 | pool_id, 177 | pool_base_account, 178 | pool_quote_account, 179 | } 180 | } 181 | } 182 | 183 | /// Advanced market maker bot with multi-wallet support and sophisticated randomization 184 | pub struct MarketMaker { 185 | config: MarketMakerConfig, 186 | wallet_pool: Arc>, 187 | logger: Logger, 188 | is_running: Arc>, 189 | recent_trades: Arc>>, 190 | trade_counter: Arc>, 191 | current_wallet: Arc>>>, 192 | wallet_change_counter: Arc>, 193 | token_activities: Arc>>, 194 | last_activity_report: Arc>, 195 | price_monitor: GlobalPriceMonitor, 196 | dynamic_ratio_manager: GlobalDynamicRatioManager, 197 | volume_wave_manager: GlobalVolumeWaveManager, 198 | guardian_mode: GlobalGuardianMode, 199 | dex_manager: Arc>>, 200 | } 201 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anchor_client::solana_sdk::signature::Signer; 2 | use solana_vntr_sniper::{ 3 | common::{config::Config, constants::RUN_MSG, cache::WALLET_TOKEN_ACCOUNTS}, 4 | engine::{ 5 | market_maker::{start_market_maker, MarketMakerConfig}, 6 | }, 7 | services::{telegram, cache_maintenance, blockhash_processor::BlockhashProcessor}, 8 | core::token, 9 | }; 10 | use solana_program_pack::Pack; 11 | use anchor_client::solana_sdk::pubkey::Pubkey; 12 | use anchor_client::solana_sdk::transaction::Transaction; 13 | use anchor_client::solana_sdk::system_instruction; 14 | use anchor_client::solana_sdk::signature::Keypair; 15 | use std::str::FromStr; 16 | use colored::Colorize; 17 | use spl_token::instruction::sync_native; 18 | use spl_token::ui_amount_to_amount; 19 | use spl_associated_token_account::get_associated_token_address; 20 | use std::sync::Arc; 21 | use std::fs; 22 | use std::path::Path; 23 | use anchor_client::solana_client::rpc_config::RpcSendTransactionConfig; 24 | use anchor_client::solana_sdk::commitment_config::CommitmentLevel; 25 | use solana_transaction_status; 26 | 27 | #[tokio::main] 28 | async fn main() { 29 | /* Initial Settings */ 30 | let config = Config::new().await; 31 | let config = config.lock().await; 32 | 33 | /* Running Bot */ 34 | let run_msg = RUN_MSG; 35 | println!("{}", run_msg); 36 | 37 | // Initialize blockhash processor 38 | match BlockhashProcessor::new(config.app_state.rpc_client.clone()).await { 39 | Ok(processor) => { 40 | if let Err(e) = processor.start().await { 41 | eprintln!("Failed to start blockhash processor: {}", e); 42 | return; 43 | } 44 | println!("Blockhash processor started successfully"); 45 | }, 46 | Err(e) => { 47 | eprintln!("Failed to initialize blockhash processor: {}", e); 48 | return; 49 | } 50 | } 51 | 52 | // Parse command line arguments 53 | let args: Vec = std::env::args().collect(); 54 | if args.len() > 1 { 55 | // Check for wallet generation argument 56 | if args.contains(&"--wallet".to_string()) { 57 | println!("Generating wallets..."); 58 | 59 | match generate_wallets().await { 60 | Ok(_) => { 61 | println!("✅ Wallet generation completed successfully!"); 62 | return; 63 | }, 64 | Err(e) => { 65 | eprintln!("❌ Failed to generate wallets: {}", e); 66 | return; 67 | } 68 | } 69 | } 70 | // Check for command line arguments 71 | else if args.contains(&"--wrap".to_string()) { 72 | println!("Wrapping SOL to WSOL..."); 73 | 74 | // Get wrap amount from .env 75 | let wrap_amount = std::env::var("WRAP_AMOUNT") 76 | .ok() 77 | .and_then(|v| v.parse::().ok()) 78 | .unwrap_or(0.1); 79 | 80 | match wrap_sol(&config, wrap_amount).await { 81 | Ok(_) => { 82 | println!("Successfully wrapped {} SOL to WSOL", wrap_amount); 83 | return; 84 | }, 85 | Err(e) => { 86 | eprintln!("Failed to wrap SOL: {}", e); 87 | return; 88 | } 89 | } 90 | } else if args.contains(&"--unwrap".to_string()) { 91 | println!("Unwrapping WSOL to SOL..."); 92 | 93 | match unwrap_sol(&config).await { 94 | Ok(_) => { 95 | println!("Successfully unwrapped WSOL to SOL"); 96 | return; 97 | }, 98 | Err(e) => { 99 | eprintln!("Failed to unwrap WSOL: {}", e); 100 | return; 101 | } 102 | } 103 | } else if args.contains(&"--close".to_string()) { 104 | println!("Closing all token accounts..."); 105 | 106 | match close_all_token_accounts(&config).await { 107 | Ok(_) => { 108 | println!("Successfully closed all token accounts"); 109 | return; 110 | }, 111 | Err(e) => { 112 | eprintln!("Failed to close all token accounts: {}", e); 113 | return; 114 | } 115 | } 116 | } else if args.contains(&"--check-tokens".to_string()) { 117 | println!("Token monitoring feature disabled in this version"); 118 | return; 119 | } else if args.contains(&"--distribute".to_string()) { 120 | println!("Distributing SOL to all wallets and converting to WSOL..."); 121 | 122 | match distribute_sol(&config).await { 123 | Ok(_) => { 124 | println!("✅ SOL distribution and WSOL conversion completed successfully!"); 125 | return; 126 | }, 127 | Err(e) => { 128 | eprintln!("❌ Failed to distribute SOL: {}", e); 129 | return; 130 | } 131 | } 132 | } else if args.contains(&"--collect".to_string()) { 133 | println!("🔍 Checking wallet balances and collecting all funds..."); 134 | println!("📊 This will: sell all tokens, close WSOL accounts, and collect SOL to main wallet"); 135 | 136 | match collect_sol(&config).await { 137 | Ok(_) => { 138 | println!("✅ Collection completed successfully!"); 139 | return; 140 | }, 141 | Err(e) => { 142 | eprintln!("❌ Failed to complete collection: {}", e); 143 | return; 144 | } 145 | } 146 | } 147 | } 148 | 149 | // Initialize Telegram bot 150 | match telegram::init().await { 151 | Ok(_) => println!("Telegram bot initialized successfully"), 152 | Err(e) => println!("Failed to initialize Telegram bot: {}. Continuing without notifications.", e), 153 | } 154 | 155 | // Initialize token account list 156 | initialize_token_account_list(&config).await; 157 | 158 | // Start cache maintenance service (clean up expired cache entries every 60 seconds) 159 | cache_maintenance::start_cache_maintenance(60).await; 160 | println!("Cache maintenance service started"); 161 | 162 | // Market maker mode - no need for target addresses 163 | 164 | // Create stealth market maker config with 100 wallets 165 | let market_maker_config = MarketMakerConfig::stealth_mode( 166 | config.yellowstone_grpc_http.clone(), 167 | config.yellowstone_grpc_token.clone(), 168 | std::sync::Arc::new(config.app_state.clone()), 169 | config.target_token_mint.clone(), 170 | config.coin_creator.clone(), 171 | config.dex_type.clone(), 172 | config.pool_id.clone(), 173 | config.pool_base_account.clone(), 174 | config.pool_quote_account.clone(), 175 | ); 176 | 177 | // Start the advanced stealth market maker bot 178 | println!("🚀 Starting Advanced Stealth Market Maker for mint: {}", config.target_token_mint); 179 | println!("🎯 Using 100 wallets with sophisticated randomization"); 180 | println!("💰 Buy amount ratio: 50% - 90% of wrapped WSOL"); 181 | println!("🎲 70% Buy / 30% Sell ratio"); 182 | println!("🔄 Wallet rotation every 2 trades"); 183 | println!("⏰ Randomized intervals: 10 minutes - 2 hours"); 184 | println!("📊 Activity reports every 30 minutes"); 185 | println!("🎯 Buy: amount_in = WSOL lamports, minimum_amount_out = 0"); 186 | println!("🎯 Sell: amount_in = token balance, minimum_amount_out = 0"); 187 | 188 | if let Err(e) = start_market_maker(market_maker_config).await { 189 | eprintln!("Advanced Market Maker error: {}", e); 190 | 191 | // Send error notification via Telegram 192 | if let Err(te) = telegram::send_error_notification(&format!("Advanced Market Maker bot crashed: {}", e)).await { 193 | eprintln!("Failed to send Telegram notification: {}", te); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/dex/pump_fun.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Arc, time::Duration}; 2 | use anyhow::{anyhow, Result}; 3 | use tokio::time::Instant; 4 | use borsh_derive::{BorshDeserialize, BorshSerialize}; 5 | use colored::Colorize; 6 | use serde::{Deserialize, Serialize}; 7 | use anchor_client::solana_sdk::{ 8 | instruction::{AccountMeta, Instruction}, 9 | pubkey::Pubkey, 10 | signature::Keypair, 11 | signer::Signer, 12 | system_program, 13 | }; 14 | use spl_associated_token_account::{ 15 | get_associated_token_address, 16 | }; 17 | use spl_token::{ui_amount_to_amount, state::{Account, Mint}}; 18 | use solana_program_pack::Pack; 19 | use tokio::sync::OnceCell; 20 | use lru::LruCache; 21 | use std::num::NonZeroUsize; 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 | // Bonding curve info struct for calculations 30 | #[derive(Clone, Debug)] 31 | pub struct BondingCurveInfo { 32 | pub bonding_curve: Pubkey, 33 | pub new_virtual_token_reserve: u64, 34 | pub new_virtual_sol_reserve: u64, 35 | } 36 | 37 | // Constants for cache 38 | const CACHE_SIZE: usize = 1000; 39 | 40 | // Thread-safe cache with LRU eviction policy 41 | static TOKEN_ACCOUNT_CACHE: OnceCell> = OnceCell::const_new(); 42 | 43 | async fn init_caches() { 44 | TOKEN_ACCOUNT_CACHE.get_or_init(|| async { 45 | LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()) 46 | }).await; 47 | } 48 | 49 | pub const TEN_THOUSAND: u64 = 10000; 50 | pub const TOKEN_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; 51 | pub const RENT_PROGRAM: &str = "SysvarRent111111111111111111111111111111111"; 52 | pub const ASSOCIATED_TOKEN_PROGRAM: &str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; 53 | pub const PUMP_GLOBAL: &str = "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"; 54 | pub const PUMP_FEE_RECIPIENT: &str = "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"; 55 | 56 | #[derive(Clone)] 57 | pub struct Pump { 58 | pub rpc_nonblocking_client: Arc, 59 | pub keypair: Arc, 60 | pub rpc_client: Option>, 61 | } 62 | 63 | impl Pump { 64 | pub fn new( 65 | rpc_nonblocking_client: Arc, 66 | rpc_client: Arc, 67 | keypair: Arc, 68 | ) -> Self { 69 | // Initialize caches on first use 70 | tokio::spawn(init_caches()); 71 | 72 | Self { 73 | rpc_nonblocking_client, 74 | keypair, 75 | rpc_client: Some(rpc_client), 76 | } 77 | } 78 | 79 | async fn check_token_account_cache(&self, account: Pubkey) -> bool { 80 | WALLET_TOKEN_ACCOUNTS.contains(&account) 81 | } 82 | 83 | /// Calculate SOL amount out for sell using virtual reserves 84 | pub fn calculate_sell_sol_amount( 85 | token_amount_in: u64, 86 | virtual_sol_reserves: u64, 87 | virtual_token_reserves: u64, 88 | ) -> u64 { 89 | if token_amount_in == 0 || virtual_sol_reserves == 0 || virtual_token_reserves == 0 { 90 | return 0; 91 | } 92 | 93 | // PumpFun bonding curve formula for sell: 94 | // sol_out = (token_in * virtual_sol_reserves) / (virtual_token_reserves + token_in) 95 | let token_amount_in_u128 = token_amount_in as u128; 96 | let virtual_sol_reserves_u128 = virtual_sol_reserves as u128; 97 | let virtual_token_reserves_u128 = virtual_token_reserves as u128; 98 | 99 | let numerator = token_amount_in_u128.saturating_mul(virtual_sol_reserves_u128); 100 | let denominator = virtual_token_reserves_u128.saturating_add(token_amount_in_u128); 101 | 102 | if denominator == 0 { 103 | return 0; 104 | } 105 | 106 | numerator.checked_div(denominator).unwrap_or(0) as u64 107 | } 108 | 109 | /// Calculate price using virtual reserves 110 | pub fn calculate_price_from_virtual_reserves( 111 | virtual_sol_reserves: u64, 112 | virtual_token_reserves: u64, 113 | ) -> f64 { 114 | if virtual_token_reserves == 0 { 115 | return 0.0; 116 | } 117 | 118 | // Price = virtual_sol_reserves / virtual_token_reserves 119 | (virtual_sol_reserves as f64) / (virtual_token_reserves as f64) 120 | } 121 | 122 | /// Get quote for DexManager interface 123 | pub async fn get_quote( 124 | &self, 125 | input_mint: &str, 126 | output_mint: &str, 127 | amount: u64, 128 | ) -> Result { 129 | let native_mint_str = spl_token::native_mint::ID.to_string(); 130 | 131 | // Determine if this is a buy (SOL -> Token) or sell (Token -> SOL) 132 | let is_buy = input_mint == native_mint_str; 133 | let token_mint = if is_buy { output_mint } else { input_mint }; 134 | 135 | // Get bonding curve info 136 | let pump_program = Pubkey::from_str(PUMP_FUN_PROGRAM)?; 137 | let (_, _, bonding_curve_reserves) = get_bonding_curve_account( 138 | self.rpc_client.clone().unwrap(), 139 | Pubkey::from_str(token_mint)?, 140 | pump_program, 141 | ).await?; 142 | 143 | if is_buy { 144 | // Calculate how many tokens we get for the given SOL amount 145 | Ok(Self::calculate_buy_token_amount( 146 | amount, 147 | bonding_curve_reserves.virtual_sol_reserves, 148 | bonding_curve_reserves.virtual_token_reserves, 149 | )) 150 | } else { 151 | // Calculate how much SOL we get for the given token amount 152 | Ok(Self::calculate_sell_sol_amount( 153 | amount, 154 | bonding_curve_reserves.virtual_sol_reserves, 155 | bonding_curve_reserves.virtual_token_reserves, 156 | )) 157 | } 158 | } 159 | } 160 | 161 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 162 | pub struct RaydiumInfo { 163 | pub base: f64, 164 | pub quote: f64, 165 | pub price: f64, 166 | } 167 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 168 | pub struct PumpInfo { 169 | pub mint: String, 170 | pub bonding_curve: String, 171 | pub associated_bonding_curve: String, 172 | pub raydium_pool: Option, 173 | pub raydium_info: Option, 174 | pub complete: bool, 175 | pub virtual_sol_reserves: u64, 176 | pub virtual_token_reserves: u64, 177 | pub total_supply: u64, 178 | } 179 | 180 | #[derive(Debug, BorshSerialize, BorshDeserialize)] 181 | pub struct BondingCurveAccount { 182 | pub discriminator: u64, 183 | pub virtual_token_reserves: u64, 184 | pub virtual_sol_reserves: u64, 185 | pub real_token_reserves: u64, 186 | pub real_sol_reserves: u64, 187 | pub token_total_supply: u64, 188 | pub complete: bool, 189 | pub creator: Pubkey, // Added missing coin_creator/creator field 190 | } 191 | 192 | #[derive(Debug, Clone)] 193 | pub struct BondingCurveData { 194 | pub discriminator: [u8; 8], 195 | pub virtual_token_reserves: u64, 196 | pub virtual_sol_reserves: u64, 197 | pub real_token_reserves: u64, 198 | pub real_sol_reserves: u64, 199 | pub token_total_supply: u64, 200 | pub complete: bool, 201 | pub creator: Pubkey, 202 | } 203 | 204 | 205 | 206 | #[derive(Debug, BorshSerialize, BorshDeserialize)] 207 | pub struct BondingCurveReserves { 208 | pub virtual_token_reserves: u64, 209 | pub virtual_sol_reserves: u64, 210 | } 211 | 212 | fn max_amount_with_slippage(input_amount: u64, slippage_bps: u64) -> u64 { 213 | input_amount 214 | .checked_mul(slippage_bps.checked_add(TEN_THOUSAND).unwrap()) 215 | .unwrap() 216 | .checked_div(TEN_THOUSAND) 217 | .unwrap() 218 | } 219 | 220 | pub fn get_pda(mint: &Pubkey, program_id: &Pubkey ) -> Result { 221 | let seeds = [b"bonding-curve".as_ref(), mint.as_ref()]; 222 | let (bonding_curve, _bump) = Pubkey::find_program_address(&seeds, program_id); 223 | Ok(bonding_curve) 224 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Solana Volume Bot – Solana Market Maker 2 | 3 | This project is solana marker maker & solana volume bot with a simple, non‑technical setup, it can generate wallets, distribute funds, auto buy/sell, manage Solana WSOL, and push Telegram alerts — a practical token volume simulator for multi‑wallet trading campaigns. 4 | 5 | ### Why this is useful 6 | - **Boost perceived activity**: Simulate realistic buy/sell flows and volume waves. 7 | - **Multi‑wallet stealth**: Rotate across many wallets to avoid obvious patterns. 8 | - **One‑click tasks**: Generate wallets, wrap/unwrap WSOL, distribute and collect SOL with simple commands. 9 | - **Non‑stop operation**: Once started, the market maker runs autonomously with safe defaults. 10 | 11 | --- 12 | 13 | ## Let's Connect!, 14 | 15 | 16 | Gmail 17 | 18 | 19 | Telegram 20 | 21 | 22 | Discord 23 | 24 | 25 | --- 26 | 27 | ## Quick Start 28 | 1. Install Rust and Git on your computer. 29 | 2. Copy this project to your machine. 30 | 3. Create a `.env` file (settings) using the example below. 31 | 4. Run one‑time tasks (optional): generate wallets, distribute SOL, wrap SOL. 32 | 5. Start the bot – it will handle buying/selling automatically. 33 | 34 | --- 35 | 36 | ## Features 37 | - **Stealth market maker**: randomized intervals, buy/sell ratios, wallet rotation 38 | - **Multi‑wallet orchestration**: generate, fund, and operate many wallets 39 | - **WSOL management**: wrap/unwrap SOL, close empty token accounts 40 | - **Safety & monitoring**: guardian mode, price thresholds, Telegram notifications 41 | - **Cache & reliability**: blockhash processing, cache maintenance for smoother ops 42 | 43 | --- 44 | 45 | ## Prerequisites 46 | - Rust toolchain: `https://www.rust-lang.org/tools/install` 47 | - Git: `https://git-scm.com/downloads` 48 | - A Solana wallet private key (base58, long form) with some SOL for fees and trading 49 | - A reliable Solana RPC URL 50 | - Optional: Yellowstone gRPC endpoint and token (for advanced streaming) 51 | 52 | --- 53 | 54 | ## Configure (.env) 55 | Create a file named `.env` in the project root with your settings. You can start with these keys; adjust values for your token and environment. 56 | 57 | ```env 58 | # Required RPC 59 | RPC_HTTP=https://your-solana-rpc.example.com 60 | 61 | # Wallet (base58 long private key) 62 | PRIVATE_KEY=YourBase58PrivateKeyString 63 | 64 | # Yellowstone (optional, recommended for scale) 65 | YELLOWSTONE_GRPC_HTTP=https://grpc.yellowstone.example.com 66 | YELLOWSTONE_GRPC_TOKEN=your-grpc-api-token 67 | 68 | # Target token and DEX selection 69 | TARGET_TOKEN_MINT=YourTokenMintAddress 70 | COIN_CREATOR=CreatorPubkeyIfUsingPumpFun 71 | 72 | # DEX: 0 = Raydium CPMM, 1 = Raydium Launchpad, 2 = Pump.fun 73 | DEX=2 74 | 75 | # Raydium pool config (used only when DEX != 2/Pump.fun) 76 | POOL_ID= 77 | POOL_BASE_ACCOUNT= 78 | POOL_QUOTE_ACCOUNT= 79 | 80 | # Trading amounts and limits 81 | MIN_BUY_AMOUNT=0.02 # SOL amount floor 82 | MAX_BUY_AMOUNT=0.10 # SOL amount cap per trade 83 | MIN_SOL=0.005 # keep minimum SOL in wallet 84 | MINIMAL_BALANCE_FOR_FEE=0.01 # reserve for fees 85 | MINIMAL_WSOL_BALANCE_FOR_TRADING=0.001 86 | 87 | # Fast trading strategy 88 | SELLING_TIME_AFTER_BUYING=1 # seconds to wait before selling 89 | INTERVAL=10 # seconds between trade cycles 90 | 91 | # Advanced randomization & safety 92 | MIN_SELL_DELAY_HOURS=24 93 | MAX_SELL_DELAY_HOURS=72 94 | PRICE_CHANGE_THRESHOLD=0.15 95 | MIN_BUY_RATIO=0.67 96 | MAX_BUY_RATIO=0.73 97 | VOLUME_WAVE_ACTIVE_HOURS=2 98 | VOLUME_WAVE_SLOW_HOURS=6 99 | GUARDIAN_MODE_ENABLED=true 100 | GUARDIAN_DROP_THRESHOLD=0.10 101 | 102 | # Slippage and sizing 103 | SLIPPAGE=10000 # in basis points; capped internally to 25000 104 | TOKEN_AMOUNT=0.001 # default buy quantity (qty mode) 105 | 106 | # Optional helper flags (read by commands) 107 | WALLET_COUNT=100 # used by --wallet generation 108 | WRAP_AMOUNT=0.5 # used by --wrap 109 | IS_CHECK_TARGET_WALLET_TOKEN_ACCOUNT=false 110 | ``` 111 | 112 | Notes: 113 | - For Pump.fun (`DEX=2`), SOL is used directly; WSOL wrapping is skipped in trading. 114 | - For Raydium (`DEX=0` or `1`), set `POOL_ID`, `POOL_BASE_ACCOUNT`, and `POOL_QUOTE_ACCOUNT`. 115 | 116 | --- 117 | 118 | ## Common Tasks (One‑time / On‑demand) 119 | Run from the project root after building. These commands perform a task and then exit. 120 | 121 | ```bash 122 | # 1) Generate wallets (saved under ./wallet) 123 | cargo run --release -- --wallet 124 | 125 | # 2) Distribute SOL from main wallet to all generated wallets 126 | cargo run --release -- --distribute 127 | 128 | # 3) Wrap SOL to WSOL (uses WRAP_AMOUNT from .env) 129 | cargo run --release -- --wrap 130 | 131 | # 4) Unwrap WSOL back to SOL 132 | cargo run --release -- --unwrap 133 | 134 | # 5) Close empty token accounts 135 | cargo run --release -- --close 136 | 137 | # 6) Collect everything back to main wallet 138 | # Sells target tokens, unwraps WSOL, closes empties, sends SOL to main 139 | cargo run --release -- --collect 140 | ``` 141 | 142 | --- 143 | 144 | ## Start the Market Maker (Continuous) 145 | When you run without extra flags, the bot starts its continuous, stealth market‑making loop: 146 | 147 | ```bash 148 | cargo run --release 149 | ``` 150 | 151 | What happens: 152 | - Reads your `.env` into an internal config 153 | - Starts a blockhash processor and cache maintenance service 154 | - Initializes Telegram (if configured) for basic alerts 155 | - Creates a stealth market maker plan (multi‑wallet rotation, randomized intervals/ratios) 156 | - Starts BUY/SELL cycles on your selected DEX for `TARGET_TOKEN_MINT` 157 | 158 | You can stop it with `Ctrl + C`. 159 | 160 | --- 161 | 162 | ## Telegram Alerts 163 | The bot can send basic notifications and crash alerts to Telegram. Configure your Telegram settings as required by your environment or wrapper service. If initialization fails, the bot continues without alerts. 164 | 165 | --- 166 | 167 | ## Safety Tips 168 | - Start with small amounts until you’re comfortable. 169 | - Keep extra SOL in the main wallet to avoid stalls from fees. 170 | - For Raydium, verify pool IDs and accounts before starting. 171 | - Use `--collect` to consolidate after testing/campaigns. 172 | - Always back up your private key; never share it publicly. 173 | 174 | --- 175 | 176 | ## Troubleshooting 177 | - "Missing environment variable" – add the key/value to your `.env`. 178 | - "Invalid PRIVATE_KEY length" – ensure you pasted the full base58 key (long string). 179 | - Transactions not sending – check `RPC_HTTP` reliability and rate limits. 180 | - No buys/sells – confirm `TARGET_TOKEN_MINT`, `DEX` selection, and pool config for Raydium. 181 | - Balance issues – use `--distribute`, `--wrap`, `--unwrap`, `--close`, or `--collect` as appropriate. 182 | 183 | --- 184 | 185 | ## SEO / Discoverability 186 | Keywords: Solana market maker, Solana trading bot, Pump.fun bot, Raydium bot, DEX volume bot, token volume simulator, Solana WSOL, multi‑wallet trading, stealth trading, liquidity simulation. 187 | 188 | GitHub Topics (suggested): `solana` `dex` `trading-bot` `market-maker` `pumpfun` `raydium` `rust` `web3` `blockchain` `liquidity` 189 | 190 | Repository Description (suggested): 191 | “A non‑technical, SEO‑optimized Solana DEX market‑making bot for Pump.fun and Raydium. Generates wallets, distributes funds, and runs stealth buy/sell cycles with randomized timing and Telegram alerts.” 192 | 193 | --- 194 | 195 | ## How it Works (High‑Level) 196 | - Entry point: `src/main.rs` sets up config, blockhash processor, Telegram, cache, and starts the market maker. 197 | - Engines: `src/engine/market_maker.rs` handles BUY/SELL loops with wallet rotation and randomization; `src/engine/random_trader.rs` offers a slower randomized variant. 198 | - DEX layer: `src/dex/` integrates Pump.fun and Raydium; selection controlled by `DEX` in `.env`. 199 | - Helpers: commands in `main` let you generate wallets, wrap/unwrap WSOL, distribute/collect SOL, and clean up accounts. 200 | 201 | --- 202 | 203 | ## 📞 Contact Information 204 | For questions, feedback, or collaboration opportunities, feel free to reach out: 205 | 206 | 📱 **Telegram**: [@A5-](https://t.me/github_a5) 207 | --- 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /src/common/dynamic_ratios.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use tokio::sync::Mutex; 3 | use tokio::time::{Duration, Instant}; 4 | use rand::Rng; 5 | use colored::Colorize; 6 | use chrono::Datelike; 7 | use crate::common::logger::Logger; 8 | 9 | /// Dynamic ratio manager that changes buy/sell ratios weekly 10 | pub struct DynamicRatioManager { 11 | current_buy_ratio: f64, 12 | min_buy_ratio: f64, 13 | max_buy_ratio: f64, 14 | last_change_time: Instant, 15 | change_interval: Duration, 16 | logger: Logger, 17 | } 18 | 19 | impl DynamicRatioManager { 20 | /// Create a new dynamic ratio manager 21 | pub fn new(min_buy_ratio: f64, max_buy_ratio: f64, change_interval_hours: u64) -> Self { 22 | let mut rng = rand::thread_rng(); 23 | let initial_ratio = min_buy_ratio + (max_buy_ratio - min_buy_ratio) * rng.gen::(); 24 | 25 | let logger = Logger::new("[DYNAMIC-RATIOS] => ".purple().bold().to_string()); 26 | logger.log(format!("🎲 Dynamic ratio manager initialized with initial buy ratio: {:.1}%", 27 | initial_ratio * 100.0).purple().to_string()); 28 | 29 | Self { 30 | current_buy_ratio: initial_ratio, 31 | min_buy_ratio, 32 | max_buy_ratio, 33 | last_change_time: Instant::now(), 34 | change_interval: Duration::from_secs(change_interval_hours * 3600), 35 | logger, 36 | } 37 | } 38 | 39 | /// Get the current buy ratio, updating it if needed 40 | pub fn get_current_buy_ratio(&mut self) -> f64 { 41 | let now = Instant::now(); 42 | 43 | // Check if it's time to change the ratio 44 | if now.duration_since(self.last_change_time) >= self.change_interval { 45 | self.update_ratio(); 46 | } 47 | 48 | self.current_buy_ratio 49 | } 50 | 51 | /// Force update the ratio (for testing or manual changes) 52 | pub fn update_ratio(&mut self) { 53 | let mut rng = rand::thread_rng(); 54 | let old_ratio = self.current_buy_ratio; 55 | 56 | // Generate new random ratio within bounds 57 | self.current_buy_ratio = self.min_buy_ratio + 58 | (self.max_buy_ratio - self.min_buy_ratio) * rng.gen::(); 59 | 60 | self.last_change_time = Instant::now(); 61 | 62 | self.logger.log(format!( 63 | "🔄 Buy ratio changed from {:.1}% to {:.1}% (Sell ratio: {:.1}%)", 64 | old_ratio * 100.0, 65 | self.current_buy_ratio * 100.0, 66 | (1.0 - self.current_buy_ratio) * 100.0 67 | ).purple().bold().to_string()); 68 | } 69 | 70 | /// Get time until next ratio change 71 | pub fn time_until_next_change(&self) -> Duration { 72 | let elapsed = Instant::now().duration_since(self.last_change_time); 73 | if elapsed >= self.change_interval { 74 | Duration::from_secs(0) 75 | } else { 76 | self.change_interval - elapsed 77 | } 78 | } 79 | 80 | /// Get ratio statistics 81 | pub fn get_ratio_stats(&self) -> RatioStats { 82 | RatioStats { 83 | current_buy_ratio: self.current_buy_ratio, 84 | current_sell_ratio: 1.0 - self.current_buy_ratio, 85 | min_buy_ratio: self.min_buy_ratio, 86 | max_buy_ratio: self.max_buy_ratio, 87 | last_change_ago: Instant::now().duration_since(self.last_change_time), 88 | next_change_in: self.time_until_next_change(), 89 | } 90 | } 91 | 92 | /// Set custom bounds for buy ratio 93 | pub fn set_ratio_bounds(&mut self, min_buy_ratio: f64, max_buy_ratio: f64) { 94 | self.min_buy_ratio = min_buy_ratio.max(0.0).min(1.0); 95 | self.max_buy_ratio = max_buy_ratio.max(0.0).min(1.0); 96 | 97 | // Ensure min <= max 98 | if self.min_buy_ratio > self.max_buy_ratio { 99 | std::mem::swap(&mut self.min_buy_ratio, &mut self.max_buy_ratio); 100 | } 101 | 102 | // Update current ratio if it's outside new bounds 103 | if self.current_buy_ratio < self.min_buy_ratio { 104 | self.current_buy_ratio = self.min_buy_ratio; 105 | } else if self.current_buy_ratio > self.max_buy_ratio { 106 | self.current_buy_ratio = self.max_buy_ratio; 107 | } 108 | 109 | self.logger.log(format!( 110 | "⚙️ Ratio bounds updated: {:.1}% - {:.1}% (Current: {:.1}%)", 111 | self.min_buy_ratio * 100.0, 112 | self.max_buy_ratio * 100.0, 113 | self.current_buy_ratio * 100.0 114 | ).yellow().to_string()); 115 | } 116 | 117 | /// Apply trend bias to ratios (for market conditions) 118 | pub fn apply_trend_bias(&mut self, bias: TrendBias) { 119 | let bias_factor = match bias { 120 | TrendBias::BullishStrong => 0.1, // +10% towards buying 121 | TrendBias::BullishMild => 0.05, // +5% towards buying 122 | TrendBias::Neutral => 0.0, // No bias 123 | TrendBias::BearishMild => -0.05, // +5% towards selling 124 | TrendBias::BearishStrong => -0.1, // +10% towards selling 125 | }; 126 | 127 | // Apply bias but keep within bounds 128 | let biased_ratio = (self.current_buy_ratio + bias_factor) 129 | .max(self.min_buy_ratio) 130 | .min(self.max_buy_ratio); 131 | 132 | if biased_ratio != self.current_buy_ratio { 133 | let old_ratio = self.current_buy_ratio; 134 | self.current_buy_ratio = biased_ratio; 135 | 136 | self.logger.log(format!( 137 | "📈 Trend bias applied ({:?}): {:.1}% -> {:.1}%", 138 | bias, 139 | old_ratio * 100.0, 140 | self.current_buy_ratio * 100.0 141 | ).blue().to_string()); 142 | } 143 | } 144 | } 145 | 146 | /// Trend bias for adjusting ratios based on market conditions 147 | #[derive(Debug, Clone, Copy)] 148 | pub enum TrendBias { 149 | BullishStrong, 150 | BullishMild, 151 | Neutral, 152 | BearishMild, 153 | BearishStrong, 154 | } 155 | 156 | /// Statistics about current ratio state 157 | #[derive(Debug, Clone)] 158 | pub struct RatioStats { 159 | pub current_buy_ratio: f64, 160 | pub current_sell_ratio: f64, 161 | pub min_buy_ratio: f64, 162 | pub max_buy_ratio: f64, 163 | pub last_change_ago: Duration, 164 | pub next_change_in: Duration, 165 | } 166 | 167 | /// Global dynamic ratio manager instance 168 | pub type GlobalDynamicRatioManager = Arc>; 169 | 170 | /// Create a global dynamic ratio manager 171 | pub fn create_global_dynamic_ratio_manager( 172 | min_buy_ratio: f64, 173 | max_buy_ratio: f64, 174 | change_interval_hours: u64 175 | ) -> GlobalDynamicRatioManager { 176 | Arc::new(Mutex::new(DynamicRatioManager::new( 177 | min_buy_ratio, 178 | max_buy_ratio, 179 | change_interval_hours 180 | ))) 181 | } 182 | 183 | /// Weekly ratio manager with automatic Sunday changes 184 | pub struct WeeklyRatioManager { 185 | dynamic_manager: DynamicRatioManager, 186 | last_sunday: Option, 187 | } 188 | 189 | impl WeeklyRatioManager { 190 | /// Create a weekly ratio manager that changes every Sunday 191 | pub fn new(min_buy_ratio: f64, max_buy_ratio: f64) -> Self { 192 | Self { 193 | dynamic_manager: DynamicRatioManager::new(min_buy_ratio, max_buy_ratio, 168), // 168 hours = 1 week 194 | last_sunday: None, 195 | } 196 | } 197 | 198 | /// Get current ratio, updating if it's a new week 199 | pub fn get_current_buy_ratio(&mut self) -> f64 { 200 | let now = chrono::Utc::now().naive_utc().date(); 201 | let current_sunday = self.get_last_sunday(now); 202 | 203 | // Check if we've entered a new week 204 | if self.last_sunday.is_none() || self.last_sunday.unwrap() != current_sunday { 205 | self.dynamic_manager.update_ratio(); 206 | self.last_sunday = Some(current_sunday); 207 | 208 | self.dynamic_manager.logger.log(format!( 209 | "📅 New week detected (Sunday {}). Ratio updated to {:.1}%", 210 | current_sunday, 211 | self.dynamic_manager.current_buy_ratio * 100.0 212 | ).purple().bold().to_string()); 213 | } 214 | 215 | self.dynamic_manager.current_buy_ratio 216 | } 217 | 218 | /// Get the date of the last Sunday from a given date 219 | fn get_last_sunday(&self, date: chrono::NaiveDate) -> chrono::NaiveDate { 220 | let days_since_sunday = date.weekday().num_days_from_sunday(); 221 | date - chrono::Duration::days(days_since_sunday as i64) 222 | } 223 | 224 | /// Get statistics 225 | pub fn get_stats(&self) -> RatioStats { 226 | self.dynamic_manager.get_ratio_stats() 227 | } 228 | } -------------------------------------------------------------------------------- /src/common/wallet_pool.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs; 3 | use std::path::Path; 4 | use std::sync::Arc; 5 | use anchor_client::solana_sdk::signature::Keypair; 6 | use anchor_client::solana_sdk::signer::Signer; 7 | use colored::Colorize; 8 | use rand::seq::SliceRandom; 9 | use rand::Rng; 10 | use crate::common::logger::Logger; 11 | 12 | /// Wallet profile types that determine trading behavior 13 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 14 | pub enum WalletProfile { 15 | FrequentSeller, // Sells often, shorter hold times 16 | LongTermHolder, // Holds for long periods, rarely sells 17 | BalancedTrader, // Balanced buy/sell behavior 18 | Aggressive, // More frequent trading, higher amounts 19 | Conservative, // Less frequent trading, smaller amounts 20 | } 21 | 22 | impl WalletProfile { 23 | /// Get the sell probability for this wallet profile 24 | pub fn get_sell_probability(&self) -> f64 { 25 | match self { 26 | WalletProfile::FrequentSeller => 0.45, // 45% chance of selling 27 | WalletProfile::LongTermHolder => 0.15, // 15% chance of selling 28 | WalletProfile::BalancedTrader => 0.30, // 30% chance of selling 29 | WalletProfile::Aggressive => 0.35, // 35% chance of selling 30 | WalletProfile::Conservative => 0.25, // 25% chance of selling 31 | } 32 | } 33 | 34 | /// Get the minimum hold time in hours for this wallet profile 35 | pub fn get_min_hold_time_hours(&self) -> u64 { 36 | match self { 37 | WalletProfile::FrequentSeller => 6, // 6 hours minimum 38 | WalletProfile::LongTermHolder => 72, // 72 hours minimum (3 days) 39 | WalletProfile::BalancedTrader => 24, // 24 hours minimum 40 | WalletProfile::Aggressive => 4, // 4 hours minimum 41 | WalletProfile::Conservative => 48, // 48 hours minimum (2 days) 42 | } 43 | } 44 | 45 | /// Get the maximum hold time in hours for this wallet profile 46 | pub fn get_max_hold_time_hours(&self) -> u64 { 47 | match self { 48 | WalletProfile::FrequentSeller => 48, // 48 hours maximum (2 days) 49 | WalletProfile::LongTermHolder => 168, // 168 hours maximum (7 days) 50 | WalletProfile::BalancedTrader => 96, // 96 hours maximum (4 days) 51 | WalletProfile::Aggressive => 24, // 24 hours maximum 52 | WalletProfile::Conservative => 120, // 120 hours maximum (5 days) 53 | } 54 | } 55 | 56 | /// Get the trading amount multiplier for this wallet profile 57 | pub fn get_amount_multiplier(&self) -> f64 { 58 | match self { 59 | WalletProfile::FrequentSeller => 0.8, // 80% of base amount 60 | WalletProfile::LongTermHolder => 1.2, // 120% of base amount 61 | WalletProfile::BalancedTrader => 1.0, // 100% of base amount 62 | WalletProfile::Aggressive => 1.5, // 150% of base amount 63 | WalletProfile::Conservative => 0.6, // 60% of base amount 64 | } 65 | } 66 | 67 | /// Get the trading frequency multiplier for this wallet profile 68 | pub fn get_frequency_multiplier(&self) -> f64 { 69 | match self { 70 | WalletProfile::FrequentSeller => 0.7, // 70% of base interval (more frequent) 71 | WalletProfile::LongTermHolder => 2.0, // 200% of base interval (less frequent) 72 | WalletProfile::BalancedTrader => 1.0, // 100% of base interval 73 | WalletProfile::Aggressive => 0.5, // 50% of base interval (more frequent) 74 | WalletProfile::Conservative => 1.5, // 150% of base interval (less frequent) 75 | } 76 | } 77 | 78 | /// Randomly assign a wallet profile based on realistic distribution 79 | pub fn random_profile() -> Self { 80 | let mut rng = rand::thread_rng(); 81 | let random_value = rng.gen::(); 82 | 83 | match random_value { 84 | x if x < 0.20 => WalletProfile::FrequentSeller, // 20% 85 | x if x < 0.35 => WalletProfile::LongTermHolder, // 15% 86 | x if x < 0.70 => WalletProfile::BalancedTrader, // 35% 87 | x if x < 0.85 => WalletProfile::Aggressive, // 15% 88 | _ => WalletProfile::Conservative, // 15% 89 | } 90 | } 91 | } 92 | 93 | /// Wallet information including profile and trading history 94 | #[derive(Debug, Clone)] 95 | pub struct WalletInfo { 96 | pub keypair: Arc, 97 | pub profile: WalletProfile, 98 | pub usage_count: u32, 99 | pub last_buy_time: Option, 100 | pub last_sell_time: Option, 101 | pub total_buys: u32, 102 | pub total_sells: u32, 103 | pub created_at: tokio::time::Instant, 104 | } 105 | 106 | impl WalletInfo { 107 | /// Update buy statistics 108 | pub fn record_buy(&mut self) { 109 | self.usage_count += 1; 110 | self.total_buys += 1; 111 | self.last_buy_time = Some(tokio::time::Instant::now()); 112 | } 113 | 114 | /// Update sell statistics 115 | pub fn record_sell(&mut self) { 116 | self.usage_count += 1; 117 | self.total_sells += 1; 118 | self.last_sell_time = Some(tokio::time::Instant::now()); 119 | } 120 | } 121 | 122 | 123 | impl WalletPool { 124 | 125 | 126 | /// Load a single wallet from a file 127 | fn load_wallet_from_file(path: &Path) -> Result { 128 | let private_key = fs::read_to_string(path) 129 | .map_err(|e| format!("Failed to read wallet file: {}", e))? 130 | .trim() 131 | .to_string(); 132 | 133 | if private_key.len() < 85 { 134 | return Err(format!("Invalid private key length: {}", private_key.len())); 135 | } 136 | 137 | let keypair = Keypair::from_base58_string(&private_key); 138 | Ok(keypair) 139 | } 140 | 141 | 142 | 143 | /// Record a buy transaction for a wallet 144 | pub fn record_buy_for_wallet(&mut self, wallet_pubkey: &anchor_client::solana_sdk::pubkey::Pubkey) { 145 | if let Some(wallet) = self.wallets.iter_mut().find(|w| w.pubkey() == *wallet_pubkey) { 146 | wallet.record_buy(); 147 | } 148 | } 149 | 150 | /// Get wallet count 151 | pub fn wallet_count(&self) -> usize { 152 | self.wallets.len() 153 | } 154 | 155 | /// Get wallet usage statistics 156 | pub fn get_usage_stats(&self) -> HashMap { 157 | self.wallets.iter() 158 | .map(|w| (w.pubkey().to_string(), w.usage_count)) 159 | .collect() 160 | } 161 | 162 | /// Get wallet profile statistics 163 | pub fn get_profile_stats(&self) -> HashMap { 164 | let mut stats = HashMap::new(); 165 | for wallet in &self.wallets { 166 | *stats.entry(wallet.profile).or_insert(0) += 1; 167 | } 168 | stats 169 | } 170 | 171 | /// Reset usage statistics 172 | pub fn reset_usage_stats(&mut self) { 173 | for wallet in &mut self.wallets { 174 | wallet.usage_count = 0; 175 | } 176 | self.logger.log("📊 Wallet usage statistics reset".yellow().to_string()); 177 | } 178 | 179 | /// Get least used wallets (for balancing) 180 | pub fn get_least_used_wallets(&self, count: usize) -> Vec> { 181 | let mut wallet_pairs: Vec<_> = self.wallets.iter() 182 | .map(|wallet| (wallet.keypair.clone(), wallet.usage_count)) 183 | .collect(); 184 | 185 | // Sort by usage count (ascending) 186 | wallet_pairs.sort_by_key(|(_, usage)| *usage); 187 | 188 | wallet_pairs.into_iter() 189 | .take(count) 190 | .map(|(keypair, _)| keypair) 191 | .collect() 192 | } 193 | 194 | } 195 | 196 | /// Trade type for tracking recent trades 197 | #[derive(Debug, Clone, Copy, PartialEq)] 198 | pub enum TradeType { 199 | Buy, 200 | Sell, 201 | } 202 | 203 | /// Advanced randomization configuration 204 | #[derive(Debug, Clone)] 205 | pub struct RandomizationConfig { 206 | pub min_amount_sol: f64, 207 | pub max_amount_sol: f64, 208 | pub base_buy_interval_ms: u64, 209 | pub base_sell_interval_ms: u64, 210 | pub buy_sell_ratio: f64, // 0.7 = 70% buy, 30% sell 211 | pub wallet_rotation_frequency: u32, // Change wallet every N trades 212 | pub enable_realistic_pauses: bool, 213 | pub max_consecutive_same_wallet: u32, 214 | } 215 | 216 | impl Default for RandomizationConfig { 217 | fn default() -> Self { 218 | Self { 219 | min_amount_sol: 0.03, 220 | max_amount_sol: 0.55, 221 | base_buy_interval_ms: 600_000, // 10 minutes base (600 seconds) 222 | base_sell_interval_ms: 900_000, // 15 minutes base (900 seconds) 223 | buy_sell_ratio: 0.7, 224 | wallet_rotation_frequency: 3, // Change wallet every 3 trades 225 | enable_realistic_pauses: true, 226 | max_consecutive_same_wallet: 5, 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /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 | use crate::dex::raydium_cpmm::RaydiumCPMM; 9 | 10 | /// TTL Cache entry that stores a value with an expiration time 11 | pub struct CacheEntry { 12 | pub value: T, 13 | pub expires_at: Instant, 14 | } 15 | 16 | impl CacheEntry { 17 | pub fn new(value: T, ttl_seconds: u64) -> Self { 18 | Self { 19 | value, 20 | expires_at: Instant::now() + Duration::from_secs(ttl_seconds), 21 | } 22 | } 23 | 24 | pub fn is_expired(&self) -> bool { 25 | Instant::now() > self.expires_at 26 | } 27 | } 28 | 29 | /// Token account cache 30 | pub struct TokenAccountCache { 31 | accounts: RwLock>>>, 32 | default_ttl: u64, 33 | } 34 | 35 | impl TokenAccountCache { 36 | pub fn new(default_ttl: u64) -> Self { 37 | Self { 38 | accounts: RwLock::new(HashMap::new()), 39 | default_ttl, 40 | } 41 | } 42 | 43 | pub fn get(&self, key: &Pubkey) -> Option> { 44 | let accounts = self.accounts.read().unwrap(); 45 | if let Some(entry) = accounts.get(key) { 46 | if !entry.is_expired() { 47 | return Some(entry.value.clone()); 48 | } 49 | } 50 | None 51 | } 52 | 53 | pub fn insert(&self, key: Pubkey, value: StateWithExtensionsOwned, ttl: Option) { 54 | let ttl = ttl.unwrap_or(self.default_ttl); 55 | let mut accounts = self.accounts.write().unwrap(); 56 | accounts.insert(key, CacheEntry::new(value, ttl)); 57 | } 58 | 59 | pub fn remove(&self, key: &Pubkey) { 60 | let mut accounts = self.accounts.write().unwrap(); 61 | accounts.remove(key); 62 | } 63 | 64 | pub fn clear_expired(&self) { 65 | let mut accounts = self.accounts.write().unwrap(); 66 | accounts.retain(|_, entry| !entry.is_expired()); 67 | } 68 | 69 | // Get the current size of the cache 70 | pub fn size(&self) -> usize { 71 | let accounts = self.accounts.read().unwrap(); 72 | accounts.len() 73 | } 74 | } 75 | 76 | /// Token mint cache 77 | pub struct TokenMintCache { 78 | mints: RwLock>>>, 79 | default_ttl: u64, 80 | } 81 | 82 | impl TokenMintCache { 83 | pub fn new(default_ttl: u64) -> Self { 84 | Self { 85 | mints: RwLock::new(HashMap::new()), 86 | default_ttl, 87 | } 88 | } 89 | 90 | pub fn get(&self, key: &Pubkey) -> Option> { 91 | let mints = self.mints.read().unwrap(); 92 | if let Some(entry) = mints.get(key) { 93 | if !entry.is_expired() { 94 | return Some(entry.value.clone()); 95 | } 96 | } 97 | None 98 | } 99 | 100 | pub fn insert(&self, key: Pubkey, value: StateWithExtensionsOwned, ttl: Option) { 101 | let ttl = ttl.unwrap_or(self.default_ttl); 102 | let mut mints = self.mints.write().unwrap(); 103 | mints.insert(key, CacheEntry::new(value, ttl)); 104 | } 105 | 106 | pub fn remove(&self, key: &Pubkey) { 107 | let mut mints = self.mints.write().unwrap(); 108 | mints.remove(key); 109 | } 110 | 111 | pub fn clear_expired(&self) { 112 | let mut mints = self.mints.write().unwrap(); 113 | mints.retain(|_, entry| !entry.is_expired()); 114 | } 115 | 116 | // Get the current size of the cache 117 | pub fn size(&self) -> usize { 118 | let mints = self.mints.read().unwrap(); 119 | mints.len() 120 | } 121 | } 122 | 123 | /// PumpSwap pool cache 124 | pub struct PoolCache { 125 | pools: RwLock>>, 126 | default_ttl: u64, 127 | } 128 | 129 | impl PoolCache { 130 | pub fn new(default_ttl: u64) -> Self { 131 | Self { 132 | pools: RwLock::new(HashMap::new()), 133 | default_ttl, 134 | } 135 | } 136 | 137 | pub fn get(&self, mint: &Pubkey) -> Option { 138 | let pools = self.pools.read().unwrap(); 139 | if let Some(entry) = pools.get(mint) { 140 | if !entry.is_expired() { 141 | return Some(entry.value.clone()); 142 | } 143 | } 144 | None 145 | } 146 | 147 | pub fn insert(&self, mint: Pubkey, pool: RaydiumCPMM, ttl: Option) { 148 | let ttl = ttl.unwrap_or(self.default_ttl); 149 | let mut pools = self.pools.write().unwrap(); 150 | pools.insert(mint, CacheEntry::new(pool, ttl)); 151 | } 152 | 153 | pub fn remove(&self, mint: &Pubkey) { 154 | let mut pools = self.pools.write().unwrap(); 155 | pools.remove(mint); 156 | } 157 | 158 | pub fn clear_expired(&self) { 159 | let mut pools = self.pools.write().unwrap(); 160 | pools.retain(|_, entry| !entry.is_expired()); 161 | } 162 | 163 | // Get the current size of the cache 164 | pub fn size(&self) -> usize { 165 | let pools = self.pools.read().unwrap(); 166 | pools.len() 167 | } 168 | } 169 | 170 | /// Simple wallet token account tracker 171 | pub struct WalletTokenAccounts { 172 | accounts: RwLock>, 173 | } 174 | 175 | impl WalletTokenAccounts { 176 | pub fn new() -> Self { 177 | Self { 178 | accounts: RwLock::new(HashSet::new()), 179 | } 180 | } 181 | 182 | pub fn contains(&self, account: &Pubkey) -> bool { 183 | let accounts = self.accounts.read().unwrap(); 184 | accounts.contains(account) 185 | } 186 | 187 | pub fn insert(&self, account: Pubkey) -> bool { 188 | let mut accounts = self.accounts.write().unwrap(); 189 | accounts.insert(account) 190 | } 191 | 192 | pub fn remove(&self, account: &Pubkey) -> bool { 193 | let mut accounts = self.accounts.write().unwrap(); 194 | accounts.remove(account) 195 | } 196 | 197 | pub fn get_all(&self) -> HashSet { 198 | let accounts = self.accounts.read().unwrap(); 199 | accounts.clone() 200 | } 201 | 202 | pub fn clear(&self) { 203 | let mut accounts = self.accounts.write().unwrap(); 204 | accounts.clear(); 205 | } 206 | 207 | pub fn size(&self) -> usize { 208 | let accounts = self.accounts.read().unwrap(); 209 | accounts.len() 210 | } 211 | } 212 | 213 | /// Target wallet token list tracker 214 | pub struct TargetWalletTokens { 215 | tokens: RwLock>, 216 | } 217 | 218 | impl TargetWalletTokens { 219 | pub fn new() -> Self { 220 | Self { 221 | tokens: RwLock::new(HashSet::new()), 222 | } 223 | } 224 | 225 | pub fn contains(&self, token_mint: &str) -> bool { 226 | let tokens = self.tokens.read().unwrap(); 227 | tokens.contains(token_mint) 228 | } 229 | 230 | pub fn insert(&self, token_mint: String) -> bool { 231 | let mut tokens = self.tokens.write().unwrap(); 232 | tokens.insert(token_mint) 233 | } 234 | 235 | pub fn remove(&self, token_mint: &str) -> bool { 236 | let mut tokens = self.tokens.write().unwrap(); 237 | tokens.remove(token_mint) 238 | } 239 | 240 | pub fn get_all(&self) -> HashSet { 241 | let tokens = self.tokens.read().unwrap(); 242 | tokens.clone() 243 | } 244 | 245 | pub fn clear(&self) { 246 | let mut tokens = self.tokens.write().unwrap(); 247 | tokens.clear(); 248 | } 249 | 250 | pub fn size(&self) -> usize { 251 | let tokens = self.tokens.read().unwrap(); 252 | tokens.len() 253 | } 254 | } 255 | 256 | /// Bought token tracking information 257 | #[derive(Clone, Debug)] 258 | pub struct BoughtTokenInfo { 259 | pub mint: String, 260 | pub token_account: Pubkey, 261 | pub amount: f64, 262 | pub buy_time: Instant, 263 | pub buy_signature: String, 264 | pub protocol: String, 265 | } 266 | 267 | /// Bought tokens tracker 268 | pub struct BoughtTokensTracker { 269 | tokens: RwLock>, 270 | } 271 | 272 | impl BoughtTokensTracker { 273 | pub fn new() -> Self { 274 | Self { 275 | tokens: RwLock::new(HashMap::new()), 276 | } 277 | } 278 | 279 | pub fn add_bought_token(&self, mint: String, token_account: Pubkey, amount: f64, buy_signature: String, protocol: String) { 280 | let mut tokens = self.tokens.write().unwrap(); 281 | tokens.insert(mint.clone(), BoughtTokenInfo { 282 | mint, 283 | token_account, 284 | amount, 285 | buy_time: Instant::now(), 286 | buy_signature, 287 | protocol, 288 | }); 289 | } 290 | 291 | pub fn has_token(&self, mint: &str) -> bool { 292 | let tokens = self.tokens.read().unwrap(); 293 | tokens.contains_key(mint) 294 | } 295 | 296 | pub fn get_token_info(&self, mint: &str) -> Option { 297 | let tokens = self.tokens.read().unwrap(); 298 | tokens.get(mint).cloned() 299 | } 300 | 301 | pub fn remove_token(&self, mint: &str) -> bool { 302 | let mut tokens = self.tokens.write().unwrap(); 303 | tokens.remove(mint).is_some() 304 | } 305 | 306 | pub fn get_all_tokens(&self) -> Vec { 307 | let tokens = self.tokens.read().unwrap(); 308 | tokens.values().cloned().collect() 309 | } 310 | 311 | pub fn clear(&self) { 312 | let mut tokens = self.tokens.write().unwrap(); 313 | tokens.clear(); 314 | } 315 | 316 | pub fn size(&self) -> usize { 317 | let tokens = self.tokens.read().unwrap(); 318 | tokens.len() 319 | } 320 | 321 | pub fn update_token_balance(&self, mint: &str, new_amount: f64) { 322 | let mut tokens = self.tokens.write().unwrap(); 323 | if let Some(token_info) = tokens.get_mut(mint) { 324 | token_info.amount = new_amount; 325 | } 326 | } 327 | } 328 | 329 | // Global cache instances with reasonable TTL values 330 | lazy_static! { 331 | pub static ref TOKEN_ACCOUNT_CACHE: TokenAccountCache = TokenAccountCache::new(60); // 60 seconds TTL 332 | pub static ref TOKEN_MINT_CACHE: TokenMintCache = TokenMintCache::new(300); // 5 minutes TTL 333 | pub static ref POOL_CACHE: PoolCache = PoolCache::new(30); // 30 seconds TTL 334 | pub static ref WALLET_TOKEN_ACCOUNTS: WalletTokenAccounts = WalletTokenAccounts::new(); 335 | pub static ref TARGET_WALLET_TOKENS: TargetWalletTokens = TargetWalletTokens::new(); 336 | pub static ref BOUGHT_TOKENS: BoughtTokensTracker = BoughtTokensTracker::new(); 337 | } -------------------------------------------------------------------------------- /src/common/guardian_mode.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use tokio::sync::Mutex; 3 | use tokio::time::{Duration, Instant}; 4 | use std::collections::VecDeque; 5 | use colored::Colorize; 6 | use crate::common::logger::Logger; 7 | 8 | /// Guardian mode manager that protects against rapid price drops 9 | pub struct GuardianMode { 10 | enabled: bool, 11 | drop_threshold: f64, 12 | price_history: VecDeque, 13 | guardian_active: bool, 14 | activation_time: Option, 15 | guardian_duration: Duration, 16 | logger: Logger, 17 | intervention_strength: InterventionStrength, 18 | cooldown_period: Duration, 19 | last_intervention: Option, 20 | } 21 | 22 | impl GuardianMode { 23 | /// Create a new guardian mode instance 24 | pub fn new(enabled: bool, drop_threshold: f64) -> Self { 25 | let logger = Logger::new("[GUARDIAN-MODE] => ".red().bold().to_string()); 26 | 27 | if enabled { 28 | logger.log(format!("🛡️ Guardian mode initialized (Drop threshold: {:.1}%)", 29 | drop_threshold * 100.0).green().to_string()); 30 | } else { 31 | logger.log("🛡️ Guardian mode disabled".yellow().to_string()); 32 | } 33 | 34 | Self { 35 | enabled, 36 | drop_threshold, 37 | price_history: VecDeque::with_capacity(50), 38 | guardian_active: false, 39 | activation_time: None, 40 | guardian_duration: Duration::from_secs(30 * 60), // Active for 30 minutes 41 | logger, 42 | intervention_strength: InterventionStrength::Medium, 43 | cooldown_period: Duration::from_secs(2 * 60 * 60), // 2 hour cooldown between interventions 44 | last_intervention: None, 45 | } 46 | } 47 | 48 | /// Add a new price point and check for intervention triggers 49 | pub fn add_price_point(&mut self, price: f64, volume: f64) { 50 | if !self.enabled { 51 | return; 52 | } 53 | 54 | let snapshot = PriceSnapshot { 55 | price, 56 | volume, 57 | timestamp: Instant::now(), 58 | }; 59 | 60 | self.price_history.push_back(snapshot); 61 | 62 | // Keep only recent price history (last 30 minutes) 63 | let cutoff_time = Instant::now() - Duration::from_secs(30 * 60); 64 | while let Some(front) = self.price_history.front() { 65 | if front.timestamp < cutoff_time { 66 | self.price_history.pop_front(); 67 | } else { 68 | break; 69 | } 70 | } 71 | 72 | // Check if we should activate guardian mode 73 | self.check_activation_trigger(); 74 | 75 | // Update guardian status 76 | self.update_guardian_status(); 77 | } 78 | 79 | /// Check if conditions are met to activate guardian mode 80 | fn check_activation_trigger(&mut self) { 81 | if self.guardian_active || self.price_history.len() < 5 { 82 | return; 83 | } 84 | 85 | // Check cooldown period 86 | if let Some(last_intervention) = self.last_intervention { 87 | if Instant::now().duration_since(last_intervention) < self.cooldown_period { 88 | return; 89 | } 90 | } 91 | 92 | // Analyze price drop over different time windows 93 | let drop_detected = self.detect_rapid_drop(); 94 | 95 | if drop_detected { 96 | self.activate_guardian(); 97 | } 98 | } 99 | 100 | /// Detect rapid price drops using multiple time windows 101 | fn detect_rapid_drop(&self) -> bool { 102 | let now = Instant::now(); 103 | 104 | // Check 5-minute drop 105 | let five_min_drop = self.calculate_price_drop(Duration::from_secs(5 * 60)); 106 | 107 | // Check 10-minute drop 108 | let ten_min_drop = self.calculate_price_drop(Duration::from_secs(10 * 60)); 109 | 110 | // Check 15-minute drop 111 | let fifteen_min_drop = self.calculate_price_drop(Duration::from_secs(15 * 60)); 112 | 113 | // Trigger if any timeframe exceeds threshold 114 | let rapid_drop = five_min_drop > self.drop_threshold || 115 | ten_min_drop > self.drop_threshold * 0.8 || // Slightly lower threshold for longer timeframe 116 | fifteen_min_drop > self.drop_threshold * 0.7; 117 | 118 | if rapid_drop { 119 | self.logger.log(format!( 120 | "📉 Rapid price drop detected! 5min: {:.1}%, 10min: {:.1}%, 15min: {:.1}%", 121 | five_min_drop * 100.0, 122 | ten_min_drop * 100.0, 123 | fifteen_min_drop * 100.0 124 | ).red().bold().to_string()); 125 | } 126 | 127 | rapid_drop 128 | } 129 | 130 | /// Calculate price drop over a specific duration 131 | fn calculate_price_drop(&self, duration: Duration) -> f64 { 132 | let cutoff_time = Instant::now() - duration; 133 | 134 | // Find earliest price in the timeframe 135 | let earliest_price = self.price_history 136 | .iter() 137 | .find(|snapshot| snapshot.timestamp >= cutoff_time) 138 | .map(|snapshot| snapshot.price); 139 | 140 | // Get latest price 141 | let latest_price = self.price_history 142 | .back() 143 | .map(|snapshot| snapshot.price); 144 | 145 | match (earliest_price, latest_price) { 146 | (Some(early), Some(late)) if early > 0.0 => { 147 | (early - late) / early // Positive value indicates drop 148 | }, 149 | _ => 0.0, 150 | } 151 | } 152 | 153 | /// Activate guardian mode 154 | fn activate_guardian(&mut self) { 155 | self.guardian_active = true; 156 | self.activation_time = Some(Instant::now()); 157 | self.last_intervention = Some(Instant::now()); 158 | 159 | // Determine intervention strength based on drop severity 160 | let recent_drop = self.calculate_price_drop(Duration::from_secs(5 * 60)); 161 | self.intervention_strength = if recent_drop > self.drop_threshold * 1.5 { 162 | InterventionStrength::Strong 163 | } else if recent_drop > self.drop_threshold * 1.2 { 164 | InterventionStrength::Medium 165 | } else { 166 | InterventionStrength::Light 167 | }; 168 | 169 | self.logger.log(format!( 170 | "🚨 GUARDIAN MODE ACTIVATED! Strength: {:?} | Drop: {:.1}%", 171 | self.intervention_strength, 172 | recent_drop * 100.0 173 | ).red().bold().to_string()); 174 | } 175 | 176 | /// Update guardian mode status (deactivate if duration exceeded) 177 | fn update_guardian_status(&mut self) { 178 | if !self.guardian_active { 179 | return; 180 | } 181 | 182 | if let Some(activation_time) = self.activation_time { 183 | if Instant::now().duration_since(activation_time) >= self.guardian_duration { 184 | self.deactivate_guardian(); 185 | } 186 | } 187 | } 188 | 189 | /// Deactivate guardian mode 190 | fn deactivate_guardian(&mut self) { 191 | self.guardian_active = false; 192 | self.activation_time = None; 193 | 194 | self.logger.log("✅ Guardian mode deactivated".green().to_string()); 195 | } 196 | 197 | /// Check if guardian mode is currently active 198 | pub fn is_active(&self) -> bool { 199 | self.guardian_active 200 | } 201 | 202 | /// Get the current intervention strength 203 | pub fn get_intervention_strength(&self) -> InterventionStrength { 204 | if self.guardian_active { 205 | self.intervention_strength 206 | } else { 207 | InterventionStrength::None 208 | } 209 | } 210 | 211 | /// Get frequency multiplier (faster trading when guardian is active) 212 | pub fn get_frequency_multiplier(&self) -> f64 { 213 | if !self.guardian_active { 214 | return 1.0; 215 | } 216 | 217 | match self.intervention_strength { 218 | InterventionStrength::None => 1.0, 219 | InterventionStrength::Light => 0.7, // 30% faster 220 | InterventionStrength::Medium => 0.5, // 50% faster 221 | InterventionStrength::Strong => 0.3, // 70% faster 222 | } 223 | } 224 | 225 | /// Get buy bias (increased probability of buying when guardian is active) 226 | pub fn get_buy_bias(&self) -> f64 { 227 | if !self.guardian_active { 228 | return 0.0; 229 | } 230 | 231 | match self.intervention_strength { 232 | InterventionStrength::None => 0.0, 233 | InterventionStrength::Light => 0.1, // +10% buy probability 234 | InterventionStrength::Medium => 0.2, // +20% buy probability 235 | InterventionStrength::Strong => 0.3, // +30% buy probability 236 | } 237 | } 238 | 239 | /// Get amount multiplier (larger trades when guardian is active) 240 | pub fn get_amount_multiplier(&self) -> f64 { 241 | if !self.guardian_active { 242 | return 1.0; 243 | } 244 | 245 | match self.intervention_strength { 246 | InterventionStrength::None => 1.0, 247 | InterventionStrength::Light => 1.2, // 20% larger trades 248 | InterventionStrength::Medium => 1.5, // 50% larger trades 249 | InterventionStrength::Strong => 2.0, // 100% larger trades 250 | } 251 | } 252 | 253 | /// Get guardian status information 254 | pub fn get_status(&self) -> GuardianStatus { 255 | let time_remaining = if let Some(activation_time) = self.activation_time { 256 | self.guardian_duration.saturating_sub(Instant::now().duration_since(activation_time)) 257 | } else { 258 | Duration::from_secs(0) 259 | }; 260 | 261 | let cooldown_remaining = if let Some(last_intervention) = self.last_intervention { 262 | self.cooldown_period.saturating_sub(Instant::now().duration_since(last_intervention)) 263 | } else { 264 | Duration::from_secs(0) 265 | }; 266 | 267 | GuardianStatus { 268 | enabled: self.enabled, 269 | active: self.guardian_active, 270 | intervention_strength: self.get_intervention_strength(), 271 | time_remaining, 272 | cooldown_remaining, 273 | recent_price_drop: self.calculate_price_drop(Duration::from_secs(5 * 60)), 274 | } 275 | } 276 | 277 | /// Force activate guardian mode (for testing) 278 | pub fn force_activate(&mut self, strength: InterventionStrength) { 279 | self.intervention_strength = strength; 280 | self.activate_guardian(); 281 | } 282 | 283 | /// Force deactivate guardian mode 284 | pub fn force_deactivate(&mut self) { 285 | self.deactivate_guardian(); 286 | } 287 | 288 | /// Update settings 289 | pub fn update_settings(&mut self, enabled: bool, drop_threshold: f64) { 290 | self.enabled = enabled; 291 | self.drop_threshold = drop_threshold; 292 | 293 | self.logger.log(format!( 294 | "⚙️ Guardian settings updated: Enabled: {}, Threshold: {:.1}%", 295 | enabled, drop_threshold * 100.0 296 | ).yellow().to_string()); 297 | } 298 | } 299 | 300 | /// Price snapshot for tracking price history 301 | #[derive(Debug, Clone)] 302 | struct PriceSnapshot { 303 | price: f64, 304 | volume: f64, 305 | timestamp: Instant, 306 | } 307 | 308 | /// Intervention strength levels 309 | #[derive(Debug, Clone, Copy, PartialEq)] 310 | pub enum InterventionStrength { 311 | None, 312 | Light, 313 | Medium, 314 | Strong, 315 | } 316 | 317 | /// Guardian mode status information 318 | #[derive(Debug, Clone)] 319 | pub struct GuardianStatus { 320 | pub enabled: bool, 321 | pub active: bool, 322 | pub intervention_strength: InterventionStrength, 323 | pub time_remaining: Duration, 324 | pub cooldown_remaining: Duration, 325 | pub recent_price_drop: f64, 326 | } 327 | 328 | /// Global guardian mode instance 329 | pub type GlobalGuardianMode = Arc>; 330 | 331 | /// Create a global guardian mode instance 332 | pub fn create_global_guardian_mode(enabled: bool, drop_threshold: f64) -> GlobalGuardianMode { 333 | Arc::new(Mutex::new(GuardianMode::new(enabled, drop_threshold))) 334 | } 335 | 336 | /// Guardian mode configuration 337 | #[derive(Debug, Clone)] 338 | pub struct GuardianConfig { 339 | pub enabled: bool, 340 | pub drop_threshold: f64, 341 | pub guardian_duration_minutes: u64, 342 | pub cooldown_hours: u64, 343 | pub max_interventions_per_day: u32, 344 | } 345 | 346 | impl Default for GuardianConfig { 347 | fn default() -> Self { 348 | Self { 349 | enabled: true, 350 | drop_threshold: 0.10, // 10% drop triggers guardian 351 | guardian_duration_minutes: 30, 352 | cooldown_hours: 2, 353 | max_interventions_per_day: 6, 354 | } 355 | } 356 | } -------------------------------------------------------------------------------- /src/engine/random_trader.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::time::Duration; 3 | use tokio::time::{sleep, Instant}; 4 | use anyhow::Result; 5 | use colored::Colorize; 6 | use anchor_client::solana_sdk::signature::Signature; 7 | use anchor_client::solana_sdk::signer::Signer; 8 | use std::sync::atomic::{AtomicU64, Ordering}; 9 | use anchor_client::solana_client::rpc_config::RpcSendTransactionConfig; 10 | use anchor_client::solana_sdk::commitment_config::CommitmentLevel; 11 | use solana_transaction_status; 12 | 13 | use crate::{ 14 | common::{config::{AppState, Config}, logger::Logger}, 15 | dex::raydium_cpmm::RaydiumCPMM, 16 | engine::swap::{SwapDirection, SwapInType}, 17 | common::config::SwapConfig, 18 | }; 19 | 20 | #[derive(Clone)] 21 | pub struct RandomTrader { 22 | app_state: Arc, 23 | raydium_cpmm: RaydiumCPMM, 24 | target_mint: String, 25 | logger: Logger, 26 | is_running: Arc>, 27 | counter: Arc, // For deterministic "randomness" 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | pub struct RandomTraderConfig { 32 | pub min_buy_amount: f64, 33 | pub max_buy_amount: f64, 34 | pub min_sell_percentage: f64, 35 | pub max_sell_percentage: f64, 36 | pub min_interval_seconds: u64, 37 | pub max_interval_seconds: u64, 38 | } 39 | 40 | impl Default for RandomTraderConfig { 41 | fn default() -> Self { 42 | Self { 43 | min_buy_amount: 0.001, // 0.001 SOL minimum 44 | max_buy_amount: 0.01, // 0.01 SOL maximum 45 | min_sell_percentage: 0.1, // 10% minimum 46 | max_sell_percentage: 0.5, // 50% maximum 47 | min_interval_seconds: 30, // 30 seconds minimum 48 | max_interval_seconds: 300, // 5 minutes maximum 49 | } 50 | } 51 | } 52 | 53 | impl RandomTrader { 54 | pub fn new(app_state: Arc, target_mint: String, pool_id: String, pool_base_account: String, pool_quote_account: String) -> Result { 55 | let raydium_cpmm = RaydiumCPMM::new( 56 | app_state.wallet.clone(), 57 | Some(app_state.rpc_client.clone()), 58 | Some(app_state.rpc_nonblocking_client.clone()), 59 | pool_id, 60 | pool_base_account, 61 | pool_quote_account, 62 | ).map_err(|e| anyhow::anyhow!("Failed to create RaydiumCPMM instance: {}", e))?; 63 | 64 | Ok(Self { 65 | app_state, 66 | raydium_cpmm, 67 | target_mint, 68 | logger: Logger::new("[RANDOM-TRADER] => ".magenta().to_string()), 69 | is_running: Arc::new(tokio::sync::RwLock::new(false)), 70 | counter: Arc::new(AtomicU64::new(0)), 71 | }) 72 | } 73 | 74 | /// Generate pseudo-random number using atomic counter 75 | fn next_pseudo_random(&self) -> u64 { 76 | let counter = self.counter.fetch_add(1, Ordering::SeqCst); 77 | // Simple linear congruential generator 78 | (counter.wrapping_mul(1103515245).wrapping_add(12345)) & 0x7fffffff 79 | } 80 | 81 | /// Generate random value in range using pseudo-random 82 | fn random_in_range(&self, min: u64, max: u64) -> u64 { 83 | if min >= max { 84 | return min; 85 | } 86 | let range = max - min; 87 | let random = self.next_pseudo_random(); 88 | min + (random % range) 89 | } 90 | 91 | /// Generate random float in range 92 | fn random_float_in_range(&self, min: f64, max: f64) -> f64 { 93 | if min >= max { 94 | return min; 95 | } 96 | let random = self.next_pseudo_random() as f64 / (0x7fffffff as f64); 97 | min + (max - min) * random 98 | } 99 | 100 | /// Start the random trading engine with buy-then-sell pattern using SELLING_TIME_AFTER_BUYING 101 | pub async fn start(&self, config: RandomTraderConfig) -> Result<()> { 102 | { 103 | let mut running = self.is_running.write().await; 104 | if *running { 105 | return Err(anyhow::anyhow!("Random trader is already running")); 106 | } 107 | *running = true; 108 | } 109 | 110 | self.logger.log("Starting buy-then-sell trading engine...".green().to_string()); 111 | self.logger.log(format!("Target mint: {}", self.target_mint)); 112 | self.logger.log(format!("Config: {:?}", config)); 113 | 114 | // Get SELLING_TIME_AFTER_BUYING from global config 115 | let selling_delay = { 116 | let global_config = Config::get().await; 117 | global_config.selling_time_after_buying 118 | }; 119 | 120 | self.logger.log(format!("🕐 Selling delay after buying: {} seconds", selling_delay).cyan().to_string()); 121 | 122 | // Main trading loop: buy -> wait -> sell -> repeat 123 | while self.is_running().await { 124 | // Generate random interval before next cycle 125 | let cycle_interval = self.random_in_range(config.min_interval_seconds, config.max_interval_seconds); 126 | self.logger.log(format!("⏰ Next trading cycle in {} seconds", cycle_interval).yellow().to_string()); 127 | sleep(Duration::from_secs(cycle_interval)).await; 128 | 129 | if !self.is_running().await { 130 | break; 131 | } 132 | 133 | // Step 1: Execute buy 134 | self.logger.log("💰 STEP 1: Executing BUY...".green().bold().to_string()); 135 | match self.execute_random_buy(&config).await { 136 | Ok(()) => { 137 | self.logger.log("✅ Buy successful, waiting before selling...".green().to_string()); 138 | 139 | // Step 2: Wait for SELLING_TIME_AFTER_BUYING 140 | self.logger.log(format!("⏳ STEP 2: Waiting {} seconds before selling...", selling_delay).yellow().to_string()); 141 | sleep(Duration::from_secs(selling_delay)).await; 142 | 143 | if !self.is_running().await { 144 | break; 145 | } 146 | 147 | // Step 3: Execute sell (100% of tokens) 148 | self.logger.log("💸 STEP 3: Executing SELL ALL...".blue().bold().to_string()); 149 | if let Err(e) = self.execute_sell_all().await { 150 | self.logger.log(format!("❌ Sell failed: {}", e).red().to_string()); 151 | // Continue to next cycle even if sell fails 152 | } 153 | }, 154 | Err(e) => { 155 | self.logger.log(format!("❌ Buy failed: {}", e).red().to_string()); 156 | // Continue to next cycle even if buy fails 157 | } 158 | } 159 | } 160 | 161 | Ok(()) 162 | } 163 | 164 | /// Stop the random trading engine 165 | pub async fn stop(&self) { 166 | let mut running = self.is_running.write().await; 167 | *running = false; 168 | self.logger.log("Random trading engine stopped".red().to_string()); 169 | } 170 | 171 | /// Check if the trader is running 172 | pub async fn is_running(&self) -> bool { 173 | *self.is_running.read().await 174 | } 175 | 176 | /// Execute a random buy 177 | async fn execute_random_buy(&self, config: &RandomTraderConfig) -> Result<()> { 178 | // Calculate random amount 179 | let buy_amount = self.random_float_in_range(config.min_buy_amount, config.max_buy_amount); 180 | 181 | self.logger.log(format!( 182 | "Executing random buy - Amount: {} SOL", 183 | buy_amount 184 | ).green().to_string()); 185 | 186 | // Create swap config for buy 187 | let swap_config = SwapConfig { 188 | mint: self.target_mint.clone(), 189 | swap_direction: SwapDirection::Buy, 190 | in_type: SwapInType::Qty, 191 | amount_in: buy_amount, 192 | slippage: 1000, // 10% slippage 193 | max_buy_amount: buy_amount, 194 | }; 195 | 196 | // Execute the swap 197 | let start_time = Instant::now(); 198 | match self.raydium_cpmm.build_swap_from_default_info(swap_config).await { 199 | Ok((keypair, instructions, token_price)) => { 200 | self.logger.log(format!("Token price: ${:.8}", token_price)); 201 | 202 | // Send transaction 203 | match self.send_swap_transaction(&keypair, instructions).await { 204 | Ok(signature) => { 205 | self.logger.log(format!( 206 | "✅ Random buy successful! Amount: {} SOL, Signature: {}, Time: {:?}", 207 | buy_amount, signature, start_time.elapsed() 208 | ).green().bold().to_string()); 209 | }, 210 | Err(e) => { 211 | self.logger.log(format!("❌ Random buy transaction failed: {}", e).red().to_string()); 212 | return Err(e); 213 | } 214 | } 215 | }, 216 | Err(e) => { 217 | self.logger.log(format!("❌ Random buy preparation failed: {}", e).red().to_string()); 218 | return Err(e); 219 | } 220 | } 221 | 222 | Ok(()) 223 | } 224 | 225 | /// Execute sell all tokens (100%) 226 | async fn execute_sell_all(&self) -> Result<()> { 227 | self.logger.log("Executing sell ALL tokens (100%)".blue().to_string()); 228 | 229 | // Create swap config for selling 100% of tokens 230 | let swap_config = SwapConfig { 231 | mint: self.target_mint.clone(), 232 | swap_direction: SwapDirection::Sell, 233 | in_type: SwapInType::Pct, 234 | amount_in: 1.0, // Sell 100% of tokens 235 | slippage: 1000, // 10% slippage 236 | max_buy_amount: 0.0, // Not used for sells 237 | }; 238 | 239 | // Execute the swap 240 | let start_time = Instant::now(); 241 | match self.raydium_cpmm.build_swap_from_default_info(swap_config).await { 242 | Ok((keypair, instructions, token_price)) => { 243 | self.logger.log(format!("Token price: ${:.8}", token_price)); 244 | 245 | // Send transaction 246 | match self.send_swap_transaction(&keypair, instructions).await { 247 | Ok(signature) => { 248 | self.logger.log(format!( 249 | "✅ Sell ALL successful! Percentage: 100%, Signature: {}, Time: {:?}", 250 | signature, start_time.elapsed() 251 | ).blue().bold().to_string()); 252 | }, 253 | Err(e) => { 254 | self.logger.log(format!("❌ Sell ALL transaction failed: {}", e).red().to_string()); 255 | return Err(e); 256 | } 257 | } 258 | }, 259 | Err(e) => { 260 | self.logger.log(format!("❌ Sell ALL preparation failed: {}", e).red().to_string()); 261 | return Err(e); 262 | } 263 | } 264 | 265 | Ok(()) 266 | } 267 | 268 | /// Send swap transaction to the network (SKIP SIMULATION for on-chain testing) 269 | async fn send_swap_transaction( 270 | &self, 271 | keypair: &Arc, 272 | instructions: Vec, 273 | ) -> Result { 274 | use anchor_client::solana_sdk::transaction::Transaction; 275 | 276 | // Get recent blockhash 277 | let recent_blockhash = self.app_state.rpc_client 278 | .get_latest_blockhash() 279 | .map_err(|e| anyhow::anyhow!("Failed to get recent blockhash: {}", e))?; 280 | 281 | // Create and sign transaction 282 | let transaction = Transaction::new_signed_with_payer( 283 | &instructions, 284 | Some(&keypair.pubkey()), 285 | &[keypair.as_ref()], 286 | recent_blockhash, 287 | ); 288 | 289 | self.logger.log("🚀 Sending swap transaction with SKIP SIMULATION for on-chain testing".yellow().to_string()); 290 | self.logger.log(format!("📊 Transaction size: {} bytes", transaction.message_data().len()).cyan().to_string()); 291 | 292 | // Configure to skip simulation for on-chain testing 293 | let config = RpcSendTransactionConfig { 294 | skip_preflight: true, 295 | preflight_commitment: Some(CommitmentLevel::Finalized.into()), 296 | encoding: Some(solana_transaction_status::UiTransactionEncoding::Base64), 297 | max_retries: Some(0), // No retries to see exact error 298 | min_context_slot: None, 299 | }; 300 | 301 | // Send transaction directly to blockchain (skip simulation) 302 | let signature = self.app_state.rpc_nonblocking_client 303 | .send_transaction_with_config(&transaction, config) 304 | .await 305 | .map_err(|e| anyhow::anyhow!("Failed to send swap transaction (skip simulation): {}", e))?; 306 | 307 | self.logger.log(format!("🎯 ON-CHAIN swap transaction sent (simulation bypassed): {}", signature).green().to_string()); 308 | self.logger.log(format!("🔗 Check transaction: https://solscan.io/tx/{}", signature).blue().to_string()); 309 | 310 | Ok(signature) 311 | } 312 | } --------------------------------------------------------------------------------