├── .cargo └── config.toml ├── .env.example ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Makefile ├── README.md ├── TIME_SERIES_ANALYSIS.md ├── build.sh ├── check_error.sh ├── check_error2.sh ├── log ├── _pumpfun_buy_tx.log ├── _pumpfun_mintTo_tx.log ├── _pumpfun_sell_tx.log ├── pumpfun_buy_tx_1746445226684.log ├── pumpfun_buy_tx_1746445226689.log ├── pumpfun_buy_tx_1746445227390.log ├── pumpfun_sell_tx_1746445226685.log └── pumpfun_sell_tx_1746445227405.log ├── prototype ├── Cargo.toml └── src │ ├── lib.rs │ └── main.rs ├── src ├── common │ ├── blacklist.rs │ ├── config.rs │ ├── constants.rs │ ├── logger.rs │ ├── mod.rs │ └── whitelist.rs ├── core │ ├── mod.rs │ ├── token.rs │ └── tx.rs ├── dex │ ├── mod.rs │ └── pump_fun.rs ├── engine │ ├── advanced_trading.rs │ ├── bonding_curve.rs │ ├── enhanced_monitor.rs │ ├── enhanced_token_trader.rs │ ├── mod.rs │ ├── monitor.rs │ ├── risk_management.rs │ ├── swap.rs │ ├── token_buying.rs │ ├── token_list_manager.rs │ ├── token_selling.rs │ └── token_tracker.rs ├── error │ └── mod.rs ├── fix_test.rs ├── lib.rs ├── main.rs ├── services │ ├── jito.rs │ ├── mod.rs │ ├── nozomi.rs │ ├── telegram.rs │ └── zeroslot.rs └── tests │ ├── dev_wallet_test.rs │ └── mod.rs └── test_logs.txt /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(windows)'] 2 | rustflags = ["-C", "target-feature=+crt-static"] 3 | 4 | # For protobuf/tonic/grpc compatibility on Windows 5 | [env] 6 | PROTOC_NO_VENDOR = "1" -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | THRESHOLD_SELL=10000000000 # 2 SOL 2 | THRESHOLD_BUY=3000000000 # 1 sol 3 | MAX_WAIT_TIME=650000 # 650 seconds 4 | PRIVATE_KEY= 5 | RPC_HTTP= 6 | RPC_WSS= 7 | YELLOWSTONE_GRPC_HTTP= 8 | YELLOWSTONE_GRPC_TOKEN= 9 | YELLOWSTONE_PING_INTERVAL= 10 | YELLOWSTONE_RECONNECT_DELAY= 11 | YELLOWSTONE_MAX_RETRIES= 12 | SLIPPAGE= 13 | JITO_BLOCK_ENGINE_URL= 14 | JITO_PRIORITY_FEE= 15 | ZERO_SLOT_URL= 16 | ZERO_SLOT_TIP_VALUE= 17 | NOZOMI_URL= 18 | NOZOMI_TIP_VALUE= 19 | JITO_TIP_VALUE= 20 | TIME_EXCEED= 21 | TOKEN_AMOUNT= 22 | COUNTER= 23 | MAX_DEV_BUY = 24 | MIN_DEV_BUY = 25 | # BLOXROUTE SETTINGS 26 | NETWORK= 27 | REGION= 28 | AUTH_HEADER= 29 | BLOXROUTE_TIP_VALUE= 30 | #nozimi 31 | UNIT_PRICE= 32 | UNIT_LIMIT= 33 | #jito in sell 34 | USE_JITO= 35 | DOWNING_PERCENT= 36 | TELE_BOT_TOKEN= 37 | TELE_CHAT_ID= 38 | # Market Cap Filter (in thousands) 39 | MIN_MARKET_CAP= 40 | MAX_MARKET_CAP= 41 | MARKET_CAP_ENABLED= 42 | # Volume Filter (in thousands) 43 | MIN_VOLUME= 44 | MAX_VOLUME= 45 | VOLUME_ENABLED= 46 | # Buy/Sell Count Filter 47 | MIN_NUMBER_OF_BUY_SELL= 48 | MAX_NUMBER_OF_BUY_SELL= 49 | BUY_SELL_COUNT_ENABLED= 50 | # SOL Invested Filter 51 | SOL_INVESTED= 52 | SOL_INVESTED_ENABLED= 53 | # Launcher SOL Balance Filter 54 | MIN_LAUNCHER_SOL_BALANCE= 55 | MAX_LAUNCHER_SOL_BALANCE= 56 | LAUNCHER_SOL_ENABLED= 57 | # Dev Buy & Bundle Check Filter 58 | DEV_BUY_ENABLED= 59 | BUNDLE_CHECK= 60 | # Sell all the tokens before ending the program 61 | SELL_ALL_TOKENS= 62 | # Take Profit and Stop Loss Settings 63 | STOP_LOSS= 64 | TAKE_PROFIT= 65 | TAKE_PROFIT_PERCENT= 66 | STOP_LOSS_PERCENT= 67 | # Token Lifetime Settings 68 | MIN_LAST_TIME= # Minimum token lifetime in milliseconds 69 | # Additional Trading Parameters 70 | BUY_SELL_PERCENT= # Percentage to follow target buys/sells 71 | LIMIT_WAIT_TIME= # Maximum wait time for limited buys in milliseconds 72 | LIMIT_BUY_AMOUNT_IN_LIMIT_WAIT_TIME= # Maximum SOL to buy within limit wait time 73 | REVIEW_CYCLE_DURATION= # Duration of review cycle in milliseconds 74 | # Time and Price Delta Thresholds 75 | TIME_DELTA_THRESHOLD= # Time threshold in seconds for monitoring price changes 76 | PRICE_DELTA_THRESHOLD= # Price change percentage threshold for triggering alerts 77 | # Advanced Trading Systems 78 | MIN_BUY_CONFIDENCE= # Minimum confidence level for buying (percentage) 79 | MIN_SELL_CONFIDENCE= # Minimum confidence level for selling (percentage) 80 | DAILY_BUY_BUDGET= # Maximum SOL to spend per day 81 | 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | target 3 | blacklist.txt 4 | yellowstone-grpc-client 5 | pumpfun-sdk 6 | jupiter-sdk 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana-vntr-sniper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | dotenv = "0.15" 8 | chrono = "0.4.26" 9 | clap = { version = "4.5.7", features = ["derive"] } 10 | anyhow = "1.0.62" 11 | serde = "1.0.145" 12 | serde_json = "1.0.86" 13 | tokio = { version = "1.21.2", features = ["full"] } 14 | tokio-tungstenite = { version = "0.23.1", features = ["native-tls"] } 15 | tokio-stream = "0.1.11" 16 | anchor-client = { version = "0.31.0", features = ["async"] } 17 | anchor-lang = "=0.31.0" 18 | yellowstone-grpc-client = "4.2.1" 19 | yellowstone-grpc-proto = "4.2.1" 20 | spl-token = { version = "4.0.0", features = ["no-entrypoint"] } 21 | spl-token-2022 = { version = "6.0.0", features = ["no-entrypoint"] } 22 | spl-associated-token-account = { version = "6.0.0", features = [ 23 | "no-entrypoint", 24 | ] } 25 | spl-token-client = "0.13.0" 26 | base64 = "0.13" 27 | rand = "0.8.5" 28 | borsh = { version = "1.5.3"} 29 | borsh-derive = "1.5.3" 30 | colored = "3.0.0" 31 | reqwest = { version = "0.11.27", features = ["json", "socks", "native-tls"] } 32 | lazy_static = "1.5.0" 33 | bs58 = "0.4" 34 | bs64 = "0.1.2" 35 | bincode = "1.3.3" 36 | tokio-js-set-interval = "1.3.0" 37 | bytemuck = "1.21.0" 38 | indicatif = "0.17.8" 39 | tracing = "0.1.40" 40 | futures-util = "0.3.30" 41 | maplit = "1.0.2" 42 | jito-json-rpc-client = { git = "https://github.com/jwest951227/jito-block-engine-json-rpc-client.git", branch="v2.1.1", package = "jito-block-engine-json-rpc-client" } 43 | futures = "0.3.31" 44 | log = "0.4.20" 45 | url = "2.4.1" 46 | tonic = { version = "0.11.0", features = ["tls", "tls-roots"] } 47 | tonic-health = "0.11.0" 48 | solana-program-pack = "2.1.1" 49 | 50 | # Pin solana dependencies to ensure compatibility 51 | solana-pubkey = "=2.1.1" 52 | solana-program = "=2.1.1" 53 | solana-sdk = "=2.1.1" 54 | solana-client = "=2.1.1" -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Variables 2 | TARGET_X86_64 = x86_64-pc-windows-gnu 3 | TARGET_I686 = i686-pc-windows-gnu 4 | PROJECT_NAME = solana-vntr-sniper # Change this to your project name 5 | CARGO = cargo 6 | 7 | # Target to install prerequisites 8 | .PHONY: install 9 | install: 10 | sudo apt update 11 | sudo apt install -y mingw-w64 12 | rustup target add $(TARGET_X86_64) 13 | rustup target add $(TARGET_I686) 14 | 15 | # pm2 to install prerequisites 16 | .PHONY: pm2 17 | pm2: 18 | pm2 start target/release/solana-vntr-sniper 19 | 20 | # Target to build for x86_64 Windows 21 | .PHONY: build-x86_64 22 | build-x86_64: 23 | $(CARGO) build --target=$(TARGET_X86_64) --release 24 | 25 | # Target to build for i686 Windows 26 | .PHONY: build-i686 27 | build-i686: 28 | $(CARGO) build --target=$(TARGET_I686) --release 29 | 30 | # Target to clean the project 31 | .PHONY: clean 32 | clean: 33 | $(CARGO) clean 34 | 35 | # Start the server 36 | .PHONY: start 37 | start: 38 | pm2 start 0 39 | 40 | # Stop the server 41 | .PHONY: stop 42 | stop: 43 | pm2 stop 0 44 | 45 | # Stop the server 46 | .PHONY: build 47 | build: 48 | $(CARGO) clean 49 | $(CARGO) build -r 50 | 51 | # Target to display help 52 | .PHONY: help 53 | help: 54 | @echo "Makefile commands:" 55 | @echo " install - Install necessary packages and configure Rust targets" 56 | @echo " build-x86_64 - Build for 64-bit Windows" 57 | @echo " build-i686 - Build for 32-bit Windows" 58 | @echo " clean - Clean the target directory" 59 | @echo " help - Display this help message" 60 | @echo " start - Start the server" 61 | @echo " stop - Stop the server" 62 | @echo " build - Build the server" 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pump-Fun-Pump-Swap-Sniper-Copy-Trading-Bot 2 | 3 | This project is a Rust-based trading bot for the Solana blockchain that specializes in: 4 | 5 | 1. Real-time monitoring of new token launches 6 | 2. Automated trading with configurable strategies 7 | 3. Token filtering based on various metrics 8 | 4. Profit-taking and stop-loss management 9 | 5. Telegram integration for notifications and control 10 | 11 | The bot uses Yellowstone gRPC for real-time transaction monitoring and supports multiple transaction submission methods including Jito, ZeroSlot, and standard RPC. 12 | 13 | ## Overview 14 | Pump-Fun-Pump-Swap-Sniper-Copy-Trading-Bot is a high-performance Rust-based trading bot that specializes in real-time copy trading on Solana DEXs like Pump.fun and PumpSwap, using advanced transaction monitoring to snipe and replicate profitable trades instantly. 15 | 16 | ## Let's Connect!, 17 | 18 | 19 | Gmail 20 | 21 | 22 | Telegram 23 | 24 | 25 | Discord 26 | 27 | 28 | ## Features 29 | 30 | - Dev wallet detection (identifies the creator/signer of token mint transactions) 31 | - Notification deduplication to prevent multiple alerts for the same token 32 | - Configurable filter settings via Telegram UI 33 | - Real-time token monitoring 34 | - Telegram notifications for tradable tokens 35 | 36 | ## Architecture 37 | 38 | ### Core Components 39 | 40 | #### 1. Monitoring System 41 | - `EnhancedMonitor`: Advanced monitoring system that tracks token transactions and price movements 42 | - `TokenTracker`: Tracks token metrics including price, volume, and buy/sell counts 43 | - Uses Yellowstone gRPC to subscribe to real-time blockchain updates 44 | 45 | #### 2. Trading Engine 46 | - `SellManager`: Manages selling strategies with take-profit and stop-loss mechanisms 47 | - `Pump`: Handles interactions with the Pump.fun DEX 48 | - Supports multiple swap directions (buy/sell) and input types (quantity/percentage) 49 | 50 | #### 3. Transaction Handling 51 | - Multiple transaction submission methods: 52 | - Standard RPC 53 | - Jito MEV-protected transactions 54 | - ZeroSlot for faster transaction processing 55 | - Priority fee calculation and management 56 | 57 | #### 4. Notification System 58 | - Telegram integration for: 59 | - Real-time trade notifications 60 | - Token discovery alerts 61 | - Remote command execution 62 | - Filter configuration 63 | 64 | ### Key Features 65 | 66 | #### Token Filtering 67 | Filter tokens based on: 68 | - Market cap range 69 | - Volume thresholds 70 | - Buy/sell transaction counts 71 | - Developer buy amount 72 | - Launcher SOL balance 73 | - Bundle transaction detection 74 | 75 | #### Trading Strategies 76 | - Take profit with configurable percentage 77 | - Stop loss with configurable percentage 78 | - Dynamic retracement-based selling 79 | - Time-based selling 80 | - Partial selling to secure profits 81 | 82 | #### Blacklisting 83 | - Maintain a blacklist of addresses to avoid trading with known scams or problematic tokens 84 | 85 | ## Configuration 86 | 87 | The bot uses environment variables for configuration, which can be loaded from a `.env` file: 88 | 89 | ## How to run 90 | 91 | - To build the bot 92 | ``` 93 | make build 94 | ``` 95 | 96 | - To start the bot 97 | ``` 98 | make start 99 | ``` 100 | 101 | - To stop the bot 102 | ``` 103 | make stop 104 | ``` 105 | 106 | https://docs.moralis.com/web3-data-api/solana/reference#token-api- 107 | 108 | Rust langauge 109 | - Copy bot star2ng on Pumpfun dex, copy the target, mul2 targets op2on 110 | - A `: Manually sell a specific token 171 | - `/tp `: Set take profit for a token 172 | - `/sl `: Set stop loss for a token 173 | 174 | ## Technical Details 175 | 176 | ### Dependencies 177 | 178 | - `anchor_client`: Solana program interaction 179 | - `yellowstone-grpc-proto`: Real-time blockchain data 180 | - `tokio`: Asynchronous runtime 181 | - `spl_token`: Solana token operations 182 | - `reqwest`: HTTP client for API interactions 183 | - `serde`: Serialization/deserialization 184 | - `colored`: Terminal output formatting 185 | 186 | ### Key Modules 187 | 188 | - `common`: Configuration, constants, and utilities 189 | - `core`: Core functionality for tokens and transactions 190 | - `dex`: DEX-specific implementations (Pump.fun) 191 | - `engine`: Trading engine and monitoring systems 192 | - `services`: External service integrations (Telegram, Jito, ZeroSlot) 193 | - `error`: Error handling 194 | 195 | ## Advanced Features 196 | 197 | ### Dynamic Retracement Selling 198 | 199 | The bot implements a sophisticated retracement-based selling strategy that: 200 | - Tracks the highest price point (ATH) for each token 201 | - Calculates retracement percentages from ATH 202 | - Triggers partial sells at different retracement levels based on overall PnL 203 | - Adjusts selling percentages based on profit levels 204 | 205 | ### Bundle Transaction Detection 206 | 207 | Detects and analyzes bundle transactions to identify potential token manipulations: 208 | - Monitors for multiple transactions in a single bundle 209 | - Identifies developer buying patterns 210 | - Helps avoid tokens with suspicious transaction patterns 211 | 212 | ### Automatic Token Analysis 213 | 214 | For each new token, the bot analyzes: 215 | - Bonding curve parameters 216 | - Virtual and real reserves 217 | - Market cap and liquidity 218 | - Developer wallet activity 219 | - Historical price movements 220 | 221 | ## Security Considerations 222 | 223 | - Private keys are stored in environment variables 224 | - Blacklist functionality to avoid known scam tokens 225 | - Transaction simulation before submission 226 | - Error handling and retry mechanisms 227 | 228 | ## How to Run 229 | 230 | ### Normal Operation 231 | 232 | ```bash 233 | cargo run 234 | ``` 235 | 236 | ### Run Dev Wallet Test 237 | 238 | To test the dev wallet detection and notification deduplication functionality: 239 | 240 | ```bash 241 | cargo run -- --test-dev-wallet 242 | ``` 243 | 244 | This will: 245 | 1. Create a simulated token 246 | 2. Identify the dev wallet (the signer of the mint transaction) 247 | 3. Send a notification via Telegram (if credentials are provided) 248 | 4. Attempt to send a second notification for the same token to test deduplication 249 | 250 | ## Configuration 251 | 252 | Set the following environment variables before running: 253 | 254 | ```bash 255 | # Telegram Settings 256 | export TELEGRAM_BOT_TOKEN="your_bot_token" 257 | export TELEGRAM_CHAT_ID="your_chat_id" 258 | 259 | # Filter Settings 260 | export MARKET_CAP_ENABLED="true" 261 | export MIN_MARKET_CAP="8.0" 262 | export MAX_MARKET_CAP="15.0" 263 | 264 | export VOLUME_ENABLED="true" 265 | export MIN_VOLUME="5.0" 266 | export MAX_VOLUME="12.0" 267 | 268 | export BUY_SELL_COUNT_ENABLED="true" 269 | export MIN_NUMBER_OF_BUY_SELL="50" 270 | export MAX_NUMBER_OF_BUY_SELL="2000" 271 | 272 | export SOL_INVESTED="1.0" 273 | export SOL_INVESTED_ENABLED="true" 274 | 275 | export LAUNCHER_SOL_ENABLED="true" 276 | export MIN_LAUNCHER_SOL_BALANCE="0.0" 277 | export MAX_LAUNCHER_SOL_BALANCE="1.0" 278 | 279 | export DEV_BUY_ENABLED="true" 280 | export MIN_DEV_BUY="5.0" 281 | export MAX_DEV_BUY="30.0" 282 | 283 | export BUNDLE_CHECK="true" 284 | ``` 285 | 286 | ## Telegram Commands 287 | 288 | - `/start` or `/filters` - Display filter settings UI 289 | - `/config` - Show configuration file location 290 | 291 | @src current project don't use token age. 292 | we have to calculate that : get token created time in from_json function and save it on ParsedTransactionInfo struct , and use it in real filter logic 293 | 294 | --- 295 | 296 | ## 📞 Contact Information 297 | For questions, feedback, or collaboration opportunities, feel free to reach out: 298 | 299 |
300 | 301 | 📧 **Email**: [fenrow325@gmail.com](mailto:fenrow325@gmail.com) 302 | 📱 **Telegram**: [@fenroW](https://t.me/fenrow) 303 | 🎮 **Discord**: [@fenroW](https://discord.com/users/fenrow_325) 304 | 305 |
306 | 307 | --- -------------------------------------------------------------------------------- /TIME_SERIES_ANALYSIS.md: -------------------------------------------------------------------------------- 1 | # Time Series Analysis & Advanced Trading Strategies 2 | 3 | ## Overview 4 | 5 | This implementation enhances the existing pump.fun token trading bot with comprehensive time series analysis capabilities, sophisticated entry and exit criteria, advanced risk management, and market-aware trading strategies. 6 | 7 | ## Key Components 8 | 9 | ### Time Series Analysis Module 10 | - **Data Collection**: Efficiently captures and stores token price, volume, and transaction history. 11 | - **Technical Indicators**: Calculates various technical indicators including: 12 | - Simple Moving Averages (SMA) - 5, 10, 20, 50 periods 13 | - Exponential Moving Averages (EMA) - 5, 10, 20, 50 periods 14 | - Relative Strength Index (RSI) 15 | - Moving Average Convergence Divergence (MACD) 16 | - Bollinger Bands 17 | - Price Momentum 18 | - Rate of Change 19 | - **Signal Generation**: Produces actionable buy/sell signals with confidence scores. 20 | - **Serialization Support**: Properly handles data persistence with serializable types. 21 | 22 | ### Enhanced Entry Criteria 23 | - **Momentum-Based Entry**: Identifies tokens with positive price momentum. 24 | - **Buy/Sell Ratio Analysis**: Favors tokens with strong buying pressure. 25 | - **RSI Conditions**: Buys oversold tokens (RSI < 30) and sells overbought tokens (RSI > 70). 26 | - **Moving Average Crossovers**: Detects golden crosses (short-term MA crossing above long-term MA) for entries. 27 | - **Bollinger Band Rebounds**: Identifies price rebounds from lower Bollinger Band. 28 | - **Token Age Verification**: Validates that tokens are indeed new launches. 29 | 30 | ### Dynamic Exit Strategies 31 | - **Volatility-Adjusted Take Profit**: Sets take profit levels based on Bollinger Band width. 32 | - **Multiple Profit Targets**: Implements partial exits at various profit levels. 33 | - **Trailing Stops**: Dynamically adjusts stop loss as price increases. 34 | - **Time-Based Exit Adjustments**: Tightens stops the longer a position is held. 35 | 36 | ### Advanced Risk Management 37 | - **Position Sizing**: Calculates position size based on: 38 | - Technical indicator confidence levels 39 | - Portfolio risk percentage 40 | - Token volatility 41 | - **Daily Budget Controls**: Enforces maximum daily exposure. 42 | - **Risk-Adjusted Stop Losses**: Tighter stops for higher-risk tokens. 43 | 44 | ### Bonding Curve Analysis 45 | - **Curve Steepness Evaluation**: Identifies optimal entry points on bonding curves. 46 | - **Liquidity Depth Analysis**: Ensures sufficient liquidity before trade execution. 47 | - **Price Impact Calculation**: Estimates slippage for different position sizes. 48 | 49 | ## Configuration Parameters 50 | 51 | | Parameter | Default | Description | 52 | |-----------|---------|-------------| 53 | | MIN_BUY_CONFIDENCE | 65 | Minimum confidence score to execute a buy (0-100) | 54 | | MIN_SELL_CONFIDENCE | 70 | Minimum confidence score to execute a sell (0-100) | 55 | | DAILY_BUY_BUDGET | 2.0 | Maximum SOL to spend per day | 56 | | MAX_TIME_SERIES_POINTS | 100 | Number of data points to store per token | 57 | | TIME_SERIES_INTERVAL_SECS | 15 | Interval between data points in seconds | 58 | | TAKE_PROFIT_PERCENT | 20.0 | Default take profit percentage | 59 | | STOP_LOSS_PERCENT | 10.0 | Default stop loss percentage | 60 | 61 | ## Integration 62 | 63 | The time series analysis module seamlessly integrates with the existing token trading system: 64 | 65 | 1. **Data Collection**: Token data is continuously collected from the token tracker. 66 | 2. **Signal Generation**: Technical analysis generates buy/sell signals with confidence scores. 67 | 3. **Buy Execution**: High-confidence buy signals are routed to the buy manager. 68 | 4. **Dynamic Exit Parameters**: Exit parameters are adjusted based on market conditions. 69 | 5. **Sell Execution**: Sell signals trigger the sell manager with optimized parameters. 70 | 71 | ## Usage 72 | 73 | The enhanced trading system can be started using: 74 | 75 | ```rust 76 | start_enhanced_trading_system( 77 | app_state, 78 | swap_config, 79 | blacklist_enabled, 80 | take_profit_percent, 81 | stop_loss_percent, 82 | telegram_bot_token, 83 | telegram_chat_id, 84 | ).await 85 | ``` 86 | 87 | ## Technical Indicator Implementation Details 88 | 89 | ### Simple Moving Average (SMA) 90 | Calculates the average price over a specified period. 91 | 92 | ```rust 93 | fn calculate_sma(&self, prices: &[f64], period: usize) -> f64 { 94 | if prices.len() < period { 95 | return 0.0; 96 | } 97 | 98 | let sum: f64 = prices[prices.len() - period..].iter().sum(); 99 | sum / period as f64 100 | } 101 | ``` 102 | 103 | ### Exponential Moving Average (EMA) 104 | Gives more weight to recent prices for faster response to price changes. 105 | 106 | ```rust 107 | fn calculate_ema(&self, prices: &[f64], period: usize) -> f64 { 108 | if prices.len() < period { 109 | return 0.0; 110 | } 111 | 112 | let mut ema = self.calculate_sma(prices, period); 113 | let multiplier = 2.0 / (period as f64 + 1.0); 114 | 115 | for i in prices.len() - period + 1..prices.len() { 116 | ema = (prices[i] - ema) * multiplier + ema; 117 | } 118 | 119 | ema 120 | } 121 | ``` 122 | 123 | ### Relative Strength Index (RSI) 124 | Measures the speed and change of price movements, indicating overbought/oversold conditions. 125 | 126 | ```rust 127 | fn calculate_rsi(&self, prices: &[f64], period: usize) -> f64 { 128 | if prices.len() < period + 1 { 129 | return 50.0; // Default to neutral 130 | } 131 | 132 | let mut gains = 0.0; 133 | let mut losses = 0.0; 134 | 135 | for i in prices.len() - period..prices.len() { 136 | let change = prices[i] - prices[i - 1]; 137 | if change >= 0.0 { 138 | gains += change; 139 | } else { 140 | losses -= change; 141 | } 142 | } 143 | 144 | if losses == 0.0 { 145 | return 100.0; // All gains, no losses 146 | } 147 | 148 | let rs = gains / losses; 149 | 100.0 - (100.0 / (1.0 + rs)) 150 | } 151 | ``` 152 | 153 | ### Bollinger Bands 154 | Shows price volatility and potential reversal points using standard deviation. 155 | 156 | ```rust 157 | fn calculate_bollinger_bands(&self, prices: &[f64], period: usize, std_dev_multiplier: f64) -> (f64, f64, f64) { 158 | if prices.len() < period { 159 | let price = prices.last().unwrap_or(&0.0); 160 | return (*price, *price, *price); 161 | } 162 | 163 | // Calculate SMA 164 | let sma = self.calculate_sma(prices, period); 165 | 166 | // Calculate Standard Deviation 167 | let price_slice = &prices[prices.len() - period..]; 168 | let sum_squares: f64 = price_slice.iter().map(|p| (p - sma).powi(2)).sum(); 169 | let std_dev = (sum_squares / period as f64).sqrt(); 170 | 171 | // Calculate bands 172 | let upper_band = sma + (std_dev_multiplier * std_dev); 173 | let lower_band = sma - (std_dev_multiplier * std_dev); 174 | 175 | (sma, upper_band, lower_band) 176 | } 177 | ``` 178 | 179 | ## Performance and Optimization 180 | 181 | The implementation is designed for efficiency with: 182 | 183 | - **Interval-Based Data Collection**: Controls data frequency to avoid excessive CPU usage. 184 | - **Fixed-Size Data Storage**: Limits memory usage by maintaining a fixed-size data window. 185 | - **Mutex Guards**: Properly managed to avoid deadlocks across async boundaries. 186 | - **Isolated Function Pattern**: Ensures MutexGuard isn't held across await points. 187 | - **Serializable Data Structures**: Time series data can be serialized for persistence. 188 | 189 | ## Data Serialization 190 | 191 | To handle persistence, we've implemented serializable versions of our data structures: 192 | 193 | ```rust 194 | // For internal use with timestamps 195 | #[derive(Debug, Clone)] 196 | pub struct TimeSeriesDataPoint { 197 | pub timestamp: Instant, // Non-serializable std::time::Instant 198 | pub price: f64, 199 | pub volume: f64, 200 | pub buy_count: u32, 201 | pub sell_count: u32, 202 | pub market_cap: Option, 203 | pub iso_timestamp: DateTime, // For serialization 204 | } 205 | 206 | // For persistence/serialization 207 | #[derive(Debug, Clone, Serialize, Deserialize)] 208 | pub struct SerializableTimeSeriesDataPoint { 209 | pub timestamp: String, // ISO 8601 timestamp string 210 | pub price: f64, 211 | pub volume: f64, 212 | pub buy_count: u32, 213 | pub sell_count: u32, 214 | pub market_cap: Option, 215 | } 216 | ``` 217 | 218 | ## Extending the System 219 | 220 | The time series analysis system is designed for easy extension: 221 | 222 | 1. **Adding New Indicators**: Implement additional technical indicators by adding new calculation methods. 223 | 2. **Custom Signal Logic**: Develop sophisticated signal generation by modifying `generate_signals()`. 224 | 3. **Risk Profile Integration**: Connect with external risk assessment systems for better position sizing. 225 | 4. **Market-Wide Analysis**: Incorporate Solana ecosystem-wide metrics for market sentiment. 226 | 5. **Data Persistence**: Implement storage and retrieval using the serializable data structures. -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Shell script for cross-compiling Rust projects for Windows from Ubuntu 4 | 5 | set -e 6 | 7 | # Constants 8 | TARGET_X86_64="x86_64-pc-windows-gnu" 9 | TARGET_I686="i686-pc-windows-gnu" 10 | 11 | # Function to print messages 12 | function echo_info() { 13 | echo "[INFO] $1" 14 | } 15 | 16 | # Update and install required packages 17 | echo_info "Updating package list..." 18 | sudo apt update 19 | 20 | echo_info "Installing mingw-w64..." 21 | sudo apt install -y mingw-w64 22 | 23 | echo_info "Adding Rust targets..." 24 | rustup target add $TARGET_X86_64 25 | rustup target add $TARGET_I686 26 | 27 | # Build the project for x86_64 Windows 28 | echo_info "Building for 64-bit Windows..." 29 | cargo build --target=$TARGET_X86_64 30 | 31 | # Build the project for i686 Windows 32 | echo_info "Building for 32-bit Windows..." 33 | cargo build --target=$TARGET_I686 34 | 35 | echo_info "Build completed!" 36 | -------------------------------------------------------------------------------- /check_error.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if the original errors are still in the compiled output 4 | echo "Checking for E0282 type annotation errors..." 5 | cargo check --lib 2>&1 | grep -A 2 "error\[E0282\].*line.*760" 6 | cargo check --lib 2>&1 | grep -A 2 "error\[E0282\].*line.*854" 7 | cargo check --lib 2>&1 | grep -A 2 "error\[E0282\].*line.*879" 8 | 9 | echo "Checking for E0424 self reference errors..." 10 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*759" 11 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*760" 12 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*761" 13 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*762" 14 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*764" 15 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*765" 16 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*854" 17 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*875" 18 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*876" 19 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*877" 20 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*879" 21 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*880" 22 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*881" 23 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*882" 24 | cargo check --lib 2>&1 | grep -A 2 "error\[E0424\].*self.*line.*883" 25 | 26 | echo "If no output appears above this line, the errors have been fixed!" -------------------------------------------------------------------------------- /check_error2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if the errors are still in the compiled output 4 | echo "Checking for E0308 mismatched types errors..." 5 | cargo check --lib 2>&1 | grep -A 2 "error\[E0308\].*expected .Arc., found .Arc>." 6 | cargo check --lib 2>&1 | grep -A 2 "error\[E0308\].*expected .&Arc., found .&Arc>." 7 | 8 | echo "Checking for E0599 no method named lock errors..." 9 | cargo check --lib 2>&1 | grep -A 2 "error\[E0599\].*no method named .lock. found for struct" 10 | 11 | echo "If no output appears above this line, the errors have been fixed!" -------------------------------------------------------------------------------- /log/_pumpfun_buy_tx.log: -------------------------------------------------------------------------------- 1 | Transaction: SubscribeUpdateTransaction { transaction: Some(SubscribeUpdateTransactionInfo { signature: [231, 110, 219, 40, 194, 48, 159, 224, 64, 185, 72, 40, 168, 2, 69, 226, 239, 126, 38, 209, 254, 29, 225, 101, 224, 24, 137, 63, 14, 34, 236, 227, 201, 236, 80, 71, 92, 40, 179, 100, 16, 27, 164, 195, 214, 170, 46, 3, 248, 217, 220, 197, 142, 135, 113, 246, 184, 230, 118, 190, 224, 242, 216, 7], is_vote: false, transaction: Some(Transaction { signatures: [[231, 110, 219, 40, 194, 48, 159, 224, 64, 185, 72, 40, 168, 2, 69, 226, 239, 126, 38, 209, 254, 29, 225, 101, 224, 24, 137, 63, 14, 34, 236, 227, 201, 236, 80, 71, 92, 40, 179, 100, 16, 27, 164, 195, 214, 170, 46, 3, 248, 217, 220, 197, 142, 135, 113, 246, 184, 230, 118, 190, 224, 242, 216, 7]], message: Some(Message { header: Some(MessageHeader { num_required_signatures: 1, num_readonly_signed_accounts: 0, num_readonly_unsigned_accounts: 11 }), account_keys: [[215, 90, 98, 179, 164, 254, 47, 56, 199, 123, 194, 154, 88, 243, 24, 180, 161, 93, 154, 11, 240, 234, 124, 99, 29, 99, 51, 42, 173, 205, 178, 10], [166, 81, 232, 61, 150, 239, 175, 228, 103, 54, 184, 154, 165, 44, 16, 237, 80, 73, 139, 251, 252, 49, 187, 81, 103, 186, 24, 83, 45, 121, 140, 35], [131, 132, 116, 41, 46, 103, 90, 148, 180, 54, 236, 176, 169, 152, 137, 66, 50, 138, 131, 221, 198, 35, 56, 2, 150, 18, 103, 197, 205, 97, 23, 203], [92, 174, 255, 29, 106, 19, 188, 217, 204, 140, 122, 141, 187, 167, 218, 214, 203, 150, 110, 135, 81, 181, 211, 166, 218, 90, 141, 68, 214, 218, 67, 6], [130, 200, 126, 247, 228, 126, 15, 82, 199, 194, 75, 52, 94, 185, 36, 147, 111, 193, 170, 96, 148, 179, 109, 56, 81, 42, 191, 62, 141, 129, 25, 252], [51, 197, 17, 193, 120, 182, 64, 61, 192, 147, 230, 110, 135, 1, 200, 181, 70, 180, 109, 186, 103, 67, 204, 181, 102, 233, 227, 55, 226, 241, 127, 141], [136, 241, 255, 163, 162, 223, 230, 23, 189, 196, 227, 87, 50, 81, 163, 34, 227, 252, 174, 129, 229, 164, 87, 57, 14, 100, 117, 28, 0, 164, 101, 226], [3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, 187, 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0], [10, 241, 195, 67, 33, 136, 202, 58, 99, 81, 53, 161, 58, 24, 149, 27, 168, 54, 240, 158, 220, 246, 219, 95, 93, 80, 90, 50, 112, 80, 255, 153], [140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89], [39, 179, 89, 60, 111, 7, 199, 143, 177, 185, 92, 14, 57, 0, 253, 57, 205, 108, 197, 206, 12, 16, 222, 95, 16, 236, 230, 12, 40, 11, 116, 95], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169], [147, 255, 101, 186, 115, 58, 105, 199, 69, 59, 167, 240, 106, 127, 213, 38, 55, 179, 121, 200, 2, 79, 247, 45, 37, 182, 36, 45, 78, 138, 128, 37], [58, 134, 94, 105, 238, 15, 84, 128, 202, 188, 246, 99, 87, 228, 220, 47, 24, 213, 141, 69, 193, 234, 116, 137, 251, 55, 35, 217, 121, 60, 114, 166], [6, 167, 213, 23, 25, 44, 92, 81, 33, 140, 201, 76, 61, 74, 241, 127, 88, 218, 238, 8, 155, 161, 253, 68, 227, 219, 217, 138, 0, 0, 0, 0], [172, 241, 54, 235, 1, 252, 28, 78, 136, 61, 35, 200, 181, 132, 74, 181, 154, 55, 246, 106, 221, 87, 197, 233, 172, 59, 83, 224, 89, 211, 92, 100], [1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, 182, 253, 109, 24, 176]], 2 | recent_blockhash: [214, 244, 173, 248, 144, 238, 253, 86, 43, 52, 158, 107, 196, 148, 27, 228, 22, 32, 221, 186, 23, 111, 117, 168, 5, 28, 164, 124, 8, 137, 247, 144], instructions: [CompiledInstruction { program_id_index: 7, accounts: [8], data: [2, 160, 134, 1, 0] }, CompiledInstruction { program_id_index: 7, accounts: [], data: [3, 128, 150, 152, 0, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 9, accounts: [0, 1, 0, 10, 11, 12], data: [1] }, CompiledInstruction { program_id_index: 13, accounts: [14, 2, 10, 3, 4, 1, 0, 11, 12, 15, 16, 17], data: [0, 192, 158, 230, 5, 0, 0, 0, 0, 252, 162, 190, 204, 117, 0, 0, 0] }, CompiledInstruction { program_id_index: 11, accounts: [0, 5], data: [2, 0, 0, 0, 64, 66, 15, 0, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 11, accounts: [0, 6], data: [2, 0, 0, 0, 64, 66, 15, 0, 0, 0, 0, 0] }], versioned: false, address_table_lookups: [] }) }), meta: Some(TransactionStatusMeta { err: None, fee: 1005000, pre_balances: [202203953, 0, 5747167575356, 42043608356, 2039280, 701124783572, 14375310, 1, 0, 731913600, 1461600, 1, 934087680, 1141440, 290898417, 1009200, 137104014, 1141440], post_balances: [98159673, 2039280, 5747168555555, 42141628157, 2039280, 701125783572, 15375310, 1, 0, 731913600, 1461600, 1, 934087680, 1141440, 290898417, 1009200, 137104014, 1141440], inner_instructions: [InnerInstructions { index: 2, instructions: [InnerInstruction { program_id_index: 12, accounts: [10], data: [21, 7, 0], stack_height: Some(2) }, InnerInstruction { program_id_index: 11, accounts: [0, 1], data: [0, 0, 0, 0, 240, 29, 31, 0, 0, 0, 0, 0, 165, 0, 0, 0, 0, 0, 0, 0, 6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169], stack_height: Some(2) }, InnerInstruction { program_id_index: 12, accounts: [1], data: [22], stack_height: Some(2) }, InnerInstruction { program_id_index: 12, accounts: [1, 10], data: [18, 215, 90, 98, 179, 164, 254, 47, 56, 199, 123, 194, 154, 88, 243, 24, 180, 161, 93, 154, 11, 240, 234, 124, 99, 29, 99, 51, 42, 173, 205, 178, 10], stack_height: Some(2) }] }, InnerInstructions { index: 3, instructions: [InnerInstruction { program_id_index: 17, accounts: [14, 2, 10, 3, 4, 1, 0, 11, 12, 15, 16, 17], data: [102, 6, 61, 18, 1, 218, 235, 234, 200, 246, 23, 92, 141, 0, 0, 0, 192, 158, 230, 5, 0, 0, 0, 0], stack_height: Some(2) }, InnerInstruction { program_id_index: 12, accounts: [4, 1, 3], data: [3, 200, 246, 23, 92, 141, 0, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 11, accounts: [0, 3], data: [2, 0, 0, 0, 217, 169, 215, 5, 0, 0, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 11, accounts: [0, 2], data: [2, 0, 0, 0, 231, 244, 14, 0, 0, 0, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 17, accounts: [16], data: [228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238, 39, 179, 89, 60, 111, 7, 199, 143, 177, 185, 92, 14, 57, 0, 253, 57, 205, 108, 197, 206, 12, 16, 222, 95, 16, 236, 230, 12, 40, 11, 116, 95, 217, 169, 215, 5, 0, 0, 0, 0, 200, 246, 23, 92, 141, 0, 0, 0, 1, 215, 90, 98, 179, 164, 254, 47, 56, 199, 123, 194, 154, 88, 243, 24, 180, 161, 93, 154, 11, 240, 234, 124, 99, 29, 99, 51, 42, 173, 205, 178, 10, 170, 163, 24, 104, 0, 0, 0, 0, 253, 26, 209, 203, 16, 0, 0, 0, 109, 119, 15, 49, 214, 149, 1, 0, 253, 110, 173, 207, 9, 0, 0, 0, 109, 223, 252, 228, 68, 151, 0, 0], stack_height: Some(3) }] }], inner_instructions_none: false, log_messages: ["Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", "Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", "Program log: CreateIdempotent", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", "Program log: Instruction: GetAccountDataSize", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1569 of 94295 compute units", "Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program 11111111111111111111111111111111 invoke [2]", "Program 11111111111111111111111111111111 success", "Program log: Initialize the associated token account", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", "Program log: Instruction: InitializeImmutableOwner", "Program log: Please upgrade to SPL Token 2022 for immutable owner support", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1405 of 87708 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", "Program log: Instruction: InitializeAccount3", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4188 of 83826 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 20345 of 99700 compute units", "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", "Program AxiomQpD1TrYEHNYLts8h3ko1NHdtxfgNgHryj2hJJx4 invoke [1]", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]", "Program log: Instruction: Buy", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", "Program log: Instruction: Transfer", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 58452 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program 11111111111111111111111111111111 invoke [3]", "Program 11111111111111111111111111111111 success", "Program 11111111111111111111111111111111 invoke [3]", "Program 11111111111111111111111111111111 success", "Program data: vdt/007mYe4ns1k8bwfHj7G5XA45AP05zWzFzgwQ3l8Q7OYMKAt0X9mp1wUAAAAAyPYXXI0AAAAB11pis6T+LzjHe8KaWPMYtKFdmgvw6nxjHWMzKq3NsgqqoxhoAAAAAP0a0csQAAAAbXcPMdaVAQD9bq3PCQAAAG3f/ORElwAA", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [3]", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 1997 of 45903 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 32499 of 75630 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program AxiomQpD1TrYEHNYLts8h3ko1NHdtxfgNgHryj2hJJx4 consumed 36300 of 79355 compute units", "Program AxiomQpD1TrYEHNYLts8h3ko1NHdtxfgNgHryj2hJJx4 success", "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success", "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"], log_messages_none: false, 3 | pre_token_balances: [ 4 | TokenBalance { account_index: 4, mint: "3fyWzCoaEzJewjXfhrHFEndHWVGdVLWZEAQoQ86Lpump", ui_token_amount: Some(UiTokenAmount { ui_amount: 373829290.810933, decimals: 6, amount: "373829290810933", ui_amount_string: "373829290.810933" }), owner: "7EoH1YUsASscDWxgk364EqnpL2HbUrP8APDFZVkLkegu", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], 5 | post_token_balances: [TokenBalance { account_index: 1, mint: "3fyWzCoaEzJewjXfhrHFEndHWVGdVLWZEAQoQ86Lpump", ui_token_amount: Some(UiTokenAmount { ui_amount: 607135.463112, decimals: 6, amount: "607135463112", ui_amount_string: "607135.463112" }), owner: "FVebN3A8HcSC8ZmuMhXRM4prfWc2WbZYoWEVrRj6gZiR", program_id: pump"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, 6 | TokenBalance { account_index: 4, mint: "3fyWzCoaEzJewjXfhrHFEndHWVGdVLWZEAQoQ86Lpump", ui_token_amount: Some(UiTokenAmount { ui_amount: 373222155.347821, decimals: 6, amount: "373222155347821", ui_amount_string: "373222155.347821" }), owner: "7EoH1YUsASscDWxgk364EqnpL2HbUrP8APDFZVkLkegu", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], rewards: [], loaded_writable_addresses: [], loaded_readonly_addresses: [], return_data: None, return_data_none: true, compute_units_consumed: Some(57245) }), index: 1077 }), slot: 337984963 } 7 | 8 | -------------------------------------------------------------------------------- /log/_pumpfun_sell_tx.log: -------------------------------------------------------------------------------- 1 | Transaction: SubscribeUpdateTransaction { transaction: Some(SubscribeUpdateTransactionInfo { signature: [12, 91, 5, 74, 226, 66, 253, 119, 219, 111, 249, 219, 249, 99, 226, 185, 13, 97, 165, 114, 246, 108, 83, 177, 130, 217, 53, 87, 158, 215, 150, 6, 64, 79, 221, 174, 141, 248, 75, 12, 167, 37, 237, 74, 148, 204, 112, 4, 250, 166, 17, 231, 59, 82, 56, 249, 170, 223, 249, 81, 5, 43, 49, 6], is_vote: false, transaction: Some(Transaction { signatures: [[12, 91, 5, 74, 226, 66, 253, 119, 219, 111, 249, 219, 249, 99, 226, 185, 13, 97, 165, 114, 246, 108, 83, 177, 130, 217, 53, 87, 158, 215, 150, 6, 64, 79, 221, 174, 141, 248, 75, 12, 167, 37, 237, 74, 148, 204, 112, 4, 250, 166, 17, 231, 59, 82, 56, 249, 170, 223, 249, 81, 5, 43, 49, 6]], message: Some(Message { header: Some(MessageHeader { num_required_signatures: 1, num_readonly_signed_accounts: 0, num_readonly_unsigned_accounts: 9 }), account_keys: [[39, 145, 191, 193, 93, 236, 41, 194, 20, 126, 83, 64, 86, 160, 18, 70, 177, 136, 116, 92, 227, 244, 102, 21, 251, 156, 105, 221, 75, 227, 106, 24], [96, 140, 204, 29, 252, 233, 97, 180, 59, 119, 156, 25, 21, 5, 166, 226, 211, 191, 69, 213, 164, 219, 70, 24, 173, 118, 200, 45, 97, 117, 69, 53], [27, 144, 16, 57, 1, 238, 122, 71, 88, 77, 9, 173, 21, 79, 6, 84, 204, 190, 77, 43, 19, 59, 79, 142, 242, 166, 89, 221, 6, 184, 220, 206], [36, 158, 224, 196, 119, 130, 145, 63, 167, 254, 33, 54, 221, 89, 238, 238, 151, 246, 8, 211, 14, 216, 101, 233, 207, 102, 61, 5, 150, 252, 176, 188], [249, 171, 170, 213, 74, 239, 240, 20, 68, 52, 160, 171, 122, 212, 100, 80, 8, 232, 241, 147, 16, 52, 37, 69, 1, 184, 158, 23, 130, 216, 79, 190], [94, 17, 59, 3, 115, 173, 69, 49, 209, 145, 126, 20, 153, 40, 44, 162, 190, 127, 83, 39, 52, 84, 226, 251, 203, 13, 89, 144, 253, 113, 234, 58], [136, 241, 255, 163, 162, 223, 230, 23, 189, 196, 227, 87, 50, 81, 163, 34, 227, 252, 174, 129, 229, 164, 87, 57, 14, 100, 117, 28, 0, 164, 101, 226], [3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, 187, 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0], [10, 241, 195, 67, 33, 136, 202, 58, 99, 81, 53, 161, 58, 24, 149, 27, 168, 54, 240, 158, 220, 246, 219, 95, 93, 80, 90, 50, 112, 80, 255, 153], [1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, 182, 253, 109, 24, 176], [58, 134, 94, 105, 238, 15, 84, 128, 202, 188, 246, 99, 87, 228, 220, 47, 24, 213, 141, 69, 193, 234, 116, 137, 251, 55, 35, 217, 121, 60, 114, 166], [170, 31, 134, 105, 13, 14, 157, 198, 242, 89, 53, 114, 159, 157, 213, 145, 162, 101, 173, 197, 125, 246, 12, 150, 47, 76, 145, 216, 139, 113, 120, 255], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89], [6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169], [172, 241, 54, 235, 1, 252, 28, 78, 136, 61, 35, 200, 181, 132, 74, 181, 154, 55, 246, 106, 221, 87, 197, 233, 172, 59, 83, 224, 89, 211, 92, 100]], recent_blockhash: [214, 244, 173, 248, 144, 238, 253, 86, 43, 52, 158, 107, 196, 148, 27, 228, 22, 32, 221, 186, 23, 111, 117, 168, 5, 28, 164, 124, 8, 137, 247, 144], instructions: [CompiledInstruction { program_id_index: 7, accounts: [8], data: [2, 160, 134, 1, 0] }, CompiledInstruction { program_id_index: 7, accounts: [], data: [3, 0, 9, 61, 0, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 9, accounts: [10, 1, 11, 2, 3, 4, 0, 12, 13, 14, 15, 9], data: [51, 230, 133, 164, 1, 127, 131, 173, 142, 200, 39, 111, 54, 4, 0, 0, 188, 64, 117, 7, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 12, accounts: [0, 5], data: [2, 0, 0, 0, 155, 221, 23, 0, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 12, accounts: [0, 6], data: [2, 0, 0, 0, 128, 26, 6, 0, 0, 0, 0, 0] }], versioned: false, address_table_lookups: [] }) }), meta: Some(TransactionStatusMeta { err: None, fee: 405000, pre_balances: [1123324762, 5133659401479, 3217206694, 2039280, 2039280, 13984546529416, 2954590, 1, 0, 1141440, 290898417, 1461600, 1, 731913600, 934087680, 137104014], post_balances: [1277361698, 5133660981338, 3059220840, 2039280, 2039280, 13984548093475, 3354590, 1, 0, 1141440, 290898417, 1461600, 1, 731913600, 934087680, 137104014], inner_instructions: [InnerInstructions { index: 2, instructions: [InnerInstruction { program_id_index: 14, accounts: [4, 3, 0], data: [3, 142, 200, 39, 111, 54, 4, 0, 0], stack_height: Some(2) }, InnerInstruction { program_id_index: 9, accounts: [15], data: [228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238, 170, 31, 134, 105, 13, 14, 157, 198, 242, 89, 53, 114, 159, 157, 213, 145, 162, 101, 173, 197, 125, 246, 12, 150, 47, 76, 145, 216, 139, 113, 120, 255, 62, 172, 106, 9, 0, 0, 0, 0, 142, 200, 39, 111, 54, 4, 0, 0, 0, 39, 145, 191, 193, 93, 236, 41, 194, 20, 126, 83, 64, 86, 160, 18, 70, 177, 136, 116, 92, 227, 244, 102, 21, 251, 156, 105, 221, 75, 227, 106, 24, 169, 163, 24, 104, 0, 0, 0, 0, 104, 229, 82, 178, 7, 0, 0, 0, 200, 13, 176, 49, 167, 117, 3, 0, 104, 57, 47, 182, 0, 0, 0, 0, 200, 117, 157, 229, 21, 119, 2, 0], stack_height: Some(2) }] }], inner_instructions_none: false, log_messages: ["Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", "Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [1]", "Program log: Instruction: Sell", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", "Program log: Instruction: Transfer", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 79582 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program data: vdt/007mYe6qH4ZpDQ6dxvJZNXKfndWRomWtxX32DJYvTJHYi3F4/z6sagkAAAAAjsgnbzYEAAAAJ5G/wV3sKcIUflNAVqASRrGIdFzj9GYV+5xp3UvjahipoxhoAAAAAGjlUrIHAAAAyA2wMad1AwBoOS+2AAAAAMh1neUVdwIA", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 1997 of 71081 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 31391 of 99700 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success", "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"], log_messages_none: false, 2 | pre_token_balances: [ 3 | TokenBalance { 4 | account_index: 3, 5 | mint: "CT6BzZbuB6EWxLU3JNELKA5uRGy3Mjf1PzSnvuxvpump", 6 | ui_token_amount: Some(UiTokenAmount { ui_amount: 896154044.11833, decimals: 6, amount: "896154044118330", ui_amount_string: "896154044.11833" }), owner: "2rbRFsc3PYqEkYjqpkyC9wUsnehdywhQjC9YXU12bhmP", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 7 | }, 8 | TokenBalance { account_index: 4, mint: "CT6BzZbuB6EWxLU3JNELKA5uRGy3Mjf1PzSnvuxvpump", ui_token_amount: Some(UiTokenAmount { ui_amount: 4631839.62331, decimals: 6, amount: "4631839623310", ui_amount_string: "4631839.62331" }), owner: "3fToUvzR4UhmXumybMRHEgpniGkhcMbaMo8SEkEFiswm", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], post_token_balances: [TokenBalance { account_index: 3, mint: "CT6BzZbuB6EWxLU3JNELKA5uRGy3Mjf1PzSnvuxvpump", ui_token_amount: Some(UiTokenAmount { ui_amount: 900785883.74164, decimals: 6, amount: "900785883741640", ui_amount_string: "900785883.74164" }), owner: "2rbRFsc3PYqEkYjqpkyC9wUsnehdywhQjC9YXU12bhmP", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, TokenBalance { account_index: 4, mint: "CT6BzZbuB6EWxLU3JNELKA5uRGy3Mjf1PzSnvuxvpump", ui_token_amount: Some(UiTokenAmount { ui_amount: 0.0, decimals: 6, amount: "0", ui_amount_string: "0" }), owner: "3fToUvzR4UhmXumybMRHEgpniGkhcMbaMo8SEkEFiswm", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], rewards: [], loaded_writable_addresses: [], loaded_readonly_addresses: [], return_data: None, return_data_none: true, compute_units_consumed: Some(31991) }), index: 36 }), slot: 337984962 } 9 | -------------------------------------------------------------------------------- /log/pumpfun_buy_tx_1746445226684.log: -------------------------------------------------------------------------------- 1 | Transaction: SubscribeUpdateTransaction { transaction: Some(SubscribeUpdateTransactionInfo { signature: [194, 238, 212, 200, 131, 24, 8, 61, 146, 51, 78, 250, 98, 80, 224, 161, 156, 139, 162, 159, 102, 241, 195, 152, 113, 187, 35, 29, 9, 253, 115, 4, 93, 175, 1, 133, 83, 0, 178, 183, 59, 250, 21, 165, 231, 127, 101, 168, 79, 82, 35, 251, 99, 132, 181, 60, 247, 227, 133, 197, 118, 237, 71, 9], is_vote: false, transaction: Some(Transaction { signatures: [[194, 238, 212, 200, 131, 24, 8, 61, 146, 51, 78, 250, 98, 80, 224, 161, 156, 139, 162, 159, 102, 241, 195, 152, 113, 187, 35, 29, 9, 253, 115, 4, 93, 175, 1, 133, 83, 0, 178, 183, 59, 250, 21, 165, 231, 127, 101, 168, 79, 82, 35, 251, 99, 132, 181, 60, 247, 227, 133, 197, 118, 237, 71, 9]], message: Some(Message { header: Some(MessageHeader { num_required_signatures: 1, num_readonly_signed_accounts: 0, num_readonly_unsigned_accounts: 11 }), account_keys: [[192, 69, 57, 70, 187, 133, 249, 39, 206, 254, 167, 159, 67, 81, 213, 172, 208, 207, 150, 38, 71, 226, 202, 186, 164, 24, 224, 252, 86, 156, 167, 205], [49, 127, 120, 203, 80, 114, 71, 38, 221, 40, 76, 67, 203, 203, 54, 178, 254, 136, 182, 194, 63, 46, 188, 250, 215, 106, 201, 159, 86, 245, 0, 80], [94, 101, 149, 63, 58, 105, 164, 215, 240, 34, 97, 10, 102, 159, 185, 136, 208, 121, 72, 4, 89, 96, 155, 245, 50, 133, 166, 124, 33, 95, 207, 251], [120, 82, 28, 177, 121, 206, 187, 133, 137, 181, 86, 162, 213, 236, 148, 210, 73, 134, 130, 253, 249, 187, 42, 245, 173, 100, 228, 145, 204, 65, 83, 218], [133, 140, 195, 137, 26, 80, 167, 203, 50, 66, 39, 205, 36, 45, 190, 143, 85, 116, 28, 207, 157, 180, 102, 131, 223, 226, 52, 183, 123, 32, 62, 80], [138, 85, 7, 116, 153, 201, 1, 165, 192, 17, 247, 161, 85, 86, 253, 215, 193, 65, 14, 149, 102, 212, 192, 136, 14, 119, 227, 209, 185, 167, 16, 127], [173, 17, 230, 164, 252, 41, 68, 164, 250, 130, 81, 190, 248, 21, 66, 110, 27, 251, 40, 198, 182, 100, 102, 119, 96, 124, 106, 217, 245, 102, 166, 70], [224, 111, 193, 136, 58, 37, 13, 157, 155, 142, 70, 238, 114, 248, 189, 114, 111, 104, 5, 231, 64, 207, 121, 84, 160, 99, 143, 3, 101, 112, 160, 121], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, 182, 253, 109, 24, 176], [3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, 187, 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0], [6, 155, 136, 87, 254, 171, 129, 132, 251, 104, 127, 99, 70, 24, 192, 53, 218, 196, 57, 220, 26, 235, 59, 85, 152, 160, 240, 0, 0, 0, 0, 1], [6, 167, 213, 23, 25, 44, 92, 81, 33, 140, 201, 76, 61, 74, 241, 127, 88, 218, 238, 8, 155, 161, 253, 68, 227, 219, 217, 138, 0, 0, 0, 0], [6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169], [58, 134, 94, 105, 238, 15, 84, 128, 202, 188, 246, 99, 87, 228, 220, 47, 24, 213, 141, 69, 193, 234, 116, 137, 251, 55, 35, 217, 121, 60, 114, 166], [98, 146, 246, 22, 89, 15, 27, 63, 244, 22, 250, 12, 40, 145, 90, 167, 84, 27, 219, 66, 154, 196, 157, 102, 214, 3, 191, 185, 228, 142, 21, 49], [140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89], [172, 241, 54, 235, 1, 252, 28, 78, 136, 61, 35, 200, 181, 132, 74, 181, 154, 55, 246, 106, 221, 87, 197, 233, 172, 59, 83, 224, 89, 211, 92, 100], [194, 29, 204, 235, 167, 123, 6, 175, 116, 83, 36, 173, 25, 143, 121, 124, 239, 67, 186, 222, 40, 75, 33, 19, 198, 65, 15, 228, 91, 226, 0, 194]], recent_blockhash: [28, 163, 65, 88, 219, 104, 19, 158, 179, 193, 7, 221, 7, 201, 41, 112, 226, 55, 135, 177, 223, 215, 117, 200, 146, 32, 54, 95, 60, 68, 78, 106], instructions: [CompiledInstruction { program_id_index: 10, accounts: [], data: [2, 160, 134, 1, 0] }, CompiledInstruction { program_id_index: 10, accounts: [], data: [3, 80, 195, 0, 0, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 18, accounts: [9, 0, 11, 15, 14, 6, 15, 5, 4, 2, 7, 17, 8, 13, 16, 12, 3, 1], data: [254, 170, 88, 67, 216, 196, 208, 47, 17, 81, 240, 253, 115, 54, 192, 239, 95, 1, 1, 0, 184, 100, 217, 69, 0, 0, 0, 35, 252, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 161, 7, 0, 0, 0, 0, 0, 192, 132, 207, 79, 0, 0, 0, 0, 196, 156, 41, 116, 36, 40, 0, 0, 1, 203, 40, 43, 26, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 8, accounts: [0, 3], data: [2, 0, 0, 0, 35, 252, 44, 0, 0, 0, 0, 0] }], versioned: true, address_table_lookups: [] }) }), meta: Some(TransactionStatusMeta { err: None, fee: 10000, pre_balances: [575955985409, 1559040, 443583060, 14269297, 2039280, 4536687, 5445267414362, 2039280, 1, 1141440, 1, 1035589724898, 1009200, 934087680, 290898417, 1461800, 731913600, 137104014, 1141440], post_balances: [575955066558, 1559040, 0, 17217428, 2039280, 441708746, 5445271786083, 2039280, 1, 1141440, 1, 1035589724898, 1009200, 934087680, 290898417, 1461800, 731913600, 137104014, 1141440], inner_instructions: [InnerInstructions { index: 2, instructions: [InnerInstruction { program_id_index: 13, accounts: [2, 0, 0, 0], data: [9], stack_height: Some(2) }, InnerInstruction { program_id_index: 9, accounts: [14, 6, 15, 5, 4, 7, 0, 8, 13, 12, 17, 9], data: [102, 6, 61, 18, 1, 218, 235, 234, 172, 242, 207, 217, 3, 14, 0, 0, 100, 108, 81, 26, 0, 0, 0, 0], stack_height: Some(2) }, InnerInstruction { program_id_index: 13, accounts: [4, 7, 5], data: [3, 172, 242, 207, 217, 3, 14, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 8, accounts: [0, 5], data: [2, 0, 0, 0, 91, 183, 14, 26, 0, 0, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 8, accounts: [0, 6], data: [2, 0, 0, 0, 9, 181, 66, 0, 0, 0, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 9, accounts: [17], data: [228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238, 98, 146, 246, 22, 89, 15, 27, 63, 244, 22, 250, 12, 40, 145, 90, 167, 84, 27, 219, 66, 154, 196, 157, 102, 214, 3, 191, 185, 228, 142, 21, 49, 91, 183, 14, 26, 0, 0, 0, 0, 172, 242, 207, 217, 3, 14, 0, 0, 1, 192, 69, 57, 70, 187, 133, 249, 39, 206, 254, 167, 159, 67, 81, 213, 172, 208, 207, 150, 38, 71, 226, 202, 186, 164, 24, 224, 252, 86, 156, 167, 205, 169, 163, 24, 104, 0, 0, 0, 0, 202, 212, 78, 22, 7, 0, 0, 0, 241, 139, 109, 232, 207, 193, 3, 0, 202, 40, 43, 26, 0, 0, 0, 0, 241, 243, 90, 156, 62, 195, 2, 0], stack_height: Some(3) }] }], inner_instructions_none: false, log_messages: ["Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", "Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", "Program E4kT3ceuMGBUfV4DvUFePe1c8anPN6wPi2yd5nFxBVpm invoke [1]", "Program log: 0 swap", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", "Program log: Instruction: CloseAccount", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2997 of 91505 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]", "Program log: Instruction: Buy", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", "Program log: Instruction: Transfer", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 59685 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program 11111111111111111111111111111111 invoke [3]", "Program 11111111111111111111111111111111 success", "Program 11111111111111111111111111111111 invoke [3]", "Program 11111111111111111111111111111111 success", "Program data: vdt/007mYe5ikvYWWQ8bP/QW+gwokVqnVBvbQprEnWbWA7+55I4VMVu3DhoAAAAArPLP2QMOAAABwEU5RruF+SfO/qefQ1HVrNDPliZH4sq6pBjg/Facp82poxhoAAAAAMrUThYHAAAA8Ytt6M/BAwDKKCsaAAAAAPHzWpw+wwIA", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [3]", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 1997 of 47136 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 36999 of 81363 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program E4kT3ceuMGBUfV4DvUFePe1c8anPN6wPi2yd5nFxBVpm consumed 56020 of 99700 compute units", "Program E4kT3ceuMGBUfV4DvUFePe1c8anPN6wPi2yd5nFxBVpm success", "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"], log_messages_none: false, 2 | 3 | pre_token_balances: [TokenBalance { account_index: 2, mint: "So11111111111111111111111111111111111111112", ui_token_amount: Some(UiTokenAmount { ui_amount: 0.44154378, decimals: 9, amount: "441543780", ui_amount_string: "0.44154378" }), owner: "DwYVzJaAW683T474NP2nAe7S84cLrbhsUWqCcKko9Lr8", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, TokenBalance { account_index: 4, mint: "7dnvoiZYAsL26NZjAgrSm28mZJggU9D2pHfF7rKQMSxg", ui_token_amount: Some(UiTokenAmount { ui_amount: 999933333.99107, decimals: 6, amount: "999933333991070", ui_amount_string: "999933333.99107" }), owner: "AJzTpn6D6eKiz2S6FeptUxnP1frABWQwGXya5yDeS2Rt", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, TokenBalance { account_index: 7, mint: "7dnvoiZYAsL26NZjAgrSm28mZJggU9D2pHfF7rKQMSxg", ui_token_amount: Some(UiTokenAmount { ui_amount: 0.0, decimals: 6, amount: "0", ui_amount_string: "0" }), owner: "DwYVzJaAW683T474NP2nAe7S84cLrbhsUWqCcKko9Lr8", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], 4 | 5 | post_token_balances: [TokenBalance { account_index: 4, mint: "7dnvoiZYAsL26NZjAgrSm28mZJggU9D2pHfF7rKQMSxg", ui_token_amount: Some(UiTokenAmount { ui_amount: 984523632.01637, decimals: 6, amount: "984523632016370", ui_amount_string: "984523632.01637" }), owner: "AJzTpn6D6eKiz2S6FeptUxnP1frABWQwGXya5yDeS2Rt", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, TokenBalance { account_index: 7, mint: "7dnvoiZYAsL26NZjAgrSm28mZJggU9D2pHfF7rKQMSxg", ui_token_amount: Some(UiTokenAmount { ui_amount: 15409701.9747, decimals: 6, amount: "15409701974700", ui_amount_string: "15409701.9747" }), owner: "DwYVzJaAW683T474NP2nAe7S84cLrbhsUWqCcKko9Lr8", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], rewards: [], loaded_writable_addresses: [], loaded_readonly_addresses: [], return_data: None, return_data_none: true, compute_units_consumed: Some(56470) }), index: 1107 }), slot: 337984962 } -------------------------------------------------------------------------------- /log/pumpfun_buy_tx_1746445226689.log: -------------------------------------------------------------------------------- 1 | Transaction: SubscribeUpdateTransaction { 2 | transaction: 3 | Some(SubscribeUpdateTransactionInfo { 4 | signature: 5 | [135, 76, 177, 85, 202, 186, 214, 77, 8, 76, 216, 103, 120, 119, 123, 215, 123, 244, 150, 167, 8, 169, 211, 166, 75, 254, 15, 168, 236, 139, 219, 160, 89, 131, 49, 58, 110, 10, 220, 112, 55, 38, 236, 137, 249, 149, 106, 222, 96, 251, 195, 111, 201, 142, 65, 37, 82, 14, 236, 246, 87, 163, 245, 5 6 | ], 7 | is_vote: false, 8 | transaction: 9 | Some(Transaction 10 | { 11 | signatures: 12 | [[135, 76, 177, 85, 202, 186, 214, 77, 8, 76, 216, 103, 120, 119, 123, 215, 123, 244, 150, 167, 8, 169, 211, 166, 75, 254, 15, 168, 236, 139, 219, 160, 89, 131, 49, 58, 110, 10, 220, 112, 55, 38, 236, 137, 249, 149, 106, 222, 96, 251, 195, 111, 201, 142, 65, 37, 82, 14, 236, 246, 87, 163, 245, 5]], 13 | message: 14 | Some( 15 | Message { header: Some(MessageHeader { num_required_signatures: 1, num_readonly_signed_accounts: 0, num_readonly_unsigned_accounts: 11 }), account_keys: [[113, 194, 9, 213, 111, 117, 31, 199, 106, 14, 153, 255, 113, 120, 185, 72, 137, 231, 136, 44, 151, 55, 78, 60, 108, 162, 119, 3, 113, 28, 12, 51], [132, 145, 160, 203, 65, 208, 144, 37, 63, 212, 138, 57, 87, 233, 173, 129, 88, 41, 237, 131, 18, 195, 153, 37, 28, 130, 158, 221, 77, 165, 221, 163], [99, 131, 115, 0, 14, 162, 44, 178, 100, 211, 74, 255, 100, 160, 75, 94, 250, 191, 187, 116, 221, 205, 4, 137, 151, 177, 152, 21, 71, 215, 209, 16], [129, 2, 99, 162, 94, 172, 199, 1, 208, 88, 136, 35, 185, 190, 155, 108, 109, 207, 217, 232, 71, 64, 167, 108, 150, 23, 234, 95, 144, 125, 19, 247], [113, 249, 38, 10, 33, 37, 8, 91, 213, 225, 214, 91, 253, 41, 215, 96, 117, 78, 160, 217, 244, 45, 211, 182, 139, 233, 173, 199, 195, 3, 120, 216], [94, 17, 59, 3, 115, 173, 69, 49, 209, 145, 126, 20, 153, 40, 44, 162, 190, 127, 83, 39, 52, 84, 226, 251, 203, 13, 89, 144, 253, 113, 234, 58], [120, 82, 28, 177, 121, 206, 187, 133, 137, 181, 86, 162, 213, 236, 148, 210, 73, 134, 130, 253, 249, 187, 42, 245, 173, 100, 228, 145, 204, 65, 83, 218], [3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, 187, 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0], [10, 241, 195, 67, 33, 136, 202, 58, 99, 81, 53, 161, 58, 24, 149, 27, 168, 54, 240, 158, 220, 246, 219, 95, 93, 80, 90, 50, 112, 80, 255, 153], [140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89], [30, 74, 58, 31, 194, 57, 195, 217, 54, 50, 102, 207, 20, 89, 62, 48, 133, 87, 182, 208, 96, 216, 100, 29, 64, 60, 202, 30, 58, 183, 233, 31], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169], [147, 255, 101, 186, 115, 58, 105, 199, 69, 59, 167, 240, 106, 127, 213, 38, 55, 179, 121, 200, 2, 79, 247, 45, 37, 182, 36, 45, 78, 138, 128, 37], [58, 134, 94, 105, 238, 15, 84, 128, 202, 188, 246, 99, 87, 228, 220, 47, 24, 213, 141, 69, 193, 234, 116, 137, 251, 55, 35, 217, 121, 60, 114, 166], [6, 167, 213, 23, 25, 44, 92, 81, 33, 140, 201, 76, 61, 74, 241, 127, 88, 218, 238, 8, 155, 161, 253, 68, 227, 219, 217, 138, 0, 0, 0, 0], [172, 241, 54, 235, 1, 252, 28, 78, 136, 61, 35, 200, 181, 132, 74, 181, 154, 55, 246, 106, 221, 87, 197, 233, 172, 59, 83, 224, 89, 211, 92, 100], [1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, 182, 253, 109, 24, 176] 16 | ], 17 | recent_blockhash: [214, 244, 173, 248, 144, 238, 253, 86, 43, 52, 158, 107, 196, 148, 27, 228, 22, 32, 221, 186, 23, 111, 117, 168, 5, 28, 164, 124, 8, 137, 247, 144], 18 | instructions: [ 19 | CompiledInstruction { program_id_index: 7, accounts: [8], data: [2, 160, 134, 1, 0] }, CompiledInstruction { program_id_index: 7, accounts: [], data: [3, 160, 134, 1, 0, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 9, accounts: [0, 1, 0, 10, 11, 12], data: [1] }, CompiledInstruction { program_id_index: 13, accounts: [14, 2, 10, 3, 4, 1, 0, 11, 12, 15, 16, 17], data: [0, 32, 111, 33, 4, 0, 0, 0, 0, 143, 150, 41, 88, 138, 0, 0, 0] }, CompiledInstruction { program_id_index: 11, accounts: [0, 5], data: [2, 0, 0, 0, 96, 174, 10, 0, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 11, accounts: [0, 6], data: [2, 0, 0, 0, 16, 39, 0, 0, 0, 0, 0, 0] }], versioned: false, address_table_lookups: [] }) }), meta: Some(TransactionStatusMeta { err: None, fee: 15000, pre_balances: [2638556108, 0, 5920687668686, 20599917815, 2039280, 13984548093475, 17235825, 1, 0, 731913600, 1461600, 1, 934087680, 1141440, 290898417, 1009200, 137104014, 1141440], post_balances: [2566491828, 2039280, 5920688354825, 20668531676, 2039280, 13984548793475, 17245825, 1, 0, 731913600, 1461600, 1, 934087680, 1141440, 290898417, 1009200, 137104014, 1141440], inner_instructions: [InnerInstructions { index: 2, instructions: [InnerInstruction { program_id_index: 12, accounts: [10], data: [21, 7, 0], stack_height: Some(2) }, InnerInstruction { program_id_index: 11, accounts: [0, 1], data: [0, 0, 0, 0, 240, 29, 31, 0, 0, 0, 0, 0, 165, 0, 0, 0, 0, 0, 0, 0, 6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169], stack_height: Some(2) }, InnerInstruction { program_id_index: 12, accounts: [1], data: [22], stack_height: Some(2) }, InnerInstruction { program_id_index: 12, accounts: [1, 10], data: [18, 113, 194, 9, 213, 111, 117, 31, 199, 106, 14, 153, 255, 113, 120, 185, 72, 137, 231, 136, 44, 151, 55, 78, 60, 108, 162, 119, 3, 113, 28, 12, 51], stack_height: Some(2) }] }, InnerInstructions { index: 3, instructions: [InnerInstruction { program_id_index: 17, accounts: [14, 2, 10, 3, 4, 1, 0, 11, 12, 15, 16, 17], data: [102, 6, 61, 18, 1, 218, 235, 234, 177, 114, 146, 153, 200, 0, 0, 0, 32, 111, 33, 4, 0, 0, 0, 0], stack_height: Some(2) }, InnerInstruction { program_id_index: 12, accounts: [4, 1, 3], data: [3, 177, 114, 146, 153, 200, 0, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 11, accounts: [0, 3], data: [2, 0, 0, 0, 229, 246, 22, 4, 0, 0, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 11, accounts: [0, 2], data: [2, 0, 0, 0, 59, 120, 10, 0, 0, 0, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 17, accounts: [16], data: [228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238, 30, 74, 58, 31, 194, 57, 195, 217, 54, 50, 102, 207, 20, 89, 62, 48, 133, 87, 182, 208, 96, 216, 100, 29, 64, 60, 202, 30, 58, 183, 233, 31, 229, 246, 22, 4, 0, 0, 0, 0, 177, 114, 146, 153, 200, 0, 0, 0, 1, 113, 194, 9, 213, 111, 117, 31, 199, 106, 14, 153, 255, 113, 120, 185, 72, 137, 231, 136, 44, 151, 55, 78, 60, 108, 162, 119, 3, 113, 28, 12, 51, 169, 163, 24, 104, 0, 0, 0, 0, 220, 167, 235, 203, 11, 0, 0, 0, 218, 118, 240, 103, 214, 65, 2, 0, 220, 251, 199, 207, 4, 0, 0, 0, 218, 222, 221, 27, 69, 67, 1, 0], stack_height: Some(3) }] }], inner_instructions_none: false, 20 | 21 | log_messages: [ 22 | "Program ComputeBudget111111111111111111111111111111 invoke [1]", 23 | "Program ComputeBudget111111111111111111111111111111 success", 24 | "Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", 25 | "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", 26 | "Program log: CreateIdempotent", 27 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", 28 | "Program log: Instruction: GetAccountDataSize", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1569 of 86795 compute units", "Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program 11111111111111111111111111111111 invoke [2]", "Program 11111111111111111111111111111111 success", "Program log: Initialize the associated token account", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", "Program log: Instruction: InitializeImmutableOwner", "Program log: Please upgrade to SPL Token 2022 for immutable owner support", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1405 of 80208 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", "Program log: Instruction: InitializeAccount3", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4188 of 76326 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 27845 of 99700 compute units", "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", "Program AxiomQpD1TrYEHNYLts8h3ko1NHdtxfgNgHryj2hJJx4 invoke [1]", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]", 29 | "Program log: Instruction: Buy", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", "Program log: Instruction: Transfer", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 50981 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program 11111111111111111111111111111111 invoke [3]", "Program 11111111111111111111111111111111 success", "Program 11111111111111111111111111111111 invoke [3]", "Program 11111111111111111111111111111111 success", "Program data: vdt/007mYe4eSjofwjnD2TYyZs8UWT4whVe20GDYZB1APMoeOrfpH+X2FgQAAAAAsXKSmcgAAAABccIJ1W91H8dqDpn/cXi5SInniCyXN048bKJ3A3EcDDOpoxhoAAAAANyn68sLAAAA2nbwZ9ZBAgDc+8fPBAAAANre3RtFQwEA", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [3]", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 1997 of 38432 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 32471 of 68131 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program AxiomQpD1TrYEHNYLts8h3ko1NHdtxfgNgHryj2hJJx4 consumed 36271 of 71855 compute units", "Program AxiomQpD1TrYEHNYLts8h3ko1NHdtxfgNgHryj2hJJx4 success", "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success", "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"], log_messages_none: false, 30 | 31 | pre_token_balances: [TokenBalance { account_index: 4, mint: "33EsucfvG76ShJakpoWVr5iYLQsKhcA6eWF1wmFApump", ui_token_amount: Some(UiTokenAmount { ui_amount: 563200646.011275, decimals: 6, amount: "563200646011275", ui_amount_string: "563200646.011275" }), owner: "9gbhx5e6dQpY7xAofiVcHatKRp5PjGYWCqqxfbTqF5in", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], 32 | 33 | post_token_balances: [TokenBalance { account_index: 1, mint: "33EsucfvG76ShJakpoWVr5iYLQsKhcA6eWF1wmFApump", ui_token_amount: Some(UiTokenAmount { ui_amount: 861569.970865, decimals: 6, amount: "861569970865", ui_amount_string: "861569.970865" }), owner: "8f4gM9bVY7suZhbkwWcAMbDwNb4vxfxPADoX8SaSsnhG", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, 34 | TokenBalance { account_index: 4, mint: "33EsucfvG76ShJakpoWVr5iYLQsKhcA6eWF1wmFApump", ui_token_amount: Some(UiTokenAmount { ui_amount: 562339076.04041, decimals: 6, amount: "562339076040410", ui_amount_string: "562339076.04041" }), owner: "9gbhx5e6dQpY7xAofiVcHatKRp5PjGYWCqqxfbTqF5in", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], rewards: [], loaded_writable_addresses: [], loaded_readonly_addresses: [], return_data: None, return_data_none: true, compute_units_consumed: Some(64716) }), index: 1374 }), slot: 337984962 } 35 | -------------------------------------------------------------------------------- /log/pumpfun_sell_tx_1746445226685.log: -------------------------------------------------------------------------------- 1 | Transaction: SubscribeUpdateTransaction { transaction: Some(SubscribeUpdateTransactionInfo { signature: [68, 214, 64, 108, 241, 152, 143, 207, 77, 217, 250, 41, 44, 214, 197, 201, 134, 175, 123, 40, 7, 243, 224, 134, 45, 181, 35, 21, 184, 4, 85, 132, 203, 231, 35, 79, 52, 211, 75, 235, 88, 58, 104, 11, 85, 214, 194, 34, 57, 82, 144, 24, 180, 148, 33, 78, 41, 107, 120, 126, 208, 22, 203, 2], is_vote: false, transaction: Some(Transaction { signatures: [[68, 214, 64, 108, 241, 152, 143, 207, 77, 217, 250, 41, 44, 214, 197, 201, 134, 175, 123, 40, 7, 243, 224, 134, 45, 181, 35, 21, 184, 4, 85, 132, 203, 231, 35, 79, 52, 211, 75, 235, 88, 58, 104, 11, 85, 214, 194, 34, 57, 82, 144, 24, 180, 148, 33, 78, 41, 107, 120, 126, 208, 22, 203, 2]], message: Some(Message { header: Some(MessageHeader { num_required_signatures: 1, num_readonly_signed_accounts: 0, num_readonly_unsigned_accounts: 11 }), account_keys: [[192, 69, 57, 70, 187, 133, 249, 39, 206, 254, 167, 159, 67, 81, 213, 172, 208, 207, 150, 38, 71, 226, 202, 186, 164, 24, 224, 252, 86, 156, 167, 205], [49, 127, 120, 203, 80, 114, 71, 38, 221, 40, 76, 67, 203, 203, 54, 178, 254, 136, 182, 194, 63, 46, 188, 250, 215, 106, 201, 159, 86, 245, 0, 80], [94, 101, 149, 63, 58, 105, 164, 215, 240, 34, 97, 10, 102, 159, 185, 136, 208, 121, 72, 4, 89, 96, 155, 245, 50, 133, 166, 124, 33, 95, 207, 251], [120, 82, 28, 177, 121, 206, 187, 133, 137, 181, 86, 162, 213, 236, 148, 210, 73, 134, 130, 253, 249, 187, 42, 245, 173, 100, 228, 145, 204, 65, 83, 218], [133, 140, 195, 137, 26, 80, 167, 203, 50, 66, 39, 205, 36, 45, 190, 143, 85, 116, 28, 207, 157, 180, 102, 131, 223, 226, 52, 183, 123, 32, 62, 80], [138, 85, 7, 116, 153, 201, 1, 165, 192, 17, 247, 161, 85, 86, 253, 215, 193, 65, 14, 149, 102, 212, 192, 136, 14, 119, 227, 209, 185, 167, 16, 127], [173, 17, 230, 164, 252, 41, 68, 164, 250, 130, 81, 190, 248, 21, 66, 110, 27, 251, 40, 198, 182, 100, 102, 119, 96, 124, 106, 217, 245, 102, 166, 70], [224, 111, 193, 136, 58, 37, 13, 157, 155, 142, 70, 238, 114, 248, 189, 114, 111, 104, 5, 231, 64, 207, 121, 84, 160, 99, 143, 3, 101, 112, 160, 121], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, 182, 253, 109, 24, 176], [3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, 187, 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0], [6, 155, 136, 87, 254, 171, 129, 132, 251, 104, 127, 99, 70, 24, 192, 53, 218, 196, 57, 220, 26, 235, 59, 85, 152, 160, 240, 0, 0, 0, 0, 1], [6, 167, 213, 23, 25, 44, 92, 81, 33, 140, 201, 76, 61, 74, 241, 127, 88, 218, 238, 8, 155, 161, 253, 68, 227, 219, 217, 138, 0, 0, 0, 0], [6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169], [58, 134, 94, 105, 238, 15, 84, 128, 202, 188, 246, 99, 87, 228, 220, 47, 24, 213, 141, 69, 193, 234, 116, 137, 251, 55, 35, 217, 121, 60, 114, 166], [98, 146, 246, 22, 89, 15, 27, 63, 244, 22, 250, 12, 40, 145, 90, 167, 84, 27, 219, 66, 154, 196, 157, 102, 214, 3, 191, 185, 228, 142, 21, 49], [140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89], [172, 241, 54, 235, 1, 252, 28, 78, 136, 61, 35, 200, 181, 132, 74, 181, 154, 55, 246, 106, 221, 87, 197, 233, 172, 59, 83, 224, 89, 211, 92, 100], [194, 29, 204, 235, 167, 123, 6, 175, 116, 83, 36, 173, 25, 143, 121, 124, 239, 67, 186, 222, 40, 75, 33, 19, 198, 65, 15, 228, 91, 226, 0, 194]], recent_blockhash: [28, 163, 65, 88, 219, 104, 19, 158, 179, 193, 7, 221, 7, 201, 41, 112, 226, 55, 135, 177, 223, 215, 117, 200, 146, 32, 54, 95, 60, 68, 78, 106], instructions: [CompiledInstruction { program_id_index: 10, accounts: [], data: [2, 240, 73, 2, 0] }, CompiledInstruction { program_id_index: 18, accounts: [9, 0, 15, 11, 14, 6, 15, 5, 4, 7, 2, 17, 8, 13, 16, 12, 3, 1], data: [254, 170, 88, 67, 216, 196, 208, 47, 17, 81, 240, 253, 115, 54, 192, 239, 95, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 252, 44, 0, 0, 0, 0, 0, 232, 3, 0, 0, 0, 0, 0, 0, 32, 161, 7, 0, 0, 0, 0, 0, 192, 132, 207, 79, 0, 0, 0, 0, 196, 156, 41, 116, 36, 40, 0, 0, 1, 203, 40, 43, 26, 0, 0, 0, 0] }], versioned: true, address_table_lookups: [] }) }), meta: Some(TransactionStatusMeta { err: None, fee: 5000, pre_balances: [575955066558, 1559040, 0, 17217428, 2039280, 1767450311, 5445271786083, 2039280, 1, 1141440, 1, 1035589724898, 1009200, 934087680, 290898417, 1461800, 731913600, 137104014, 1141440], post_balances: [576429686742, 0, 0, 17217625, 2039280, 1291665400, 5445276543933, 0, 1, 1141440, 1, 1035589724898, 1009200, 934087680, 290898417, 1461800, 731913600, 137104014, 1141440], inner_instructions: [InnerInstructions { index: 1, instructions: [InnerInstruction { program_id_index: 9, accounts: [14, 6, 15, 5, 4, 7, 0, 8, 16, 13, 17, 9], data: [51, 230, 133, 164, 1, 127, 131, 173, 172, 242, 207, 217, 3, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], stack_height: Some(2) }, InnerInstruction { program_id_index: 13, accounts: [7, 4, 0], data: [3, 172, 242, 207, 217, 3, 14, 0, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 9, accounts: [17], data: [228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238, 98, 146, 246, 22, 89, 15, 27, 63, 244, 22, 250, 12, 40, 145, 90, 167, 84, 27, 219, 66, 154, 196, 157, 102, 214, 3, 191, 185, 228, 142, 21, 49, 207, 230, 91, 28, 0, 0, 0, 0, 172, 242, 207, 217, 3, 14, 0, 0, 0, 192, 69, 57, 70, 187, 133, 249, 39, 206, 254, 167, 159, 67, 81, 213, 172, 208, 207, 150, 38, 71, 226, 202, 186, 164, 24, 224, 252, 86, 156, 167, 205, 169, 163, 24, 104, 0, 0, 0, 0, 248, 35, 248, 72, 7, 0, 0, 0, 217, 225, 19, 78, 175, 167, 3, 0, 248, 119, 212, 76, 0, 0, 0, 0, 217, 73, 1, 2, 30, 169, 2, 0], stack_height: Some(3) }, InnerInstruction { program_id_index: 13, accounts: [7, 0, 0, 0], data: [9], stack_height: Some(2) }, InnerInstruction { program_id_index: 8, accounts: [0, 3], data: [2, 0, 0, 0, 197, 0, 0, 0, 0, 0, 0, 0], stack_height: Some(2) }] }], inner_instructions_none: false, log_messages: ["Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", "Program E4kT3ceuMGBUfV4DvUFePe1c8anPN6wPi2yd5nFxBVpm invoke [1]", "Program log: 1 swap", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]", "Program log: Instruction: Sell", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", "Program log: Instruction: Transfer", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 116616 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program data: vdt/007mYe5ikvYWWQ8bP/QW+gwokVqnVBvbQprEnWbWA7+55I4VMc/mWxwAAAAArPLP2QMOAAAAwEU5RruF+SfO/qefQ1HVrNDPliZH4sq6pBjg/Facp82poxhoAAAAAPgj+EgHAAAA2eETTq+nAwD4d9RMAAAAANlJAQIeqQIA", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [3]", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 1997 of 108115 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 32889 of 138232 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", "Program log: Instruction: CloseAccount", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2998 of 103035 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program 11111111111111111111111111111111 invoke [2]", "Program 11111111111111111111111111111111 success", "Program E4kT3ceuMGBUfV4DvUFePe1c8anPN6wPi2yd5nFxBVpm consumed 52678 of 149850 compute units", "Program E4kT3ceuMGBUfV4DvUFePe1c8anPN6wPi2yd5nFxBVpm success"], log_messages_none: false, pre_token_balances: [TokenBalance { account_index: 4, mint: "7dnvoiZYAsL26NZjAgrSm28mZJggU9D2pHfF7rKQMSxg", ui_token_amount: Some(UiTokenAmount { ui_amount: 940386599.19851, decimals: 6, amount: "940386599198510", ui_amount_string: "940386599.19851" }), owner: "AJzTpn6D6eKiz2S6FeptUxnP1frABWQwGXya5yDeS2Rt", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, TokenBalance { account_index: 7, mint: "7dnvoiZYAsL26NZjAgrSm28mZJggU9D2pHfF7rKQMSxg", ui_token_amount: Some(UiTokenAmount { ui_amount: 15409701.9747, decimals: 6, amount: "15409701974700", ui_amount_string: "15409701.9747" }), owner: "DwYVzJaAW683T474NP2nAe7S84cLrbhsUWqCcKko9Lr8", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], post_token_balances: [TokenBalance { account_index: 4, mint: "7dnvoiZYAsL26NZjAgrSm28mZJggU9D2pHfF7rKQMSxg", ui_token_amount: Some(UiTokenAmount { ui_amount: 955796301.17321, decimals: 6, amount: "955796301173210", ui_amount_string: "955796301.17321" }), owner: "AJzTpn6D6eKiz2S6FeptUxnP1frABWQwGXya5yDeS2Rt", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], rewards: [], loaded_writable_addresses: [], loaded_readonly_addresses: [], return_data: None, return_data_none: true, compute_units_consumed: Some(52828) }), index: 1109 }), slot: 337984962 } -------------------------------------------------------------------------------- /log/pumpfun_sell_tx_1746445227405.log: -------------------------------------------------------------------------------- 1 | Transaction: SubscribeUpdateTransaction { transaction: Some(SubscribeUpdateTransactionInfo { signature: [113, 148, 126, 10, 239, 192, 138, 24, 120, 155, 31, 245, 128, 73, 156, 215, 45, 136, 245, 1, 252, 227, 205, 168, 62, 96, 40, 214, 172, 172, 166, 18, 133, 98, 19, 62, 86, 39, 166, 114, 163, 131, 90, 51, 116, 150, 19, 112, 215, 8, 213, 50, 136, 214, 170, 32, 11, 10, 22, 18, 48, 51, 210, 3], is_vote: false, transaction: Some(Transaction { signatures: [[113, 148, 126, 10, 239, 192, 138, 24, 120, 155, 31, 245, 128, 73, 156, 215, 45, 136, 245, 1, 252, 227, 205, 168, 62, 96, 40, 214, 172, 172, 166, 18, 133, 98, 19, 62, 86, 39, 166, 114, 163, 131, 90, 51, 116, 150, 19, 112, 215, 8, 213, 50, 136, 214, 170, 32, 11, 10, 22, 18, 48, 51, 210, 3]], message: Some(Message { header: Some(MessageHeader { num_required_signatures: 1, num_readonly_signed_accounts: 0, num_readonly_unsigned_accounts: 8 }), account_keys: [[23, 3, 124, 12, 110, 122, 3, 134, 191, 217, 130, 13, 68, 4, 52, 19, 75, 70, 149, 136, 165, 72, 95, 225, 243, 40, 95, 3, 151, 166, 181, 246], [80, 224, 222, 96, 206, 138, 25, 81, 79, 120, 114, 189, 205, 40, 226, 218, 96, 25, 147, 152, 9, 251, 217, 2, 110, 240, 63, 93, 230, 110, 38, 148], [130, 96, 69, 39, 145, 143, 218, 159, 43, 120, 142, 124, 213, 255, 115, 179, 148, 83, 111, 156, 248, 52, 22, 223, 11, 241, 34, 64, 114, 48, 67, 125], [173, 17, 230, 164, 252, 41, 68, 164, 250, 130, 81, 190, 248, 21, 66, 110, 27, 251, 40, 198, 182, 100, 102, 119, 96, 124, 106, 217, 245, 102, 166, 70], [206, 235, 249, 248, 68, 101, 153, 139, 124, 246, 119, 182, 210, 234, 206, 4, 20, 107, 224, 183, 50, 63, 138, 27, 59, 118, 143, 218, 245, 7, 10, 64], [252, 2, 136, 240, 169, 177, 9, 109, 17, 183, 41, 142, 215, 33, 219, 164, 225, 21, 203, 172, 28, 106, 143, 24, 45, 46, 48, 241, 83, 250, 243, 223], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, 182, 253, 109, 24, 176], [3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, 187, 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0], [6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169], [58, 134, 94, 105, 238, 15, 84, 128, 202, 188, 246, 99, 87, 228, 220, 47, 24, 213, 141, 69, 193, 234, 116, 137, 251, 55, 35, 217, 121, 60, 114, 166], [67, 216, 165, 87, 111, 32, 195, 209, 55, 57, 237, 145, 106, 8, 244, 62, 111, 206, 237, 191, 153, 29, 2, 17, 209, 83, 214, 180, 29, 231, 104, 239], [140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89], [172, 241, 54, 235, 1, 252, 28, 78, 136, 61, 35, 200, 181, 132, 74, 181, 154, 55, 246, 106, 221, 87, 197, 233, 172, 59, 83, 224, 89, 211, 92, 100]], recent_blockhash: [135, 158, 89, 48, 102, 189, 89, 214, 157, 126, 78, 155, 104, 94, 23, 98, 139, 243, 138, 153, 131, 221, 79, 252, 193, 222, 120, 182, 176, 176, 73, 25], instructions: [CompiledInstruction { program_id_index: 8, accounts: [], data: [2, 87, 21, 1, 0] }, CompiledInstruction { program_id_index: 8, accounts: [], data: [3, 218, 117, 15, 0, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 7, accounts: [10, 3, 11, 5, 2, 4, 0, 6, 12, 9, 13, 7], data: [51, 230, 133, 164, 1, 127, 131, 173, 48, 208, 16, 121, 39, 35, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0] }, CompiledInstruction { program_id_index: 6, accounts: [0, 1], data: [2, 0, 0, 0, 64, 13, 3, 0, 0, 0, 0, 0] }], versioned: false, address_table_lookups: [] }) }), meta: Some(TransactionStatusMeta { err: None, fee: 76937, pre_balances: [1684209529, 2229882417, 2039280, 5445290836849, 2039280, 3259969187, 1, 1141440, 1, 934087680, 290898417, 1461600, 731913600, 137104014], post_balances: [2948259142, 2230082417, 2039280, 5445303607825, 2039280, 1982871661, 1, 1141440, 1, 934087680, 290898417, 1461600, 731913600, 137104014], inner_instructions: [InnerInstructions { index: 2, instructions: [InnerInstruction { program_id_index: 9, accounts: [4, 2, 0], data: [3, 48, 208, 16, 121, 39, 35, 0, 0], stack_height: Some(2) }, InnerInstruction { program_id_index: 7, accounts: [13], data: [228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238, 67, 216, 165, 87, 111, 32, 195, 209, 55, 57, 237, 145, 106, 8, 244, 62, 111, 206, 237, 191, 153, 29, 2, 17, 209, 83, 214, 180, 29, 231, 104, 239, 54, 246, 30, 76, 0, 0, 0, 0, 48, 208, 16, 121, 39, 35, 0, 0, 0, 23, 3, 124, 12, 110, 122, 3, 134, 191, 217, 130, 13, 68, 4, 52, 19, 75, 70, 149, 136, 165, 72, 95, 225, 243, 40, 95, 3, 151, 166, 181, 246, 170, 163, 24, 104, 0, 0, 0, 0, 109, 28, 43, 114, 7, 0, 0, 0, 199, 197, 131, 23, 118, 147, 3, 0, 109, 112, 7, 118, 0, 0, 0, 0, 199, 45, 113, 203, 228, 148, 2, 0], stack_height: Some(2) }] }], inner_instructions_none: false, log_messages: ["Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", "Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [1]", "Program log: Instruction: Sell", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", "Program log: Instruction: Transfer", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 52082 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program data: vdt/007mYe5D2KVXbyDD0Tc57ZFqCPQ+b87tv5kdAhHRU9a0Hedo7zb2HkwAAAAAMNAQeScjAAAAFwN8DG56A4a/2YINRAQ0E0tGlYilSF/h8yhfA5emtfaqoxhoAAAAAG0cK3IHAAAAx8WDF3aTAwBtcAd2AAAAAMctccvklAIA", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 1997 of 43581 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 29890 of 70699 compute units", "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success", "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"], log_messages_none: false, pre_token_balances: [TokenBalance { account_index: 2, mint: "5ZqvGfw1Ut4rkg9iJT4pbRNXbfwtJoHoHKSZNy37pump", ui_token_amount: Some(UiTokenAmount { ui_amount: 894907898.226071, decimals: 6, amount: "894907898226071", ui_amount_string: "894907898.226071" }), owner: "HxjywZ4kUaMW7dn6cjUK7ftyHBZR3yz5aUpvJrMzmBN6", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, TokenBalance { account_index: 4, mint: "5ZqvGfw1Ut4rkg9iJT4pbRNXbfwtJoHoHKSZNy37pump", ui_token_amount: Some(UiTokenAmount { ui_amount: 38652441.841712, decimals: 6, amount: "38652441841712", ui_amount_string: "38652441.841712" }), owner: "2YqTcTMDFD2vvVHcc8Wd61zwLiWaDZvAKrzXFW1adZ3K", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], post_token_balances: [TokenBalance { account_index: 2, mint: "5ZqvGfw1Ut4rkg9iJT4pbRNXbfwtJoHoHKSZNy37pump", ui_token_amount: Some(UiTokenAmount { ui_amount: 933560340.067783, decimals: 6, amount: "933560340067783", ui_amount_string: "933560340.067783" }), owner: "HxjywZ4kUaMW7dn6cjUK7ftyHBZR3yz5aUpvJrMzmBN6", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, TokenBalance { account_index: 4, mint: "5ZqvGfw1Ut4rkg9iJT4pbRNXbfwtJoHoHKSZNy37pump", ui_token_amount: Some(UiTokenAmount { ui_amount: 0.0, decimals: 6, amount: "0", ui_amount_string: "0" }), owner: "2YqTcTMDFD2vvVHcc8Wd61zwLiWaDZvAKrzXFW1adZ3K", program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }], rewards: [], loaded_writable_addresses: [], loaded_readonly_addresses: [], return_data: None, return_data_none: true, compute_units_consumed: Some(30340) }), index: 548 }), slot: 337984964 } -------------------------------------------------------------------------------- /prototype/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pumpfun-analyzer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0.192", features = ["derive"] } 8 | serde_json = "1.0.108" 9 | tokio = { version = "1.34.0", features = ["full"] } 10 | chrono = "0.4.31" 11 | clap = { version = "4.4.10", features = ["derive"] } 12 | colored = "2.0.4" 13 | base64 = "0.21.5" 14 | bs58 = "0.5.0" 15 | anyhow = "1.0.75" 16 | regex = "1.10.2" 17 | futures = "0.3.29" 18 | lazy_static = "1.4.0" 19 | 20 | [lib] 21 | name = "pumpfun_analyzer" 22 | path = "src/lib.rs" 23 | 24 | [[bin]] 25 | name = "pumpfun-cli" 26 | path = "src/main.rs" -------------------------------------------------------------------------------- /prototype/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | use anyhow::{Result, anyhow}; 4 | use std::fs::File; 5 | use std::io::{BufRead, BufReader}; 6 | use std::path::Path; 7 | use regex::Regex; 8 | use lazy_static::lazy_static; 9 | 10 | #[derive(Debug, Serialize, Deserialize, Clone)] 11 | pub struct TokenBalance { 12 | pub account_index: usize, 13 | pub mint: String, 14 | pub ui_token_amount: Option, 15 | pub owner: String, 16 | pub program_id: String, 17 | } 18 | 19 | 20 | #[derive(Debug, Serialize, Deserialize, Clone)] 21 | pub struct PumpTransaction { 22 | pub transaction_type: TransactionType, 23 | pub signature: String, 24 | pub slot: u64, 25 | pub timestamp: Option, 26 | pub success: bool, 27 | pub fee: u64, 28 | pub log_messages: Vec, 29 | pub token_balances: TokenBalances, 30 | pub key_accounts: HashMap, 31 | } 32 | 33 | #[derive(Debug, Serialize, Deserialize, Clone)] 34 | pub struct TokenBalances { 35 | pub pre_balances: Vec, 36 | pub post_balances: Vec, 37 | } 38 | 39 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 40 | pub enum TransactionType { 41 | Buy, 42 | Sell, 43 | Mint, 44 | Unknown, 45 | } 46 | 47 | /// Parses a transaction log file and returns a PumpTransaction 48 | pub fn parse_transaction_log(file_path: &Path) -> Result { 49 | let file = File::open(file_path)?; 50 | let reader = BufReader::new(file); 51 | 52 | lazy_static! { 53 | static ref SIGNATURE_RE: Regex = Regex::new(r"Transaction Signature: \[(.*?)\]").unwrap(); 54 | static ref SLOT_RE: Regex = Regex::new(r"slot: (\d+)").unwrap(); 55 | static ref SUCCESS_RE: Regex = Regex::new(r"Transaction Success: (\w+)").unwrap(); 56 | static ref FEE_RE: Regex = Regex::new(r"fee: (\d+)").unwrap(); 57 | static ref LOG_START_RE: Regex = Regex::new(r"==== BEGIN TRANSACTION LOG ====").unwrap(); 58 | static ref LOG_END_RE: Regex = Regex::new(r"==== END TRANSACTION LOG ====").unwrap(); 59 | static ref LOG_LINE_RE: Regex = Regex::new(r"LOG\[\d+\]: (.*)").unwrap(); 60 | static ref INSTRUCTION_RE: Regex = Regex::new(r"Program log: Instruction: (\w+)").unwrap(); 61 | } 62 | 63 | let mut tx = PumpTransaction { 64 | transaction_type: TransactionType::Unknown, 65 | signature: String::new(), 66 | slot: 0, 67 | timestamp: None, 68 | success: false, 69 | fee: 0, 70 | log_messages: Vec::new(), 71 | token_balances: TokenBalances { 72 | pre_balances: Vec::new(), 73 | post_balances: Vec::new(), 74 | }, 75 | key_accounts: HashMap::new(), 76 | }; 77 | 78 | let mut in_log_section = false; 79 | let mut content = String::new(); 80 | 81 | for line in reader.lines() { 82 | let line = line?; 83 | content.push_str(&line); 84 | content.push('\n'); 85 | 86 | // Extract signature 87 | if let Some(captures) = SIGNATURE_RE.captures(&line) { 88 | if let Some(signature_match) = captures.get(1) { 89 | tx.signature = signature_match.as_str().to_string(); 90 | } 91 | } 92 | 93 | // Extract slot 94 | if let Some(captures) = SLOT_RE.captures(&line) { 95 | if let Some(slot_match) = captures.get(1) { 96 | tx.slot = slot_match.as_str().parse()?; 97 | } 98 | } 99 | 100 | // Extract success 101 | if let Some(captures) = SUCCESS_RE.captures(&line) { 102 | if let Some(success_match) = captures.get(1) { 103 | tx.success = success_match.as_str() == "true"; 104 | } 105 | } 106 | 107 | // Extract fee 108 | if let Some(captures) = FEE_RE.captures(&line) { 109 | if let Some(fee_match) = captures.get(1) { 110 | tx.fee = fee_match.as_str().parse()?; 111 | } 112 | } 113 | 114 | // Handle log section 115 | if LOG_START_RE.is_match(&line) { 116 | in_log_section = true; 117 | continue; 118 | } 119 | 120 | if LOG_END_RE.is_match(&line) { 121 | in_log_section = false; 122 | continue; 123 | } 124 | 125 | if in_log_section { 126 | if let Some(captures) = LOG_LINE_RE.captures(&line) { 127 | if let Some(log_match) = captures.get(1) { 128 | let log_message = log_match.as_str().to_string(); 129 | tx.log_messages.push(log_message.clone()); 130 | 131 | // Determine transaction type from instruction 132 | if let Some(captures) = INSTRUCTION_RE.captures(&log_message) { 133 | if let Some(instruction_match) = captures.get(1) { 134 | let instruction = instruction_match.as_str(); 135 | match instruction { 136 | "Buy" => tx.transaction_type = TransactionType::Buy, 137 | "Sell" => tx.transaction_type = TransactionType::Sell, 138 | "Create" | "MintTo" => { 139 | if tx.transaction_type != TransactionType::Buy && tx.transaction_type != TransactionType::Sell { 140 | tx.transaction_type = TransactionType::Mint; 141 | } 142 | }, 143 | _ => {} 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | // Process token balances from the content 153 | extract_token_balances(&content, &mut tx)?; 154 | 155 | // Extract key accounts 156 | extract_key_accounts(&content, &mut tx)?; 157 | 158 | Ok(tx) 159 | } 160 | 161 | fn extract_token_balances(content: &str, tx: &mut PumpTransaction) -> Result<()> { 162 | lazy_static! { 163 | static ref PRE_BALANCE_RE: Regex = Regex::new(r"pre_token_balances: \[(.*?)\]").unwrap(); 164 | static ref POST_BALANCE_RE: Regex = Regex::new(r"post_token_balances: \[(.*?)\]").unwrap(); 165 | static ref TOKEN_BALANCE_LINE_RE: Regex = Regex::new(r"TokenBalance \{ account_index: (\d+), mint: \"([^\"]+)\", ui_token_amount: Some\(UiTokenAmount \{ ui_amount: ([^,]+), decimals: (\d+), amount: \"([^\"]+)\", ui_amount_string: \"([^\"]+)\" \}\), owner: \"([^\"]+)\", program_id: \"([^\"]+)\" \}").unwrap(); 166 | } 167 | 168 | // Extract pre_token_balances 169 | if let Some(captures) = PRE_BALANCE_RE.captures(content) { 170 | if let Some(balances_match) = captures.get(1) { 171 | let balances_str = balances_match.as_str(); 172 | for captures in TOKEN_BALANCE_LINE_RE.captures_iter(balances_str) { 173 | let account_index = captures[1].parse::()?; 174 | let mint = captures[2].to_string(); 175 | let ui_amount = captures[3].parse::()?; 176 | let decimals = captures[4].parse::()?; 177 | let amount = captures[5].to_string(); 178 | let ui_amount_string = captures[6].to_string(); 179 | let owner = captures[7].to_string(); 180 | let program_id = captures[8].to_string(); 181 | 182 | tx.token_balances.pre_balances.push(TokenBalance { 183 | account_index, 184 | mint, 185 | ui_token_amount: Some(UiTokenAmount { 186 | ui_amount, 187 | decimals, 188 | amount, 189 | ui_amount_string, 190 | }), 191 | owner, 192 | program_id, 193 | }); 194 | } 195 | } 196 | } 197 | 198 | // Extract post_token_balances 199 | if let Some(captures) = POST_BALANCE_RE.captures(content) { 200 | if let Some(balances_match) = captures.get(1) { 201 | let balances_str = balances_match.as_str(); 202 | for captures in TOKEN_BALANCE_LINE_RE.captures_iter(balances_str) { 203 | let account_index = captures[1].parse::()?; 204 | let mint = captures[2].to_string(); 205 | let ui_amount = captures[3].parse::()?; 206 | let decimals = captures[4].parse::()?; 207 | let amount = captures[5].to_string(); 208 | let ui_amount_string = captures[6].to_string(); 209 | let owner = captures[7].to_string(); 210 | let program_id = captures[8].to_string(); 211 | 212 | tx.token_balances.post_balances.push(TokenBalance { 213 | account_index, 214 | mint, 215 | ui_token_amount: Some(UiTokenAmount { 216 | ui_amount, 217 | decimals, 218 | amount, 219 | ui_amount_string, 220 | }), 221 | owner, 222 | program_id, 223 | }); 224 | } 225 | } 226 | } 227 | 228 | Ok(()) 229 | } 230 | 231 | fn extract_key_accounts(content: &str, tx: &mut PumpTransaction) -> Result<()> { 232 | lazy_static! { 233 | static ref ACCOUNT_KEYS_RE: Regex = Regex::new(r"Key\[(\d+)\]: \[(.*?)\]").unwrap(); 234 | } 235 | 236 | for captures in ACCOUNT_KEYS_RE.captures_iter(content) { 237 | let index = captures[1].parse::()?; 238 | let key_data = captures[2].to_string(); 239 | tx.key_accounts.insert(format!("key_{}", index), key_data); 240 | } 241 | 242 | Ok(()) 243 | } 244 | 245 | /// Calculates the token amount difference between pre and post balances 246 | pub fn calculate_token_amount_change(tx: &PumpTransaction) -> HashMap { 247 | let mut changes = HashMap::new(); 248 | 249 | // Create a map of mint -> pre_balance 250 | let mut pre_balances = HashMap::new(); 251 | for balance in &tx.token_balances.pre_balances { 252 | if let Some(token_amount) = &balance.ui_token_amount { 253 | pre_balances.insert(balance.mint.clone(), (token_amount.ui_amount, balance.owner.clone())); 254 | } 255 | } 256 | 257 | // Calculate differences with post_balances 258 | for balance in &tx.token_balances.post_balances { 259 | if let Some(token_amount) = &balance.ui_token_amount { 260 | let key = format!("{}:{}", balance.mint, balance.owner); 261 | let pre_amount = pre_balances 262 | .get(&balance.mint) 263 | .map(|(amount, owner)| { 264 | if *owner == balance.owner { *amount } else { 0.0 } 265 | }) 266 | .unwrap_or(0.0); 267 | 268 | let change = token_amount.ui_amount - pre_amount; 269 | changes.insert(key, change); 270 | } 271 | } 272 | 273 | changes 274 | } 275 | 276 | /// Generate a summary of the transaction 277 | pub fn generate_transaction_summary(tx: &PumpTransaction) -> String { 278 | let tx_type = match tx.transaction_type { 279 | TransactionType::Buy => "Buy", 280 | TransactionType::Sell => "Sell", 281 | TransactionType::Mint => "Mint", 282 | TransactionType::Unknown => "Unknown", 283 | }; 284 | 285 | let changes = calculate_token_amount_change(tx); 286 | 287 | let mut summary = format!("Transaction Type: {}\n", tx_type); 288 | summary.push_str(&format!("Signature: {}\n", tx.signature)); 289 | summary.push_str(&format!("Slot: {}\n", tx.slot)); 290 | summary.push_str(&format!("Success: {}\n", tx.success)); 291 | summary.push_str(&format!("Fee: {} lamports\n", tx.fee)); 292 | 293 | summary.push_str("\nToken Balance Changes:\n"); 294 | for (key, change) in changes { 295 | let parts: Vec<&str> = key.split(':').collect(); 296 | if parts.len() == 2 { 297 | let (mint, owner) = (parts[0], parts[1]); 298 | let change_str = if change > 0.0 { 299 | format!("+{:.6}", change) 300 | } else { 301 | format!("{:.6}", change) 302 | }; 303 | summary.push_str(&format!(" Mint: {}\n Owner: {}\n Change: {}\n\n", mint, owner, change_str)); 304 | } 305 | } 306 | 307 | summary 308 | } -------------------------------------------------------------------------------- /prototype/src/main.rs: -------------------------------------------------------------------------------- 1 | use pumpfun_analyzer::{parse_transaction_log, generate_transaction_summary, PumpTransaction, TransactionType}; 2 | use anyhow::{Result, anyhow}; 3 | use clap::{Parser, Subcommand}; 4 | use colored::*; 5 | use std::fs::{self, File}; 6 | use std::io::Write; 7 | use std::path::{Path, PathBuf}; 8 | use std::collections::HashMap; 9 | 10 | #[derive(Parser)] 11 | #[command(name = "pumpfun-cli")] 12 | #[command(about = "CLI for analyzing PumpFun transactions", long_about = None)] 13 | struct Cli { 14 | #[command(subcommand)] 15 | command: Commands, 16 | } 17 | 18 | #[derive(Subcommand)] 19 | enum Commands { 20 | /// Parse a single transaction log file 21 | Parse { 22 | /// Path to the log file 23 | #[arg(short, long)] 24 | file: PathBuf, 25 | 26 | /// Output path for the JSON result 27 | #[arg(short, long)] 28 | output: Option, 29 | }, 30 | 31 | /// Analyze multiple transaction logs 32 | Analyze { 33 | /// Directory containing log files 34 | #[arg(short, long)] 35 | dir: PathBuf, 36 | 37 | /// Filter by transaction type (buy, sell, mint) 38 | #[arg(short, long)] 39 | transaction_type: Option, 40 | 41 | /// Output directory for analysis results 42 | #[arg(short, long)] 43 | output: Option, 44 | }, 45 | } 46 | 47 | fn process_single_file(file_path: &Path, output_path: Option<&Path>) -> Result<()> { 48 | println!("{} {}", "Parsing".green(), file_path.display()); 49 | 50 | let tx = parse_transaction_log(file_path)?; 51 | let summary = generate_transaction_summary(&tx); 52 | 53 | println!("\n{}", summary); 54 | 55 | if let Some(output) = output_path { 56 | let json = serde_json::to_string_pretty(&tx)?; 57 | let mut file = File::create(output)?; 58 | file.write_all(json.as_bytes())?; 59 | println!("{} {}", "Results saved to".green(), output.display()); 60 | } 61 | 62 | Ok(()) 63 | } 64 | 65 | fn analyze_directory(dir_path: &Path, tx_type_filter: Option<&str>, output_dir: Option<&Path>) -> Result<()> { 66 | if !dir_path.exists() || !dir_path.is_dir() { 67 | return Err(anyhow!("Invalid directory path: {}", dir_path.display())); 68 | } 69 | 70 | println!("{} {}", "Analyzing files in".green(), dir_path.display()); 71 | 72 | let mut transactions = Vec::new(); 73 | let mut type_counts = HashMap::new(); 74 | // let mut total_fees = 0u64; 75 | let mut tx_type_filter = Hash 76 | 77 | // Process files in directory 78 | for entry in fs::read_dir(dir_path)? { 79 | let entry = entry?; 80 | let path = entry.path(); 81 | 82 | if path.is_file() && path.extension().map_or(false, |ext| ext == "log") { 83 | match parse_transaction_log(&path) { 84 | Ok(tx) => { 85 | // Apply transaction type filter if specified 86 | let tx_type_str = match tx.transaction_type { 87 | TransactionType::Buy => "buy", 88 | TransactionType::Sell => "sell", 89 | TransactionType::Mint => "mint", 90 | TransactionType::Unknown => "unknown", 91 | }; 92 | 93 | if let Some(filter) = tx_type_filter { 94 | if filter.to_lowercase() != tx_type_str { 95 | continue; 96 | } 97 | } 98 | 99 | // Collect statistics 100 | *type_counts.entry(tx_type_str.to_string()).or_insert(0) += 1; 101 | total_fees += tx.fee; 102 | 103 | // Add to collections 104 | transactions.push(tx); 105 | } 106 | Err(e) => { 107 | eprintln!("{} {} - {}", "Error parsing".red(), path.display(), e); 108 | } 109 | } 110 | } 111 | } 112 | 113 | // Display summary 114 | println!("\n{}", "=== Analysis Summary ===".cyan().bold()); 115 | println!("Total transactions: {}", transactions.len()); 116 | println!("Transaction types:"); 117 | for (tx_type, count) in &type_counts { 118 | println!(" {}: {}", tx_type, count); 119 | } 120 | println!("Total fees: {} lamports", total_fees); 121 | 122 | // Save results if output directory specified 123 | if let Some(output) = output_dir { 124 | if !output.exists() { 125 | fs::create_dir_all(output)?; 126 | } 127 | 128 | // Save all transactions to a single JSON file 129 | let all_txs_path = output.join("all_transactions.json"); 130 | let json = serde_json::to_string_pretty(&transactions)?; 131 | let mut file = File::create(all_txs_path)?; 132 | file.write_all(json.as_bytes())?; 133 | 134 | // Save summary to a text file 135 | let summary_path = output.join("summary.txt"); 136 | let mut summary_file = File::create(summary_path)?; 137 | writeln!(summary_file, "=== Analysis Summary ===")?; 138 | writeln!(summary_file, "Total transactions: {}", transactions.len())?; 139 | writeln!(summary_file, "Transaction types:")?; 140 | for (tx_type, count) in &type_counts { 141 | writeln!(summary_file, " {}: {}", tx_type, count)?; 142 | } 143 | writeln!(summary_file, "Total fees: {} lamports", total_fees)?; 144 | 145 | println!("{} {}", "Results saved to".green(), output.display()); 146 | } 147 | 148 | Ok(()) 149 | } 150 | 151 | #[tokio::main] 152 | async fn main() -> Result<()> { 153 | let cli = Cli::parse(); 154 | 155 | match cli.command { 156 | Commands::Parse { file, output } => { 157 | process_single_file(&file, output.as_deref())?; 158 | } 159 | Commands::Analyze { dir, transaction_type, output } => { 160 | analyze_directory(&dir, transaction_type.as_deref(), output.as_deref())?; 161 | } 162 | } 163 | 164 | Ok(()) 165 | } -------------------------------------------------------------------------------- /src/common/blacklist.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{Error, ErrorKind}; 3 | use std::collections::HashSet; 4 | use std::path::Path; 5 | use std::time::Instant; 6 | use serde::{Serialize, Deserialize}; 7 | use tokio::sync::Mutex; 8 | use std::sync::Arc; 9 | 10 | // Use io::Result only where needed, not as a general import 11 | type Result = std::io::Result; 12 | 13 | /// Blacklist of tokens that should not be traded 14 | #[derive(Clone)] 15 | pub struct Blacklist { 16 | /// Addresses of tokens in the blacklist 17 | addresses: HashSet, 18 | /// Path to the blacklist file 19 | file_path: String, 20 | } 21 | 22 | // Implement custom serialization and deserialization 23 | impl Serialize for Blacklist { 24 | fn serialize(&self, serializer: S) -> std::result::Result 25 | where 26 | S: serde::Serializer, 27 | { 28 | // We only need to serialize the addresses, which is what we'll save to disk 29 | self.addresses.serialize(serializer) 30 | } 31 | } 32 | 33 | impl<'de> Deserialize<'de> for Blacklist { 34 | fn deserialize(deserializer: D) -> std::result::Result 35 | where 36 | D: serde::Deserializer<'de>, 37 | { 38 | // When deserializing, we only care about the addresses 39 | let addresses = HashSet::::deserialize(deserializer)?; 40 | 41 | // Default values for the other fields - these will be set properly in the new() method 42 | Ok(Self { 43 | addresses, 44 | file_path: String::new(), 45 | }) 46 | } 47 | } 48 | 49 | impl Blacklist { 50 | /// Create a new blacklist from a JSON file 51 | pub fn new(file_path: &str) -> Result { 52 | let path = Path::new(file_path); 53 | 54 | // If file doesn't exist, create an empty blacklist 55 | if !path.exists() { 56 | return Ok(Self { 57 | addresses: HashSet::new(), 58 | file_path: file_path.to_string(), 59 | }); 60 | } 61 | 62 | // Read from file 63 | let file_content = fs::read_to_string(file_path)?; 64 | let addresses: HashSet = if file_content.trim().is_empty() { 65 | HashSet::new() 66 | } else { 67 | match serde_json::from_str(&file_content) { 68 | Ok(addresses) => addresses, 69 | Err(e) => { 70 | return Err(Error::new( 71 | ErrorKind::InvalidData, 72 | format!("Failed to parse blacklist JSON: {}", e), 73 | )); 74 | } 75 | } 76 | }; 77 | 78 | Ok(Self { 79 | addresses, 80 | file_path: file_path.to_string(), 81 | }) 82 | } 83 | 84 | /// Create a new blacklist with a specified set of addresses 85 | pub fn with_addresses(addresses: HashSet, file_path: &str) -> Self { 86 | Self { 87 | addresses, 88 | file_path: file_path.to_string(), 89 | } 90 | } 91 | 92 | /// Create an empty blacklist 93 | pub fn empty(file_path: &str) -> Self { 94 | Self { 95 | addresses: HashSet::new(), 96 | file_path: file_path.to_string(), 97 | } 98 | } 99 | 100 | /// Get the number of addresses in the blacklist 101 | pub fn len(&self) -> usize { 102 | self.addresses.len() 103 | } 104 | 105 | /// Check if the blacklist is empty 106 | pub fn is_empty(&self) -> bool { 107 | self.addresses.is_empty() 108 | } 109 | 110 | /// Check if an address is in the blacklist 111 | pub fn is_blacklisted(&self, address: &str) -> bool { 112 | self.addresses.contains(address) 113 | } 114 | 115 | /// Add an address to the blacklist 116 | pub fn add_address(&mut self, address: &str) -> bool { 117 | self.addresses.insert(address.to_string()) 118 | } 119 | 120 | /// Remove an address from the blacklist 121 | pub fn remove_address(&mut self, address: &str) -> bool { 122 | self.addresses.remove(address) 123 | } 124 | 125 | /// Get all addresses in the blacklist 126 | pub fn get_addresses(&self) -> Vec { 127 | self.addresses.iter().cloned().collect() 128 | } 129 | 130 | /// Save the blacklist to the file 131 | pub fn save(&self) -> Result<()> { 132 | let json = serde_json::to_string_pretty(&self.addresses)?; 133 | fs::write(&self.file_path, json)?; 134 | Ok(()) 135 | } 136 | } 137 | 138 | /// Thread-safe blacklist manager 139 | #[derive(Clone)] 140 | pub struct BlacklistManager { 141 | blacklist: Arc>, 142 | save_interval_ms: u64, 143 | last_save: Arc>, 144 | } 145 | 146 | impl BlacklistManager { 147 | /// Create a new blacklist manager 148 | pub fn new(blacklist: Blacklist, save_interval_ms: u64) -> Self { 149 | Self { 150 | blacklist: Arc::new(Mutex::new(blacklist)), 151 | save_interval_ms, 152 | last_save: Arc::new(std::sync::Mutex::new(Instant::now())), 153 | } 154 | } 155 | 156 | /// Get a clone of the blacklist 157 | pub fn get_blacklist_arc(&self) -> Arc> { 158 | self.blacklist.clone() 159 | } 160 | 161 | /// Check if an address is in the blacklist 162 | pub async fn is_blacklisted(&self, address: &str) -> bool { 163 | let blacklist = self.blacklist.lock().await; 164 | blacklist.is_blacklisted(address) 165 | } 166 | 167 | /// Add an address to the blacklist 168 | pub async fn add_address(&self, address: &str) -> bool { 169 | let mut blacklist = self.blacklist.lock().await; 170 | let result = blacklist.add_address(address); 171 | self.check_and_save().await; 172 | result 173 | } 174 | 175 | /// Remove an address from the blacklist 176 | pub async fn remove_address(&self, address: &str) -> bool { 177 | let mut blacklist = self.blacklist.lock().await; 178 | let result = blacklist.remove_address(address); 179 | self.check_and_save().await; 180 | result 181 | } 182 | 183 | /// Check if it's time to save and save the blacklist 184 | pub async fn check_and_save(&self) -> bool { 185 | let mut last_save = self.last_save.lock().unwrap(); 186 | let elapsed = last_save.elapsed().as_millis() as u64; 187 | 188 | if elapsed >= self.save_interval_ms { 189 | // Time to save 190 | let blacklist = self.blacklist.lock().await; 191 | 192 | match blacklist.save() { 193 | Ok(_) => { 194 | *last_save = Instant::now(); 195 | true 196 | }, 197 | Err(e) => { 198 | eprintln!("Failed to save blacklist: {}", e); 199 | false 200 | } 201 | } 202 | } else { 203 | false 204 | } 205 | } 206 | 207 | /// Force save the blacklist 208 | pub async fn save(&self) -> Result<()> { 209 | let blacklist = self.blacklist.lock().await; 210 | let result = blacklist.save(); 211 | 212 | if result.is_ok() { 213 | let mut last_save = self.last_save.lock().unwrap(); 214 | *last_save = Instant::now(); 215 | } 216 | 217 | result 218 | } 219 | } 220 | 221 | #[cfg(test)] 222 | mod tests { 223 | use super::*; 224 | use tempfile::NamedTempFile; 225 | 226 | #[test] 227 | fn test_blacklist_basics() { 228 | let temp_file = NamedTempFile::new().unwrap(); 229 | let temp_path = temp_file.path().to_str().unwrap().to_string(); 230 | 231 | let mut blacklist = Blacklist::empty(&temp_path); 232 | 233 | // Test add and check 234 | assert!(blacklist.add_address("token1")); 235 | assert!(blacklist.is_blacklisted("token1")); 236 | assert_eq!(blacklist.len(), 1); 237 | 238 | // Test remove 239 | assert!(blacklist.remove_address("token1")); 240 | assert!(!blacklist.is_blacklisted("token1")); 241 | assert_eq!(blacklist.len(), 0); 242 | } 243 | 244 | #[tokio::test] 245 | async fn test_blacklist_manager() { 246 | let temp_file = NamedTempFile::new().unwrap(); 247 | let temp_path = temp_file.path().to_str().unwrap().to_string(); 248 | 249 | let blacklist = Blacklist::empty(&temp_path); 250 | let manager = BlacklistManager::new(blacklist, 5000); 251 | 252 | // Add token 253 | assert!(manager.add_address("token1").await); 254 | assert!(manager.is_blacklisted("token1").await); 255 | 256 | // Save 257 | assert!(manager.save().await.is_ok()); 258 | 259 | // Check file contents 260 | let content = fs::read_to_string(&temp_path).unwrap(); 261 | let parsed: HashSet = serde_json::from_str(&content).unwrap(); 262 | assert!(parsed.contains("token1")); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/common/constants.rs: -------------------------------------------------------------------------------- 1 | pub const INIT_MSG: &str = " 2 | @@@@@@ @@@@@@@@ @@@@@@@ @@@@@@@ @@@ @@@ @@@ @@@@@@@@ @@@@@@ 3 | @@@@@@@ @@@@@@@@ @@@@@@@ @@@@@@@ @@@ @@@@ @@@ @@@@@@@@@ @@@@@@@ 4 | !@@ @@! @@! @@! @@! @@!@!@@@ !@@ !@@ 5 | !@! !@! !@! !@! !@! !@!!@!@! !@! !@! 6 | !!@@!! @!!!:! @!! @!! !!@ @!@ !!@! !@! @!@!@ !!@@!! 7 | !!@!!! !!!!!: !!! !!! !!! !@! !!! !!! !!@!! !!@!!! 8 | !:! !!: !!: !!: !!: !!: !!! :!! !!: !:! 9 | !:! :!: :!: :!: :!: :!: !:! :!: !:: !:! 10 | :::: :: :: :::: :: :: :: :: :: ::: :::: :::: :: 11 | :: : : : :: :: : : : :: : :: :: : :: : : 12 | "; 13 | 14 | pub const RUN_MSG: &str = " 15 | @@@@@@@ @@@ @@@ @@@ @@@ @@@@@@@ @@@@@@ @@@@@@@ 16 | @@@@@@@@ @@@ @@@ @@@@ @@@ @@@@@@@@ @@@@@@@@ @@@@@@@ 17 | @@! @@@ @@! @@@ @@!@!@@@ @@! @@@ @@! @@@ @@! 18 | !@! @!@ !@! @!@ !@!!@!@! !@ @!@ !@! @!@ !@! 19 | @!@!!@! @!@ !@! @!@ !!@! @!@!@!@ @!@ !@! @!! 20 | !!@!@! !@! !!! !@! !!! !!!@!!!! !@! !!! !!! 21 | !!: :!! !!: !!! !!: !!! !!: !!! !!: !!! !!: 22 | :!: !:! :!: !:! :!: !:! :!: !:! :!: !:! :!: 23 | :: ::: ::::: :: :: :: :: :::: ::::: :: :: 24 | : : : : : : :: : :: : :: : : : : 25 | "; 26 | -------------------------------------------------------------------------------- /src/common/logger.rs: -------------------------------------------------------------------------------- 1 | use chrono::Local; 2 | use colored::*; 3 | 4 | const LOG_LEVEL: &str = "LOG"; 5 | 6 | #[derive(Clone, Debug)] 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 | fn prefix_with_date(&self) -> String { 43 | let date = Local::now(); 44 | format!( 45 | "[{}] {}", 46 | date.format(&self.date_format.as_str().blue().bold()), 47 | self.prefix 48 | ) 49 | } 50 | 51 | // Method to check if debug logging is enabled 52 | pub fn debug_enabled(&self) -> bool { 53 | LogLevel::new().is_debug() 54 | } 55 | } 56 | 57 | struct LogLevel<'a> { 58 | level: &'a str, 59 | } 60 | impl LogLevel<'_> { 61 | fn new() -> Self { 62 | let level = LOG_LEVEL; 63 | LogLevel { level } 64 | } 65 | fn is_debug(&self) -> bool { 66 | self.level.to_lowercase().eq("debug") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blacklist; 2 | pub mod config; 3 | pub mod constants; 4 | pub mod logger; 5 | pub mod whitelist; 6 | -------------------------------------------------------------------------------- /src/common/whitelist.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{Error, ErrorKind}; 3 | use std::collections::HashSet; 4 | use std::path::Path; 5 | use std::time::Instant; 6 | use serde::{Serialize, Deserialize}; 7 | use tokio::sync::Mutex; 8 | use std::sync::Arc; 9 | 10 | // Use io::Result only where needed, not as a general import 11 | type Result = std::io::Result; 12 | 13 | /// Whitelist with review cycle functionality 14 | /// 15 | /// Tracks tokens that are allowed for trading and periodically 16 | /// reviews them based on activity within a review cycle 17 | #[derive(Clone)] 18 | pub struct Whitelist { 19 | /// Addresses of tokens in the whitelist 20 | addresses: HashSet, 21 | /// Path to the whitelist file 22 | file_path: String, 23 | /// Time of the last review 24 | last_review: Instant, 25 | /// Duration of the review cycle in milliseconds 26 | review_cycle_ms: u64, 27 | /// Tokens seen in the current review cycle 28 | active_tokens: HashSet, 29 | } 30 | 31 | impl Whitelist { 32 | /// Create a new whitelist from a JSON file 33 | pub fn new(file_path: &str, review_cycle_ms: u64) -> Result { 34 | let path = Path::new(file_path); 35 | 36 | // If file doesn't exist, create an empty whitelist 37 | if !path.exists() { 38 | return Ok(Self { 39 | addresses: HashSet::new(), 40 | file_path: file_path.to_string(), 41 | last_review: Instant::now(), 42 | review_cycle_ms, 43 | active_tokens: HashSet::new(), 44 | }); 45 | } 46 | 47 | // Read from file 48 | let file_content = fs::read_to_string(file_path)?; 49 | let addresses: HashSet = if file_content.trim().is_empty() { 50 | HashSet::new() 51 | } else { 52 | match serde_json::from_str(&file_content) { 53 | Ok(addresses) => addresses, 54 | Err(e) => { 55 | return Err(Error::new( 56 | ErrorKind::InvalidData, 57 | format!("Failed to parse whitelist JSON: {}", e), 58 | )); 59 | } 60 | } 61 | }; 62 | 63 | Ok(Self { 64 | addresses, 65 | file_path: file_path.to_string(), 66 | last_review: Instant::now(), 67 | review_cycle_ms, 68 | active_tokens: HashSet::new(), 69 | }) 70 | } 71 | 72 | /// Create a new whitelist with a specified set of addresses 73 | pub fn with_addresses(addresses: HashSet, file_path: &str, review_cycle_ms: u64) -> Self { 74 | Self { 75 | addresses, 76 | file_path: file_path.to_string(), 77 | last_review: Instant::now(), 78 | review_cycle_ms, 79 | active_tokens: HashSet::new(), 80 | } 81 | } 82 | 83 | /// Create an empty whitelist 84 | pub fn empty(file_path: &str, review_cycle_ms: u64) -> Self { 85 | Self { 86 | addresses: HashSet::new(), 87 | file_path: file_path.to_string(), 88 | last_review: Instant::now(), 89 | review_cycle_ms, 90 | active_tokens: HashSet::new(), 91 | } 92 | } 93 | 94 | /// Get the number of addresses in the whitelist 95 | pub fn len(&self) -> usize { 96 | self.addresses.len() 97 | } 98 | 99 | /// Check if the whitelist is empty 100 | pub fn is_empty(&self) -> bool { 101 | self.addresses.is_empty() 102 | } 103 | 104 | /// Check if an address is in the whitelist 105 | pub fn is_whitelisted(&self, address: &str) -> bool { 106 | self.addresses.contains(address) 107 | } 108 | 109 | /// Add an address to the whitelist and mark it as active in the current cycle 110 | pub fn add_address(&mut self, address: &str) -> bool { 111 | let is_new = self.addresses.insert(address.to_string()); 112 | self.active_tokens.insert(address.to_string()); 113 | is_new 114 | } 115 | 116 | /// Remove an address from the whitelist 117 | pub fn remove_address(&mut self, address: &str) -> bool { 118 | self.addresses.remove(address) 119 | } 120 | 121 | /// Mark an address as active in the current review cycle 122 | pub fn mark_as_active(&mut self, address: &str) { 123 | // Only add to active tokens if it's in the whitelist 124 | if self.is_whitelisted(address) { 125 | self.active_tokens.insert(address.to_string()); 126 | } 127 | } 128 | 129 | /// Get all addresses in the whitelist 130 | pub fn get_addresses(&self) -> Vec { 131 | self.addresses.iter().cloned().collect() 132 | } 133 | 134 | /// Get active addresses in the current review cycle 135 | pub fn get_active_addresses(&self) -> Vec { 136 | self.active_tokens.iter().cloned().collect() 137 | } 138 | 139 | /// Check if a review cycle is complete and process it 140 | pub fn check_review_cycle(&mut self) -> bool { 141 | let elapsed = self.last_review.elapsed(); 142 | 143 | if elapsed.as_millis() as u64 >= self.review_cycle_ms { 144 | // Review cycle completed 145 | self.process_review_cycle(); 146 | true 147 | } else { 148 | false 149 | } 150 | } 151 | 152 | /// Process the review cycle by removing inactive tokens 153 | fn process_review_cycle(&mut self) { 154 | // Keep only tokens that were active in this cycle 155 | self.addresses.retain(|address| self.active_tokens.contains(address)); 156 | 157 | // Reset the active tokens for the next cycle 158 | self.active_tokens.clear(); 159 | 160 | // Reset the last review time 161 | self.last_review = Instant::now(); 162 | } 163 | 164 | /// Save the whitelist to the file 165 | pub fn save(&self) -> Result<()> { 166 | let json = serde_json::to_string_pretty(&self.addresses)?; 167 | fs::write(&self.file_path, json)?; 168 | Ok(()) 169 | } 170 | } 171 | 172 | // Implement custom serialization and deserialization 173 | impl Serialize for Whitelist { 174 | fn serialize(&self, serializer: S) -> std::result::Result 175 | where 176 | S: serde::Serializer, 177 | { 178 | // We only need to serialize the addresses, which is what we'll save to disk 179 | self.addresses.serialize(serializer) 180 | } 181 | } 182 | 183 | impl<'de> Deserialize<'de> for Whitelist { 184 | fn deserialize(deserializer: D) -> std::result::Result 185 | where 186 | D: serde::Deserializer<'de>, 187 | { 188 | // When deserializing, we only care about the addresses 189 | let addresses = HashSet::::deserialize(deserializer)?; 190 | 191 | // Default values for the other fields - these will be set properly in the new() method 192 | Ok(Self { 193 | addresses, 194 | file_path: String::new(), 195 | last_review: Instant::now(), 196 | review_cycle_ms: 0, 197 | active_tokens: HashSet::new(), 198 | }) 199 | } 200 | } 201 | 202 | /// Thread-safe whitelist manager 203 | #[derive(Clone)] 204 | pub struct WhitelistManager { 205 | whitelist: Arc>, 206 | save_interval_ms: u64, 207 | last_save: Arc>, 208 | } 209 | 210 | impl WhitelistManager { 211 | /// Create a new whitelist manager 212 | pub fn new(whitelist: Whitelist, save_interval_ms: u64) -> Self { 213 | Self { 214 | whitelist: Arc::new(Mutex::new(whitelist)), 215 | save_interval_ms, 216 | last_save: Arc::new(std::sync::Mutex::new(Instant::now())), 217 | } 218 | } 219 | 220 | /// Get a clone of the whitelist 221 | pub fn get_whitelist_arc(&self) -> Arc> { 222 | self.whitelist.clone() 223 | } 224 | 225 | /// Check if an address is in the whitelist 226 | pub async fn is_whitelisted(&self, address: &str) -> bool { 227 | let whitelist = self.whitelist.lock().await; 228 | whitelist.is_whitelisted(address) 229 | } 230 | 231 | /// Mark an address as active in the current review cycle 232 | pub async fn mark_as_active(&self, address: &str) { 233 | let mut whitelist = self.whitelist.lock().await; 234 | whitelist.mark_as_active(address); 235 | } 236 | 237 | /// Add an address to the whitelist 238 | pub async fn add_address(&self, address: &str) -> bool { 239 | let mut whitelist = self.whitelist.lock().await; 240 | let result = whitelist.add_address(address); 241 | self.check_and_save().await; 242 | result 243 | } 244 | 245 | /// Remove an address from the whitelist 246 | pub async fn remove_address(&self, address: &str) -> bool { 247 | let mut whitelist = self.whitelist.lock().await; 248 | let result = whitelist.remove_address(address); 249 | self.check_and_save().await; 250 | result 251 | } 252 | 253 | /// Check review cycle and save if needed 254 | pub async fn check_review_cycle(&self) -> bool { 255 | let mut whitelist = self.whitelist.lock().await; 256 | let result = whitelist.check_review_cycle(); 257 | 258 | if result { 259 | // If review cycle was processed, save the whitelist 260 | if let Err(e) = whitelist.save() { 261 | eprintln!("Failed to save whitelist after review cycle: {}", e); 262 | } 263 | } 264 | 265 | result 266 | } 267 | 268 | /// Check if it's time to save and save the whitelist 269 | pub async fn check_and_save(&self) -> bool { 270 | let mut last_save = self.last_save.lock().unwrap(); 271 | let elapsed = last_save.elapsed().as_millis() as u64; 272 | 273 | if elapsed >= self.save_interval_ms { 274 | // Time to save 275 | let whitelist = self.whitelist.lock().await; 276 | 277 | match whitelist.save() { 278 | Ok(_) => { 279 | *last_save = Instant::now(); 280 | true 281 | }, 282 | Err(e) => { 283 | eprintln!("Failed to save whitelist: {}", e); 284 | false 285 | } 286 | } 287 | } else { 288 | false 289 | } 290 | } 291 | 292 | /// Force save the whitelist 293 | pub async fn save(&self) -> Result<()> { 294 | let whitelist = self.whitelist.lock().await; 295 | let result = whitelist.save(); 296 | 297 | if result.is_ok() { 298 | let mut last_save = self.last_save.lock().unwrap(); 299 | *last_save = Instant::now(); 300 | } 301 | 302 | result 303 | } 304 | } 305 | 306 | #[cfg(test)] 307 | mod tests { 308 | use super::*; 309 | use tempfile::NamedTempFile; 310 | 311 | #[test] 312 | fn test_whitelist_basics() { 313 | let temp_file = NamedTempFile::new().unwrap(); 314 | let temp_path = temp_file.path().to_str().unwrap().to_string(); 315 | 316 | let mut whitelist = Whitelist::empty(&temp_path, 1000); 317 | 318 | // Test add and check 319 | assert!(whitelist.add_address("token1")); 320 | assert!(whitelist.is_whitelisted("token1")); 321 | assert_eq!(whitelist.len(), 1); 322 | 323 | // Test mark as active 324 | whitelist.mark_as_active("token1"); 325 | assert!(whitelist.active_tokens.contains("token1")); 326 | 327 | // Test review cycle 328 | assert!(!whitelist.check_review_cycle()); // Not enough time has passed 329 | 330 | // Force review cycle processing 331 | whitelist.last_review = Instant::now() - Duration::from_millis(2000); 332 | assert!(whitelist.check_review_cycle()); 333 | 334 | // After review, token1 should be removed since we cleared active_tokens 335 | assert!(!whitelist.is_whitelisted("token1")); 336 | assert_eq!(whitelist.len(), 0); 337 | } 338 | 339 | #[tokio::test] 340 | async fn test_whitelist_manager() { 341 | let temp_file = NamedTempFile::new().unwrap(); 342 | let temp_path = temp_file.path().to_str().unwrap().to_string(); 343 | 344 | let whitelist = Whitelist::empty(&temp_path, 1000); 345 | let manager = WhitelistManager::new(whitelist, 5000); 346 | 347 | // Add token 348 | assert!(manager.add_address("token1").await); 349 | assert!(manager.is_whitelisted("token1").await); 350 | 351 | // Mark as active 352 | manager.mark_as_active("token1").await; 353 | 354 | // Save 355 | assert!(manager.save().await.is_ok()); 356 | 357 | // Check file contents 358 | let content = fs::read_to_string(&temp_path).unwrap(); 359 | let parsed: HashSet = serde_json::from_str(&content).unwrap(); 360 | assert!(parsed.contains("token1")); 361 | } 362 | } -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token; 2 | pub mod tx; 3 | -------------------------------------------------------------------------------- /src/core/token.rs: -------------------------------------------------------------------------------- 1 | use anchor_client::solana_sdk::{pubkey::Pubkey, signature::Keypair}; 2 | use spl_token::state::{Account, Mint}; 3 | use solana_program_pack::Pack; 4 | use spl_token_client::{ 5 | client::{ProgramClient, ProgramRpcClient, ProgramRpcClientSendTransaction}, 6 | token::{Token, TokenError, TokenResult}, 7 | }; 8 | use std::sync::Arc; 9 | 10 | pub fn get_associated_token_address( 11 | client: Arc, 12 | keypair: Arc, 13 | address: &Pubkey, 14 | owner: &Pubkey, 15 | ) -> Pubkey { 16 | let token_client = Token::new( 17 | Arc::new(ProgramRpcClient::new( 18 | client.clone(), 19 | ProgramRpcClientSendTransaction, 20 | )), 21 | &spl_token::ID, 22 | address, 23 | None, 24 | Arc::new(Keypair::from_bytes(&keypair.to_bytes()).expect("failed to copy keypair")), 25 | ); 26 | token_client.get_associated_token_address(owner) 27 | } 28 | 29 | pub async fn get_account_info( 30 | client: Arc, 31 | address: Pubkey, 32 | account: Pubkey, 33 | ) -> TokenResult { 34 | let program_client = Arc::new(ProgramRpcClient::new( 35 | client.clone(), 36 | ProgramRpcClientSendTransaction, 37 | )); 38 | let account_data = program_client 39 | .get_account(account) 40 | .await 41 | .map_err(TokenError::Client)? 42 | .ok_or(TokenError::AccountNotFound) 43 | .inspect_err(|_err| { 44 | // logger.log(format!( 45 | // "get_account_info: {} {}: mint {}", 46 | // account, err, address 47 | // )); 48 | })?; 49 | 50 | if account_data.owner != spl_token::ID { 51 | return Err(TokenError::AccountInvalidOwner); 52 | } 53 | 54 | let account_state = Account::unpack(&account_data.data)?; 55 | if account_state.mint != address { 56 | return Err(TokenError::AccountInvalidMint); 57 | } 58 | 59 | Ok(account_state) 60 | } 61 | 62 | pub async fn get_mint_info( 63 | client: Arc, 64 | _keypair: Arc, 65 | address: Pubkey, 66 | ) -> TokenResult { 67 | let program_client = Arc::new(ProgramRpcClient::new( 68 | client.clone(), 69 | ProgramRpcClientSendTransaction, 70 | )); 71 | let account = program_client 72 | .get_account(address) 73 | .await 74 | .map_err(TokenError::Client)? 75 | .ok_or(TokenError::AccountNotFound) 76 | .inspect_err(|err| println!("{} {}: mint {}", address, err, address))?; 77 | 78 | if account.owner != spl_token::ID { 79 | return Err(TokenError::AccountInvalidOwner); 80 | } 81 | 82 | let mint_state = Mint::unpack(&account.data) 83 | .map_err(|_| TokenError::AccountInvalidMint)?; 84 | let decimals: Option = None; 85 | if let Some(decimals) = decimals { 86 | if decimals != mint_state.decimals { 87 | return Err(TokenError::InvalidDecimals); 88 | } 89 | } 90 | 91 | Ok(mint_state) 92 | } 93 | -------------------------------------------------------------------------------- /src/core/tx.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::Duration}; 2 | use std::{str::FromStr, env}; 3 | use anyhow::Result; 4 | use colored::Colorize; 5 | use anchor_client::solana_client::rpc_client::RpcClient; 6 | use anchor_client::solana_sdk::{ 7 | hash::Hash, 8 | instruction::Instruction, 9 | signature::Keypair, 10 | signer::Signer, 11 | system_instruction, system_transaction, 12 | transaction::{Transaction, VersionedTransaction}, 13 | }; 14 | use spl_token::ui_amount_to_amount; 15 | 16 | use jito_json_rpc_client::jsonrpc_client::rpc_client::RpcClient as JitoRpcClient; 17 | use tokio::time::Instant; 18 | 19 | use crate::common::logger::Logger; 20 | use crate::{ 21 | services::{ 22 | jito::{self, JitoClient}, 23 | nozomi, 24 | zeroslot::{self, ZeroSlotClient}, 25 | }, 26 | }; 27 | 28 | pub async fn jito_confirm( 29 | client: &RpcClient, 30 | keypair: &Keypair, 31 | version_tx: VersionedTransaction, 32 | recent_block_hash: &Hash, 33 | logger: &Logger, 34 | ) -> Result> { 35 | 36 | 37 | } 38 | 39 | pub async fn new_signed_and_send( 40 | recent_blockhash: anchor_client::solana_sdk::hash::Hash, 41 | keypair: &Keypair, 42 | mut instructions: Vec, 43 | logger: &Logger, 44 | ) -> Result> { 45 | let start_time = Instant::now(); 46 | 47 | let mut txs = vec![]; 48 | let (tip_account, tip1_account) = jito::get_tip_account()?; 49 | 50 | // jito tip, the upper limit is 0.1 51 | let tip = jito::get_tip_value().await?; 52 | let fee = jito::get_priority_fee().await?; 53 | let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); 54 | let fee_lamports = ui_amount_to_amount(fee, spl_token::native_mint::DECIMALS); 55 | 56 | let jito_tip_instruction = 57 | system_instruction::transfer(&keypair.pubkey(), &tip_account, tip_lamports); 58 | let _jito_tip2_instruction = 59 | system_instruction::transfer(&keypair.pubkey(), &tip1_account, fee_lamports); 60 | 61 | // ADD Priority fee 62 | // ------------- 63 | let unit_limit = get_unit_limit(); 64 | let unit_price = get_unit_price(); 65 | 66 | let modify_compute_units = 67 | anchor_client::solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit( 68 | unit_limit, 69 | ); 70 | let add_priority_fee = 71 | anchor_client::solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price( 72 | unit_price, 73 | ); 74 | instructions.insert(1, modify_compute_units); 75 | instructions.insert(2, add_priority_fee); 76 | 77 | instructions.push(jito_tip_instruction); 78 | // instructions.push(jito_tip2_instruction); 79 | 80 | // send init tx 81 | let txn = Transaction::new_signed_with_payer( 82 | &instructions, 83 | Some(&keypair.pubkey()), 84 | &vec![keypair], 85 | recent_blockhash, 86 | ); 87 | 88 | // let simulate_result = client.simulate_transaction(&txn)?; 89 | // logger.log("Tx Stimulate".to_string()); 90 | // if let Some(logs) = simulate_result.value.logs { 91 | // for log in logs { 92 | // logger.log(log.to_string()); 93 | // } 94 | // } 95 | // if let Some(err) = simulate_result.value.err { 96 | // return Err(anyhow::anyhow!("{}", err)); 97 | // }; 98 | 99 | let jito_client = Arc::new(JitoClient::new( 100 | format!("{}/api/v1/transactions", *jito::BLOCK_ENGINE_URL).as_str(), 101 | )); 102 | let sig = match jito_client.send_transaction(&txn).await { 103 | Ok(signature) => signature, 104 | Err(_) => { 105 | // logger.log(format!("{}", e)); 106 | return Err(anyhow::anyhow!("Bundle status get timeout" 107 | .red() 108 | .italic() 109 | .to_string())); 110 | } 111 | }; 112 | txs.push(sig.clone().to_string()); 113 | logger.log( 114 | format!("[TXN-ELLAPSED(JITO)]: {:?}", start_time.elapsed()) 115 | .yellow() 116 | .to_string(), 117 | ); 118 | 119 | Ok(txs) 120 | } 121 | 122 | pub async fn new_signed_and_send_zeroslot( 123 | recent_blockhash: anchor_client::solana_sdk::hash::Hash, 124 | keypair: &Keypair, 125 | mut instructions: Vec, 126 | logger: &Logger, 127 | ) -> Result> { 128 | let start_time = Instant::now(); 129 | 130 | let mut txs = vec![]; 131 | let tip_account = zeroslot::get_tip_account()?; 132 | 133 | // zeroslot tip, the upper limit is 0.1 134 | let tip = zeroslot::get_tip_value().await?; 135 | let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); 136 | 137 | let zeroslot_tip_instruction = 138 | system_instruction::transfer(&keypair.pubkey(), &tip_account, tip_lamports); 139 | instructions.insert(0, zeroslot_tip_instruction); 140 | 141 | // send init tx 142 | let txn = Transaction::new_signed_with_payer( 143 | &instructions, 144 | Some(&keypair.pubkey()), 145 | &vec![keypair], 146 | recent_blockhash, 147 | ); 148 | 149 | // let simulate_result = client.simulate_transaction(&txn)?; 150 | // logger.log("Tx Stimulate".to_string()); 151 | // if let Some(logs) = simulate_result.value.logs { 152 | // for log in logs { 153 | // logger.log(log.to_string()); 154 | // } 155 | // } 156 | // if let Some(err) = simulate_result.value.err { 157 | // return Err(anyhow::anyhow!("{}", err)); 158 | // }; 159 | 160 | let zeroslot_client = Arc::new(ZeroSlotClient::new((*zeroslot::ZERO_SLOT_URL).as_str())); 161 | let sig = match zeroslot_client.send_transaction(&txn).await { 162 | Ok(signature) => signature, 163 | Err(_) => { 164 | return Err(anyhow::anyhow!("send_transaction status get timeout" 165 | .red() 166 | .italic() 167 | .to_string())); 168 | } 169 | }; 170 | txs.push(sig.clone().to_string()); 171 | logger.log( 172 | format!("[TXN-ELLAPSED]: {:?}", start_time.elapsed()) 173 | .yellow() 174 | .to_string(), 175 | ); 176 | 177 | Ok(txs) 178 | } 179 | 180 | // prioritization fee = UNIT_PRICE * UNIT_LIMIT 181 | fn get_unit_price() -> u64 { 182 | env::var("UNIT_PRICE") 183 | .ok() 184 | .and_then(|v| u64::from_str(&v).ok()) 185 | .unwrap_or(20000) 186 | } 187 | 188 | fn get_unit_limit() -> u32 { 189 | env::var("UNIT_LIMIT") 190 | .ok() 191 | .and_then(|v| u32::from_str(&v).ok()) 192 | .unwrap_or(200_000) 193 | } 194 | 195 | pub async fn new_signed_and_send_nozomi( 196 | recent_blockhash: anchor_client::solana_sdk::hash::Hash, 197 | keypair: &Keypair, 198 | mut instructions: Vec, 199 | logger: &Logger, 200 | ) -> Result> { 201 | let start_time = Instant::now(); 202 | 203 | 204 | 205 | Ok(txs) 206 | } 207 | 208 | pub async fn new_signed_and_send_spam( 209 | recent_blockhash: anchor_client::solana_sdk::hash::Hash, 210 | keypair: &std::sync::Arc, 211 | instructions: Vec, 212 | logger: &Logger, 213 | ) -> Result> { 214 | // Assuming keypair is already defined as Arc 215 | let logger_clone = logger.clone(); 216 | let keypair_clone = Arc::clone(&keypair); // Clone the Arc for the first future 217 | 218 | let logger_clone1 = logger.clone(); 219 | let keypair_clone1 = Arc::clone(&keypair); // Clone the Arc for the second future 220 | 221 | let logger_clone2 = logger.clone(); 222 | let keypair_clone2 = Arc::clone(&keypair); // Clone the Arc for the second future 223 | 224 | // Clone instructions if necessary 225 | let instructions_clone_for_jito = instructions.clone(); 226 | let instructions_clone_for_nozomi = instructions.clone(); 227 | let instructions_clone_for_zeroslot = instructions.clone(); 228 | 229 | // Create the futures for both transaction sending methods 230 | let jito_future = tokio::task::spawn(async move { 231 | new_signed_and_send( 232 | recent_blockhash, 233 | &keypair_clone, 234 | instructions_clone_for_jito, 235 | &logger_clone, 236 | ) 237 | .await 238 | }); 239 | 240 | let nozomi_future = tokio::task::spawn(async move { 241 | new_signed_and_send_nozomi( 242 | recent_blockhash, 243 | &keypair_clone1, 244 | instructions_clone_for_nozomi, 245 | &logger_clone1, 246 | ) 247 | .await 248 | }); 249 | 250 | let zeroslot_future = tokio::task::spawn(async move { 251 | new_signed_and_send_zeroslot( 252 | recent_blockhash, 253 | &keypair_clone2, 254 | instructions_clone_for_zeroslot, 255 | &logger_clone2, 256 | ) 257 | .await 258 | }); 259 | 260 | // Await both futures 261 | let results = futures::future::join_all(vec![jito_future, nozomi_future, zeroslot_future]).await; 262 | 263 | let mut successful_results = Vec::new(); 264 | let mut errors: Vec = Vec::new(); 265 | 266 | for result in results { 267 | match result { 268 | Ok(Ok(res)) => successful_results.push(res[0].clone()), // Push if success 269 | Ok(Err(e)) => errors.push(e.to_string()), // Collect error message 270 | Err(e) => errors.push(format!("Task failed: {:?}", e)), // Collect task failure 271 | } 272 | } 273 | 274 | // If there are any errors, print them 275 | if !errors.is_empty() { 276 | for error in &errors { 277 | eprintln!("{}", error); // Print errors to stderr 278 | } 279 | 280 | // If no successful results were collected, return an error 281 | if successful_results.is_empty() { 282 | return Err(anyhow::anyhow!(format!("All tasks failed with these errors: {:?}", errors))); 283 | } 284 | } 285 | 286 | Ok(successful_results) 287 | } 288 | -------------------------------------------------------------------------------- /src/dex/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pump_fun; 2 | -------------------------------------------------------------------------------- /src/dex/pump_fun.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Arc, time::Duration}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use borsh::from_slice; 5 | use borsh_derive::{BorshDeserialize, BorshSerialize}; 6 | use colored::Colorize; 7 | use serde::{Deserialize, Serialize}; 8 | use anchor_client::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 | use tokio::time::Instant; 21 | 22 | use crate::{ 23 | common::{config::SwapConfig, logger::Logger}, 24 | core::token, 25 | engine::{monitor::BondingCurveInfo, swap::{SwapDirection, SwapInType}}, 26 | }; 27 | 28 | pub const TEN_THOUSAND: u64 = 10000; 29 | pub const TOKEN_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; 30 | pub const RENT_PROGRAM: &str = "SysvarRent111111111111111111111111111111111"; 31 | pub const ASSOCIATED_TOKEN_PROGRAM: &str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; 32 | pub const PUMP_GLOBAL: &str = "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"; 33 | pub const PUMP_FEE_RECIPIENT: &str = "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"; 34 | pub const PUMP_PROGRAM: &str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; 35 | // pub const PUMP_FUN_MINT_AUTHORITY: &str = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"; 36 | pub const PUMP_ACCOUNT: &str = "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"; 37 | pub const PUMP_BUY_METHOD: u64 = 16927863322537952870; 38 | pub const PUMP_SELL_METHOD: u64 = 12502976635542562355; 39 | pub const PUMP_FUN_CREATE_IX_DISCRIMINATOR: &[u8] = &[24, 30, 200, 40, 5, 28, 7, 119]; 40 | pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000; 41 | pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000; 42 | pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000; 43 | 44 | #[derive(Clone)] 45 | pub struct Pump { 46 | pub rpc_nonblocking_client: Arc, 47 | pub keypair: Arc, 48 | pub rpc_client: Option>, 49 | } 50 | 51 | impl Pump { 52 | pub fn new( 53 | rpc_nonblocking_client: Arc, 54 | rpc_client: Arc, 55 | keypair: Arc, 56 | ) -> Self { 57 | Self { 58 | rpc_nonblocking_client, 59 | keypair, 60 | rpc_client: Some(rpc_client), 61 | } 62 | } 63 | } 64 | 65 | 66 | pub async fn get_bonding_curve_account( 67 | rpc_client: Arc, 68 | mint: Pubkey, 69 | program_id: Pubkey, 70 | ) -> Result<(Pubkey, Pubkey, BondingCurveReserves)> { 71 | let bonding_curve = get_pda(&mint, &program_id)?; 72 | let associated_bonding_curve = get_associated_token_address(&bonding_curve, &mint); 73 | let start_time = Instant::now(); 74 | // println!("Start: {:?}", start_time.elapsed()); 75 | 76 | let max_retries = 30; 77 | let time_exceed = 300; 78 | let timeout = Duration::from_millis(time_exceed); 79 | let mut retry_count = 0; 80 | let bonding_curve_data = loop { 81 | match rpc_client.get_account_data(&bonding_curve) { 82 | Ok(data) => { 83 | // println!("Done: {:?}", start_time.elapsed()); 84 | break data; 85 | } 86 | Err(err) => { 87 | retry_count += 1; 88 | if retry_count > max_retries { 89 | return Err(anyhow!( 90 | "Failed to get bonding curve account data after {} retries: {}", 91 | max_retries, 92 | err 93 | )); 94 | } 95 | if start_time.elapsed() > timeout { 96 | return Err(anyhow!( 97 | "Failed to get bonding curve account data after {:?} timeout: {}", 98 | timeout, 99 | err 100 | )); 101 | } 102 | tokio::time::sleep(Duration::from_millis(10)).await; 103 | // println!("Retry {}: {:?}", retry_count, start_time.elapsed()); 104 | } 105 | } 106 | }; 107 | 108 | let bonding_curve_account = 109 | from_slice::(&bonding_curve_data).map_err(|e| { 110 | anyhow!( 111 | "Failed to deserialize bonding curve account: {}", 112 | e.to_string() 113 | ) 114 | })?; 115 | let bonding_curve_reserves = BondingCurveReserves 116 | { 117 | virtual_token_reserves: bonding_curve_account.virtual_token_reserves, 118 | virtual_sol_reserves: bonding_curve_account.virtual_sol_reserves 119 | }; 120 | Ok(( 121 | bonding_curve, 122 | associated_bonding_curve, 123 | bonding_curve_reserves, 124 | )) 125 | } 126 | 127 | pub fn get_pda(mint: &Pubkey, program_id: &Pubkey) -> Result { 128 | let seeds = [b"bonding-curve".as_ref(), mint.as_ref()]; 129 | let (bonding_curve, _bump) = Pubkey::find_program_address(&seeds, program_id); 130 | Ok(bonding_curve) 131 | } 132 | 133 | // https://frontend-api.pump.fun/coins/8zSLdDzM1XsqnfrHmHvA9ir6pvYDjs8UXz6B2Tydd6b2 134 | // pub async fn get_pump_info( 135 | // rpc_client: Arc, 136 | // mint: str, 137 | // ) -> Result { 138 | // let mint = Pubkey::from_str(&mint)?; 139 | // let program_id = Pubkey::from_str(PUMP_PROGRAM)?; 140 | // let (bonding_curve, associated_bonding_curve, bonding_curve_account) = 141 | // get_bonding_curve_account(rpc_client, &mint, &program_id).await?; 142 | 143 | // let pump_info = PumpInfo { 144 | // mint: mint.to_string(), 145 | // bonding_curve: bonding_curve.to_string(), 146 | // associated_bonding_curve: associated_bonding_curve.to_string(), 147 | // raydium_pool: None, 148 | // raydium_info: None, 149 | // complete: bonding_curve_account.complete, 150 | // virtual_sol_reserves: bonding_curve_account.virtual_sol_reserves, 151 | // virtual_token_reserves: bonding_curve_account.virtual_token_reserves, 152 | // total_supply: bonding_curve_account.token_total_supply, 153 | // }; 154 | // Ok(pump_info) 155 | // } 156 | 157 | /// Token pool information structure 158 | #[derive(Debug, Clone)] 159 | pub struct PoolInfo { 160 | /// Token mint address 161 | pub token_mint: String, 162 | /// Current liquidity in SOL 163 | pub liquidity: f64, 164 | /// Current token price in SOL 165 | pub price: f64, 166 | /// Virtual SOL reserves (from bonding curve) 167 | pub virtual_sol_reserves: f64, 168 | /// Virtual token reserves (from bonding curve) 169 | pub virtual_token_reserves: f64, 170 | } 171 | -------------------------------------------------------------------------------- /src/engine/advanced_trading.rs: -------------------------------------------------------------------------------- 1 | // The provided Rust code defines an AdvancedTradingManager for sophisticated token trading strategies. 2 | 3 | // Its main functions and implementation details are: 4 | 5 | // Risk Profiling: Classifies tokens into risk categories (Low, Medium, High, VeryHigh) based on metrics like market cap, volume, buy/sell ratio, and token age. 6 | 7 | // Entry Criteria Calculation: Uses technical indicators (momentum, volatility, buy/sell ratio, liquidity growth, market sentiment) and bonding curve analysis to determine if a token has a buy signal and calculates an entry confidence score. 8 | 9 | // Dynamic Exit Strategy: Sets multi-level take-profit and stop-loss thresholds tailored to the token's risk profile, including trailing stops and time-based exits. 10 | 11 | // Position Sizing: Applies the Kelly criterion to determine optimal position size based on confidence and portfolio value, with safety caps. 12 | 13 | // Trade Evaluation: Evaluates tokens for entry signals, checks if trading is active, and decides position size before logging the trade decision. -------------------------------------------------------------------------------- /src/engine/bonding_curve.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use anyhow::{Result, anyhow}; 3 | use colored::Colorize; 4 | use anchor_client::solana_sdk::pubkey::Pubkey; 5 | 6 | use crate::common::logger::Logger; 7 | use crate::dex::pump_fun::Pump; 8 | 9 | /// Represents the bonding curve parameters for a token 10 | #[derive(Debug, Clone)] 11 | pub struct BondingCurve { 12 | /// Current liquidity in SOL 13 | pub liquidity: f64, 14 | /// How quickly price rises with buys (steepness) 15 | pub curve_steepness: f64, 16 | /// Price at 0 tokens bought 17 | pub initial_price: f64, 18 | /// Current token price 19 | pub current_price: f64, 20 | /// Last calculated liquidity depth 21 | pub liquidity_depth: f64, 22 | /// History of liquidity changes 23 | pub liquidity_history: Vec<(u64, f64)>, // timestamp, liquidity 24 | } 25 | 26 | impl BondingCurve { 27 | /// Create a new bonding curve analysis 28 | pub fn new(initial_price: f64, current_price: f64, liquidity: f64) -> Self { 29 | // Estimate steepness based on initial and current price 30 | let curve_steepness = if initial_price > 0.0 && current_price > initial_price { 31 | (current_price / initial_price - 1.0) / liquidity 32 | } else { 33 | 0.05 // Default fallback value 34 | }; 35 | 36 | Self { 37 | liquidity, 38 | curve_steepness, 39 | initial_price, 40 | current_price, 41 | liquidity_depth: liquidity, 42 | liquidity_history: vec![( 43 | std::time::SystemTime::now() 44 | .duration_since(std::time::UNIX_EPOCH) 45 | .unwrap_or_default() 46 | .as_secs(), 47 | liquidity 48 | )], 49 | } 50 | } 51 | 52 | /// Calculate price impact for a given buy amount in SOL 53 | pub fn calculate_price_impact(&self, buy_amount_sol: f64) -> f64 { 54 | if self.liquidity == 0.0 || buy_amount_sol == 0.0 { 55 | return 0.0; 56 | } 57 | 58 | // Simple bonding curve model: price impact increases with buy size relative to liquidity 59 | let relative_size = buy_amount_sol / self.liquidity; 60 | let impact = relative_size * self.curve_steepness * 100.0; 61 | 62 | // Cap at 100% to avoid unrealistic values 63 | impact.min(100.0) 64 | } 65 | 66 | /// Calculate optimal trade size to limit price impact 67 | pub fn calculate_optimal_trade_size(&self, max_price_impact_pct: f64) -> f64 { 68 | if self.curve_steepness == 0.0 || max_price_impact_pct <= 0.0 { 69 | return 0.0; 70 | } 71 | 72 | // Solve for buy_amount: max_price_impact = (buy_amount/liquidity) * curve_steepness * 100 73 | // buy_amount = (max_price_impact * liquidity) / (curve_steepness * 100) 74 | let optimal_size = (max_price_impact_pct * self.liquidity) / (self.curve_steepness * 100.0); 75 | 76 | // Return a reasonable value capped at 90% of liquidity 77 | optimal_size.min(self.liquidity * 0.9) 78 | } 79 | 80 | /// Calculate estimated price after a buy of a specific size 81 | pub fn estimate_price_after_buy(&self, buy_amount_sol: f64) -> f64 { 82 | let price_impact_pct = self.calculate_price_impact(buy_amount_sol); 83 | self.current_price * (1.0 + price_impact_pct / 100.0) 84 | } 85 | 86 | /// Update the bonding curve with new liquidity data 87 | pub fn update_liquidity(&mut self, new_liquidity: f64) { 88 | self.liquidity = new_liquidity; 89 | 90 | // Add to history 91 | self.liquidity_history.push(( 92 | std::time::SystemTime::now() 93 | .duration_since(std::time::UNIX_EPOCH) 94 | .unwrap_or_default() 95 | .as_secs(), 96 | new_liquidity 97 | )); 98 | 99 | // Keep history manageable 100 | if self.liquidity_history.len() > 100 { 101 | self.liquidity_history.remove(0); 102 | } 103 | 104 | // Update liquidity depth as a simple average of recent measurements 105 | if !self.liquidity_history.is_empty() { 106 | self.liquidity_depth = self.liquidity_history 107 | .iter() 108 | .map(|(_, liquidity)| *liquidity) 109 | .sum::() / self.liquidity_history.len() as f64; 110 | } 111 | } 112 | 113 | /// Calculate liquidity growth rate over the last N data points 114 | pub fn calculate_liquidity_growth_rate(&self, lookback_points: usize) -> f64 { 115 | let history_len = self.liquidity_history.len(); 116 | if history_len < 2 || lookback_points < 2 { 117 | return 0.0; 118 | } 119 | 120 | let points_to_use = lookback_points.min(history_len); 121 | let start_idx = history_len - points_to_use; 122 | 123 | let oldest_liquidity = self.liquidity_history[start_idx].1; 124 | let newest_liquidity = self.liquidity_history[history_len - 1].1; 125 | 126 | if oldest_liquidity == 0.0 { 127 | return 0.0; 128 | } 129 | 130 | ((newest_liquidity / oldest_liquidity) - 1.0) * 100.0 131 | } 132 | } 133 | 134 | /// Manager for tracking and analyzing token bonding curves 135 | pub struct BondingCurveManager { 136 | /// Bonding curves for each token mint 137 | curves: HashMap, 138 | /// Logger for events 139 | logger: Logger, 140 | } 141 | 142 | impl BondingCurveManager { 143 | /// Create a new bonding curve manager 144 | pub fn new(logger: Logger) -> Self { 145 | Self { 146 | curves: HashMap::new(), 147 | logger, 148 | } 149 | } 150 | 151 | /// Get or create bonding curve for a token 152 | pub fn get_or_create_curve(&mut self, token_mint: &str, initial_price: f64, current_price: f64, liquidity: f64) -> &mut BondingCurve { 153 | if !self.curves.contains_key(token_mint) { 154 | let curve = BondingCurve::new(initial_price, current_price, liquidity); 155 | self.curves.insert(token_mint.to_string(), curve); 156 | } 157 | 158 | self.curves.get_mut(token_mint).unwrap() 159 | } 160 | 161 | /// Update a token's bonding curve with new data 162 | pub fn update_curve(&mut self, token_mint: &str, current_price: f64, current_liquidity: f64) -> Result<()> { 163 | if let Some(curve) = self.curves.get_mut(token_mint) { 164 | // Update price 165 | curve.current_price = current_price; 166 | 167 | // Update liquidity 168 | curve.update_liquidity(current_liquidity); 169 | 170 | // Recalculate curve steepness if possible 171 | if curve.initial_price > 0.0 && current_price > curve.initial_price && current_liquidity > 0.0 { 172 | curve.curve_steepness = (current_price / curve.initial_price - 1.0) / current_liquidity; 173 | } 174 | 175 | Ok(()) 176 | } else { 177 | Err(anyhow!("No curve exists for token {}", token_mint)) 178 | } 179 | } 180 | 181 | /// Get price impact analysis for different trade sizes 182 | pub fn get_price_impact_analysis(&self, token_mint: &str) -> Result { 183 | let curve = self.curves.get(token_mint) 184 | .ok_or_else(|| anyhow!("No curve exists for token {}", token_mint))?; 185 | 186 | // Calculate optimal trade size for 5% price impact 187 | let optimal_trade_size = curve.calculate_optimal_trade_size(5.0); 188 | 189 | // Calculate impact for standard sizes 190 | let impact_1sol = curve.calculate_price_impact(1.0); 191 | let impact_5sol = curve.calculate_price_impact(5.0); 192 | 193 | Ok(BondingCurveAnalysis { 194 | token_mint: token_mint.to_string(), 195 | liquidity: curve.liquidity, 196 | liquidity_depth: curve.liquidity_depth, 197 | curve_steepness: curve.curve_steepness, 198 | optimal_trade_size, 199 | price_impact_1sol: impact_1sol, 200 | price_impact_5sol: impact_5sol, 201 | liquidity_growth_rate: curve.calculate_liquidity_growth_rate(5), 202 | }) 203 | } 204 | 205 | /// Analyze transaction for its impact on curve and price 206 | pub async fn analyze_transaction( 207 | &mut self, 208 | token_mint: &str, 209 | transaction_amount_sol: f64, 210 | is_buy: bool, 211 | swapx: &Pump, 212 | ) -> Result { 213 | // Default values 214 | let mut current_liquidity = 0.0; 215 | let mut current_price = 0.0; 216 | 217 | // Try to get actual data from DEX 218 | if let Ok(pool_info) = swapx.get_token_pool_info(&token_mint.parse::().unwrap()).await { 219 | current_liquidity = pool_info.liquidity; 220 | current_price = pool_info.price; 221 | } 222 | 223 | // Get or create curve 224 | let curve = self.get_or_create_curve(token_mint, 0.0, current_price, current_liquidity); 225 | 226 | // Update curve with new data 227 | curve.current_price = current_price; 228 | curve.update_liquidity(current_liquidity); 229 | 230 | // Calculate expected price impact 231 | let price_impact = if is_buy { 232 | curve.calculate_price_impact(transaction_amount_sol) 233 | } else { 234 | // Sell impact is typically larger and in the opposite direction 235 | -curve.calculate_price_impact(transaction_amount_sol) * 1.2 236 | }; 237 | 238 | // Calculate projected new price 239 | let projected_price = current_price * (1.0 + price_impact / 100.0); 240 | 241 | // Calculate liquidity change 242 | let liquidity_change = if is_buy { 243 | transaction_amount_sol 244 | } else { 245 | -transaction_amount_sol 246 | }; 247 | 248 | let projected_liquidity = (current_liquidity + liquidity_change).max(0.0); 249 | 250 | // Log the analysis 251 | self.logger.log(format!( 252 | "Transaction impact analysis for {}: {} SOL {} - Price impact: {}%, New price: {} SOL, New liquidity: {} SOL", 253 | token_mint, 254 | transaction_amount_sol, 255 | if is_buy { "BUY".green() } else { "SELL".red() }, 256 | price_impact, 257 | projected_price, 258 | projected_liquidity 259 | ).cyan().to_string()); 260 | 261 | Ok(TransactionImpactAnalysis { 262 | token_mint: token_mint.to_string(), 263 | transaction_amount_sol, 264 | is_buy, 265 | current_price, 266 | projected_price, 267 | price_impact_pct: price_impact, 268 | current_liquidity, 269 | projected_liquidity, 270 | }) 271 | } 272 | } 273 | 274 | /// Analysis of a bonding curve 275 | #[derive(Debug, Clone)] 276 | pub struct BondingCurveAnalysis { 277 | /// Token mint address 278 | pub token_mint: String, 279 | /// Current liquidity in SOL 280 | pub liquidity: f64, 281 | /// Liquidity depth (average of recent measurements) 282 | pub liquidity_depth: f64, 283 | /// Steepness of the bonding curve 284 | pub curve_steepness: f64, 285 | /// Optimal trade size for minimal impact (SOL) 286 | pub optimal_trade_size: f64, 287 | /// Price impact for 1 SOL buy (%) 288 | pub price_impact_1sol: f64, 289 | /// Price impact for 5 SOL buy (%) 290 | pub price_impact_5sol: f64, 291 | /// Liquidity growth rate (%) 292 | pub liquidity_growth_rate: f64, 293 | } 294 | 295 | /// Analysis of a transaction's impact on price and liquidity 296 | #[derive(Debug, Clone)] 297 | pub struct TransactionImpactAnalysis { 298 | /// Token mint address 299 | pub token_mint: String, 300 | /// Transaction amount in SOL 301 | pub transaction_amount_sol: f64, 302 | /// Whether the transaction is a buy 303 | pub is_buy: bool, 304 | /// Current token price 305 | pub current_price: f64, 306 | /// Projected price after transaction 307 | pub projected_price: f64, 308 | /// Price impact as a percentage 309 | pub price_impact_pct: f64, 310 | /// Current liquidity in SOL 311 | pub current_liquidity: f64, 312 | /// Projected liquidity after transaction 313 | pub projected_liquidity: f64, 314 | } -------------------------------------------------------------------------------- /src/engine/enhanced_monitor.rs: -------------------------------------------------------------------------------- 1 | // Connects to a Yellowstone gRPC service to subscribe to filtered Solana blockchain transaction updates in real-time. 2 | 3 | // Tracks token activity and market metrics (price changes, volume, buy/sell counts) using a TokenTracker. 4 | 5 | // Manages token lists (whitelist/blacklist) with periodic review and persistence. 6 | 7 | // Implements buying and selling logic through BuyManager and SellManager based on configurable thresholds like take profit and stop loss percentages. 8 | 9 | // Cleans up inactive tokens and records token activity data periodically. 10 | 11 | // Integrates with Telegram for sending notifications and receiving commands. 12 | 13 | // Uses asynchronous Rust (tokio) for concurrency and efficient streaming data handling. -------------------------------------------------------------------------------- /src/engine/enhanced_token_trader.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tran325/Pump-Fun-Pump-Swap-Sniper-Copy-Trading-Bot/5e26107b205ee1240305ed6300429c38d6bcd289/src/engine/enhanced_token_trader.rs -------------------------------------------------------------------------------- /src/engine/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod monitor; 2 | pub mod swap; 3 | pub mod token_tracker; 4 | pub mod token_selling; 5 | pub mod token_buying; 6 | pub mod advanced_trading; 7 | pub mod bonding_curve; 8 | pub mod risk_management; 9 | pub mod enhanced_monitor; 10 | pub mod token_list_manager; 11 | pub mod enhanced_token_trader; 12 | -------------------------------------------------------------------------------- /src/engine/monitor.rs: -------------------------------------------------------------------------------- 1 | // use std::hash::Hash; 2 | use anchor_lang::prelude::Pubkey; 3 | use anchor_client::solana_sdk::hash::Hash; 4 | use std::sync::{Arc, Mutex}; 5 | use std::time::SystemTime; 6 | use crate::common::{ 7 | blacklist::Blacklist, 8 | config::{AppState, SwapConfig}, 9 | }; 10 | use spl_associated_token_account::get_associated_token_address; 11 | use solana_program_pack::Pack; 12 | use yellowstone_grpc_proto::prelude::SubscribeUpdateTransaction; 13 | use anyhow::Result; 14 | use std::str::FromStr; 15 | use base64; 16 | use anchor_client::solana_client::rpc_client::RpcClient; 17 | use spl_token::state::Mint; 18 | use crate::dex::pump_fun::{get_pda, PUMP_PROGRAM}; 19 | // PumpFun constants 20 | pub const PUMPFUN_CREATE_DATA_PREFIX: &str = "Program data: G3KpTd7rY3Y"; 21 | pub const PUMP_FUN_BUY_OR_SELL_PROGRAM_DATA_PREFIX: &str = "Program data: vdt/007mYe"; 22 | pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 1_000_000_000; // 1 SOL in lamports 23 | pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_000_000_000_000; // 1 trillion tokens 24 | 25 | // Type definition for RequestItem 26 | pub type RequestItem = String; 27 | 28 | #[derive(Clone, Debug)] 29 | pub struct BondingCurveInfo { 30 | pub bonding_curve: Pubkey, 31 | pub new_virtual_sol_reserve: u64, 32 | pub new_virtual_token_reserve: u64, 33 | } 34 | 35 | #[derive(Clone, Debug)] 36 | pub async fn new_token_trader_pumpfun( 37 | _yellowstone_grpc_http: String, 38 | _yellowstone_grpc_token: String, 39 | _yellowstone_ping_interval: u64, 40 | _yellowstone_reconnect_delay: u64, 41 | _yellowstone_max_retries: u32, 42 | _app_state: AppState, 43 | _swap_config: SwapConfig, 44 | _blacklist: Blacklist, 45 | _time_exceed: u64, 46 | _counter_limit: u64, 47 | _min_dev_buy: u64, 48 | _max_dev_buy: u64, 49 | _telegram_bot_token: String, 50 | _telegram_chat_id: String, 51 | _bundle_check: bool, 52 | _min_last_time: u64, 53 | ) -> Result<(), String> { 54 | // ... function implementation ... 55 | Ok(()) 56 | } 57 | 58 | impl Default for BondingCurveInfo { 59 | fn default() -> Self { 60 | Self { 61 | bonding_curve: Pubkey::default(), 62 | new_virtual_sol_reserve: INITIAL_VIRTUAL_SOL_RESERVES, 63 | new_virtual_token_reserve: INITIAL_VIRTUAL_TOKEN_RESERVES, 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/engine/swap.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use serde::Deserialize; 3 | use std::str::FromStr; 4 | use anyhow::{Error, Result}; 5 | 6 | #[derive(ValueEnum, Debug, Clone, Deserialize, PartialEq)] 7 | pub enum SwapDirection { 8 | #[serde(rename = "buy")] 9 | Buy, 10 | #[serde(rename = "sell")] 11 | Sell, 12 | } 13 | impl From for u8 { 14 | fn from(value: SwapDirection) -> Self { 15 | match value { 16 | SwapDirection::Buy => 0, 17 | SwapDirection::Sell => 1, 18 | } 19 | } 20 | } 21 | 22 | #[derive(ValueEnum, Debug, Clone, Deserialize)] 23 | pub enum SwapInType { 24 | /// Quantity 25 | #[serde(rename = "qty")] 26 | Qty, 27 | /// Percentage 28 | #[serde(rename = "pct")] 29 | Pct, 30 | } 31 | 32 | impl FromStr for SwapDirection { 33 | type Err = Error; 34 | 35 | fn from_str(s: &str) -> Result { 36 | match s.to_lowercase().as_str() { 37 | "buy" => Ok(SwapDirection::Buy), 38 | "sell" => Ok(SwapDirection::Sell), 39 | _ => Err(anyhow::anyhow!("Invalid swap direction: {}", s)), 40 | } 41 | } 42 | } 43 | 44 | impl FromStr for SwapInType { 45 | type Err = Error; 46 | 47 | fn from_str(s: &str) -> Result { 48 | match s.to_lowercase().as_str() { 49 | "qty" => Ok(SwapInType::Qty), 50 | "pct" => Ok(SwapInType::Pct), 51 | _ => Err(anyhow::anyhow!("Invalid swap in type: {}", s)), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/engine/token_buying.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::sync::{Arc, Mutex}; 3 | use std::time::Duration; 4 | use anyhow::{Result, anyhow}; 5 | use colored::Colorize; 6 | use tokio::time; 7 | use tokio::time::Instant; 8 | 9 | use crate::common::logger::Logger; 10 | use crate::dex::pump_fun::Pump; 11 | use crate::engine::swap::{SwapDirection, SwapInType}; 12 | use crate::common::config::{SwapConfig, Status, LiquidityPool}; 13 | use crate::core::tx; 14 | use crate::services::telegram::TelegramService; 15 | use crate::engine::token_tracker::{TokenTracker, ExtendedTokenInfo}; 16 | use crate::engine::advanced_trading::{RiskProfile, AdvancedTradingManager}; 17 | use crate::engine::monitor::BondingCurveInfo; 18 | 19 | /// Defines a strategy for token buying with configurable parameters 20 | #[derive(Clone, Debug)] 21 | pub struct BuyStrategy { 22 | /// Mint address of the token 23 | pub token_mint: String, 24 | /// Current price of the token 25 | pub current_price: f64, 26 | /// Amount to buy in SOL 27 | pub buy_amount: f64, 28 | /// Maximum price increase tolerated for entry (%) 29 | pub max_entry_slippage: f64, 30 | /// Risk profile of the token 31 | pub risk_profile: RiskProfile, 32 | /// Minimum buy-to-sell ratio to consider purchase 33 | pub min_buy_sell_ratio: f64, 34 | /// Maximum market cap to consider (in SOL) 35 | pub max_market_cap: f64, 36 | /// Minimum launcher SOL balance 37 | pub min_launcher_sol: f64, 38 | /// Maximum launcher SOL balance 39 | pub max_launcher_sol: f64, 40 | /// Whether this is a time-sensitive opportunity 41 | pub time_sensitive: bool, 42 | /// Confidence score (0-100) 43 | pub confidence: u64, 44 | /// Timestamp when the buy strategy was created 45 | pub created_at: Instant, 46 | /// Maximum age of token to consider for buying (in seconds) 47 | pub max_token_age: u64, 48 | /// Whether buying is enabled for this token 49 | pub buying_enabled: bool, 50 | } 51 | -------------------------------------------------------------------------------- /src/engine/token_list_manager.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::time::Instant; 3 | use tokio::time::Duration; 4 | use tokio::sync::Mutex; 5 | 6 | use crate::common::{ 7 | logger::Logger, 8 | blacklist::{Blacklist, BlacklistManager}, 9 | whitelist::{Whitelist, WhitelistManager}, 10 | }; 11 | 12 | /// Manager for handling token lists (whitelist/blacklist) with review cycles 13 | #[allow(dead_code)] 14 | pub struct TokenListManager { 15 | /// Whitelist manager 16 | whitelist_manager: WhitelistManager, 17 | /// Blacklist manager 18 | blacklist_manager: BlacklistManager, 19 | /// Logger 20 | logger: Logger, 21 | /// Last save timestamp 22 | last_save: Arc>, 23 | /// Save interval in milliseconds 24 | save_interval_ms: u64, 25 | /// Tokens seen in the current review cycle 26 | active_tokens_in_cycle: Arc>>, 27 | } 28 | 29 | impl TokenListManager { 30 | /// Create a new token list manager 31 | pub fn new( 32 | whitelist_path: &str, 33 | blacklist_path: &str, 34 | review_cycle_ms: u64, 35 | save_interval_ms: u64, 36 | logger: Logger 37 | ) -> Result { 38 | // Initialize whitelist 39 | let whitelist = Whitelist::new(whitelist_path, review_cycle_ms)?; 40 | let whitelist_manager = WhitelistManager::new(whitelist.clone(), save_interval_ms); 41 | 42 | // Initialize blacklist 43 | let blacklist = Blacklist::new(blacklist_path)?; 44 | let blacklist_manager = BlacklistManager::new(blacklist.clone(), save_interval_ms); 45 | 46 | logger.log(format!( 47 | "TokenListManager initialized - Whitelist: {} tokens, Blacklist: {} tokens", 48 | whitelist.len(), 49 | blacklist.len() 50 | )); 51 | 52 | Ok(Self { 53 | whitelist_manager, 54 | blacklist_manager, 55 | logger, 56 | last_save: Arc::new(std::sync::Mutex::new(Instant::now())), 57 | save_interval_ms, 58 | active_tokens_in_cycle: Arc::new(Mutex::new(Vec::new())), 59 | }) 60 | } 61 | 62 | /// Process a token, checking blacklist/whitelist and marking as active in the current cycle 63 | pub async fn process_token(&self, token_mint: &str) -> TokenListStatus { 64 | // First check blacklist 65 | if self.blacklist_manager.is_blacklisted(token_mint).await { 66 | return TokenListStatus::Blacklisted; 67 | } 68 | 69 | // Then handle whitelist 70 | let is_whitelisted = self.whitelist_manager.is_whitelisted(token_mint).await; 71 | // Add to active tokens in this cycle 72 | let mut active_tokens = self.active_tokens_in_cycle.lock().await; 73 | if !active_tokens.contains(&token_mint.to_string()) { 74 | active_tokens.push(token_mint.to_string()); 75 | } 76 | 77 | // Mark as active in whitelist if it's there 78 | if is_whitelisted { 79 | self.whitelist_manager.mark_as_active(token_mint).await; 80 | TokenListStatus::Whitelisted 81 | } else { 82 | TokenListStatus::NotListed 83 | } 84 | } 85 | 86 | /// Check if it's time for a review cycle and process it 87 | pub async fn check_review_cycle(&self) -> bool { 88 | let review_processed = self.whitelist_manager.check_review_cycle().await; 89 | 90 | if review_processed { 91 | self.logger.log("Review cycle completed - Updated whitelist with active tokens only".to_string()); 92 | 93 | // Reset active tokens for the next cycle 94 | let mut active_tokens = self.active_tokens_in_cycle.lock().await; 95 | active_tokens.clear(); 96 | 97 | // Force save both lists 98 | let _ = self.whitelist_manager.save().await; 99 | let _ = self.blacklist_manager.save().await; 100 | } 101 | 102 | review_processed 103 | } 104 | 105 | /// Check if a token is blacklisted 106 | pub async fn is_blacklisted(&self, token_mint: &str) -> bool { 107 | self.blacklist_manager.is_blacklisted(token_mint).await 108 | } 109 | 110 | /// Check if a token is whitelisted 111 | pub async fn is_whitelisted(&self, token_mint: &str) -> bool { 112 | self.whitelist_manager.is_whitelisted(token_mint).await 113 | } 114 | 115 | /// Add a token to the blacklist 116 | pub async fn add_to_blacklist(&self, token_mint: &str) -> bool { 117 | self.blacklist_manager.add_address(token_mint).await 118 | } 119 | 120 | /// Add a token to the whitelist 121 | pub async fn add_to_whitelist(&self, token_mint: &str) -> bool { 122 | self.whitelist_manager.add_address(token_mint).await 123 | } 124 | 125 | /// Get active tokens in the current cycle 126 | pub async fn get_active_tokens(&self) -> Vec { 127 | let active_tokens = self.active_tokens_in_cycle.lock().await; 128 | active_tokens.clone() 129 | } 130 | 131 | /// Start background task for periodic saving and review cycle checking 132 | pub fn start_background_tasks(&self) { 133 | let whitelist_manager = self.whitelist_manager.clone(); 134 | let blacklist_manager = self.blacklist_manager.clone(); 135 | let logger = self.logger.clone(); 136 | let save_interval_ms = self.save_interval_ms; 137 | 138 | tokio::spawn(async move { 139 | let mut save_interval = tokio::time::interval(Duration::from_millis(save_interval_ms)); 140 | 141 | loop { 142 | save_interval.tick().await; 143 | 144 | // Check and process review cycle 145 | if whitelist_manager.check_review_cycle().await { 146 | logger.log("Review cycle completed - Updated whitelist with active tokens only".to_string()); 147 | } 148 | 149 | // Save lists 150 | if let Err(e) = whitelist_manager.save().await { 151 | logger.log(format!("Error saving whitelist: {}", e)); 152 | } 153 | 154 | if let Err(e) = blacklist_manager.save().await { 155 | logger.log(format!("Error saving blacklist: {}", e)); 156 | } 157 | } 158 | }); 159 | } 160 | 161 | /// Mark a token as active in the current review cycle 162 | pub async fn mark_as_active(&self, token_mint: &str) { 163 | // Mark as active in the whitelist 164 | self.whitelist_manager.mark_as_active(token_mint).await; 165 | 166 | // Add to active tokens in this cycle 167 | let mut active_tokens = self.active_tokens_in_cycle.lock().await; 168 | if !active_tokens.contains(&token_mint.to_string()) { 169 | active_tokens.push(token_mint.to_string()); 170 | } 171 | } 172 | } 173 | 174 | /// Status of a token in relation to the whitelist/blacklist 175 | #[derive(Debug, Clone, PartialEq)] 176 | pub enum TokenListStatus { 177 | /// Token is in the whitelist 178 | Whitelisted, 179 | /// Token is in the blacklist 180 | Blacklisted, 181 | /// Token is not in either list 182 | NotListed, 183 | } 184 | 185 | #[cfg(test)] 186 | mod tests { 187 | use super::*; 188 | use tempfile::NamedTempFile; 189 | use colored::Colorize; 190 | 191 | #[tokio::test] 192 | async fn test_token_list_manager() { 193 | let whitelist_file = NamedTempFile::new().unwrap(); 194 | let blacklist_file = NamedTempFile::new().unwrap(); 195 | 196 | let whitelist_path = whitelist_file.path().to_str().unwrap(); 197 | let blacklist_path = blacklist_file.path().to_str().unwrap(); 198 | 199 | let logger = Logger::new("[TEST] => ".blue().to_string()); 200 | 201 | let manager = TokenListManager::new( 202 | whitelist_path, 203 | blacklist_path, 204 | 1000, // 1 second review cycle 205 | 5000, // 5 second save interval 206 | logger 207 | ).unwrap(); 208 | 209 | // Test blacklist 210 | assert!(manager.add_to_blacklist("blacktoken").await); 211 | assert!(manager.is_blacklisted("blacktoken").await); 212 | 213 | // Test whitelist 214 | assert!(manager.add_to_whitelist("whitetoken").await); 215 | assert!(manager.is_whitelisted("whitetoken").await); 216 | 217 | // Test process token 218 | assert_eq!(manager.process_token("blacktoken").await, TokenListStatus::Blacklisted); 219 | assert_eq!(manager.process_token("whitetoken").await, TokenListStatus::Whitelisted); 220 | assert_eq!(manager.process_token("unlisted").await, TokenListStatus::NotListed); 221 | 222 | // Verify active tokens 223 | let active_tokens = manager.get_active_tokens().await; 224 | assert!(active_tokens.contains(&"blacktoken".to_string())); 225 | assert!(active_tokens.contains(&"whitetoken".to_string())); 226 | assert!(active_tokens.contains(&"unlisted".to_string())); 227 | } 228 | } -------------------------------------------------------------------------------- /src/engine/token_selling.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tran325/Pump-Fun-Pump-Swap-Sniper-Copy-Trading-Bot/5e26107b205ee1240305ed6300429c38d6bcd289/src/engine/token_selling.rs -------------------------------------------------------------------------------- /src/engine/token_tracker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex, RwLock}; 3 | use std::time::{Duration, Instant}; 4 | use colored::Colorize; 5 | use tokio::task::JoinHandle; 6 | use anchor_client::solana_sdk::pubkey::Pubkey; 7 | use anchor_client::solana_client::nonblocking::rpc_client::RpcClient; 8 | use anyhow::{Result, anyhow}; 9 | use spl_token::solana_program::native_token::lamports_to_sol; 10 | 11 | use crate::common::logger::Logger; 12 | use crate::engine::monitor::TransactionType; 13 | 14 | /// Represents detailed information about a token 15 | #[derive(Debug)] 16 | pub struct ExtendedTokenInfo { 17 | pub token_mint: String, 18 | pub token_name: Option, 19 | pub token_symbol: Option, 20 | pub current_token_price: f64, 21 | pub max_token_price: f64, 22 | pub initial_price: f64, 23 | pub total_supply: u64, 24 | pub buy_tx_num: u32, 25 | pub sell_tx_num: u32, 26 | pub token_mint_timestamp: Instant, 27 | pub launcher_sol_balance: Option, 28 | pub market_cap: Option, 29 | pub volume: Option, 30 | pub dev_buy_amount: Option, 31 | pub bundle_check: Option, 32 | pub active_task: Option>, 33 | pub is_monitored: bool, 34 | pub dev_wallet: Option, 35 | pub token_amount: Option, 36 | } 37 | 38 | impl ExtendedTokenInfo { 39 | pub fn new( 40 | token_mint: String, 41 | token_name: Option, 42 | token_symbol: Option, 43 | current_price: f64, 44 | total_supply: u64, 45 | dev_buy_amount: Option, 46 | launcher_sol_balance: Option, 47 | bundle_check: Option, 48 | dev_wallet: Option, 49 | ) -> Self { 50 | Self { 51 | token_mint, 52 | token_name, 53 | token_symbol, 54 | current_token_price: current_price, 55 | max_token_price: current_price, 56 | initial_price: current_price, 57 | total_supply, 58 | buy_tx_num: 1, // Start with 1 since we're detecting the initial transaction 59 | sell_tx_num: 0, 60 | token_mint_timestamp: Instant::now(), 61 | launcher_sol_balance, 62 | market_cap: Some(current_price * (total_supply as f64)), 63 | volume: dev_buy_amount, 64 | dev_buy_amount, 65 | bundle_check, 66 | active_task: None, 67 | is_monitored: false, 68 | dev_wallet, 69 | token_amount: None, 70 | } 71 | } 72 | 73 | /// Update token price and related statistics 74 | pub fn update_price(&mut self, new_price: f64, is_buy: bool) { 75 | self.current_token_price = new_price; 76 | 77 | // Update max price if current price is higher 78 | if new_price > self.max_token_price { 79 | self.max_token_price = new_price; 80 | } 81 | 82 | // Update transaction counts 83 | if is_buy { 84 | self.buy_tx_num += 1; 85 | } else { 86 | self.sell_tx_num += 1; 87 | } 88 | 89 | // Update market cap based on new price 90 | if let Some(mc) = self.market_cap.as_mut() { 91 | *mc = new_price * (self.total_supply as f64); 92 | } 93 | 94 | // Update volume metrics 95 | if let Some(vol) = self.volume.as_mut() { 96 | *vol += new_price; 97 | } 98 | } 99 | 100 | /// Calculate price delta percentage from max price 101 | pub fn price_delta_percent(&self) -> f64 { 102 | if self.max_token_price == 0.0 { 103 | return 0.0; 104 | } 105 | 106 | ((self.max_token_price - self.current_token_price) / self.max_token_price) * 100.0 107 | } 108 | 109 | /// Check if token has been active for longer than the specified duration 110 | pub fn exceeds_time_threshold(&self, threshold: Duration) -> bool { 111 | self.token_mint_timestamp.elapsed() > threshold 112 | } 113 | 114 | /// Convert to a telegram-compatible TokenInfo 115 | pub fn to_telegram_token_info(&self) -> crate::services::telegram::TokenInfo { 116 | // Calculate buy/sell ratio 117 | let total_tx = self.buy_tx_num + self.sell_tx_num; 118 | let buy_ratio = if total_tx > 0 { 119 | Some((self.buy_tx_num as f64 / total_tx as f64 * 100.0) as i32) 120 | } else { 121 | None 122 | }; 123 | 124 | let sell_ratio = if total_tx > 0 { 125 | Some((self.sell_tx_num as f64 / total_tx as f64 * 100.0) as i32) 126 | } else { 127 | None 128 | }; 129 | 130 | // Estimate volume buy/sell based on ratio 131 | let volume_value = self.volume.unwrap_or(0.0); 132 | let volume_buy = if let Some(ratio) = buy_ratio { 133 | Some(volume_value * (ratio as f64 / 100.0)) 134 | } else { 135 | None 136 | }; 137 | 138 | let volume_sell = if let Some(ratio) = sell_ratio { 139 | Some(volume_value * (ratio as f64 / 100.0)) 140 | } else { 141 | None 142 | }; 143 | 144 | // Token price relative to initial price (simplified as 1.0x for now) 145 | // In a real implementation, you'd compare to some base price 146 | let token_price = Some(1.0); // Placeholder, should be calculated from real data 147 | 148 | // Get SOL balance from launcher_sol_balance 149 | let sol_balance = self.launcher_sol_balance; 150 | 151 | crate::services::telegram::TokenInfo { 152 | address: self.token_mint.clone(), 153 | name: self.token_name.clone(), 154 | symbol: self.token_symbol.clone(), 155 | market_cap: self.market_cap, 156 | volume: self.volume, 157 | buy_sell_count: Some((self.buy_tx_num + self.sell_tx_num) as i32), 158 | dev_buy_amount: self.dev_buy_amount, 159 | launcher_sol_balance: self.launcher_sol_balance, 160 | bundle_check: self.bundle_check, 161 | // ATH is equivalent to max_token_price in this context 162 | ath: Some(self.max_token_price), 163 | // Add new fields from screenshot 164 | buy_ratio, 165 | sell_ratio, 166 | total_buy_sell: Some(total_tx as i32), 167 | volume_buy, 168 | volume_sell, 169 | token_price, 170 | dev_wallet: self.dev_wallet.clone(), 171 | sol_balance, 172 | } 173 | } 174 | 175 | // Manual implementation of clone since JoinHandle doesn't implement Clone 176 | pub fn clone_without_task(&self) -> Self { 177 | Self { 178 | token_mint: self.token_mint.clone(), 179 | token_name: self.token_name.clone(), 180 | token_symbol: self.token_symbol.clone(), 181 | current_token_price: self.current_token_price, 182 | max_token_price: self.max_token_price, 183 | initial_price: self.initial_price, 184 | total_supply: self.total_supply, 185 | buy_tx_num: self.buy_tx_num, 186 | sell_tx_num: self.sell_tx_num, 187 | token_mint_timestamp: self.token_mint_timestamp, 188 | launcher_sol_balance: self.launcher_sol_balance, 189 | market_cap: self.market_cap, 190 | volume: self.volume, 191 | dev_buy_amount: self.dev_buy_amount, 192 | bundle_check: self.bundle_check, 193 | active_task: None, // Don't clone the task 194 | is_monitored: self.is_monitored, 195 | dev_wallet: self.dev_wallet.clone(), // Include the dev wallet in the clone 196 | token_amount: self.token_amount, 197 | } 198 | } 199 | } 200 | 201 | #[derive(Debug, Clone)] 202 | pub struct TokenFilter { 203 | pub min_launcher_sol: f64, 204 | pub max_launcher_sol: f64, 205 | pub min_market_cap: f64, 206 | pub max_market_cap: f64, 207 | pub min_volume: f64, 208 | pub max_volume: f64, 209 | pub min_buy_sell_count: u32, 210 | pub max_buy_sell_count: u32, 211 | pub price_delta_threshold: f64, 212 | pub time_delta_threshold: u64, 213 | } 214 | 215 | impl Default for TokenFilter { 216 | fn default() -> Self { 217 | Self { 218 | min_launcher_sol: 0.1, 219 | max_launcher_sol: 100.0, 220 | min_market_cap: 0.1, 221 | max_market_cap: 1_000_000.0, 222 | min_volume: 0.1, 223 | max_volume: 1_000_000.0, 224 | min_buy_sell_count: 1, 225 | max_buy_sell_count: 1000, 226 | price_delta_threshold: 10.0, 227 | time_delta_threshold: 3600, 228 | } 229 | } 230 | } 231 | 232 | #[derive(Debug, Clone)] 233 | pub struct TrackedTokenInfo { 234 | pub token_mint: String, 235 | pub token_name: Option, 236 | pub token_symbol: Option, 237 | pub price: f64, 238 | pub market_cap: f64, 239 | pub volume: f64, 240 | pub buy_count: u32, 241 | pub sell_count: u32, 242 | pub last_updated: Instant, 243 | pub first_seen: Instant, 244 | pub price_history: Vec<(Instant, f64)>, 245 | pub highest_price: f64, 246 | pub lowest_price: f64, 247 | pub launcher_sol: f64, 248 | } 249 | 250 | impl TrackedTokenInfo { 251 | pub fn new(token_mint: String, token_name: Option, token_symbol: Option, price: f64, launcher_sol: f64) -> Self { 252 | let now = Instant::now(); 253 | Self { 254 | token_mint, 255 | token_name, 256 | token_symbol, 257 | price, 258 | market_cap: price, 259 | volume: price, 260 | buy_count: 1, 261 | sell_count: 0, 262 | last_updated: now, 263 | first_seen: now, 264 | price_history: vec![(now, price)], 265 | highest_price: price, 266 | lowest_price: price, 267 | launcher_sol, 268 | } 269 | } 270 | 271 | pub fn update_price(&mut self, new_price: f64) { 272 | let now = Instant::now(); 273 | self.price = new_price; 274 | self.last_updated = now; 275 | self.price_history.push((now, new_price)); 276 | 277 | if new_price > self.highest_price { 278 | self.highest_price = new_price; 279 | } 280 | 281 | if new_price < self.lowest_price { 282 | self.lowest_price = new_price; 283 | } 284 | 285 | // Keep only the last 100 price points 286 | if self.price_history.len() > 100 { 287 | self.price_history.remove(0); 288 | } 289 | } 290 | 291 | pub fn record_buy(&mut self, amount: f64) { 292 | self.buy_count += 1; 293 | self.volume += amount; 294 | } 295 | 296 | pub fn record_sell(&mut self, amount: f64) { 297 | self.sell_count += 1; 298 | self.volume += amount; 299 | } 300 | 301 | pub fn calculate_price_change(&self, duration: Duration) -> Option { 302 | let now = Instant::now(); 303 | let threshold_time = now.checked_sub(duration)?; 304 | 305 | // Find the oldest price point within the specified duration 306 | let oldest_price_in_duration = self.price_history.iter() 307 | .find(|(time, _)| *time >= threshold_time) 308 | .map(|(_, price)| *price); 309 | 310 | match oldest_price_in_duration { 311 | Some(old_price) => { 312 | if old_price == 0.0 { 313 | return Some(0.0); 314 | } 315 | Some((self.price - old_price) / old_price * 100.0) 316 | }, 317 | None => None, 318 | } 319 | } 320 | 321 | pub fn passes_filter(&self, filter: &TokenFilter) -> bool { 322 | // Check launcher SOL bounds 323 | if self.launcher_sol < filter.min_launcher_sol || self.launcher_sol > filter.max_launcher_sol { 324 | return false; 325 | } 326 | 327 | // Check market cap bounds 328 | if self.market_cap < filter.min_market_cap || self.market_cap > filter.max_market_cap { 329 | return false; 330 | } 331 | 332 | // Check volume bounds 333 | if self.volume < filter.min_volume || self.volume > filter.max_volume { 334 | return false; 335 | } 336 | 337 | // Check transaction count bounds 338 | let total_transactions = self.buy_count + self.sell_count; 339 | if total_transactions < filter.min_buy_sell_count || total_transactions > filter.max_buy_sell_count { 340 | return false; 341 | } 342 | 343 | // Check price delta if we have enough history 344 | if let Some(price_change) = self.calculate_price_change(Duration::from_secs(filter.time_delta_threshold)) { 345 | if price_change.abs() < filter.price_delta_threshold { 346 | return false; 347 | } 348 | } 349 | 350 | true 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/error/mod.rs: -------------------------------------------------------------------------------- 1 | //! Error types for the Pump.fun SDK. 2 | //! 3 | //! This module defines the `ClientError` enum, which encompasses various error types that can occur when interacting with the Pump.fun program. 4 | //! It includes specific error cases for bonding curve operations, metadata uploads, Solana client errors, and more. 5 | //! 6 | //! The `ClientError` enum provides a comprehensive set of error types to help developers handle and debug issues that may arise during interactions with the Pump.fun program. 7 | //! 8 | //! # Error Types 9 | //! 10 | //! - `BondingCurveNotFound`: The bonding curve account was not found. 11 | //! - `BondingCurveError`: An error occurred while interacting with the bonding curve. 12 | //! - `BorshError`: An error occurred while serializing or deserializing data using Borsh. 13 | //! - `SolanaClientError`: An error occurred while interacting with the Solana RPC client. 14 | //! - `UploadMetadataError`: An error occurred while uploading metadata to IPFS. 15 | //! - `InvalidInput`: Invalid input parameters were provided. 16 | //! - `InsufficientFunds`: Insufficient funds for a transaction. 17 | //! - `SimulationError`: Transaction simulation failed. 18 | //! - `RateLimitExceeded`: Rate limit exceeded. 19 | 20 | use serde_json::Error; 21 | use anchor_client::solana_client::{ 22 | client_error::ClientError as SolanaClientError, pubsub_client::PubsubClientError, 23 | }; 24 | use anchor_client::solana_sdk::pubkey::ParsePubkeyError; 25 | 26 | // Define our own Error type alias to avoid confusion with the imported Error 27 | pub type ErrorType = ClientError; 28 | 29 | // #[derive(Debug)] 30 | // #[allow(dead_code)] 31 | // pub struct AppError(anyhow::Error); 32 | 33 | // impl From for AppError 34 | // where 35 | // E: Into, 36 | // { 37 | // fn from(err: E) -> Self { 38 | // Self(err.into()) 39 | // } 40 | // } 41 | 42 | #[derive(Debug)] 43 | pub enum ClientError { 44 | /// Bonding curve account was not found 45 | BondingCurveNotFound, 46 | /// Error related to bonding curve operations 47 | BondingCurveError(&'static str), 48 | /// Error deserializing data using Borsh 49 | BorshError(std::io::Error), 50 | /// Error from Solana RPC client 51 | SolanaClientError(anchor_client::solana_client::client_error::ClientError), 52 | /// Error uploading metadata 53 | UploadMetadataError(Box), 54 | /// Invalid input parameters 55 | InvalidInput(&'static str), 56 | /// Insufficient funds for transaction 57 | InsufficientFunds, 58 | /// Transaction simulation failed 59 | SimulationError(String), 60 | /// Rate limit exceeded 61 | RateLimitExceeded, 62 | 63 | OrderLimitExceeded, 64 | 65 | ExternalService(String), 66 | 67 | Redis(String, String), 68 | 69 | Solana(String, String), 70 | 71 | Parse(String, String), 72 | 73 | Pubkey(String, String), 74 | 75 | Jito(String, String), 76 | 77 | Join(String), 78 | 79 | Subscribe(String, String), 80 | 81 | Send(String, String), 82 | 83 | Other(String), 84 | 85 | InvalidData(String), 86 | 87 | PumpFunBuy(String), 88 | 89 | PumpFunSell(String), 90 | 91 | Timeout(String, String), 92 | 93 | Duplicate(String), 94 | 95 | InvalidEventType, 96 | 97 | ChannelClosed, 98 | } 99 | 100 | impl std::fmt::Display for ClientError { 101 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 102 | match self { 103 | Self::BondingCurveNotFound => write!(f, "Bonding curve not found"), 104 | Self::BondingCurveError(msg) => write!(f, "Bonding curve error: {}", msg), 105 | Self::BorshError(err) => write!(f, "Borsh serialization error: {}", err), 106 | Self::SolanaClientError(err) => write!(f, "Solana client error: {}", err), 107 | Self::UploadMetadataError(err) => write!(f, "Metadata upload error: {}", err), 108 | Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), 109 | Self::InsufficientFunds => write!(f, "Insufficient funds for transaction"), 110 | Self::SimulationError(msg) => write!(f, "Transaction simulation failed: {}", msg), 111 | Self::ExternalService(msg) => write!(f, "External service error: {}", msg), 112 | Self::RateLimitExceeded => write!(f, "Rate limit exceeded"), 113 | Self::OrderLimitExceeded => write!(f, "Order limit exceeded"), 114 | Self::Solana(msg, details) => write!(f, "Solana error: {}, details: {}", msg, details), 115 | Self::Parse(msg, details) => write!(f, "Parse error: {}, details: {}", msg, details), 116 | Self::Jito(msg, details) => write!(f, "Jito error: {}, details: {}", msg, details), 117 | Self::Redis(msg, details) => write!(f, "Redis error: {}, details: {}", msg, details), 118 | Self::Join(msg) => write!(f, "Task join error: {}", msg), 119 | Self::Pubkey(msg, details) => write!(f, "Pubkey error: {}, details: {}", msg, details), 120 | Self::Subscribe(msg, details) => { 121 | write!(f, "Subscribe error: {}, details: {}", msg, details) 122 | } 123 | Self::Send(msg, details) => write!(f, "Send error: {}, details: {}", msg, details), 124 | Self::Other(msg) => write!(f, "Other error: {}", msg), 125 | Self::PumpFunBuy(msg) => write!(f, "PumpFun buy error: {}", msg), 126 | Self::PumpFunSell(msg) => write!(f, "PumpFun sell error: {}", msg), 127 | Self::InvalidData(msg) => write!(f, "Invalid data: {}", msg), 128 | Self::Timeout(msg, details) => { 129 | write!(f, "Operation timed out: {}, details: {}", msg, details) 130 | } 131 | Self::Duplicate(msg) => write!(f, "Duplicate event: {}", msg), 132 | Self::InvalidEventType => write!(f, "Invalid event type"), 133 | Self::ChannelClosed => write!(f, "Channel closed"), 134 | } 135 | } 136 | } 137 | impl std::error::Error for ClientError { 138 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 139 | match self { 140 | Self::BorshError(err) => Some(err), 141 | Self::SolanaClientError(err) => Some(err), 142 | Self::UploadMetadataError(err) => Some(err.as_ref()), 143 | Self::ExternalService(_) => None, 144 | Self::Redis(_, _) => None, 145 | Self::Solana(_, _) => None, 146 | Self::Parse(_, _) => None, 147 | Self::Jito(_, _) => None, 148 | Self::Join(_) => None, 149 | Self::Pubkey(_, _) => None, 150 | Self::Subscribe(_, _) => None, 151 | Self::Send(_, _) => None, 152 | Self::Other(_) => None, 153 | Self::PumpFunBuy(_) => None, 154 | Self::PumpFunSell(_) => None, 155 | Self::Timeout(_, _) => None, 156 | Self::Duplicate(_) => None, 157 | Self::InvalidEventType => None, 158 | Self::ChannelClosed => None, 159 | _ => None, 160 | } 161 | } 162 | } 163 | 164 | impl From for ClientError { 165 | fn from(error: SolanaClientError) -> Self { 166 | ClientError::Solana("Solana client error".to_string(), error.to_string()) 167 | } 168 | } 169 | 170 | impl From for ClientError { 171 | fn from(error: PubsubClientError) -> Self { 172 | ClientError::Solana("PubSub client error".to_string(), error.to_string()) 173 | } 174 | } 175 | 176 | impl From for ClientError { 177 | fn from(error: ParsePubkeyError) -> Self { 178 | ClientError::Pubkey("Pubkey error".to_string(), error.to_string()) 179 | } 180 | } 181 | 182 | impl From for ClientError { 183 | fn from(err: Error) -> Self { 184 | ClientError::Parse("JSON serialization error".to_string(), err.to_string()) 185 | } 186 | } 187 | 188 | pub type ClientResult = Result; 189 | -------------------------------------------------------------------------------- /src/fix_test.rs: -------------------------------------------------------------------------------- 1 | fn main() { println!("Hello, world!"); } 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Solana VNTR Sniper Crate 2 | // Add the recursion limit to handle the TokenListManager 3 | #![recursion_limit = "256"] 4 | 5 | pub mod common; 6 | pub mod core; 7 | pub mod dex; 8 | pub mod engine; 9 | pub mod error; 10 | pub mod services; 11 | // pub mod enhanced_token_trader; // Removed - consolidated into engine/enhanced_token_trader.rs 12 | pub mod tests; 13 | 14 | pub use engine::monitor::new_token_trader_pumpfun; 15 | pub use engine::enhanced_token_trader::start as start_enhanced_trading_system; 16 | pub use error::ErrorType as Error; 17 | 18 | // No duplicate module declaration needed 19 | -------------------------------------------------------------------------------- /src/services/jito.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use indicatif::{ProgressBar, ProgressStyle}; 3 | use rand::{seq::IteratorRandom, thread_rng}; 4 | use serde::Deserialize; 5 | use serde_json::Value; 6 | use anchor_client::solana_sdk::pubkey::Pubkey; 7 | use std::{future::Future, str::FromStr, sync::LazyLock, time::Duration}; 8 | use tokio::time::{sleep, Instant}; 9 | 10 | use crate::common::config::import_env_var; 11 | 12 | pub static BLOCK_ENGINE_URL: LazyLock = 13 | LazyLock::new(|| import_env_var("JITO_BLOCK_ENGINE_URL")); 14 | 15 | pub fn get_tip_account() -> Result<(Pubkey, Pubkey)> { 16 | let accounts = [ 17 | "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5".to_string(), 18 | "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt".to_string(), 19 | "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL".to_string(), 20 | "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT".to_string(), 21 | "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe".to_string(), 22 | "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh".to_string(), 23 | "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49".to_string(), 24 | 25 | ]; 26 | let mut rng = thread_rng(); 27 | let tip_account = match accounts.iter().choose(&mut rng) { 28 | Some(acc) => Ok(Pubkey::from_str(acc).inspect_err(|err| { 29 | println!("jito: failed to parse Pubkey: {:?}", err); 30 | })?), 31 | None => Err(anyhow!("jito: no tip accounts available")), 32 | }; 33 | let tip1_account = Pubkey::from_str("JitoFSvbiCrygnx4HZzau4LdeyBU6VUeyf9jt8F8bMk") 34 | .inspect_err(|err| { 35 | println!("jito: failed to parse Pubkey: {:?}", err); 36 | })?; 37 | let tip_account = tip_account?; 38 | Ok((tip_account, tip1_account)) 39 | } 40 | // unit sol 41 | pub async fn get_tip_value() -> Result { 42 | // If TIP_VALUE is set, use it 43 | if let Ok(tip_value) = std::env::var("JITO_TIP_VALUE") { 44 | match f64::from_str(&tip_value) { 45 | Ok(value) => Ok(value), 46 | Err(_) => { 47 | println!( 48 | "Invalid JITO_TIP_VALUE in environment variable: '{}'. Falling back to percentile calculation.", 49 | tip_value 50 | ); 51 | Err(anyhow!("Invalid TIP_VALUE in environment variable")) 52 | } 53 | } 54 | } else { 55 | Err(anyhow!("JITO_TIP_VALUE environment variable not set")) 56 | } 57 | } 58 | 59 | pub async fn get_priority_fee() -> Result { 60 | // If TIP_VALUE is set, use it 61 | if let Ok(priority_fee) = std::env::var("JITO_PRIORITY_FEE") { 62 | match f64::from_str(&priority_fee) { 63 | Ok(value) => Ok(value), 64 | Err(_) => { 65 | println!( 66 | "Invalid JITO_PRIORITY_FEE in environment variable: '{}'. Falling back to percentile calculation.", 67 | priority_fee 68 | ); 69 | Err(anyhow!("Invalid JITO_PRIORITY_FEE in environment variable")) 70 | } 71 | } 72 | } else { 73 | Err(anyhow!("JITO_PRIORITY_FEE environment variable not set")) 74 | } 75 | } 76 | 77 | #[derive(Deserialize, Debug)] 78 | pub struct BundleStatus { 79 | pub bundle_id: String, 80 | pub transactions: Vec, 81 | pub slot: u64, 82 | pub confirmation_status: String, 83 | pub err: ErrorStatus, 84 | } 85 | #[derive(Deserialize, Debug)] 86 | pub struct ErrorStatus { 87 | #[serde(rename = "Ok")] 88 | pub ok: Option<()>, 89 | } 90 | 91 | pub async fn wait_for_bundle_confirmation( 92 | fetch_statuses: F, 93 | bundle_id: String, 94 | interval: Duration, 95 | timeout: Duration, 96 | ) -> Result> 97 | where 98 | F: Fn(String) -> Fut, 99 | Fut: Future>>, 100 | { 101 | let progress_bar = new_progress_bar(); 102 | let start_time = Instant::now(); 103 | 104 | loop { 105 | let statuses = fetch_statuses(bundle_id.clone()).await?; 106 | 107 | if let Some(status) = statuses.first() { 108 | let bundle_status: BundleStatus = 109 | serde_json::from_value(status.clone()).inspect_err(|err| { 110 | println!( 111 | "Failed to parse JSON when get_bundle_statuses, err: {}", 112 | err, 113 | ); 114 | })?; 115 | 116 | println!("{:?}", bundle_status); 117 | match bundle_status.confirmation_status.as_str() { 118 | "finalized" | "confirmed" => { 119 | progress_bar.finish_and_clear(); 120 | println!( 121 | "Finalized bundle {}: {}", 122 | bundle_id, bundle_status.confirmation_status 123 | ); 124 | // // print tx 125 | // bundle_status 126 | // .transactions 127 | // .iter() 128 | // .for_each(|tx| println!("https://solscan.io/tx/{}", tx)); 129 | return Ok(bundle_status.transactions[0..2].to_vec()); 130 | } 131 | _ => { 132 | progress_bar.set_message(format!( 133 | "Finalizing bundle {}: {}", 134 | bundle_id, bundle_status.confirmation_status 135 | )); 136 | } 137 | } 138 | } else { 139 | progress_bar.set_message(format!("Finalizing bundle {}: {}", bundle_id, "None")); 140 | } 141 | 142 | // check loop exceeded 1 minute, 143 | if start_time.elapsed() > timeout { 144 | println!("Loop exceeded {:?}, breaking out.", timeout); 145 | return Err(anyhow!("Bundle status get timeout")); 146 | } 147 | 148 | // Wait for a certain duration before retrying 149 | sleep(interval).await; 150 | } 151 | } 152 | pub fn new_progress_bar() -> ProgressBar { 153 | let progress_bar = ProgressBar::new(42); 154 | progress_bar.set_style( 155 | ProgressStyle::default_spinner() 156 | .template("{spinner:.green} {wide_msg}") 157 | .expect("ProgressStyle::template direct input to be correct"), 158 | ); 159 | progress_bar.enable_steady_tick(Duration::from_millis(100)); 160 | progress_bar 161 | } 162 | 163 | use crate::error::ClientError; 164 | use bincode; 165 | use bs64; 166 | use reqwest; 167 | use serde_json::json; 168 | use anchor_client::solana_sdk::{ 169 | commitment_config::CommitmentConfig, signature::Signature, transaction::Transaction, 170 | }; 171 | 172 | pub const MAX_RETRIES: u8 = 3; 173 | pub const RETRY_DELAY: Duration = Duration::from_millis(200); 174 | 175 | #[derive(Debug, Clone)] 176 | pub struct TransactionConfig { 177 | pub skip_preflight: bool, 178 | pub preflight_commitment: CommitmentConfig, 179 | pub encoding: String, 180 | pub last_n_blocks: u64, 181 | } 182 | 183 | impl Default for TransactionConfig { 184 | fn default() -> Self { 185 | Self { 186 | skip_preflight: true, 187 | preflight_commitment: CommitmentConfig::confirmed(), 188 | encoding: "base64".to_string(), 189 | last_n_blocks: 100, 190 | } 191 | } 192 | } 193 | 194 | #[derive(Clone, Debug)] 195 | pub struct JitoClient { 196 | endpoint: String, 197 | client: reqwest::Client, 198 | config: TransactionConfig, 199 | } 200 | 201 | impl JitoClient { 202 | pub fn new(endpoint: &str) -> Self { 203 | Self { 204 | endpoint: endpoint.to_string(), 205 | client: reqwest::Client::new(), 206 | config: TransactionConfig::default(), 207 | } 208 | } 209 | 210 | pub fn new(endpoint : &str) -> self { 211 | 212 | } 213 | 214 | pub async fn send_transaction( 215 | &self, 216 | transaction: &Transaction, 217 | ) -> Result { 218 | let wire_transaction = bincode::serialize(transaction).map_err(|e| { 219 | ClientError::Parse( 220 | "Transaction serialization failed".to_string(), 221 | e.to_string(), 222 | ) 223 | })?; 224 | 225 | let encoded_tx = &bs64::encode(&wire_transaction); 226 | 227 | for retry in 0..MAX_RETRIES { 228 | match self.try_send_transaction(encoded_tx).await { 229 | Ok(signature) => { 230 | return Signature::from_str(&signature).map_err(|e| { 231 | ClientError::Parse("Invalid signature".to_string(), e.to_string()) 232 | }); 233 | } 234 | Err(e) => { 235 | println!("Retry {} failed: {:?}", retry, e); 236 | if retry == MAX_RETRIES - 1 { 237 | return Err(e); 238 | } 239 | // tokio::time::sleep(RETRY_DELAY).await; 240 | } 241 | } 242 | } 243 | 244 | Err(ClientError::Other("Max retries exceeded".to_string())) 245 | } 246 | 247 | async fn try_send_transaction(&self, encoded_tx: &str) -> Result { 248 | let params = json!([ 249 | encoded_tx, 250 | { 251 | "skipPreflight": self.config.skip_preflight, 252 | "preflightCommitment": self.config.preflight_commitment.commitment, 253 | "encoding": self.config.encoding, 254 | "maxRetries": MAX_RETRIES, 255 | "minContextSlot": null 256 | } 257 | ]); 258 | 259 | let response = self.send_request("sendTransaction", params).await?; 260 | 261 | response["result"] 262 | .as_str() 263 | .map(|s| s.to_string()) 264 | .ok_or_else(|| { 265 | ClientError::Parse( 266 | "Invalid response format".to_string(), 267 | "Missing result field".to_string(), 268 | ) 269 | }) 270 | } 271 | 272 | async fn send_request(&self, method: &str, params: Value) -> Result { 273 | let request_body = json!({ 274 | "jsonrpc": "2.0", 275 | "id": 1, 276 | "method": method, 277 | "params": params 278 | }); 279 | 280 | let response = self 281 | .client 282 | .post(&self.endpoint) 283 | .header("Content-Type", "application/json") 284 | .json(&request_body) 285 | .send() 286 | .await 287 | .map_err(|e| ClientError::Solana("Request failed".to_string(), e.to_string()))?; 288 | 289 | let response_data: Value = response 290 | .json() 291 | .await 292 | .map_err(|e| ClientError::Parse("Invalid JSON response".to_string(), e.to_string()))?; 293 | 294 | if let Some(error) = response_data.get("error") { 295 | return Err(ClientError::Solana( 296 | "RPC error".to_string(), 297 | error.to_string(), 298 | )); 299 | } 300 | 301 | Ok(response_data) 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod jito; 2 | pub mod nozomi; 3 | pub mod zeroslot; 4 | pub mod telegram; 5 | -------------------------------------------------------------------------------- /src/services/nozomi.rs: -------------------------------------------------------------------------------- 1 | use crate::common::config::import_env_var; 2 | use anyhow::{anyhow, Result}; 3 | use rand::{seq::IteratorRandom, thread_rng}; 4 | use anchor_client::solana_sdk::pubkey::Pubkey; 5 | use std::{str::FromStr, sync::LazyLock}; 6 | usd std::{str::new_price, sync::LazyLock} 7 | 8 | pub static NOZOMI_URL: LazyLock = LazyLock::new(|| import_env_var("NOZOMI_URL")); 9 | -------------------------------------------------------------------------------- /src/services/zeroslot.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ClientError; 2 | use anyhow::{anyhow, Result}; 3 | use rand::{seq::IteratorRandom, thread_rng}; 4 | use serde_json::{json, Value}; 5 | use anchor_client::solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction}; 6 | use std::{str::FromStr, sync::LazyLock}; 7 | 8 | use crate::common::config::import_env_var; 9 | 10 | pub static ZERO_SLOT_URL: LazyLock = LazyLock::new(|| import_env_var("ZERO_SLOT_URL")); 11 | -------------------------------------------------------------------------------- /src/tests/dev_wallet_test.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use crate::engine::token_tracker::{TokenTracker, StreamEvent}; 3 | use crate::common::logger::Logger; 4 | use crate::services::telegram::TelegramService; 5 | use colored::Colorize; 6 | use tokio::time::Duration; 7 | use std::env; 8 | 9 | /// Runs a test of the dev wallet identification and notification deduplication features 10 | /// 11 | /// This function will: 12 | /// 1. Create a token tracker 13 | /// 2. Add a test token to the tracker 14 | /// 3. Simulate transactions for the token 15 | /// 4. Try to identify the dev wallet 16 | /// 5. Send a notification via Telegram (if credentials are available) 17 | /// 6. Test notification deduplication by trying to send another notification for the same token 18 | pub async fn run_dev_wallet_test() -> Result<(), Box> { 19 | // Initialize logger 20 | let logger = Logger::new("[TEST DEV WALLET] => ".green().bold().to_string()); 21 | logger.log("Starting test script for dev wallet identification".to_string()); 22 | 23 | // Load environment variables for Telegram 24 | let telegram_bot_token = env::var("TELEGRAM_BOT_TOKEN").unwrap_or_default(); 25 | let telegram_chat_id = env::var("TELEGRAM_CHAT_ID").unwrap_or_default(); 26 | 27 | // Initialize Telegram service if credentials are provided 28 | let telegram_service = if !telegram_bot_token.is_empty() && !telegram_chat_id.is_empty() { 29 | let service = TelegramService::new(telegram_bot_token.clone(), telegram_chat_id.clone(), 1); 30 | Some(Arc::new(service)) 31 | } else { 32 | logger.log("Telegram credentials not provided, notifications will be disabled".red().to_string()); 33 | None 34 | }; 35 | 36 | // Initialize token tracker with default parameters 37 | let token_tracker = Arc::new(TokenTracker::new( 38 | logger.clone(), 39 | 10.0, // price_check_threshold 40 | 3600, // time_threshold_secs 41 | 1.0, // min_market_cap 42 | 1000.0, // max_market_cap 43 | 1.0, // min_volume 44 | 1000.0, // max_volume 45 | 5, // min_buy_sell_count 46 | 1000, // max_buy_sell_count 47 | 0.1, // min_launcher_sol_balance 48 | 100.0 // max_launcher_sol_balance 49 | )); 50 | 51 | // Create a simulated token mint event 52 | let test_token_mint = format!("TestToken{}", chrono::Utc::now().timestamp()); 53 | let _stream_event = StreamEvent::TokenMint { 54 | token_mint: test_token_mint.clone(), 55 | dev_buy_amount: 10.0, // Dev bought 10 SOL worth 56 | launcher_sol_balance: 5.0, // Launcher has 5 SOL 57 | token_price: 0.001, // Initial token price 58 | bundle_check: true, // Bundle check passed 59 | }; 60 | 61 | logger.log(format!("Simulating token mint event for token: {}", test_token_mint).cyan().to_string()); 62 | 63 | // Add the token to the tracker 64 | let _token = token_tracker.add_token( 65 | test_token_mint.clone(), 66 | 0.001, // Initial price 67 | 5.0, // Launcher SOL 68 | Some(10.0), // Dev buy amount 69 | Some(5.0), // Launcher SOL balance 70 | Some(true), // Bundle check 71 | Some("TEST".to_string()), // Token name 72 | Some("TST".to_string()), // Token symbol 73 | ).await?; 74 | 75 | logger.log("Token added to tracker".green().to_string()); 76 | 77 | // Wait a moment for processing 78 | tokio::time::sleep(Duration::from_secs(1)).await; 79 | 80 | // Simulate token transactions to increase buy/sell count 81 | for i in 0..10 { 82 | let is_buy = i % 3 != 0; // 2/3 are buys, 1/3 are sells 83 | let amount = if is_buy { 0.5 } else { 0.3 }; 84 | 85 | logger.log(format!( 86 | "Simulating {} transaction #{} for {} SOL", 87 | if is_buy { "BUY" } else { "SELL" }, 88 | i + 1, 89 | amount 90 | ).cyan().to_string()); 91 | 92 | token_tracker.update_token( 93 | &test_token_mint, 94 | amount, 95 | is_buy 96 | ).await?; 97 | 98 | // Short delay between transactions 99 | tokio::time::sleep(Duration::from_millis(200)).await; 100 | } 101 | 102 | // Get the token information from the tracker 103 | if let Some(token_info) = token_tracker.get_token(&test_token_mint).await { 104 | logger.log(format!( 105 | "Token {} has {} buys and {} sells", 106 | test_token_mint, 107 | token_info.buy_count, 108 | token_info.sell_count 109 | ).green().to_string()); 110 | 111 | // Get extended token info 112 | let extended_tokens = token_tracker.get_extended_tokens_map_arc(); 113 | let extended_token_info = { 114 | let guard = extended_tokens.lock().unwrap(); 115 | guard.get(&test_token_mint).map(|token| token.clone_without_task()) 116 | }; 117 | 118 | if let Some(ext_info) = extended_token_info { 119 | logger.log(format!( 120 | "Extended token info: dev wallet = {}", 121 | ext_info.dev_wallet.as_ref().unwrap_or(&"Not identified".to_string()) 122 | ).green().to_string()); 123 | 124 | // Send notification via Telegram if available 125 | if let Some(telegram) = &telegram_service { 126 | logger.log("Sending Telegram notification...".to_string()); 127 | 128 | let telegram_token_info = ext_info.to_telegram_token_info(); 129 | 130 | match telegram.send_token_notification(&telegram_token_info).await { 131 | Ok(_) => logger.log("Telegram notification sent successfully".green().to_string()), 132 | Err(e) => logger.log(format!("Failed to send Telegram notification: {}", e).red().to_string()), 133 | }; 134 | 135 | // Wait a moment for the notification to be processed 136 | tokio::time::sleep(Duration::from_secs(2)).await; 137 | 138 | // Try to send another notification for the same token - this should be prevented 139 | logger.log("Attempting to send another notification for the same token...".yellow().to_string()); 140 | match telegram.send_token_notification(&telegram_token_info).await { 141 | Ok(_) => logger.log("Second notification sent - duplicate prevention failed!".red().to_string()), 142 | Err(e) => logger.log(format!("Second notification properly blocked: {}", e).green().to_string()), 143 | }; 144 | } 145 | } else { 146 | logger.log("Extended token info not found".red().to_string()); 147 | } 148 | } else { 149 | logger.log("Token not found in tracker".red().to_string()); 150 | } 151 | 152 | logger.log("Test script completed".green().to_string()); 153 | Ok(()) 154 | } -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dev_wallet_test; 2 | 3 | // Export test functions if needed 4 | pub use dev_wallet_test::run_dev_wallet_test; --------------------------------------------------------------------------------