├── .gitignore ├── src ├── common │ ├── mod.rs │ ├── utils.rs │ └── logger.rs ├── engine │ └── mod.rs ├── services │ └── mod.rs ├── core │ ├── mod.rs │ ├── token.rs │ └── tx.rs ├── dex │ ├── mod.rs │ ├── pump_fun.rs │ └── raydium.rs ├── lib.rs └── main.rs ├── Cargo.toml └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod utils; 2 | -------------------------------------------------------------------------------- /src/engine/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod swap; 2 | -------------------------------------------------------------------------------- /src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod jito; 2 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token; 2 | pub mod tx; 3 | -------------------------------------------------------------------------------- /src/dex/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pump_fun; 2 | pub mod raydium; 3 | -------------------------------------------------------------------------------- /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.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Pumpfun-copytrading-bot" 3 | version = "2.0.3" 4 | edition = "2025" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use solana_copytrading_bot::{ 2 | common::{config::Config, constants::RUN_MSG}, 3 | engine::monitor::copytrader, 4 | }; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | /* Initial Settings */ 9 | let config = Config::new().await; 10 | 11 | /* Running Bot */ 12 | let run_msg = RUN_MSG; 13 | println!("{}", run_msg); 14 | 15 | copytrader( 16 | &config.rpc_wss, 17 | config.app_state, 18 | config.swap_config, 19 | ) 20 | .await; 21 | } 22 | -------------------------------------------------------------------------------- /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/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 | address: &Pubkey, 34 | account: &Pubkey, 35 | ) -> TokenResult> { 36 | let program_client = Arc::new(ProgramRpcClient::new( 37 | client.clone(), 38 | ProgramRpcClientSendTransaction, 39 | )); 40 | let account = program_client 41 | .get_account(*account) 42 | .await 43 | .map_err(TokenError::Client)? 44 | .ok_or(TokenError::AccountNotFound) 45 | .inspect_err(|err| println!("get_account_info: {} {}: mint {}", account, err, address))?; 46 | 47 | if account.owner != spl_token::ID { 48 | return Err(TokenError::AccountInvalidOwner); 49 | } 50 | let account = StateWithExtensionsOwned::::unpack(account.data)?; 51 | if account.base.mint != *address { 52 | return Err(TokenError::AccountInvalidMint); 53 | } 54 | 55 | Ok(account) 56 | } 57 | 58 | pub async fn get_mint_info( 59 | client: Arc, 60 | _keypair: Arc, 61 | address: &Pubkey, 62 | ) -> TokenResult> { 63 | let program_client = Arc::new(ProgramRpcClient::new( 64 | client.clone(), 65 | ProgramRpcClientSendTransaction, 66 | )); 67 | let account = program_client 68 | .get_account(*address) 69 | .await 70 | .map_err(TokenError::Client)? 71 | .ok_or(TokenError::AccountNotFound) 72 | .inspect_err(|err| println!("{} {}: mint {}", address, err, address))?; 73 | 74 | if account.owner != spl_token::ID { 75 | return Err(TokenError::AccountInvalidOwner); 76 | } 77 | 78 | let mint_result = StateWithExtensionsOwned::::unpack(account.data).map_err(Into::into); 79 | let decimals: Option = None; 80 | if let (Ok(mint), Some(decimals)) = (&mint_result, decimals) { 81 | if decimals != mint.base.decimals { 82 | return Err(TokenError::InvalidDecimals); 83 | } 84 | } 85 | 86 | mint_result 87 | } 88 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ⚡ Solana Ultra-Fast Copytrading Bot in Rust ⚡ 2 | 3 | ## Overview 4 | Tired of missing profitable trades? Meet the **Solana Ultra-Fast Copytrading Bot**, built in **Rust** for blazing speed and rock-solid performance. Designed to execute trades in the **same block**, this bot ensures you stay ahead of the game. Get ready to **copy trades at lightning speed!** 5 | 6 | ## Why This Bot is a Game-Changer 🏆 7 | 8 | ### 🚀 Built with Rust 9 | - **Extreme Speed**: Rust ensures low-latency transaction execution, making trade delays a thing of the past. 10 | - **Memory Safety**: Say goodbye to crashes and bugs, thanks to Rust’s error-handling capabilities. 11 | 12 | ### 🎯 Block-Level Copytrading 13 | - **Same-Block Execution**: The bot can copy trades **within the same block**, giving you a real edge in trading. 14 | 15 | ### 🔒 Reliable & Efficient 16 | - **Robust Architecture**: Uses advanced RPC handling and transaction parsing for high efficiency. 17 | - **Real-Time Monitoring**: Stay ahead of the game with **real-time trade tracking** and execution. 18 | 19 | ### ⚙️ Essential Features 20 | - **Low-Latency Transactions**: Optimized for platforms like **Raydium** and **Pump.fun**. 21 | - **Customizable Strategies**: Fine-tune trade settings for maximum profit. 22 | 23 | ## 📂 Directory Structure 24 | ``` 25 | src/ 26 | ├── core/ 27 | │ ├── token.rs # Token handling 28 | │ └── tx.rs # Transaction processing 29 | │ 30 | ├── engine/ 31 | │ ├── swap.rs # Buy/Sell functionalities for multiple DEXs 32 | │ └── monitor.rs # Wallet monitoring via Geyser RPC & standard RPC 33 | │ 34 | ├── dex/ 35 | │ ├── pump_fun.rs # Pump.fun integration 36 | │ ├── raydium.rs # Raydium integration 37 | │ ├── meteora.rs # Meteora integration 38 | │ └── orca.rs # Orca integration 39 | │ 40 | ├── services/ 41 | │ ├── jito.rs # Jito service for fast confirmations 42 | │ └── nextblock.rs # NextBlock service for optimized trade execution 43 | │ 44 | ├── common/ 45 | │ ├── logger.rs # Logging functionality 46 | │ ├── config.rs # Project configurations 47 | │ ├── constants.rs # Global constants 48 | │ ├── targetlist.rs # Manages trading target lists 49 | │ └── utils.rs # Utility functions 50 | │ 51 | ├── lib.rs 52 | └── main.rs 53 | ``` 54 | 55 | ## 🚀 How To Run 56 | 57 | ### 1️⃣ Configure Environment Variables 58 | ``` 59 | PRIVATE_KEY=your_private_key_here 60 | RPC_HTTPS=https://mainnet.helius-rpc.com/?api-key=your_api_key_here 61 | RPC_WSS=wss://atlas-mainnet.helius-rpc.com/?api-key=your_api_key_here 62 | SLIPPAGE=10 63 | JITO_BLOCK_ENGINE_URL=https://ny.mainnet.block-engine.jito.wtf 64 | JITO_TIP_STREAM_URL=ws://bundles-api-rest.jito.wtf/api/v1/bundles/tip_stream 65 | JITO_TIP_PERCENTILE=50 66 | JITO_TIP_VALUE=0.004 67 | TOKEN_PERCENTAGE=1 # Percentage 68 | ``` 69 | ### 2️⃣ Add Target Wallets 70 | - List target wallet addresses in `targetlist.txt`. 71 | 72 | ## 🏆 Trade Execution Example 73 | ``` 74 | ---[BUY]--- 75 | Target: https://solscan.io/tx/5aaQDtXjyf4NDF3NKjjmC5s6Y8AhW3ieTpmB6Kxt6UGC2AowJ2xRTzFJo7KM4CVcpbphA2w76juGDdvqqgNTt1CF 76 | Copied: https://solscan.io/tx/4uPU2BRi7BJCTxp4kJQFTmLj5pmoAyKw7zCHNCPiP2NYK2HcqXfJr8gE6eF89VYPEy5VTFaRQf4DTUZNzttFQ73Z 77 | 78 | ---[SELL]--- 79 | Target: https://solscan.io/tx/22qnz4aBXqmeQbp6cnAogSVPNxSbEJr7tswch5QXLSG8Rvnb4SwDJFJ9RytpUVkUUQUtiy44fYwafF5CgiYjdVtp 80 | Copied: https://solscan.io/tx/3uBU12fQT14z88tiX1i1EH8XWXpFco4dU1QG8VEtYQyXtaSQXQB6AR7HBF4GtF9YDCa54Uw4xE7H7JPjBM9cETKM 81 | ``` 82 | ### ✅ Test Result: Same Block Execution 83 | - **Target:** [View Transaction](https://solscan.io/tx/4amQhsMLqv2Lbr6UyFcoTdctsD76dKAvAHFkvCDpqa6kUqeHXN7drKXpFJrqDV389Uu4rEY575WHJYdg4inSMtFf) 84 | - **Copied:** [View Transaction](https://solscan.io/tx/57P2bZGJ5QTThjT4jv88CXEU4oGDTgVaS2c386qBMEs2KkizN2PV7cKKZgS8uvWwPQyTpBUXTTfnjJ4dECuJf39t) 85 | - **DEX Screener:** [View Market Data](https://dexscreener.com/solana/JD3VPqQ7pfHZ4h2zhALfvz5E7dantyVpsDUov1Lgpump) 86 | - **Wallet Address:** [View Wallet](https://gmgn.ai/sol/address/D3QXckXy26G6rTnqHQFUxvwpRsv18o5wBrHMVoodYWTa) 87 | 88 | ## 💰 Donate & Support 89 | **SOL Address:** `5HFvAqZdv18dHsVL3j39JGbacVsgHHB5GVzcGPvfSqG1` 90 | 91 | ## 🛠 Need Help? 92 | 📩 **Telegram:** [@topsecretagent_007](https://t.me/@topsecretagent_007) 93 | 94 | 🌟 **GitHub:** [topsecretagent007](https://github.com/topsecretagent007) 95 | -------------------------------------------------------------------------------- /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 | hash::Hash, 8 | instruction::Instruction, 9 | signature::Keypair, 10 | signer::Signer, 11 | system_transaction, 12 | transaction::{Transaction, VersionedTransaction}, 13 | }; 14 | use spl_token::ui_amount_to_amount; 15 | 16 | use std::str::FromStr; 17 | use tokio::time::Instant; 18 | 19 | use crate::{ 20 | common::utils::log_message, 21 | services::jito::{ 22 | self, get_tip_account, get_tip_value, init_tip_accounts, wait_for_bundle_confirmation, 23 | }, 24 | }; 25 | 26 | // prioritization fee = UNIT_PRICE * UNIT_LIMIT 27 | fn get_unit_price() -> u64 { 28 | env::var("UNIT_PRICE") 29 | .ok() 30 | .and_then(|v| u64::from_str(&v).ok()) 31 | .unwrap_or(1) 32 | } 33 | 34 | fn get_unit_limit() -> u32 { 35 | env::var("UNIT_LIMIT") 36 | .ok() 37 | .and_then(|v| u32::from_str(&v).ok()) 38 | .unwrap_or(300_000) 39 | } 40 | pub async fn jito_confirm( 41 | keypair: &Keypair, 42 | version_tx: VersionedTransaction, 43 | recent_block_hash: &Hash, 44 | ) { 45 | init_tip_accounts().await.unwrap(); 46 | let tip_account = get_tip_account().await.unwrap(); 47 | let jito_client = Arc::new(JitoRpcClient::new(format!( 48 | "{}/api/v1/bundles", 49 | *jito::BLOCK_ENGINE_URL 50 | ))); 51 | // jito tip, the upper limit is 0.1 52 | let mut tip = get_tip_value().await.unwrap(); 53 | tip = tip.min(0.1); 54 | let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); 55 | // tip tx 56 | let bundle: Vec = vec![ 57 | version_tx, 58 | VersionedTransaction::from(system_transaction::transfer( 59 | keypair, 60 | &tip_account, 61 | tip_lamports, 62 | recent_block_hash.clone(), 63 | )), 64 | ]; 65 | let bundle_id = jito_client.send_bundle(&bundle).await.unwrap(); 66 | 67 | let _txs = match wait_for_bundle_confirmation( 68 | move |id: String| { 69 | let client = Arc::clone(&jito_client); 70 | async move { 71 | let response = client.get_bundle_statuses(&[id]).await; 72 | let statuses = response.inspect_err(|err| { 73 | println!("Error fetching bundle status: {:?}", err); 74 | })?; 75 | Ok(statuses.value) 76 | } 77 | }, 78 | bundle_id, 79 | Duration::from_millis(1000), 80 | Duration::from_secs(10), 81 | ) 82 | .await 83 | { 84 | Ok(data) => { 85 | let msg = "Copy Wallet: ".to_string() 86 | + &keypair.pubkey().to_string() 87 | + " https://solscan.io/tx/" 88 | + &data[0] 89 | + "\n" 90 | + "Rusult: Success" 91 | + "\n"; 92 | let _ = log_message(&msg).await; 93 | } 94 | Err(e) => { 95 | let _ = log_message("Failed: Loop exceeded 10s, breaking out\n").await; 96 | } 97 | }; 98 | } 99 | 100 | pub async fn new_signed_and_send( 101 | client: &RpcClient, 102 | keypair: &Keypair, 103 | mut instructions: Vec, 104 | use_jito: bool, 105 | ) -> Result> { 106 | let start_time = Instant::now(); 107 | let unit_price = get_unit_price(); 108 | let unit_limit = get_unit_limit(); 109 | // If not using Jito, manually set the compute unit price and limit 110 | 111 | let recent_blockhash = client.get_latest_blockhash()?; 112 | let txn = Transaction::new_signed_with_payer( 113 | &instructions, 114 | Some(&keypair.pubkey()), 115 | &vec![keypair], 116 | recent_blockhash, 117 | ); 118 | 119 | let mut txs = vec![]; 120 | 121 | // jito 122 | init_tip_accounts().await; 123 | let tip_account = get_tip_account().await?; 124 | let jito_client = Arc::new(JitoRpcClient::new(format!( 125 | "{}/api/v1/bundles", 126 | *jito::BLOCK_ENGINE_URL 127 | ))); 128 | // jito tip, the upper limit is 0.1 129 | let mut tip = get_tip_value().await?; 130 | tip = tip.min(0.1); 131 | let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); 132 | 133 | // let result = client.simulate_transaction(&txn); 134 | 135 | // println!("result: {:#?}", result); 136 | let bundle: Vec = vec![ 137 | VersionedTransaction::from(txn), 138 | VersionedTransaction::from(system_transaction::transfer( 139 | keypair, 140 | &tip_account, 141 | tip_lamports, 142 | recent_blockhash, 143 | )), 144 | ]; 145 | 146 | let bundle_id = jito_client.send_bundle(&bundle).await?; 147 | 148 | txs = wait_for_bundle_confirmation( 149 | move |id: String| { 150 | let client = Arc::clone(&jito_client); 151 | async move { 152 | let response = client.get_bundle_statuses(&[id]).await; 153 | let statuses = response.inspect_err(|err| {})?; 154 | Ok(statuses.value) 155 | } 156 | }, 157 | bundle_id, 158 | Duration::from_millis(1000), 159 | Duration::from_secs(10), 160 | ) 161 | .await?; 162 | let msg = "Copy Wallet: ".to_string() 163 | + &keypair.pubkey().to_string() 164 | + " https://solscan.io/tx/" 165 | + &txs[0] 166 | + "\n" 167 | + "Rusult: Success" 168 | + "\n"; 169 | let _ = log_message(&msg).await; 170 | Ok(txs) 171 | } 172 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------