├── .gitignore ├── src ├── core │ ├── mod.rs │ ├── token.rs │ └── tx.rs ├── common │ ├── mod.rs │ ├── utils.rs │ └── logger.rs ├── engine │ ├── mod.rs │ └── monitor │ │ └── mod.rs ├── services │ └── mod.rs ├── dex │ ├── mod.rs │ ├── pump_fun.rs │ └── raydium.rs ├── lib.rs └── main.rs ├── Cargo.toml ├── Cargo.lock ├── .env.example └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | target 3 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token; 2 | pub mod tx; 3 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod logger; 2 | pub mod utils; 3 | -------------------------------------------------------------------------------- /src/engine/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod monitor; 2 | pub mod swap; 3 | -------------------------------------------------------------------------------- /src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod jito; 2 | pub mod nextblock; 3 | -------------------------------------------------------------------------------- /src/engine/monitor/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod helius; 2 | pub mod yellowstone; 3 | -------------------------------------------------------------------------------- /src/dex/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pump_fun; 2 | pub mod raydium; 3 | pub mod meteora; 4 | pub mod orca; 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "raydium-pump-snipe-bot" 3 | version = "0.1.0" 4 | edition = "2021" 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | pub mod core; 3 | pub mod dex; 4 | pub mod engine; 5 | pub mod services; 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "raydium-pump-snipe-bot" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | RPC_HTTPS= 2 | RPC_WSS= 3 | DEVNET_RPC_HTTPS= 4 | RAYDIUM_LPV4= 5 | LOG_INSTRUCTION= 6 | PRIVATE_KEY= 7 | SLIPPAGE=10 8 | JITO_BLOCK_ENGINE_URL= 9 | JITO_TIP_STREAM_URL= 10 | JITO_TIP_PERCENTILE= 11 | JITO_TIP_VALUE=0.004 12 | UNIT_PRICE= 13 | UNIT_LIMIT= 14 | -------------------------------------------------------------------------------- /src/common/utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair}; 3 | use std::{env, sync::Arc}; 4 | 5 | #[derive(Clone)] 6 | pub struct AppState { 7 | pub rpc_client: Arc, 8 | pub rpc_nonblocking_client: Arc, 9 | pub wallet: Arc, 10 | } 11 | 12 | pub fn import_env_var(key: &str) -> String { 13 | env::var(key).unwrap_or_else(|_| panic!("Environment variable {} is not set", key)) 14 | } 15 | 16 | pub fn create_rpc_client() -> Result> { 17 | let rpc_https = import_env_var("RPC_HTTPS"); 18 | let rpc_client = solana_client::rpc_client::RpcClient::new_with_commitment( 19 | rpc_https, 20 | CommitmentConfig::processed(), 21 | ); 22 | Ok(Arc::new(rpc_client)) 23 | } 24 | 25 | pub async fn create_nonblocking_rpc_client( 26 | ) -> Result> { 27 | let rpc_https = import_env_var("RPC_HTTPS"); 28 | let rpc_client = solana_client::nonblocking::rpc_client::RpcClient::new_with_commitment( 29 | rpc_https, 30 | CommitmentConfig::processed(), 31 | ); 32 | Ok(Arc::new(rpc_client)) 33 | } 34 | 35 | pub fn import_wallet() -> Result> { 36 | let priv_key = import_env_var("PRIVATE_KEY"); 37 | let wallet: Keypair = Keypair::from_base58_string(priv_key.as_str()); 38 | 39 | Ok(Arc::new(wallet)) 40 | } 41 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use raydium_pump_snipe_bot::{ 3 | common::{ 4 | logger::Logger, 5 | utils::{ 6 | create_nonblocking_rpc_client, create_rpc_client, import_env_var, import_wallet, 7 | AppState, 8 | }, 9 | }, 10 | engine::monitor::{pumpfun_monitor, raydium_monitor}, 11 | services::jito, 12 | }; 13 | use solana_sdk::signer::Signer; 14 | 15 | #[tokio::main] 16 | async fn main() { 17 | let logger = Logger::new("[INIT] => ".to_string()); 18 | 19 | dotenv().ok(); 20 | let rpc_wss = import_env_var("RPC_WSS"); 21 | let rpc_client = create_rpc_client().unwrap(); 22 | let rpc_nonblocking_client = create_nonblocking_rpc_client().await.unwrap(); 23 | let wallet = import_wallet().unwrap(); 24 | let wallet_cloned = wallet.clone(); 25 | 26 | let state = AppState { 27 | rpc_client, 28 | rpc_nonblocking_client, 29 | wallet, 30 | }; 31 | let slippage = import_env_var("SLIPPAGE").parse::().unwrap_or(5); 32 | let use_jito = true; 33 | if use_jito { 34 | jito::init_tip_accounts().await.unwrap(); 35 | } 36 | 37 | logger.log(format!( 38 | "Successfully Set the environment variables.\n\t\t\t\t [Web Socket RPC]: {},\n\t\t\t\t [Wallet]: {:?},\n\t\t\t\t [Slippage]: {}\n", 39 | rpc_wss, wallet_cloned.pubkey(), slippage 40 | )); 41 | // raydium_monitor(&rpc_wss, state, slippage, use_jito).await; 42 | pumpfun_monitor(&rpc_wss, state, slippage, use_jito).await; 43 | } 44 | -------------------------------------------------------------------------------- /src/common/logger.rs: -------------------------------------------------------------------------------- 1 | use chrono::Local; 2 | 3 | const LOG_LEVEL: &str = "LOG"; 4 | 5 | pub struct Logger { 6 | prefix: String, 7 | date_format: String, 8 | } 9 | 10 | impl Logger { 11 | // Constructor function to create a new Logger instance 12 | pub fn new(prefix: String) -> Self { 13 | Logger { 14 | prefix, 15 | date_format: String::from("%Y-%m-%d %H:%M:%S"), 16 | } 17 | } 18 | 19 | // Method to log a message with a prefix 20 | pub fn log(&self, message: String) -> String { 21 | let log = format!("{} {}", self.prefix_with_date(), message); 22 | println!("{}", log); 23 | log 24 | } 25 | 26 | pub fn debug(&self, message: String) -> String { 27 | let log = format!("{} [{}] {}", self.prefix_with_date(), "DEBUG", message); 28 | if LogLevel::new().is_debug() { 29 | println!("{}", log); 30 | } 31 | log 32 | } 33 | pub fn error(&self, message: String) -> String { 34 | let log = format!("{} [{}] {}", self.prefix_with_date(), "ERROR", message); 35 | println!("{}", log); 36 | 37 | log 38 | } 39 | 40 | fn prefix_with_date(&self) -> String { 41 | let date = Local::now(); 42 | format!( 43 | "[{}] {}", 44 | date.format(self.date_format.as_str()), 45 | self.prefix 46 | ) 47 | } 48 | } 49 | 50 | struct LogLevel<'a> { 51 | level: &'a str, 52 | } 53 | impl LogLevel<'_> { 54 | fn new() -> Self { 55 | let level = LOG_LEVEL; 56 | LogLevel { level } 57 | } 58 | fn is_debug(&self) -> bool { 59 | self.level.to_lowercase().eq("debug") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/core/token.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{pubkey::Pubkey, signature::Keypair}; 2 | use spl_token_2022::{ 3 | extension::StateWithExtensionsOwned, 4 | state::{Account, Mint}, 5 | }; 6 | use spl_token_client::{ 7 | client::{ProgramClient, ProgramRpcClient, ProgramRpcClientSendTransaction}, 8 | token::{Token, TokenError, TokenResult}, 9 | }; 10 | use std::sync::Arc; 11 | 12 | pub fn get_associated_token_address( 13 | client: Arc, 14 | keypair: Arc, 15 | address: &Pubkey, 16 | owner: &Pubkey, 17 | ) -> Pubkey { 18 | let token_client = Token::new( 19 | Arc::new(ProgramRpcClient::new( 20 | client.clone(), 21 | ProgramRpcClientSendTransaction, 22 | )), 23 | &spl_token::ID, 24 | address, 25 | None, 26 | Arc::new(Keypair::from_bytes(&keypair.to_bytes()).expect("failed to copy keypair")), 27 | ); 28 | token_client.get_associated_token_address(owner) 29 | } 30 | 31 | pub async fn get_account_info( 32 | client: Arc, 33 | _keypair: Arc, 34 | address: &Pubkey, 35 | account: &Pubkey, 36 | ) -> TokenResult> { 37 | let program_client = Arc::new(ProgramRpcClient::new( 38 | client.clone(), 39 | ProgramRpcClientSendTransaction, 40 | )); 41 | let account = program_client 42 | .get_account(*account) 43 | .await 44 | .map_err(TokenError::Client)? 45 | .ok_or(TokenError::AccountNotFound) 46 | .inspect_err(|err| println!("get_account_info: {} {}: mint {}", account, err, address))?; 47 | 48 | if account.owner != spl_token::ID { 49 | return Err(TokenError::AccountInvalidOwner); 50 | } 51 | let account = StateWithExtensionsOwned::::unpack(account.data)?; 52 | if account.base.mint != *address { 53 | return Err(TokenError::AccountInvalidMint); 54 | } 55 | 56 | Ok(account) 57 | } 58 | 59 | pub async fn get_mint_info( 60 | client: Arc, 61 | _keypair: Arc, 62 | address: &Pubkey, 63 | ) -> TokenResult> { 64 | let program_client = Arc::new(ProgramRpcClient::new( 65 | client.clone(), 66 | ProgramRpcClientSendTransaction, 67 | )); 68 | let account = program_client 69 | .get_account(*address) 70 | .await 71 | .map_err(TokenError::Client)? 72 | .ok_or(TokenError::AccountNotFound) 73 | .inspect_err(|err| println!("{} {}: mint {}", address, err, address))?; 74 | 75 | if account.owner != spl_token::ID { 76 | return Err(TokenError::AccountInvalidOwner); 77 | } 78 | 79 | let mint_result = StateWithExtensionsOwned::::unpack(account.data).map_err(Into::into); 80 | let decimals: Option = None; 81 | if let (Ok(mint), Some(decimals)) = (&mint_result, decimals) { 82 | if decimals != mint.base.decimals { 83 | return Err(TokenError::InvalidDecimals); 84 | } 85 | } 86 | 87 | mint_result 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

Solana Ultra-Fast Token Sniper Bot on Raydium & Pumpfun (Rust)

2 | 3 | ## Overview 4 | 5 | Introducing the **Solana Ultra-Fast Token Sniper Bot**, a high-performance **Rust-based** bot engineered to snipe newly launched tokens on **Raydium** and **Pumpfun** at unparalleled speeds. Designed for precision and efficiency, this sniper bot leverages **low-latency execution, gRPC data feeds, and MEV optimization** to give traders an edge in volatile markets. 6 | 7 | ## Key Features 8 | 9 | ### 🚀 Speed & Efficiency 10 | - **Real-Time Token Detection**: Instantly detects new token listings on Raydium and Pump.fun. 11 | - **Ultra-Low-Latency Execution**: Uses **Jito bundles and gRPC streaming** for near-instant transactions. 12 | - **Optimized Rust Performance**: Memory-safe, concurrency-efficient architecture for **lightning-fast trades**. 13 | 14 | ### 🔒 Security & Reliability 15 | - **Private Key Protection**: Does not expose private keys in logs or memory. 16 | - **Block Malicious Wallets**: Supports a blacklist feature to avoid frontrunning wallets. 17 | - **Custom Slippage & Risk Controls**: Configurable slippage, auto-sell, and stop-loss functions. 18 | 19 | ### 📊 Advanced Trading Strategies 20 | - **Dynamic Buy & Sell Triggers**: Automates purchases & sales based on market trends. 21 | - **Volume-Based Execution**: Responds to large transactions to follow whale movements. 22 | - **Auto-Sell Protection**: Ensures exit from positions within a defined time frame. 23 | 24 | ### 🛠️ Deep Integration with Solana Ecosystem 25 | - **Helius & Yellowstone gRPC Support**: Connects to **multiple data feeds** for real-time insights. 26 | - **Jito Block Engine**: Enhances transaction confirmation speed using **bundled transactions**. 27 | - **DEX Compatibility**: Works seamlessly with **Raydium, Pump.fun, Meteora, and Orca**. 28 | 29 | --- 30 | 31 | ### 🎯 Trading Strategy 32 | 33 | - **Buy Entry:** Executes a purchase when a $1,000+ token buy is detected. 34 | - **Sell Exit:** Triggers a sell when a $300+ token sale is detected. 35 | - **Time-Limit Protection:** If a trade remains open for more than 60 seconds, an auto-sell is initiated. 36 | - **Customizable Parameters:** Modify buy/sell thresholds & time-frame to fit personal strategy. 37 | 38 | ### 🛠️ How to Run 39 | 1. Configure Environment Variables 40 | ```plaintext 41 | PRIVATE_KEY=your_private_key_here 42 | RPC_HTTPS=https://mainnet.helius-rpc.com/?api-key=your_api_key_here 43 | RPC_WSS=wss://atlas-mainnet.helius-rpc.com/?api-key=your_api_key_here 44 | DEVNET_RPC_HTTPS=https://devnet.helius-rpc.com/?api-key=your_api_key_here 45 | RAYDIUM_LPV4=675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 46 | SLIPPAGE=10 47 | JITO_BLOCK_ENGINE_URL=https://ny.mainnet.block-engine.jito.wtf 48 | JITO_TIP_STREAM_URL=ws://bundles-api-rest.jito.wtf/api/v1/bundles/tip_stream 49 | JITO_TIP_PERCENTILE=50 50 | YELLOWSTONE_RPC_HTTP=http://elite.rpc.solanavibestation.com/?api_key=your_api_key_here 51 | YELLOWSTONE_RPC_WSS=ws://elite.rpc.solanavibestation.com/?api_key=your_api_key_here 52 | JITO_TIP_VALUE=0.004 53 | BUY_THRESHOLD=1000 54 | SELL_THRESHOLD=300 55 | TIME_EXCEED=60 56 | ``` 57 | 2. Add the wallet address you want to block on a new line and save the file. 58 | ``` 59 | 0x1234567890abcdef1234567890abcdef12345678 60 | 0xabcdef1234567890abcdef1234567890abcdef12 61 | ``` 62 | 3. Run 63 | ``` 64 | rustc main.rs 65 | .\main.exe 66 | ``` 67 | --- 68 | ## 📞 Support & Contact 69 | 70 | For support and further inquiries, please connect via Telegram: 📞 [rust_devv](https://t.me/rust_devv). 71 | 72 | -------------------------------------------------------------------------------- /src/core/tx.rs: -------------------------------------------------------------------------------- 1 | use std::{env, sync::Arc, time::Duration}; 2 | 3 | use anyhow::Result; 4 | use jito_json_rpc_client::jsonrpc_client::rpc_client::RpcClient as JitoRpcClient; 5 | use solana_client::rpc_client::RpcClient; 6 | use solana_sdk::{ 7 | instruction::Instruction, 8 | signature::Keypair, 9 | signer::Signer, 10 | system_transaction, 11 | transaction::{Transaction, VersionedTransaction}, 12 | }; 13 | use spl_token::ui_amount_to_amount; 14 | 15 | use std::str::FromStr; 16 | use tokio::time::Instant; 17 | 18 | use crate::{ 19 | common::logger::Logger, 20 | services::jito::{self, get_tip_account, get_tip_value, wait_for_bundle_confirmation}, 21 | }; 22 | 23 | // prioritization fee = UNIT_PRICE * UNIT_LIMIT 24 | fn get_unit_price() -> u64 { 25 | env::var("UNIT_PRICE") 26 | .ok() 27 | .and_then(|v| u64::from_str(&v).ok()) 28 | .unwrap_or(1) 29 | } 30 | 31 | fn get_unit_limit() -> u32 { 32 | env::var("UNIT_LIMIT") 33 | .ok() 34 | .and_then(|v| u32::from_str(&v).ok()) 35 | .unwrap_or(300_000) 36 | } 37 | 38 | pub async fn new_signed_and_send( 39 | client: &RpcClient, 40 | keypair: &Keypair, 41 | mut instructions: Vec, 42 | use_jito: bool, 43 | logger: &Logger, 44 | ) -> Result> { 45 | let unit_price = get_unit_price(); 46 | let unit_limit = get_unit_limit(); 47 | // If not using Jito, manually set the compute unit price and limit 48 | if !use_jito { 49 | let modify_compute_units = 50 | solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price( 51 | unit_price, 52 | ); 53 | let add_priority_fee = 54 | solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit( 55 | unit_limit, 56 | ); 57 | instructions.insert(0, modify_compute_units); 58 | instructions.insert(1, add_priority_fee); 59 | } 60 | // send init tx 61 | let recent_blockhash = client.get_latest_blockhash()?; 62 | let txn = Transaction::new_signed_with_payer( 63 | &instructions, 64 | Some(&keypair.pubkey()), 65 | &vec![keypair], 66 | recent_blockhash, 67 | ); 68 | 69 | let start_time = Instant::now(); 70 | let mut txs = vec![]; 71 | if use_jito { 72 | // jito 73 | let tip_account = get_tip_account().await?; 74 | let jito_client = Arc::new(JitoRpcClient::new(format!( 75 | "{}/api/v1/bundles", 76 | *jito::BLOCK_ENGINE_URL 77 | ))); 78 | // jito tip, the upper limit is 0.1 79 | let mut tip = get_tip_value().await?; 80 | tip = tip.min(0.1); 81 | let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); 82 | logger.log(format!( 83 | "tip account: {}, tip(sol): {}, lamports: {}", 84 | tip_account, tip, tip_lamports 85 | )); 86 | // tip tx 87 | let bundle: Vec = vec![ 88 | VersionedTransaction::from(txn), 89 | VersionedTransaction::from(system_transaction::transfer( 90 | keypair, 91 | &tip_account, 92 | tip_lamports, 93 | recent_blockhash, 94 | )), 95 | ]; 96 | let bundle_id = jito_client.send_bundle(&bundle).await?; 97 | logger.log(format!("bundle_id: {}", bundle_id)); 98 | 99 | txs = wait_for_bundle_confirmation( 100 | move |id: String| { 101 | let client = Arc::clone(&jito_client); 102 | async move { 103 | let response = client.get_bundle_statuses(&[id]).await; 104 | let statuses = response.inspect_err(|err| { 105 | logger.log(format!("Error fetching bundle status: {:?}", err)); 106 | })?; 107 | Ok(statuses.value) 108 | } 109 | }, 110 | bundle_id, 111 | Duration::from_millis(1000), 112 | Duration::from_secs(10), 113 | ) 114 | .await?; 115 | } else { 116 | let sig = common::rpc::send_txn(client, &txn, true)?; 117 | logger.log(format!("signature: {:#?}", sig)); 118 | txs.push(sig.to_string()); 119 | } 120 | 121 | logger.log(format!("tx ellapsed: {:?}", start_time.elapsed())); 122 | Ok(txs) 123 | } 124 | -------------------------------------------------------------------------------- /src/dex/pump_fun.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Arc}; 2 | 3 | use anyhow::{anyhow, Context, Result}; 4 | use borsh::from_slice; 5 | use borsh_derive::{BorshDeserialize, BorshSerialize}; 6 | use raydium_amm::math::U128; 7 | use serde::{Deserialize, Serialize}; 8 | use solana_sdk::{ 9 | instruction::{AccountMeta, Instruction}, 10 | pubkey::Pubkey, 11 | signature::Keypair, 12 | signer::Signer, 13 | system_program, 14 | }; 15 | use spl_associated_token_account::{ 16 | get_associated_token_address, instruction::create_associated_token_account, 17 | }; 18 | use spl_token::{amount_to_ui_amount, ui_amount_to_amount}; 19 | use spl_token_client::token::TokenError; 20 | 21 | use crate::{ 22 | common::{logger::Logger, utils::SwapConfig}, 23 | core::{token, tx}, 24 | engine::swap::{SwapDirection, SwapInType}, 25 | }; 26 | pub const TEN_THOUSAND: u64 = 10000; 27 | pub const TOKEN_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; 28 | pub const RENT_PROGRAM: &str = "SysvarRent111111111111111111111111111111111"; 29 | pub const ASSOCIATED_TOKEN_PROGRAM: &str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; 30 | pub const PUMP_GLOBAL: &str = "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"; 31 | pub const PUMP_FEE_RECIPIENT: &str = "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"; 32 | pub const PUMP_PROGRAM: &str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; 33 | // pub const PUMP_FUN_MINT_AUTHORITY: &str = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"; 34 | pub const PUMP_ACCOUNT: &str = "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"; 35 | pub const PUMP_BUY_METHOD: u64 = 16927863322537952870; 36 | pub const PUMP_SELL_METHOD: u64 = 12502976635542562355; 37 | 38 | pub struct Pump { 39 | pub rpc_nonblocking_client: Arc, 40 | pub keypair: Arc, 41 | pub rpc_client: Option>, 42 | } 43 | 44 | impl Pump { 45 | pub fn new( 46 | rpc_nonblocking_client: Arc, 47 | rpc_client: Arc, 48 | keypair: Arc, 49 | ) -> Self { 50 | Self { 51 | rpc_nonblocking_client, 52 | keypair, 53 | rpc_client: Some(rpc_client), 54 | } 55 | } 56 | 57 | pub async fn swap(&self, mint: &str, swap_config: SwapConfig) -> Result> { 58 | let logger = Logger::new("[SWAP IN PUMP.FUN] => ".to_string()); 59 | let slippage_bps = swap_config.slippage * 100; 60 | let owner = self.keypair.pubkey(); 61 | let mint = 62 | Pubkey::from_str(mint).map_err(|e| anyhow!("failed to parse mint pubkey: {}", e))?; 63 | let program_id = spl_token::ID; 64 | let native_mint = spl_token::native_mint::ID; 65 | 66 | let (token_in, token_out, pump_method) = match swap_config.swap_direction { 67 | SwapDirection::Buy => (native_mint, mint, PUMP_BUY_METHOD), 68 | SwapDirection::Sell => (mint, native_mint, PUMP_SELL_METHOD), 69 | }; 70 | let pump_program = Pubkey::from_str(PUMP_PROGRAM)?; 71 | let (bonding_curve, associated_bonding_curve, bonding_curve_account) = 72 | get_bonding_curve_account(self.rpc_client.clone().unwrap(), &mint, &pump_program) 73 | .await?; 74 | 75 | let in_ata = token::get_associated_token_address( 76 | self.rpc_nonblocking_client.clone(), 77 | self.keypair.clone(), 78 | &token_in, 79 | &owner, 80 | ); 81 | let out_ata = token::get_associated_token_address( 82 | self.rpc_nonblocking_client.clone(), 83 | self.keypair.clone(), 84 | &token_out, 85 | &owner, 86 | ); 87 | 88 | let mut create_instruction = None; 89 | let mut close_instruction = None; 90 | 91 | tx::new_signed_and_send( 92 | &client, 93 | &self.keypair, 94 | instructions, 95 | swap_config.use_jito, 96 | &logger, 97 | ) 98 | .await 99 | } 100 | } 101 | 102 | fn min_amount_with_slippage(input_amount: u64, slippage_bps: u64) -> u64 { 103 | input_amount 104 | .checked_mul(TEN_THOUSAND.checked_sub(slippage_bps).unwrap()) 105 | .unwrap() 106 | .checked_div(TEN_THOUSAND) 107 | .unwrap() 108 | } 109 | fn max_amount_with_slippage(input_amount: u64, slippage_bps: u64) -> u64 { 110 | input_amount 111 | .checked_mul(slippage_bps.checked_add(TEN_THOUSAND).unwrap()) 112 | .unwrap() 113 | .checked_div(TEN_THOUSAND) 114 | .unwrap() 115 | } 116 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 117 | pub struct RaydiumInfo { 118 | pub base: f64, 119 | pub quote: f64, 120 | pub price: f64, 121 | } 122 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 123 | pub struct PumpInfo { 124 | pub mint: String, 125 | pub bonding_curve: String, 126 | pub associated_bonding_curve: String, 127 | pub raydium_pool: Option, 128 | pub raydium_info: Option, 129 | pub complete: bool, 130 | pub virtual_sol_reserves: u64, 131 | pub virtual_token_reserves: u64, 132 | pub total_supply: u64, 133 | } 134 | 135 | #[derive(Debug, BorshSerialize, BorshDeserialize)] 136 | pub struct BondingCurveAccount { 137 | pub discriminator: u64, 138 | pub virtual_token_reserves: u64, 139 | pub virtual_sol_reserves: u64, 140 | pub real_token_reserves: u64, 141 | pub real_sol_reserves: u64, 142 | pub token_total_supply: u64, 143 | pub complete: bool, 144 | } 145 | 146 | pub async fn get_bonding_curve_account( 147 | rpc_client: Arc, 148 | mint: &Pubkey, 149 | program_id: &Pubkey, 150 | ) -> Result<(Pubkey, Pubkey, BondingCurveAccount)> { 151 | let bonding_curve = get_pda(mint, program_id)?; 152 | let associated_bonding_curve = get_associated_token_address(&bonding_curve, mint); 153 | let bonding_curve_data = rpc_client 154 | .get_account_data(&bonding_curve) 155 | .inspect_err(|err| { 156 | println!( 157 | "Failed to get bonding curve account data: {}, err: {}", 158 | bonding_curve, err 159 | ); 160 | })?; 161 | 162 | let bonding_curve_account = 163 | from_slice::(&bonding_curve_data).map_err(|e| { 164 | anyhow!( 165 | "Failed to deserialize bonding curve account: {}", 166 | e.to_string() 167 | ) 168 | })?; 169 | 170 | Ok(( 171 | bonding_curve, 172 | associated_bonding_curve, 173 | bonding_curve_account, 174 | )) 175 | } 176 | 177 | pub fn get_pda(mint: &Pubkey, program_id: &Pubkey) -> Result { 178 | let seeds = [b"bonding-curve".as_ref(), mint.as_ref()]; 179 | let (bonding_curve, _bump) = Pubkey::find_program_address(&seeds, program_id); 180 | Ok(bonding_curve) 181 | } 182 | 183 | // https://frontend-api.pump.fun/coins/8zSLdDzM1XsqnfrHmHvA9ir6pvYDjs8UXz6B2Tydd6b2 184 | pub async fn get_pump_info( 185 | rpc_client: Arc, 186 | mint: &str, 187 | ) -> Result { 188 | let mint = Pubkey::from_str(mint)?; 189 | let program_id = Pubkey::from_str(PUMP_PROGRAM)?; 190 | let (bonding_curve, associated_bonding_curve, bonding_curve_account) = 191 | get_bonding_curve_account(rpc_client, &mint, &program_id).await?; 192 | 193 | let pump_info = PumpInfo { 194 | mint: mint.to_string(), 195 | bonding_curve: bonding_curve.to_string(), 196 | associated_bonding_curve: associated_bonding_curve.to_string(), 197 | raydium_pool: None, 198 | raydium_info: None, 199 | complete: bonding_curve_account.complete, 200 | virtual_sol_reserves: bonding_curve_account.virtual_sol_reserves, 201 | virtual_token_reserves: bonding_curve_account.virtual_token_reserves, 202 | total_supply: bonding_curve_account.token_total_supply, 203 | }; 204 | Ok(pump_info) 205 | } 206 | -------------------------------------------------------------------------------- /src/dex/raydium.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{ 3 | logger::Logger, 4 | utils::{import_env_var, SwapConfig}, 5 | }, 6 | core::{ 7 | token::{get_account_info, get_associated_token_address, get_mint_info}, 8 | tx, 9 | }, 10 | engine::swap::{SwapDirection, SwapInType}, 11 | }; 12 | use amm_cli::AmmSwapInfoResult; 13 | use anyhow::{anyhow, Context, Result}; 14 | use bytemuck; 15 | use raydium_amm::state::{AmmInfo, Loadable}; 16 | use reqwest::Proxy; 17 | use serde::Deserialize; 18 | use solana_client::rpc_filter::{Memcmp, RpcFilterType}; 19 | use solana_sdk::{ 20 | instruction::Instruction, program_pack::Pack, pubkey::Pubkey, signature::Keypair, 21 | signer::Signer, system_instruction, 22 | }; 23 | use spl_associated_token_account::instruction::create_associated_token_account; 24 | use spl_token::{amount_to_ui_amount, state::Account, ui_amount_to_amount}; 25 | use spl_token_client::token::TokenError; 26 | use std::{str::FromStr, sync::Arc}; 27 | 28 | pub const AMM_PROGRAM: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"; 29 | 30 | #[derive(Debug, Deserialize)] 31 | pub struct PoolInfo { 32 | pub success: bool, 33 | pub data: PoolData, 34 | } 35 | 36 | #[derive(Debug, Deserialize)] 37 | pub struct PoolData { 38 | // pub count: u32, 39 | pub data: Vec, 40 | } 41 | 42 | impl PoolData { 43 | pub fn get_pool(&self) -> Option { 44 | self.data.first().cloned() 45 | } 46 | } 47 | 48 | #[derive(Debug, Deserialize, Clone)] 49 | pub struct Pool { 50 | pub id: String, 51 | #[serde(rename = "programId")] 52 | pub program_id: String, 53 | #[serde(rename = "mintA")] 54 | pub mint_a: Mint, 55 | #[serde(rename = "mintB")] 56 | pub mint_b: Mint, 57 | #[serde(rename = "marketId")] 58 | pub market_id: String, 59 | } 60 | 61 | #[derive(Debug, Deserialize, Clone)] 62 | pub struct Mint { 63 | pub address: String, 64 | pub symbol: String, 65 | pub name: String, 66 | pub decimals: u8, 67 | } 68 | 69 | pub struct Raydium { 70 | pub rpc_nonblocking_client: Arc, 71 | pub rpc_client: Option>, 72 | pub keypair: Arc, 73 | pub pool_id: Option, 74 | } 75 | 76 | impl Raydium { 77 | pub fn new( 78 | rpc_nonblocking_client: Arc, 79 | rpc_client: Arc, 80 | keypair: Arc, 81 | ) -> Self { 82 | Self { 83 | rpc_nonblocking_client, 84 | keypair, 85 | rpc_client: Some(rpc_client), 86 | pool_id: None, 87 | } 88 | } 89 | 90 | pub async fn swap( 91 | &self, 92 | swap_config: SwapConfig, 93 | amm_pool_id: Pubkey, 94 | pool_state: AmmInfo, 95 | ) -> Result> { 96 | let logger = Logger::new(format!( 97 | "[SWAP IN RAYDIUM]({}) => ", 98 | chrono::Utc::now().timestamp() 99 | )); 100 | let slippage_bps = swap_config.slippage * 100; 101 | let owner = self.keypair.pubkey(); 102 | let program_id = spl_token::ID; 103 | let native_mint = spl_token::native_mint::ID; 104 | let mint = pool_state.coin_vault_mint; 105 | 106 | let (token_in, token_out, user_input_token, swap_base_in) = match ( 107 | swap_config.swap_direction.clone(), 108 | pool_state.coin_vault_mint == native_mint, 109 | ) { 110 | (SwapDirection::Buy, true) => (native_mint, mint, pool_state.coin_vault, true), 111 | (SwapDirection::Buy, false) => (native_mint, mint, pool_state.pc_vault, true), 112 | (SwapDirection::Sell, true) => (mint, native_mint, pool_state.pc_vault, true), 113 | (SwapDirection::Sell, false) => (mint, native_mint, pool_state.coin_vault, true), 114 | }; 115 | 116 | logger.log(format!( 117 | "token_in:{}, token_out:{}, user_input_token:{}, swap_base_in:{}", 118 | token_in, token_out, user_input_token, swap_base_in 119 | )); 120 | 121 | let in_ata = get_associated_token_address( 122 | self.rpc_nonblocking_client.clone(), 123 | self.keypair.clone(), 124 | &token_in, 125 | &owner, 126 | ); 127 | let out_ata = get_associated_token_address( 128 | self.rpc_nonblocking_client.clone(), 129 | self.keypair.clone(), 130 | &token_out, 131 | &owner, 132 | ); 133 | 134 | let mut create_instruction = None; 135 | let mut close_instruction = None; 136 | 137 | tx::new_signed_and_send( 138 | &client, 139 | &self.keypair, 140 | instructions, 141 | swap_config.use_jito, 142 | &logger, 143 | ) 144 | .await 145 | } 146 | } 147 | 148 | pub fn amm_swap( 149 | amm_program: &Pubkey, 150 | result: AmmSwapInfoResult, 151 | user_owner: &Pubkey, 152 | user_source: &Pubkey, 153 | user_destination: &Pubkey, 154 | amount_specified: u64, 155 | other_amount_threshold: u64, 156 | swap_base_in: bool, 157 | ) -> Result { 158 | let swap_instruction = if swap_base_in { 159 | raydium_amm::instruction::swap_base_in( 160 | amm_program, 161 | &result.pool_id, 162 | &result.amm_authority, 163 | &result.amm_open_orders, 164 | &result.amm_coin_vault, 165 | &result.amm_pc_vault, 166 | &result.market_program, 167 | &result.market, 168 | &result.market_bids, 169 | &result.market_asks, 170 | &result.market_event_queue, 171 | &result.market_coin_vault, 172 | &result.market_pc_vault, 173 | &result.market_vault_signer, 174 | user_source, 175 | user_destination, 176 | user_owner, 177 | amount_specified, 178 | other_amount_threshold, 179 | )? 180 | } else { 181 | raydium_amm::instruction::swap_base_out( 182 | amm_program, 183 | &result.pool_id, 184 | &result.amm_authority, 185 | &result.amm_open_orders, 186 | &result.amm_coin_vault, 187 | &result.amm_pc_vault, 188 | &result.market_program, 189 | &result.market, 190 | &result.market_bids, 191 | &result.market_asks, 192 | &result.market_event_queue, 193 | &result.market_coin_vault, 194 | &result.market_pc_vault, 195 | &result.market_vault_signer, 196 | user_source, 197 | user_destination, 198 | user_owner, 199 | other_amount_threshold, 200 | amount_specified, 201 | )? 202 | }; 203 | 204 | Ok(swap_instruction) 205 | } 206 | 207 | pub async fn get_pool_state( 208 | rpc_client: Arc, 209 | pool_id: Option<&str>, 210 | mint: Option<&str>, 211 | logger: &Logger, 212 | ) -> Result<(Pubkey, AmmInfo)> { 213 | if let Some(pool_id) = pool_id { 214 | logger.log(format!("[FIND POOL STATE BY pool_id]: {}", pool_id)); 215 | let amm_pool_id = Pubkey::from_str(pool_id)?; 216 | let pool_data = common::rpc::get_account(&rpc_client, &amm_pool_id)? 217 | .ok_or(anyhow!("NotFoundPool: pool state not found"))?; 218 | let pool_state: &AmmInfo = 219 | bytemuck::from_bytes(&pool_data[0..core::mem::size_of::()]); 220 | Ok((amm_pool_id, *pool_state)) 221 | } else if let Some(mint) = mint { 222 | // find pool by mint via rpc 223 | if let Ok(pool_state) = get_pool_state_by_mint(rpc_client.clone(), mint, logger).await { 224 | return Ok(pool_state); 225 | } 226 | // find pool by mint via raydium api 227 | let pool_data = get_pool_info(&spl_token::native_mint::ID.to_string(), mint).await; 228 | if let Ok(pool_data) = pool_data { 229 | let pool = pool_data 230 | .get_pool() 231 | .ok_or(anyhow!("NotFoundPool: pool not found in raydium api"))?; 232 | let amm_pool_id = Pubkey::from_str(&pool.id)?; 233 | logger.log(format!("[FIND POOL STATE BY raydium api]: {}", amm_pool_id)); 234 | let pool_data = common::rpc::get_account(&rpc_client, &amm_pool_id)? 235 | .ok_or(anyhow!("NotFoundPool: pool state not found"))?; 236 | let pool_state: &AmmInfo = 237 | bytemuck::from_bytes(&pool_data[0..core::mem::size_of::()]); 238 | 239 | return Ok((amm_pool_id, *pool_state)); 240 | } 241 | Err(anyhow!("NotFoundPool: pool state not found")) 242 | } else { 243 | Err(anyhow!("NotFoundPool: pool state not found")) 244 | } 245 | } 246 | 247 | pub async fn get_pool_state_by_mint( 248 | rpc_client: Arc, 249 | mint: &str, 250 | logger: &Logger, 251 | ) -> Result<(Pubkey, AmmInfo)> { 252 | logger.log(format!("[FIND POOL STATE BY mint]: {}", mint)); 253 | let pairs = vec![ 254 | // pump pool 255 | ( 256 | Some(spl_token::native_mint::ID), 257 | Pubkey::from_str(mint).ok(), 258 | ), 259 | // general pool 260 | ( 261 | Pubkey::from_str(mint).ok(), 262 | Some(spl_token::native_mint::ID), 263 | ), 264 | ]; 265 | 266 | let pool_len = core::mem::size_of::() as u64; 267 | let amm_program = Pubkey::from_str(AMM_PROGRAM)?; 268 | // Find matching AMM pool from mint pairs by filter 269 | let mut found_pools = None; 270 | for (coin_mint, pc_mint) in pairs { 271 | logger.log(format!( 272 | "get_pool_state_by_mint filter: coin_mint: {:?}, pc_mint: {:?}", 273 | coin_mint, pc_mint 274 | )); 275 | let filters = match (coin_mint, pc_mint) { 276 | (None, None) => Some(vec![RpcFilterType::DataSize(pool_len)]), 277 | (Some(coin_mint), None) => Some(vec![ 278 | RpcFilterType::Memcmp(Memcmp::new_base58_encoded(400, &coin_mint.to_bytes())), 279 | RpcFilterType::DataSize(pool_len), 280 | ]), 281 | (None, Some(pc_mint)) => Some(vec![ 282 | RpcFilterType::Memcmp(Memcmp::new_base58_encoded(432, &pc_mint.to_bytes())), 283 | RpcFilterType::DataSize(pool_len), 284 | ]), 285 | (Some(coin_mint), Some(pc_mint)) => Some(vec![ 286 | RpcFilterType::Memcmp(Memcmp::new_base58_encoded(400, &coin_mint.to_bytes())), 287 | RpcFilterType::Memcmp(Memcmp::new_base58_encoded(432, &pc_mint.to_bytes())), 288 | RpcFilterType::DataSize(pool_len), 289 | ]), 290 | }; 291 | let pools = 292 | common::rpc::get_program_accounts_with_filters(&rpc_client, amm_program, filters) 293 | .unwrap(); 294 | if !pools.is_empty() { 295 | found_pools = Some(pools); 296 | break; 297 | } 298 | } 299 | 300 | match found_pools { 301 | Some(pools) => { 302 | let pool = &pools[0]; 303 | let pool_state = AmmInfo::load_from_bytes(&pools[0].1.data)?; 304 | Ok((pool.0, *pool_state)) 305 | } 306 | None => Err(anyhow!("NotFoundPool: pool state not found")), 307 | } 308 | } 309 | 310 | // get pool info 311 | // https://api-v3.raydium.io/pools/info/mint?mint1=So11111111111111111111111111111111111111112&mint2=EzM2d8JVpzfhV7km3tUsR1U1S4xwkrPnWkM4QFeTpump&poolType=standard&poolSortField=default&sortType=desc&pageSize=10&page=1 312 | pub async fn get_pool_info(mint1: &str, mint2: &str) -> Result { 313 | let mut client_builder = reqwest::Client::builder(); 314 | let http_proxy = import_env_var("HTTP_PROXY"); 315 | let proxy = Proxy::all(http_proxy)?; 316 | client_builder = client_builder.proxy(proxy); 317 | let client = client_builder.build()?; 318 | 319 | let result = client 320 | .get("https://api-v3.raydium.io/pools/info/mint") 321 | .query(&[ 322 | ("mint1", mint1), 323 | ("mint2", mint2), 324 | ("poolType", "standard"), 325 | ("poolSortField", "default"), 326 | ("sortType", "desc"), 327 | ("pageSize", "1"), 328 | ("page", "1"), 329 | ]) 330 | .send() 331 | .await? 332 | .json::() 333 | .await 334 | .context("Failed to parse pool info JSON")?; 335 | Ok(result.data) 336 | } 337 | --------------------------------------------------------------------------------