├── .env_example ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── black_list.json ├── readme.md ├── src ├── cache.rs ├── client.rs ├── config.rs ├── constants.rs ├── engine.rs ├── jito.rs ├── lib.rs ├── main.rs ├── pumpfun.rs ├── types.rs └── utils.rs └── white_list.json /.env_example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY= 2 | 3 | RPC_URL= 4 | GRPC_URL= 5 | JITO_BLOCK_ENGINE_URL= 6 | # 本机的话就不需要修改 7 | # 如果是远程服务器,需要修改 8 | # Redis url 9 | REDIS_URL=redis://127.0.0.1/ 10 | 11 | UNIT_PRICE=20000 12 | UNIT_LIMIT=200000 13 | 14 | # 基础设置 15 | # Base settings 16 | PRICE_INCREASE_BUY_LOWER=0.05 17 | PRICE_INCREASE_BUY_UPPER=0.10 18 | # 流动性数量,按sol个数来 19 | # Market cap in SOL 20 | MARKET_CAP=30.0 21 | 22 | # 策略,STRATEGY 或 SNIPER 23 | # Strategy or Sniper 24 | MODE=STRATEGY 25 | 26 | # 白名单设置 27 | # White list 28 | WHITE_DECREASE_SELL=-0.05 29 | WHITE_INCREASE_SELL=0.2 30 | WHITE_BUY_AMOUNT=10000 31 | WHITE_SLIPPAGE_BPS=1000 32 | WHITE_USE_JITO=true 33 | WHITE_TIP=0.0001 34 | WHITE_MAX_HOLD_TIME=6000 35 | WHITE_MAX_RETRIES=3 36 | 37 | # 普通设置 38 | # Normal 39 | USUAL_DECREASE_SELL=-0.1 40 | USUAL_INCREASE_SELL=0.2 41 | USUAL_BUY_AMOUNT=10000 42 | USUAL_SLIPPAGE_BPS=1000 43 | USUAL_USE_JITO=true 44 | USUAL_TIP=0.0001 45 | USUAL_MAX_HOLD_TIME=6000 46 | USUAL_MAX_RETRIES=3 47 | 48 | TOP10_PERCENT=0.4 49 | SOL_THRESHOLD=40.0 50 | TX_VOLUME=100.0 51 | 52 | # 黑白名单位置 53 | # Black and white list 54 | BLACK_LIST_PATH="./black_list.json" 55 | WHITE_LIST_PATH="./white_list.json" 56 | 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.env -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "new_bot" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | 7 | 8 | [dependencies] 9 | # ------------------------ 10 | # 异常处理与错误管理 11 | # ------------------------ 12 | 13 | # 强大的错误处理库,提供灵活的错误类型和上下文信息 14 | anyhow = "1.0.95" 15 | 16 | 17 | # ------------------------ 18 | # 数据序列化与反序列化 19 | # ------------------------ 20 | 21 | # 序列化和反序列化支持,常用于 JSON、YAML、Bincode 等格式 22 | serde = { version = "1.0.217", features = ["derive"] } 23 | serde_json = "1.0.138" # 处理 JSON 格式的序列化和反序列化 24 | bincode = "1.3.3" # 高效的二进制编码格式 25 | borsh = "1.5.5" # 专门用于高效序列化数据的二进制格式,常用于 Solana 生态 26 | base64 = "0.22.1" 27 | 28 | # ------------------------ 29 | # Solana 相关依赖 30 | # ------------------------ 31 | 32 | # Solana 客户端,用于与 Solana 网络进行交互 33 | solana-client = "2.1.13" 34 | solana-program = "2.1.13" # Solana 程序 (智能合约) 开发支持 35 | solana-sdk = "2.1.13" # Solana SDK 提供核心工具 36 | solana-transaction-status = "2.1.13" # 获取和解析交易状态信息 37 | solana-transaction-status-client-types = "2.1.13" # Solana 交易状态客户端类型 38 | 39 | # ------------------------ 40 | # Token 相关依赖 41 | # ------------------------ 42 | 43 | # Solana 代币账户相关操作,提供代币账户的管理与处理 44 | spl-associated-token-account = "6.0.0" 45 | 46 | # Solana SPL Token 标准库,代币的创建、管理和交易 47 | spl-token = "7.0.0" 48 | 49 | # ------------------------ 50 | # 网络与异步编程 51 | # ------------------------ 52 | 53 | # 异步编程工具,Solana 的很多操作需要异步处理 54 | tokio = { version = "1.43.0", features = ["full"] } 55 | 56 | # 处理异步流和任务的工具集,常与 `tokio` 配合使用 57 | futures-util = "0.3.31" 58 | 59 | # 网络请求 60 | reqwest = { version = "0.12.12", features = ["json"] } 61 | 62 | # ------------------------ 63 | # 日志与监控 64 | # ------------------------ 65 | 66 | # 高度可定制的日志框架,支持多种日志级别和格式 67 | tracing = "0.1.41" 68 | 69 | # 用于 `tracing` 的日志收集器,支持集成到日志处理系统中 70 | tracing-subscriber = "0.3" 71 | 72 | # ------------------------ 73 | # 环境变量管理 74 | # ------------------------ 75 | 76 | # 读取 `.env` 文件中的环境变量 77 | dotenv = "0.15.0" 78 | 79 | # ------------------------ 80 | # 随机数生成 81 | # ------------------------ 82 | 83 | # 随机数生成库,提供多种随机数生成策略 84 | rand = "0.9.0" 85 | 86 | # ------------------------ 87 | # 进度条和用户交互 88 | # ------------------------ 89 | 90 | # 创建进度条和动态消息显示,常用于 CLI 工具 91 | indicatif = "0.17.11" 92 | 93 | # ------------------------ 94 | # gRPC 客户端 95 | # ------------------------ 96 | 97 | # 用于与 Yellowstone gRPC 服务进行交互的客户端库 98 | yellowstone-grpc-client = "5.0.0" 99 | 100 | # gRPC 相关的 Proto 文件定义,用于数据结构的序列化和反序列化 101 | yellowstone-grpc-proto = "5.0.0" 102 | 103 | # ------------------------ 104 | # SDK 与开发工具 105 | # ------------------------ 106 | 107 | # Jito SDK 用于访问和操作 Jito 生态 108 | jito-sdk-rust = "0.1.0" 109 | redis = {version ="0.29.0", features=["tokio-comp","json","connection-manager","r2d2"] } 110 | once_cell = "1.20.3" 111 | r2d2 = "0.8.10" 112 | dashmap = "6.1.0" 113 | tokio-tungstenite = "0.26.2" 114 | -------------------------------------------------------------------------------- /black_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | "EKrqffucmkxNKgvvbvfKtEki4k5KFPEh3zMjnriwYgta", 3 | "Ea6D8vVUJVKt4KA4StYpdapaE4EMFdPh19ALykUGQCXz", 4 | "25VWtPuHrTGyKKk5jWvycbjdbyrhbMRR9svCqFtU3yD8", 5 | "BposQY3dk111pNXmBUepaFthb3xd9GZpcmRZjpDU9Yxh", 6 | "J7hr8R2Q8jfEgXo5FQobi1QHTzDSZVnxtdUqKKMsjGS4", 7 | "4wWPrUmcfxtSH6Xqfe6BXMwXgTR6sfC6kwPYMjravvzR", 8 | "F1JBobKe7WrqGLwYyqEihZWgrGdaiWjTuM5HWq1a4vfZ", 9 | "5fNJbfzz1num6Uh1BMsS1T26xqz7Fob7VKNxh5Kqxkiz" 10 | ] -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Solana Auto Sniping Bot Based on Black and White Lists 2 | 3 | ## This bot has helped me earn hundreds of SOL in the past few months 4 | 5 | 6 | # English 7 | ## Overall Architecture Design 8 | 9 | Driven by gRPC, collecting and analyzing all PumpFun transaction information 10 | 11 | First, update balances of all tokens 12 | 13 | During analysis, there are 4 situations: 14 | 1. buy => Update token info 15 | 2. sell => Update token info 16 | 3. create => Update and create new token info 17 | 4. complete => Delete token info 18 | 19 | After analysis, if the price of the currently purchased token has been updated, it is transmitted through a channel; at the same time, if a token triggers purchase conditions, the data required for purchase is returned 20 | 21 | Determine whether the token has been purchased before and whether it exists in token info 22 | 23 | Start a new thread for buying and selling operations 24 | 25 | 1. Buy according to the information in token info 26 | 27 | 2. Monitor token info information in real time and analyze whether it needs to be sold 28 | 29 | 3. Sell when reaching the time threshold or selling conditions 30 | 31 | # User Documentation 32 | 33 | 1. Redis installation 34 | 35 | Installation: 36 | ``` 37 | sudo apt update 38 | sudo apt install -y redis 39 | ``` 40 | 41 | Check if successful: 42 | ``` 43 | redis-server --version 44 | ``` 45 | A version output indicates success 46 | 47 | Set to start automatically on boot: 48 | ``` 49 | sudo systemctl enable redis 50 | ``` 51 | 52 | 2. Configure relevant addresses in white_list.json and black_list.json 53 | 54 | 3. Set up the configuration file properly 55 | 56 | 4. Execute cargo run 57 | 58 | # 中文 59 | 60 | ## 整体架构思考 61 | 62 | 以grpc驱动,收集所有pumpfun的交易信息进行分析 63 | 64 | 首先更新所有代币对应的balances 65 | 66 | 分析过程中有4种情况: 67 | 1. buy => 更新token info 68 | 2. sell => 更新token info 69 | 3. create => 更新并新建token info 70 | 4. complete => 删除token info 71 | 72 | 分析完毕后,如果当前购买的代币的价格出现了更新,则通过通道传递;同时,某个代币触发了购买条件,则返回购买所需数据 73 | 74 | 判断代币是否被购买过,是否存在token info中 75 | 76 | 开一个新的线程进行买入卖出操作 77 | 78 | 1. 根据token info的信息,进行买入 79 | 80 | 2. 实时监控token info的信息,并分析是否需要卖出 81 | 82 | 3. 到达时间阈值或卖出条件,进行卖出 83 | 84 | # 使用文档 85 | 86 | 1. redis 安装 87 | 88 | 安装: 89 | sudo apt update 90 | 91 | sudo apt install -y redis 92 | 93 | 检查是否成功: 94 | redis-server --version 95 | 有输出版本即可 96 | 97 | 设置开机自启 98 | 99 | sudo systemctl enable redis 100 | 101 | 2. 在white_list.json和black_list.json中配置相关的地址 102 | 103 | 3. 在配置文件中配好 104 | 105 | 4. 执行cargo run 106 | 107 | # 作者 108 | ## tg: @wceth 109 | ## 承担定制各类solana与EVM合约,狙击以及跟单机器人,链上复杂数据处理, 如果开发需求请联系上方tg 110 | 111 | # Author 112 | ## Telegram: @wceth 113 | ## Specializes in customizing various Solana and EVM contracts, sniping and copy-trading bots, and complex on-chain data processing. For development inquiries, please contact via the Telegram above. 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use redis::{aio::MultiplexedConnection, AsyncCommands, RedisResult}; 4 | use solana_sdk::timing::timestamp; 5 | 6 | use crate::{constants::MARKET_CAP, types::CreateEvent}; 7 | const TOKEN_SET_KEY: &str = "token_info_set"; 8 | 9 | // ! blockhash 相关 10 | pub async fn get_block_hash_str(conn: &mut MultiplexedConnection) -> RedisResult { 11 | redis::cmd("get").arg("blockhash").query_async(conn).await 12 | } 13 | 14 | // ! 白名单 相关 15 | pub async fn white_contains(conn: &mut MultiplexedConnection, user: &str) -> RedisResult { 16 | conn.exists(format!("white_list:{}", user)).await 17 | } 18 | pub async fn add_to_whitelist(conn: &mut MultiplexedConnection, user: &str) -> RedisResult<()> { 19 | conn.set(format!("white_list:{}", user), "1").await 20 | } 21 | pub async fn remove_to_whitelist(conn: &mut MultiplexedConnection, user: &str) -> RedisResult<()> { 22 | conn.del(format!("white_list:{}", user)).await 23 | } 24 | // ! 黑名单 相关 25 | pub async fn black_contains(conn: &mut MultiplexedConnection, user: &str) -> RedisResult { 26 | conn.exists(format!("black_list:{}", user)).await 27 | } 28 | pub async fn add_to_blacklist(conn: &mut MultiplexedConnection, user: &str) -> RedisResult<()> { 29 | conn.set(format!("black_list:{}", user), "1").await 30 | } 31 | 32 | // ! 代币 相关 33 | pub async fn add_token_info( 34 | conn: &mut MultiplexedConnection, 35 | create: &CreateEvent, 36 | ) -> RedisResult<()> { 37 | // info = dev|mk|create_time 38 | let info = format!("{}|{}|{}", create.user, 0, timestamp()); 39 | conn.hset(TOKEN_SET_KEY, create.mint.to_string(), info) 40 | .await 41 | } 42 | 43 | pub async fn update_mk( 44 | conn: &mut MultiplexedConnection, 45 | mint: &str, 46 | market_cap: f32, 47 | ) -> RedisResult<()> { 48 | match conn.hget::<_, _, String>(TOKEN_SET_KEY, mint).await { 49 | Ok(old_info) => { 50 | let splits: Vec<_> = old_info.split("|").collect(); 51 | 52 | let (dev, create_time) = (splits[0], splits[2]); 53 | let new_info = format!("{}|{}|{}", dev, market_cap, create_time); 54 | conn.hset(TOKEN_SET_KEY, mint, new_info).await 55 | } 56 | Err(_) => Ok(()), 57 | } 58 | } 59 | 60 | pub async fn check_mk(conn: &mut MultiplexedConnection) -> RedisResult<()> { 61 | // 获取所有数据 62 | match conn 63 | .hgetall::<'_, _, HashMap>(TOKEN_SET_KEY) 64 | .await 65 | { 66 | Ok(result) => { 67 | for (_, info) in result { 68 | let splits: Vec<_> = info.split("|").collect(); 69 | let (dev, mk, create_time) = ( 70 | splits[0], 71 | splits[1].parse::().unwrap(), 72 | splits[2].parse::().unwrap(), 73 | ); 74 | 75 | // 超时 76 | if create_time + 300_000 < timestamp() && mk < *MARKET_CAP { 77 | // 在白名单内,则去除 78 | if white_contains(conn, dev).await? { 79 | remove_to_whitelist(conn, dev).await?; 80 | } 81 | // 加入黑名单 82 | add_to_blacklist(conn, dev).await?; 83 | } 84 | } 85 | Ok(()) 86 | } 87 | _ => Ok(()), 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod test { 93 | use std::{thread::sleep, time::Duration}; 94 | 95 | use solana_sdk::pubkey::Pubkey; 96 | 97 | use crate::{ 98 | cache::{add_token_info, black_contains, check_mk, update_mk, white_contains}, 99 | constants::REDIS_URL, 100 | types::CreateEvent, 101 | }; 102 | 103 | #[tokio::test] 104 | async fn test() -> anyhow::Result<()> { 105 | dotenv::dotenv().ok(); 106 | let redis = redis::Client::open(REDIS_URL.to_string())?; 107 | let mut con = redis.get_multiplexed_async_connection().await?; 108 | // 1. 添加一个token info 109 | let dev = Pubkey::new_unique(); 110 | let mint = Pubkey::new_unique(); 111 | add_token_info( 112 | &mut con, 113 | &CreateEvent { 114 | name: "".to_string(), 115 | symbol: "".to_string(), 116 | uri: "".to_string(), 117 | mint, 118 | bonding_curve: Pubkey::new_unique(), 119 | user: dev, 120 | }, 121 | ) 122 | .await?; 123 | 124 | // 2. 更新mk 125 | update_mk(&mut con, &mint.to_string(), 100.0).await?; 126 | 127 | // 3. 停顿后检查 128 | sleep(Duration::from_secs(11)); 129 | check_mk(&mut con).await?; 130 | 131 | // 在黑名单,不在白名单 132 | assert!(black_contains(&mut con, &dev.to_string()).await?); 133 | assert!(!white_contains(&mut con, &dev.to_string()).await?); 134 | Ok(()) 135 | } 136 | } 137 | 138 | /* 139 | 140 | 用一个数组,记录当前检查过的所有代币信息,具体是 mint|dev|mk|create_time 141 | 142 | 当一笔交易进入,如果有需要,则更新每个代币的mk 143 | 144 | 每30s,检查一次所有代币,只要create_time至今超过5分钟,并且mk还低于7000,则将该代币的dev从白名单|普通人 -> 黑名单 145 | 146 | */ 147 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use futures_util::Stream; 3 | use std::{collections::HashMap, time::Duration}; 4 | use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient}; 5 | use yellowstone_grpc_proto::{ 6 | geyser::{ 7 | CommitmentLevel, SubscribeRequest, SubscribeRequestFilterAccounts, 8 | SubscribeRequestFilterBlocks, SubscribeRequestFilterBlocksMeta, 9 | SubscribeRequestFilterTransactions, SubscribeUpdate, 10 | }, 11 | tonic::Status, 12 | }; 13 | /// grpc需要的交易过滤map 14 | /// Add English annotation translation 15 | /// transactions filter map 16 | type TransactionsFilterMap = HashMap; 17 | /// grpc需要的区块过滤map 18 | /// blocks filter map 19 | type BlocksFilterMap = HashMap; 20 | /// grpc需要的账户过滤map 21 | /// account filter map 22 | type AccountFilterMap = HashMap; 23 | /// blockhash filter map 24 | type BlockMetaFilterMap = HashMap; 25 | /// grpc结构体,参数仅有 url 26 | /// grpc structure, parameters only url 27 | pub struct GrpcClient { 28 | endpoint: String, 29 | } 30 | 31 | impl GrpcClient { 32 | pub fn new(endpoint: String) -> Self { 33 | Self { endpoint } 34 | } 35 | 36 | /// 订阅区块 37 | /// Subscribe block 38 | pub async fn subscribe_block( 39 | &self, 40 | account_include: Vec, // 关注的地址, include addresses 41 | include_transactions: Option, // 是否包含所有交易, whether to include all transactions 42 | include_accounts: Option, // 是否包含所有账户更新, whether to include all account updates 43 | include_entries: Option, // 默认false, default false 44 | ) -> Result>> { 45 | // 创建client 46 | let mut client = GeyserGrpcClient::build_from_shared(self.endpoint.clone())? 47 | .tls_config(ClientTlsConfig::new().with_native_roots())? 48 | .connect_timeout(Duration::from_secs(10)) 49 | .timeout(Duration::from_secs(60)) 50 | .connect() 51 | .await?; 52 | // 过滤规则 53 | let mut blocks: BlocksFilterMap = HashMap::new(); 54 | blocks.insert( 55 | "client".to_owned(), 56 | SubscribeRequestFilterBlocks { 57 | account_include, 58 | include_transactions, 59 | include_accounts, 60 | include_entries, 61 | }, 62 | ); 63 | 64 | // 构建request 65 | // build request 66 | let subscribe_request = SubscribeRequest { 67 | blocks, 68 | commitment: Some(CommitmentLevel::Confirmed.into()), 69 | ..Default::default() 70 | }; 71 | 72 | // 返回流 73 | // return stream 74 | let (_, stream) = client 75 | .subscribe_with_request(Some(subscribe_request)) 76 | .await?; 77 | Ok(stream) 78 | } 79 | 80 | /// 订阅指定地址的账户信息更新 81 | /// Subscribe transaction 82 | pub async fn subscribe_transaction( 83 | &self, 84 | account_include: Vec, // 包含在内的地址相关交易都会收到, include addresses 85 | account_exclude: Vec, // 不包含这些地址的相关交易都会收到, exclude addresses 86 | account_required: Vec, // 必须要包含的地址, required addresses 87 | commitment: CommitmentLevel, // 确认级别, commitment level 88 | ) -> Result>> { 89 | // client 90 | let mut client = GeyserGrpcClient::build_from_shared(self.endpoint.clone())? 91 | .tls_config(ClientTlsConfig::new().with_native_roots())? 92 | .connect_timeout(Duration::from_secs(10)) 93 | .timeout(Duration::from_secs(60)) 94 | .connect() 95 | .await?; 96 | 97 | // 过滤规则 98 | // filter rules 99 | let mut transactions: TransactionsFilterMap = HashMap::new(); 100 | transactions.insert( 101 | "client".to_string(), 102 | SubscribeRequestFilterTransactions { 103 | vote: None, 104 | failed: None, 105 | signature: None, 106 | account_include, 107 | account_exclude, 108 | account_required, 109 | }, 110 | ); 111 | 112 | let mut metas: BlockMetaFilterMap = HashMap::new(); 113 | metas.insert("client".to_string(), SubscribeRequestFilterBlocksMeta {}); 114 | // request 115 | let subscribe_request = SubscribeRequest { 116 | transactions, 117 | blocks_meta: metas, 118 | commitment: Some(commitment.into()), 119 | ..Default::default() 120 | }; 121 | 122 | // 返回流 123 | // return stream 124 | let (_, stream) = client 125 | .subscribe_with_request(Some(subscribe_request)) 126 | .await?; 127 | 128 | Ok(stream) 129 | } 130 | 131 | pub async fn subscribe_account_updates( 132 | &self, 133 | account: Vec, 134 | commitment: CommitmentLevel, 135 | ) -> Result>> { 136 | // client 137 | let mut client = GeyserGrpcClient::build_from_shared(self.endpoint.clone())? 138 | .tls_config(ClientTlsConfig::new().with_native_roots())? 139 | .connect_timeout(Duration::from_secs(10)) 140 | .timeout(Duration::from_secs(60)) 141 | .connect() 142 | .await?; 143 | // 过滤规则 144 | // filter rules 145 | let mut accounts: AccountFilterMap = HashMap::new(); 146 | accounts.insert( 147 | "client".to_string(), 148 | SubscribeRequestFilterAccounts { 149 | account, 150 | owner: vec![], 151 | filters: vec![], 152 | nonempty_txn_signature: None, 153 | }, 154 | ); 155 | 156 | // request 157 | let subscribe_request = SubscribeRequest { 158 | accounts, 159 | commitment: Some(commitment.into()), 160 | ..Default::default() 161 | }; 162 | 163 | // 返回流 164 | // return stream 165 | let (_, stream) = client 166 | .subscribe_with_request(Some(subscribe_request)) 167 | .await?; 168 | 169 | Ok(stream) 170 | } 171 | 172 | /// 获取最新的blockhash 173 | /// Get latest blockhash 174 | pub async fn get_latest_blockhash(&self) -> Result { 175 | let mut client = GeyserGrpcClient::build_from_shared(self.endpoint.clone())? 176 | .tls_config(ClientTlsConfig::new().with_native_roots())? 177 | .connect_timeout(Duration::from_secs(10)) 178 | .timeout(Duration::from_secs(60)) 179 | .connect() 180 | .await?; 181 | let response = client.get_latest_blockhash(None).await?; 182 | Ok(response.blockhash) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use anyhow::Result; 4 | 5 | #[derive(Debug, Copy, Clone, Default)] 6 | pub struct UserConfig { 7 | pub buy_amount: u64, 8 | pub slippage_bps: f64, 9 | pub use_jito: bool, 10 | pub tip: f64, 11 | pub max_retries: u8, 12 | pub max_hold_time: u64, 13 | 14 | pub decrease_sell: f32, 15 | pub increase_sell: f32, 16 | } 17 | 18 | impl UserConfig { 19 | /// 从环境变量初始化 UserConfig 20 | /// Initialize UserConfig from environment variables 21 | pub fn from_env() -> Result { 22 | let buy_amount = env::var("BUY_AMOUNT")?.parse::().unwrap_or_default(); 23 | let slippage_bps = env::var("SLIPPAGE_BPS")?.parse::().unwrap_or_default(); 24 | let use_jito = env::var("USE_JITO")?.parse::().unwrap_or_default(); 25 | let tip = env::var("TIP")?.parse::().unwrap_or_default(); 26 | let max_retries = env::var("MAX_RETRIES")?.parse::().unwrap_or(3); 27 | let max_hold_time = env::var("MAX_HOLD_TIME")? 28 | .parse::() 29 | .unwrap_or_default(); 30 | let decrease_sell = env::var("DECREASE_SELL")? 31 | .parse::() 32 | .unwrap_or_default(); 33 | let increase_sell = env::var("INCREASE_SELL")? 34 | .parse::() 35 | .unwrap_or_default(); 36 | 37 | Ok(Self { 38 | buy_amount, 39 | slippage_bps, 40 | use_jito, 41 | tip, 42 | max_retries, 43 | max_hold_time, 44 | decrease_sell, 45 | increase_sell, 46 | }) 47 | } 48 | 49 | pub fn from_env_with_prefix(prefix: &str) -> Result { 50 | let buy_amount = env::var(format!("{prefix}_BUY_AMOUNT"))? 51 | .parse::() 52 | .unwrap_or_default(); 53 | let slippage_bps = env::var(format!("{prefix}_SLIPPAGE_BPS"))? 54 | .parse::() 55 | .unwrap_or_default(); 56 | let use_jito = env::var(format!("{prefix}_USE_JITO"))? 57 | .parse::() 58 | .unwrap_or_default(); 59 | let tip = env::var(format!("{prefix}_TIP"))? 60 | .parse::() 61 | .unwrap_or_default(); 62 | let max_retries = env::var(format!("{prefix}_MAX_RETRIES"))? 63 | .parse::() 64 | .unwrap_or(3); 65 | let max_hold_time = env::var(format!("{prefix}_MAX_HOLD_TIME"))? 66 | .parse::() 67 | .unwrap_or_default(); 68 | let decrease_sell = env::var(format!("{prefix}_DECREASE_SELL"))? 69 | .parse::() 70 | .unwrap_or_default(); 71 | let increase_sell = env::var(format!("{prefix}_INCREASE_SELL"))? 72 | .parse::() 73 | .unwrap_or_default(); 74 | 75 | Ok(Self { 76 | buy_amount, 77 | slippage_bps, 78 | use_jito, 79 | tip, 80 | max_retries, 81 | max_hold_time, 82 | decrease_sell, 83 | increase_sell, 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use once_cell::sync::Lazy; 4 | use solana_program::pubkey; 5 | use solana_sdk::pubkey::Pubkey; 6 | 7 | pub static GRPC: Lazy = Lazy::new(|| env::var("GRPC_URL").unwrap()); 8 | pub static RPC: Lazy = Lazy::new(|| env::var("RPC_URL").unwrap()); 9 | pub static JITO: Lazy = Lazy::new(|| env::var("JITO_BLOCK_ENGINE_URL").unwrap()); 10 | pub static UNIT_PRICE: Lazy = Lazy::new(|| { 11 | env::var("UNIT_PRICE") 12 | .unwrap() 13 | .parse::() 14 | .unwrap_or(20000) 15 | }); 16 | pub static UNIT_LIMIT: Lazy = Lazy::new(|| { 17 | env::var("UNIT_LIMIT") 18 | .unwrap() 19 | .parse::() 20 | .unwrap_or(2000000) 21 | }); 22 | pub static PRIVATE_KEY: Lazy = Lazy::new(|| env::var("PRIVATE_KEY").unwrap()); 23 | pub static REDIS_URL: Lazy = Lazy::new(|| env::var("REDIS_URL").unwrap()); 24 | pub static PRICE_INCREASE_BUY_LOWER: Lazy = Lazy::new(|| { 25 | env::var("PRICE_INCREASE_BUY_LOWER") 26 | .unwrap() 27 | .parse::() 28 | .unwrap_or(0.1) 29 | }); 30 | pub static PRICE_INCREASE_BUY_UPPER: Lazy = Lazy::new(|| { 31 | env::var("PRICE_INCREASE_BUY_UPPER") 32 | .unwrap() 33 | .parse::() 34 | .unwrap_or(0.2) 35 | }); 36 | pub static MARKET_CAP: Lazy = Lazy::new(|| { 37 | env::var("MARKET_CAP") 38 | .unwrap() 39 | .parse::() 40 | .unwrap_or(30.0) 41 | }); 42 | 43 | // program相关 44 | // program related 45 | pub const SYSTEM_PROGRAM_ID: Pubkey = pubkey!("11111111111111111111111111111111"); 46 | pub const SYSTEM_RENT_PROGRAM_ID: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111"); 47 | pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); 48 | pub const ASSOC_TOKEN_ACC_PROGRAM_ID: Pubkey = 49 | pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); 50 | pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"); 51 | pub const KEY_PREFIX: &'static str = "token:info:"; 52 | 53 | // pumpfun 54 | pub const PUMPFUN_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); 55 | pub const PUMPFUN_GLOBAL: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); 56 | pub const PUMPFUN_FEE_RECIPIENT: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); 57 | pub const INIT_SOL_REVERSES: u64 = 30_000_000_000; 58 | pub const INIT_TOKEN_REVERSES: u64 = 1_073_000_191_000_000; 59 | pub const INIT_PRICE: f32 = (INIT_SOL_REVERSES as f32 / 1e9) / (INIT_TOKEN_REVERSES as f32 / 1e6); 60 | pub const PUMPFUN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000; 61 | 62 | // 标量 63 | // scalars 64 | pub const MINUTES: u64 = 60 * 1000; 65 | pub const SECONDS: u64 = 1000; 66 | -------------------------------------------------------------------------------- /src/engine.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | env, fs, 4 | str::FromStr, 5 | sync::Arc, 6 | time::Duration, 7 | }; 8 | 9 | use futures_util::StreamExt; 10 | use jito_sdk_rust::JitoJsonRpcSDK; 11 | use redis::aio::MultiplexedConnection; 12 | use reqwest::Client; 13 | use solana_client::nonblocking::rpc_client::RpcClient; 14 | use solana_sdk::{ 15 | hash::Hash, pubkey::Pubkey, signature::Keypair, signer::Signer, timing::timestamp, 16 | }; 17 | use solana_transaction_status::{ 18 | option_serializer::OptionSerializer, UiInnerInstructions, UiTransactionStatusMeta, 19 | }; 20 | use spl_associated_token_account::get_associated_token_address; 21 | use tokio::{ 22 | sync::{Mutex, RwLock}, 23 | time::timeout, 24 | }; 25 | use tracing::{error, info, warn}; 26 | use yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof; 27 | 28 | use crate::{ 29 | cache::{ 30 | add_token_info, black_contains, check_mk, get_block_hash_str, update_mk, white_contains, 31 | }, 32 | client::GrpcClient, 33 | config::UserConfig, 34 | constants::{ 35 | GRPC, INIT_PRICE, JITO, PRICE_INCREASE_BUY_LOWER, PRICE_INCREASE_BUY_UPPER, PRIVATE_KEY, 36 | PUMPFUN_PROGRAM_ID, REDIS_URL, RPC, 37 | }, 38 | pumpfun::{create_buy_transaction, create_sell_transaction}, 39 | types::{BuyAndReason, TargetEvent, TradeEvent}, 40 | utils::{ 41 | cal_pumpfun_marketcap, cal_pumpfun_price, convert_to_encoded_tx, find_bonding_curve, 42 | have_tg_or_x, send_txn_with_retry, 43 | }, 44 | }; 45 | use anyhow::{anyhow, Context, Result}; 46 | 47 | #[derive(Debug, Clone, PartialEq, Eq)] 48 | pub enum Mode { 49 | SNIPTER, 50 | STRATEGY, 51 | } 52 | impl FromStr for Mode { 53 | type Err = anyhow::Error; 54 | 55 | fn from_str(s: &str) -> Result { 56 | match s { 57 | "SNIPTER" => Ok(Mode::SNIPTER), 58 | "STRATEGY" => Ok(Mode::STRATEGY), 59 | _ => Err(anyhow!("invalid mode")), 60 | } 61 | } 62 | } 63 | pub struct Engine2 { 64 | pub rpc: Arc, 65 | pub http: reqwest::Client, 66 | pub jito_sdk: Arc, 67 | pub now_token: Arc>, 68 | pub buyed_list: Arc>>, 69 | pub transaction_lock: Arc>, 70 | pub price_channel: Arc>, 71 | pub price_receiver: Arc>>, 72 | pub buy_channel: Arc>, 73 | pub buy_receiver: Arc>>, 74 | pub sell_channel: Arc>, 75 | pub sell_receiver: Arc>>, 76 | pub keypair: Arc, 77 | pub wallet: Pubkey, 78 | pub usual_config: UserConfig, 79 | pub white_config: UserConfig, 80 | pub redis: MultiplexedConnection, 81 | pub mode: Mode, 82 | } 83 | 84 | impl Engine2 { 85 | pub async fn new( 86 | price_channel_size: usize, 87 | sig_channel_size: usize, 88 | mode: Mode, 89 | ) -> Result { 90 | let redis = redis::Client::open(REDIS_URL.to_string())?; 91 | let mut conn = redis 92 | .get_multiplexed_async_connection() 93 | .await 94 | .context("get redis connection error") 95 | .unwrap(); 96 | let black_list_path = env::var("BLACK_LIST_PATH").unwrap(); 97 | let black_file = fs::File::open(black_list_path) 98 | .context("open black list file error") 99 | .unwrap(); 100 | let list1: Vec = serde_json::from_reader(black_file) 101 | .context("black list content error") 102 | .unwrap(); 103 | 104 | for item in &list1 { 105 | let key = format!("black_list:{}", item); 106 | redis::cmd("set") 107 | .arg(key) 108 | .arg("1") 109 | .exec_async(&mut conn) 110 | .await 111 | .unwrap(); 112 | } 113 | 114 | let white_list_path = env::var("WHITE_LIST_PATH").unwrap(); 115 | let white_file = fs::File::open(white_list_path) 116 | .context("open white list file error") 117 | .unwrap(); 118 | let list2: Vec = serde_json::from_reader(white_file) 119 | .context("white list content error") 120 | .unwrap(); 121 | for item in &list2 { 122 | let key = format!("white_list:{}", item); 123 | redis::cmd("set") 124 | .arg(key) 125 | .arg("1") 126 | .exec_async(&mut conn) 127 | .await 128 | .unwrap(); 129 | } 130 | 131 | let (price_tx, price_rx) = tokio::sync::mpsc::channel(price_channel_size); 132 | let (buy_tx, buy_rx) = tokio::sync::mpsc::channel(sig_channel_size); 133 | let (sell_tx, sell_rx) = tokio::sync::mpsc::channel(sig_channel_size); 134 | 135 | let price_channel = Arc::new(price_tx); 136 | 137 | let buy_channel = Arc::new(buy_tx); 138 | 139 | let sell_channel = Arc::new(sell_tx); 140 | 141 | let keypair = Arc::new(Keypair::from_base58_string(&PRIVATE_KEY)); 142 | 143 | Ok(Self { 144 | rpc: Arc::new(RpcClient::new(RPC.to_string())), 145 | http: Client::new(), 146 | jito_sdk: Arc::new(JitoJsonRpcSDK::new(&JITO, None)), 147 | transaction_lock: Arc::new(Mutex::new(())), 148 | now_token: Arc::new(RwLock::new(Pubkey::new_unique())), 149 | buyed_list: Arc::new(Mutex::new(HashSet::new())), 150 | price_channel, 151 | price_receiver: Arc::new(Mutex::new(price_rx)), 152 | buy_channel, 153 | buy_receiver: Arc::new(Mutex::new(buy_rx)), 154 | sell_channel, 155 | sell_receiver: Arc::new(Mutex::new(sell_rx)), 156 | wallet: keypair.pubkey(), 157 | keypair, 158 | white_config: UserConfig::from_env_with_prefix("WHITE")?, 159 | usual_config: UserConfig::from_env_with_prefix("USUAL")?, 160 | redis: conn, 161 | mode, 162 | }) 163 | } 164 | 165 | pub async fn run(&self) -> Result<()> { 166 | // grpc 167 | let grpc_url = GRPC.to_string(); 168 | let grpc = GrpcClient::new(grpc_url); 169 | let mut stream = grpc 170 | .subscribe_transaction( 171 | vec![PUMPFUN_PROGRAM_ID.to_string()], 172 | vec![], 173 | vec![], 174 | yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed, 175 | ) 176 | .await?; 177 | 178 | let mut block_times = 0; 179 | // 接收消息 180 | // receive messages 181 | while let Some(Ok(sub)) = stream.next().await { 182 | if let Some(update) = sub.update_oneof { 183 | match update { 184 | UpdateOneof::Transaction(sub_tx) => { 185 | if let Some(tx_info) = sub_tx.transaction { 186 | let tx = convert_to_encoded_tx(tx_info)?; 187 | if let Some(meta) = tx.meta { 188 | let result = self.update_token_info(meta).await?; 189 | 190 | if let Some(res) = result { 191 | let _config = match res.reason { 192 | crate::types::Reason::USUAL => self.usual_config, 193 | crate::types::Reason::WHITE => self.white_config, 194 | }; 195 | 196 | let tx_lock = self.transaction_lock.clone(); 197 | let token_lock = self.now_token.clone(); 198 | let _keypair = self.keypair.clone(); 199 | let _rpc = self.rpc.clone(); 200 | let _jito = self.jito_sdk.clone(); 201 | let _price_receiver = Arc::clone(&self.price_receiver); 202 | let _buy_receiver = Arc::clone(&self.buy_receiver); 203 | let _sell_receiver = Arc::clone(&self.sell_receiver); 204 | let _buyed_list = Arc::clone(&self.buyed_list); 205 | let mut _redis = self.redis.clone(); 206 | 207 | // 管理买入卖出 208 | // manage buy and sell 209 | tokio::spawn(async move { 210 | let _guard = tx_lock.lock().await; 211 | { 212 | let mut locked = _buyed_list.lock().await; 213 | if locked.contains(&res.mint) { 214 | // info!("代币 {:?} 已购买过,不重复购买", res.mint); 215 | // directly end 216 | return; 217 | } else { 218 | locked.insert(res.mint); 219 | } 220 | }; 221 | 222 | // 更新当前的token 223 | // update current token 224 | { 225 | let mut locked = token_lock.write().await; 226 | *locked = res.mint; 227 | } 228 | info!("---------------{:?}---------------", res.reason); 229 | match manage( 230 | &mut _redis, 231 | &_rpc, 232 | &_jito, 233 | &_keypair, 234 | res, 235 | _config, 236 | _price_receiver, 237 | _buy_receiver, 238 | _sell_receiver, 239 | ) 240 | .await 241 | { 242 | Ok(_) => {} 243 | Err(e) => { 244 | if e.to_string().contains("卖出") { 245 | let _guard = tx_lock.lock().await; 246 | } 247 | } 248 | } 249 | }); 250 | } 251 | } 252 | } 253 | } 254 | 255 | UpdateOneof::BlockMeta(meta) => { 256 | block_times += 1; 257 | let mut conn = self.redis.clone(); 258 | redis::cmd("set") 259 | .arg("blockhash") 260 | .arg(&meta.blockhash) 261 | .exec_async(&mut conn) 262 | .await?; 263 | if block_times == 100 { 264 | check_mk(&mut conn).await?; 265 | block_times = 0; 266 | } 267 | } 268 | _ => {} 269 | } 270 | } 271 | } 272 | Ok(()) 273 | } 274 | 275 | // 更新token info 276 | // update token info 277 | async fn update_token_info( 278 | &self, 279 | meta: UiTransactionStatusMeta, 280 | ) -> Result> { 281 | if let OptionSerializer::Some(inner_ixs) = meta.inner_instructions { 282 | self.check_instruction(inner_ixs).await 283 | } else { 284 | Ok(None) 285 | } 286 | } 287 | 288 | // 检查内部指令 289 | // check instruction 290 | async fn check_instruction( 291 | &self, 292 | inner_ixs: Vec, 293 | ) -> Result> { 294 | let mut buy_and_reason: Option = None; 295 | let mut conn = self.redis.clone(); 296 | 297 | let mut temp_price = HashMap::new(); 298 | for inner in inner_ixs { 299 | for ix in inner.instructions { 300 | if let Ok(target_event) = TargetEvent::try_from(ix) { 301 | match target_event { 302 | TargetEvent::PumpfunBuy(buy) => { 303 | if buy.user.eq(&self.wallet) { 304 | let _ = self.buy_channel.send(buy).await; 305 | } 306 | // 基本数据 307 | // basic data 308 | let sol_reserves = buy.virtual_sol_reserves; 309 | let token_reserves = buy.virtual_token_reserves; 310 | let price = cal_pumpfun_price(sol_reserves, token_reserves); 311 | let market_cap = cal_pumpfun_marketcap(price); 312 | temp_price.insert(buy.mint, (price, market_cap)); 313 | 314 | let price_change_percent = (price - INIT_PRICE) / INIT_PRICE; 315 | // 价格波动&社交媒体 316 | // price change & social media 317 | if self.mode.eq(&Mode::STRATEGY) 318 | && price_change_percent > *PRICE_INCREASE_BUY_LOWER 319 | && price_change_percent < *PRICE_INCREASE_BUY_UPPER 320 | { 321 | buy_and_reason = Some(BuyAndReason { 322 | mint: buy.mint, 323 | reason: crate::types::Reason::USUAL, 324 | price, 325 | }); 326 | } 327 | } 328 | 329 | TargetEvent::PumpfunSell(sell) => { 330 | // 出现了出售,通道发送 331 | // sell occurred, channel sent 332 | if sell.user.eq(&self.wallet) { 333 | let _ = self.sell_channel.send(sell).await; 334 | } 335 | // 基本数据 336 | // basic data 337 | let sol_reserves = sell.virtual_sol_reserves; 338 | let token_reserves = sell.virtual_token_reserves; 339 | let price = cal_pumpfun_price(sol_reserves, token_reserves); 340 | let market_cap = cal_pumpfun_marketcap(price); 341 | 342 | temp_price.insert(sell.mint, (price, market_cap)); 343 | } 344 | 345 | TargetEvent::PumpfunCreate(create) => { 346 | let mint = create.mint; 347 | // 默认是没有 348 | // default is false 349 | if mint.to_string().ends_with("pump") { 350 | let have_x_or_tg = have_tg_or_x(&self.http, &mint.to_string()) 351 | .await 352 | .unwrap_or(false); 353 | add_token_info(&mut conn, &create).await?; 354 | let _is_white = 355 | white_contains(&mut conn, &create.user.to_string()).await?; 356 | 357 | let condition = if self.mode == Mode::SNIPTER { 358 | white_contains(&mut conn, &create.user.to_string()).await? 359 | && !black_contains(&mut conn, &create.user.to_string()) 360 | .await? 361 | || have_x_or_tg 362 | } else { 363 | white_contains(&mut conn, &create.user.to_string()).await? 364 | }; 365 | 366 | if condition { 367 | buy_and_reason = Some(BuyAndReason { 368 | mint, 369 | reason: crate::types::Reason::WHITE, 370 | price: INIT_PRICE, 371 | }); 372 | } 373 | 374 | // 非狙击模式下的额外操作 375 | // additional operations in non-sniper mode 376 | if !self.mode.eq(&Mode::SNIPTER) && have_x_or_tg { 377 | add_token_info(&mut conn, &create).await?; 378 | } 379 | } 380 | } 381 | 382 | TargetEvent::PumpfunComplete(_) => { 383 | // 安全删除 384 | // safe delete 385 | } 386 | } 387 | } 388 | } 389 | } 390 | 391 | let locked = self.now_token.read().await; 392 | for (key, (price, mk)) in temp_price { 393 | // 关注的代币价格发生变化,则发出消息 394 | // if the price of the token changes, send a message 395 | if key.eq(&locked) { 396 | let _ = self.price_channel.send(price).await; 397 | } 398 | // 更新代币的marketcap 399 | // update marketcap 400 | update_mk(&mut conn, &key.to_string(), mk).await?; 401 | } 402 | 403 | Ok(buy_and_reason) 404 | } 405 | } 406 | 407 | // 交易管理 408 | // transaction management 409 | async fn manage( 410 | redis: &mut MultiplexedConnection, 411 | rpc: &RpcClient, 412 | jito: &JitoJsonRpcSDK, 413 | keypair: &Keypair, 414 | res: BuyAndReason, 415 | config: UserConfig, 416 | price_channel: Arc>>, 417 | buy_channel: Arc>>, 418 | sell_channel: Arc>>, 419 | ) -> Result<()> { 420 | let mut price_recv = price_channel.lock().await; 421 | let mut buy_recv = buy_channel.lock().await; 422 | let sell_recv = sell_channel.lock().await; 423 | 424 | let bonding_curve = &find_bonding_curve(&res.mint); 425 | 426 | let blockhash = get_block_hash_str(redis).await?.parse::()?; 427 | // 获取锁 428 | // get lock 429 | let buy_tx = create_buy_transaction( 430 | bonding_curve, 431 | res.price, 432 | &res.mint, 433 | &keypair, 434 | config.buy_amount, 435 | config.slippage_bps, 436 | config.use_jito, 437 | config.tip, 438 | blockhash, 439 | ); 440 | 441 | // 买入只尝试一次,没买入成功就直接跳过 442 | // only try once, skip if not successful 443 | send_txn_with_retry(&jito, buy_tx, 1) 444 | .await 445 | .context("send buy transaction to jito error")?; 446 | 447 | let mut token_balance = 0; 448 | 449 | // 等待buy交易完成 450 | // wait for buy transaction to complete 451 | if let Ok(Some(_data)) = timeout(Duration::from_secs(30), buy_recv.recv()).await { 452 | info!("---------------buy token {:?} success---------------", res.mint); 453 | token_balance = _data.token_amount; 454 | } 455 | 456 | if token_balance == 0 { 457 | error!("---------------buy token {:?} failed---------------", res.mint); 458 | return Err(anyhow!("buy token failed!!!")); 459 | } 460 | 461 | let mut price = 0.0; 462 | // 价格监控 463 | let end_time = timestamp() + config.max_hold_time; 464 | while timestamp() < end_time { 465 | if let Ok(now_price) = price_recv.try_recv() { 466 | price = now_price; 467 | let price_change = (now_price - INIT_PRICE) / INIT_PRICE; 468 | info!( 469 | "---------------token {:?} {:.2}% ---------------", 470 | res.mint, 471 | price_change * 100.0 472 | ); 473 | if price_change > config.increase_sell || price_change < config.decrease_sell { 474 | info!( 475 | "---------------token {:?} {:.2}%, trigger sell---------------", 476 | res.mint, 477 | price_change * 100.0 478 | ); 479 | break; 480 | } 481 | } 482 | tokio::time::sleep(Duration::from_millis(200)).await; 483 | } 484 | 485 | // 丢弃通道接收者 486 | // drop channel receiver 487 | drop(price_recv); 488 | drop(sell_recv); 489 | drop(buy_recv); 490 | // 等待卖出 491 | // wait for sell 492 | sell_until_success( 493 | redis, 494 | rpc, 495 | jito, 496 | price_channel, 497 | sell_channel, 498 | price, 499 | bonding_curve, 500 | &res.mint, 501 | keypair, 502 | token_balance, 503 | config, 504 | ) 505 | .await 506 | .map_err(|e| anyhow!("sell failed!!! {:?}", e)) 507 | // 当前sell交易未完成,需要进行循环直到卖出 508 | // current sell transaction is not completed, need to loop until sell 509 | } 510 | 511 | pub async fn sell_until_success( 512 | redis: &mut MultiplexedConnection, 513 | rpc: &RpcClient, 514 | jito: &JitoJsonRpcSDK, 515 | price_channel: Arc>>, 516 | sell_channel: Arc>>, 517 | price: f32, 518 | bonding_curve: &Pubkey, 519 | mint: &Pubkey, 520 | keypair: &Keypair, 521 | balance: u64, 522 | config: UserConfig, 523 | ) -> Result<()> { 524 | let mut sell_times = 2; 525 | let mut price_recv = price_channel.lock().await; 526 | let mut sell_recv = sell_channel.lock().await; 527 | 528 | let blockhash = get_block_hash_str(redis).await?.parse::()?; 529 | let mut slippage = config.slippage_bps; 530 | 531 | //先尝试购买一次 532 | // first try to sell once 533 | let sell_tx = create_sell_transaction( 534 | bonding_curve, 535 | price, 536 | mint, 537 | &keypair, 538 | balance, 539 | slippage, 540 | config.use_jito, 541 | config.tip, 542 | blockhash, 543 | ) 544 | .context(format!("create sell transaction for {:?} failed", mint))?; 545 | 546 | send_txn_with_retry(&jito, sell_tx, config.max_retries) 547 | .await 548 | .context("send sell transaction to jito error")?; 549 | 550 | info!( 551 | "---------------sell token {:?} transaction sent, waiting---------------", 552 | mint 553 | ); 554 | 555 | // 等待sell交易完成 556 | // wait for sell transaction to complete 557 | if let Ok(Some(_data)) = timeout(Duration::from_secs(20), sell_recv.recv()).await { 558 | info!("---------------sell token {:?} success---------------", mint); 559 | return Ok(()); 560 | } 561 | warn!( 562 | "---------------first sell token {:?} failed---------------", 563 | mint 564 | ); 565 | 566 | // 没卖出去,再卖 567 | // didn't sell, sell again 568 | let token_ata = get_associated_token_address(&keypair.pubkey(), mint); 569 | loop { 570 | let mut price_list = vec![]; 571 | slippage = slippage * 1.1; 572 | if let Ok(_) = timeout( 573 | Duration::from_secs(4), 574 | price_recv.recv_many(&mut price_list, 100), 575 | ) 576 | .await 577 | { 578 | let now_price = price_list.last().unwrap(); 579 | info!("price list {:?}", price_list); 580 | info!( 581 | "---------------using latest price to sell token {:?} {:?} times, price {:?}---------------", 582 | mint, sell_times, now_price 583 | ); 584 | let blockhash = get_block_hash_str(redis).await?.parse::()?; 585 | 586 | // 构造一笔交易 587 | // create a transaction 588 | let sell_tx = create_sell_transaction( 589 | bonding_curve, 590 | *now_price, 591 | mint, 592 | &keypair, 593 | balance, 594 | slippage, 595 | config.use_jito, 596 | config.tip, 597 | blockhash, 598 | ) 599 | .context(format!("create sell transaction for {:?} failed", mint))?; 600 | // 发送 601 | // send 602 | send_txn_with_retry(&jito, sell_tx, config.max_retries) 603 | .await 604 | .context("send sell transaction to jito error")?; 605 | sell_times += 1; 606 | // 等待 607 | // wait 608 | if let Ok(Some(_data)) = timeout(Duration::from_secs(20), sell_recv.recv()).await { 609 | info!("---------------sell token {:?} success---------------", mint); 610 | return Ok(()); 611 | } 612 | } else { 613 | info!( 614 | "---------------using old price to sell token {:?} {:?} times---------------", 615 | mint, sell_times 616 | ); 617 | let blockhash = get_block_hash_str(redis).await?.parse::()?; 618 | 619 | let sell_tx = create_sell_transaction( 620 | bonding_curve, 621 | price, 622 | mint, 623 | &keypair, 624 | balance, 625 | slippage, 626 | config.use_jito, 627 | config.tip, 628 | blockhash, 629 | ) 630 | .context(format!("create sell transaction for {:?} failed", mint))?; 631 | // 发送 632 | // send 633 | send_txn_with_retry(&jito, sell_tx, config.max_retries) 634 | .await 635 | .context("send sell transaction to jito error")?; 636 | sell_times += 1; 637 | // 等待 638 | // wait 639 | if let Ok(Some(_data)) = timeout(Duration::from_secs(20), sell_recv.recv()).await { 640 | info!("---------------sell token {:?} success---------------", mint); 641 | return Ok(()); 642 | } 643 | } 644 | warn!("---------------sell token {:?} failed---------------", mint); 645 | if let Ok(ui_balanace) = rpc.get_token_account_balance(&token_ata).await { 646 | let balance = ui_balanace.amount.parse::()?; 647 | if balance == 0.0 { 648 | info!("---------------sell token {:?} success---------------", mint); 649 | return Ok(()); 650 | } 651 | } 652 | } 653 | } 654 | 655 | /* 656 | 657 | 1. 狙击 => 白名单 + 社交媒体 658 | 659 | 2. 普通 => 白名单 + 价格波动&社交媒体 660 | 661 | */ 662 | 663 | /* 664 | 665 | 1. Sniper => Whitelist + Social Media 666 | 667 | 2. Normal => Whitelist + Price Fluctuations & Social Media 668 | 669 | */ 670 | 671 | 672 | -------------------------------------------------------------------------------- /src/jito.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use rand::{rng, seq::IteratorRandom}; 5 | use solana_sdk::pubkey::Pubkey; 6 | use tracing::error; 7 | 8 | pub fn get_tip_account() -> Result { 9 | let accounts = [ 10 | "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", 11 | "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", 12 | "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", 13 | "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", 14 | "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", 15 | "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", 16 | "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", 17 | "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", 18 | ]; 19 | let mut rng = rng(); 20 | match accounts.iter().choose(&mut rng) { 21 | Some(acc) => Ok(Pubkey::from_str(acc).inspect_err(|err| { 22 | error!("jito: failed to parse Pubkey: {:?}", err); 23 | })?), 24 | None => Err(anyhow!("jito: no tip accounts available")), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cache; 2 | pub mod client; 3 | pub mod config; 4 | pub mod constants; 5 | pub mod engine; 6 | pub mod jito; 7 | pub mod pumpfun; 8 | pub mod types; 9 | pub mod utils; 10 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, str::FromStr}; 2 | 3 | use new_bot::engine::Engine2; 4 | 5 | use tracing::Level; 6 | use tracing_subscriber::FmtSubscriber; 7 | #[tokio::main] 8 | async fn main() -> anyhow::Result<()> { 9 | dotenv::dotenv().ok(); 10 | let subscriber = FmtSubscriber::builder() 11 | .with_max_level(Level::INFO) 12 | .finish(); 13 | 14 | tracing::subscriber::set_global_default(subscriber).expect("Failed to set global subscriber"); 15 | 16 | let engine = Engine2::new( 17 | 100, 18 | 100, 19 | new_bot::engine::Mode::from_str(&env::var("MODE")?)?, 20 | ) 21 | .await 22 | .unwrap(); 23 | engine.run().await.unwrap(); 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /src/pumpfun.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{ 2 | compute_budget::ComputeBudgetInstruction, hash::Hash, pubkey::Pubkey, signature::Keypair, 3 | signer::Signer, system_instruction, transaction::Transaction, 4 | }; 5 | use spl_associated_token_account::{ 6 | get_associated_token_address, get_associated_token_address_with_program_id, 7 | instruction::create_associated_token_account_idempotent, 8 | }; 9 | use spl_token::{instruction::close_account, ui_amount_to_amount}; 10 | 11 | use crate::{constants::*, jito::get_tip_account}; 12 | use anyhow::Result; 13 | use solana_program::instruction::{AccountMeta, Instruction}; 14 | 15 | pub fn buy_amount_out_ix( 16 | mint: &Pubkey, 17 | bonding_curve: &Pubkey, 18 | associated_bonding_curve: &Pubkey, 19 | wallet: &Pubkey, 20 | associated_token_account: &Pubkey, 21 | amount_out: u64, 22 | max_amount_in_sol: u64, 23 | ) -> Instruction { 24 | let accounts = vec![ 25 | AccountMeta::new_readonly(PUMPFUN_GLOBAL, false), 26 | AccountMeta::new(PUMPFUN_FEE_RECIPIENT, false), 27 | AccountMeta::new_readonly(*mint, false), 28 | AccountMeta::new(*bonding_curve, false), 29 | AccountMeta::new(*associated_bonding_curve, false), 30 | AccountMeta::new(*associated_token_account, false), 31 | AccountMeta::new(*wallet, true), 32 | AccountMeta::new_readonly(solana_program::system_program::id(), false), 33 | AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false), 34 | AccountMeta::new_readonly(SYSTEM_RENT_PROGRAM_ID, false), 35 | AccountMeta::new_readonly(EVENT_AUTHORITY, false), 36 | AccountMeta::new_readonly(PUMPFUN_PROGRAM_ID, false), 37 | ]; 38 | 39 | let mut data: Vec = Vec::new(); 40 | data.extend_from_slice(&[0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea]); 41 | data.extend_from_slice(&amount_out.to_le_bytes()); 42 | data.extend_from_slice(&max_amount_in_sol.to_le_bytes()); 43 | 44 | Instruction { 45 | program_id: PUMPFUN_PROGRAM_ID, 46 | accounts, 47 | data, 48 | } 49 | } 50 | 51 | pub fn sell_amount_in_ix( 52 | mint: &Pubkey, 53 | bonding_curve: &Pubkey, 54 | associated_bonding_curve: &Pubkey, 55 | wallet: &Pubkey, 56 | associated_token_account: &Pubkey, 57 | amount_in: u64, 58 | min_amount_out_sol: u64, 59 | ) -> Instruction { 60 | let accounts = vec![ 61 | AccountMeta::new_readonly(PUMPFUN_GLOBAL, false), 62 | AccountMeta::new(PUMPFUN_FEE_RECIPIENT, false), 63 | AccountMeta::new_readonly(*mint, false), 64 | AccountMeta::new(*bonding_curve, false), 65 | AccountMeta::new(*associated_bonding_curve, false), 66 | AccountMeta::new(*associated_token_account, false), 67 | AccountMeta::new(*wallet, true), 68 | AccountMeta::new_readonly(solana_program::system_program::id(), false), 69 | AccountMeta::new_readonly(ASSOC_TOKEN_ACC_PROGRAM_ID, false), 70 | AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false), 71 | AccountMeta::new_readonly(EVENT_AUTHORITY, false), 72 | AccountMeta::new_readonly(PUMPFUN_PROGRAM_ID, false), 73 | ]; 74 | 75 | let mut data: Vec = Vec::new(); 76 | // 33e685a4017f83ad 77 | data.extend_from_slice(&[0x33, 0xe6, 0x85, 0xa4, 0x01, 0x7f, 0x83, 0xad]); 78 | data.extend_from_slice(&amount_in.to_le_bytes()); 79 | data.extend_from_slice(&min_amount_out_sol.to_le_bytes()); 80 | 81 | Instruction { 82 | program_id: PUMPFUN_PROGRAM_ID, 83 | accounts, 84 | data, 85 | } 86 | } 87 | 88 | pub fn create_buy_transaction( 89 | bonding_curve: &Pubkey, 90 | price: f32, 91 | mint: &Pubkey, 92 | keypair: &Keypair, 93 | amount_in_sol: u64, 94 | slippage_bps: f64, 95 | use_jito: bool, 96 | tip: f64, 97 | recent_block_hash: Hash, 98 | ) -> Transaction { 99 | let owner = keypair.pubkey(); 100 | let amount_out = (amount_in_sol as f32 / price / 1_000.0) as u64; 101 | let max_amount_in_sol = (amount_in_sol as f64 * (10000.0 + slippage_bps) / 10000.0) as u64; 102 | 103 | let mut ixs: Vec = Vec::new(); 104 | 105 | // 不使用jito才给优先费 106 | // If not using jito, give priority fee 107 | if !use_jito { 108 | let modify_compute_units = ComputeBudgetInstruction::set_compute_unit_limit(*UNIT_LIMIT); 109 | let add_priority_fee = ComputeBudgetInstruction::set_compute_unit_price(*UNIT_PRICE); 110 | ixs.insert(0, modify_compute_units); 111 | ixs.insert(1, add_priority_fee); 112 | } 113 | 114 | let create_ata_ix = 115 | create_associated_token_account_idempotent(&owner, &owner, mint, &spl_token::id()); 116 | 117 | ixs.push(create_ata_ix.clone()); 118 | let token_ata = create_ata_ix.accounts[1].pubkey; 119 | 120 | let curve_ata = get_associated_token_address(&bonding_curve, mint); 121 | ixs.push(buy_amount_out_ix( 122 | mint, 123 | &bonding_curve, 124 | &curve_ata, 125 | &owner, 126 | &token_ata, 127 | amount_out, 128 | max_amount_in_sol, 129 | )); 130 | 131 | if use_jito { 132 | let tip_account = get_tip_account().unwrap(); 133 | let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); 134 | ixs.push(system_instruction::transfer( 135 | &owner, 136 | &tip_account, 137 | tip_lamports, 138 | )); 139 | } 140 | 141 | let tx = Transaction::new_signed_with_payer( 142 | &ixs.to_vec(), 143 | Some(&owner), 144 | &[&keypair], 145 | recent_block_hash, 146 | ); 147 | 148 | tx 149 | } 150 | 151 | pub fn create_sell_transaction( 152 | bonding_curve: &Pubkey, 153 | price: f32, 154 | mint: &Pubkey, 155 | keypair: &Keypair, 156 | amount_in_token: u64, 157 | slippage_bps: f64, 158 | use_jito: bool, 159 | tip: f64, 160 | recent_block_hash: Hash, 161 | ) -> Result { 162 | let owner = keypair.pubkey(); 163 | 164 | let amount_out_sol = (amount_in_token as f32 * price * 1000.0) as u64; 165 | let min_amount_out_sol = (amount_out_sol as f64 * (10000.0 - slippage_bps) / 10000.0) as u64; 166 | 167 | let mut ixs: Vec = Vec::new(); 168 | 169 | let modify_compute_units = ComputeBudgetInstruction::set_compute_unit_limit(*UNIT_LIMIT); 170 | let add_priority_fee = ComputeBudgetInstruction::set_compute_unit_price(*UNIT_PRICE); 171 | ixs.insert(0, modify_compute_units); 172 | ixs.insert(1, add_priority_fee); 173 | 174 | let token_ata = get_associated_token_address_with_program_id(&owner, mint, &spl_token::id()); 175 | 176 | ixs.push(sell_amount_in_ix( 177 | mint, 178 | &bonding_curve, 179 | &get_associated_token_address(bonding_curve, mint), 180 | &owner, 181 | &token_ata, 182 | amount_in_token, 183 | min_amount_out_sol, 184 | )); 185 | 186 | if use_jito { 187 | let tip_account = get_tip_account().unwrap(); 188 | let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); 189 | ixs.push(system_instruction::transfer( 190 | &owner, 191 | &tip_account, 192 | tip_lamports, 193 | )); 194 | } 195 | 196 | ixs.push(close_account( 197 | &spl_token::id(), 198 | &token_ata, 199 | &owner, 200 | &owner, 201 | &[], 202 | )?); 203 | 204 | let tx = Transaction::new_signed_with_payer( 205 | &ixs.to_vec(), 206 | Some(&owner), 207 | &[&keypair], 208 | recent_block_hash, 209 | ); 210 | 211 | Ok(tx) 212 | } 213 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use borsh::{BorshDeserialize, BorshSerialize}; 3 | use solana_sdk::{bs58, pubkey::Pubkey}; 4 | use solana_transaction_status::{UiCompiledInstruction, UiInstruction}; 5 | 6 | 7 | const PUMPFUN_CREATE_EVENT: [u8; 8] = [27, 114, 169, 77, 222, 235, 99, 118]; 8 | const PUMPFUN_COMPLETE_EVENT: [u8; 8] = [95, 114, 97, 156, 212, 46, 152, 8]; 9 | const PUMPFUN_TRADE_EVENT: [u8; 8] = [189, 219, 127, 211, 78, 230, 97, 238]; 10 | 11 | #[derive(Debug, Clone)] 12 | pub enum TargetEvent { 13 | PumpfunBuy(TradeEvent), 14 | PumpfunSell(TradeEvent), 15 | PumpfunCreate(CreateEvent), 16 | PumpfunComplete(CompleteEvent), 17 | } 18 | 19 | impl TryFrom for TargetEvent { 20 | type Error = anyhow::Error; 21 | 22 | fn try_from(inner_instruction: UiInstruction) -> Result { 23 | // 处理每一条指令 24 | match inner_instruction { 25 | solana_transaction_status::UiInstruction::Compiled(ui_compiled_instruction) => { 26 | if let Some(create) = 27 | CreateEvent::try_from_compiled_instruction(&ui_compiled_instruction) 28 | { 29 | return Ok(TargetEvent::PumpfunCreate(create)); 30 | } 31 | if let Some(complete) = 32 | CompleteEvent::try_from_compiled_instruction(&ui_compiled_instruction) 33 | { 34 | return Ok(Self::PumpfunComplete(complete)); 35 | } 36 | if let Some(trade) = 37 | TradeEvent::try_from_compiled_instruction(&ui_compiled_instruction) 38 | { 39 | if trade.is_buy { 40 | return Ok(TargetEvent::PumpfunBuy(trade)); 41 | } else { 42 | return Ok(TargetEvent::PumpfunSell(trade)); 43 | } 44 | } 45 | } 46 | _ => {} 47 | } 48 | return Err(anyhow!("failed to convert to target tx")); 49 | } 50 | } 51 | 52 | #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] 53 | pub struct CreateEvent { 54 | pub name: String, 55 | pub symbol: String, 56 | pub uri: String, 57 | pub mint: Pubkey, 58 | pub bonding_curve: Pubkey, 59 | pub user: Pubkey, 60 | } 61 | 62 | impl CreateEvent { 63 | pub fn try_from_compiled_instruction( 64 | ui_compiled_instruction: &UiCompiledInstruction, 65 | ) -> Option { 66 | let data = bs58::decode(ui_compiled_instruction.data.clone()) 67 | .into_vec() 68 | .unwrap(); 69 | if data.len() > 16 && data[8..16].eq(&PUMPFUN_CREATE_EVENT) { 70 | match CreateEvent::try_from_slice(&data[16..]) { 71 | Ok(event) => return Some(event), 72 | Err(_) => return None, 73 | } 74 | } else { 75 | return None; 76 | } 77 | } 78 | } 79 | 80 | #[derive(Debug, BorshSerialize, Clone, BorshDeserialize, Copy)] 81 | pub struct CompleteEvent { 82 | pub user: Pubkey, 83 | pub mint: Pubkey, 84 | pub bonding_curve: Pubkey, 85 | pub timestamp: i64, 86 | } 87 | 88 | impl CompleteEvent { 89 | pub fn try_from_compiled_instruction( 90 | ui_compiled_instruction: &UiCompiledInstruction, 91 | ) -> Option { 92 | let data = bs58::decode(ui_compiled_instruction.data.clone()) 93 | .into_vec() 94 | .unwrap(); 95 | if data.len() > 16 && data[8..16].eq(&PUMPFUN_COMPLETE_EVENT) { 96 | match CompleteEvent::try_from_slice(&data[16..]) { 97 | Ok(event) => return Some(event), 98 | Err(_) => return None, 99 | } 100 | } else { 101 | return None; 102 | } 103 | } 104 | } 105 | 106 | #[derive(Debug, BorshSerialize, Clone, BorshDeserialize)] 107 | pub struct BuyArgs { 108 | pub amount: u64, 109 | pub max_sol_cost: u64, 110 | } 111 | 112 | #[derive(Debug, BorshSerialize, Clone, BorshDeserialize, Copy)] 113 | pub struct TradeEvent { 114 | pub mint: Pubkey, 115 | pub sol_amount: u64, 116 | pub token_amount: u64, 117 | pub is_buy: bool, 118 | pub user: Pubkey, 119 | pub timestamp: i64, 120 | pub virtual_sol_reserves: u64, 121 | pub virtual_token_reserves: u64, 122 | pub real_sol_reserves: u64, 123 | pub real_token_reserves: u64, 124 | } 125 | 126 | impl TradeEvent { 127 | pub fn try_from_compiled_instruction( 128 | ui_compiled_instruction: &UiCompiledInstruction, 129 | ) -> Option { 130 | let data = bs58::decode(ui_compiled_instruction.data.clone()) 131 | .into_vec() 132 | .unwrap(); 133 | if data.len() > 16 && data[8..16].eq(&PUMPFUN_TRADE_EVENT) { 134 | match TradeEvent::try_from_slice(&data[16..]) { 135 | Ok(event) => return Some(event), 136 | Err(_) => return None, 137 | } 138 | } else { 139 | return None; 140 | } 141 | } 142 | } 143 | 144 | #[derive(Debug, Clone)] 145 | pub enum Reason { 146 | USUAL, 147 | WHITE, 148 | } 149 | 150 | #[derive(Debug, Clone)] 151 | pub struct BuyAndReason { 152 | pub mint: Pubkey, 153 | pub reason: Reason, 154 | pub price: f32, 155 | } 156 | 157 | #[tokio::test] 158 | async fn test() { 159 | let data = "2K7nL28PxCW8ejnyCeuMpbYAmP2pnuyvkxEQgp79nsKJzbKfMq82LAVFjwFY1xYhKmuaA8H3M5xLfFnF85Xbai9s9aaCyDETZgWMQJayFp8t1HM9ihUxb1TCcsXYVsNKDqaGANFoxSEAPLvpAXJVQHTNyAMxFcgM9s3knpLcDTYtGe7Ufq3WZ9kvAGdd"; 160 | let data = bs58::decode(data.as_bytes()).into_vec().unwrap(); 161 | println!("data {:?}", data); 162 | let result = TradeEvent::try_from_slice(&data[16..]).unwrap(); 163 | println!("result {:?}", result); 164 | } 165 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, time::Duration}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use base64::{engine::general_purpose, Engine}; 5 | use jito_sdk_rust::JitoJsonRpcSDK; 6 | use reqwest::Client; 7 | use serde_json::{json, Value}; 8 | use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction}; 9 | use solana_transaction_status::{EncodedTransactionWithStatusMeta, UiTransactionEncoding}; 10 | use tracing::warn; 11 | use yellowstone_grpc_proto::{convert_from, geyser::SubscribeUpdateTransactionInfo}; 12 | 13 | use crate::constants::PUMPFUN_PROGRAM_ID; 14 | pub fn convert_to_encoded_tx( 15 | tx_info: SubscribeUpdateTransactionInfo, 16 | ) -> Result { 17 | convert_from::create_tx_with_meta(tx_info) 18 | .unwrap() 19 | .encode(UiTransactionEncoding::Base64, Some(u8::MAX), true) 20 | .map_err(|e| anyhow!("{}", e)) 21 | } 22 | 23 | pub fn cal_pumpfun_price(virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f32 { 24 | (virtual_sol_reserves as f32 / 10f32.powi(9)) / (virtual_token_reserves as f32 / 10f32.powi(6)) 25 | } 26 | 27 | pub fn cal_pumpfun_marketcap(price: f32) -> f32 { 28 | price * 1_000_000_000_000.0 29 | } 30 | 31 | pub async fn have_tg_or_x(client: &Client, mint: &str) -> Result { 32 | let response = client 33 | .get(format!( 34 | "https://frontend-api.pump.fun/coins/{mint}?sync=false" 35 | )) 36 | .timeout(Duration::from_millis(300)) 37 | .send() 38 | .await?; 39 | 40 | if response.status().is_success() { 41 | let data: Value = response.json().await?; 42 | if data.get("twitter").is_some() 43 | || data.get("telegram").is_some() 44 | || data.get("website").is_some() 45 | { 46 | return Ok(true); 47 | } 48 | } 49 | 50 | Ok(false) 51 | } 52 | 53 | pub async fn send_txn_with_retry( 54 | jito_sdk: &JitoJsonRpcSDK, 55 | txn: Transaction, 56 | max_retries: u8, 57 | ) -> Result { 58 | let serialized_tx = general_purpose::STANDARD.encode(bincode::serialize(&txn)?); 59 | 60 | let params = json!({ 61 | "tx": serialized_tx 62 | }); 63 | 64 | // 发送交易 65 | let mut retries = 0; 66 | while retries < max_retries { 67 | match jito_sdk.send_txn(Some(params.clone()), false).await { 68 | Ok(resp) => match resp["result"].as_str() { 69 | Some(signature) => { 70 | return Ok(Signature::from_str(signature)?); 71 | } 72 | None => { 73 | retries += 1; 74 | if let Some(mes) = resp["message"].as_str() { 75 | if mes.contains("bundle contains an already processed transaction") { 76 | return Ok(Signature::new_unique()); 77 | } 78 | } 79 | warn!("failed to send transaction to jito, retry"); 80 | } 81 | }, 82 | Err(e) => { 83 | retries += 1; 84 | warn!("try to send transaction to jito failed, error: {}", e); 85 | } 86 | } 87 | tokio::time::sleep(Duration::from_secs(1)).await 88 | } 89 | 90 | Err(anyhow!("failed to send transaction to jito after {} retries", max_retries)) 91 | } 92 | 93 | pub fn find_bonding_curve(mint: &Pubkey) -> Pubkey { 94 | Pubkey::find_program_address( 95 | &["bonding-curve".as_bytes(), mint.as_ref()], 96 | &PUMPFUN_PROGRAM_ID, 97 | ) 98 | .0 99 | } 100 | -------------------------------------------------------------------------------- /white_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | "EKrqffucmkxNKgvvbvfKtEki4k5KFPEh3zMjnriwYgta", 3 | "Ea6D8vVUJVKt4KA4StYpdapaE4EMFdPh19ALykUGQCXz", 4 | "25VWtPuHrTGyKKk5jWvycbjdbyrhbMRR9svCqFtU3yD8", 5 | "BposQY3dk111pNXmBUepaFthb3xd9GZpcmRZjpDU9Yxh", 6 | "J7hr8R2Q8jfEgXo5FQobi1QHTzDSZVnxtdUqKKMsjGS4", 7 | "4wWPrUmcfxtSH6Xqfe6BXMwXgTR6sfC6kwPYMjravvzR", 8 | "F1JBobKe7WrqGLwYyqEihZWgrGdaiWjTuM5HWq1a4vfZ", 9 | "5fNJbfzz1num6Uh1BMsS1T26xqz7Fob7VKNxh5Kqxkiz", 10 | "6XNsRqJxTx3TySo9H6fxfxcHRwLj49NF1iyWEbf8MTmv", 11 | "FumkskjpkpnFSDYuXgnAx2ozMnu1q5wp2iJcRw4XpLFy", 12 | "DDt8X5nEzHPghcnpfBH2DpmS5KFLjfZXaMzn5RZtyYxp", 13 | "73cWcbp6mcGaSDENhdvft78vZBqRiCXzQNn1juGB2jPZ", 14 | "2RiwTkR9ys9jGiK42Hw1LYPJxAww5QeyiNbA4vz1VVCw", 15 | "3uCetEiVgqWu8kL3sdaBBVqjL9JaAPSNLmbigEYFoGyi", 16 | "4PkTnSMTGUsRZvUZYjzyDL37Jmt3MjacggkQUzMCdKmh", 17 | "oxvpB4fcAZ8mAJxXTNjVaoVV6QFDuEn32WSYBUMLErU", 18 | "2tti4Bjx4m6y6H8Ja5bQKBqbc1dMPtoQ96FaqTtdhGVc", 19 | "JAh9gZ2MTnJDmd93o9nzvsWahUHQMpqPUacfm6NrG8hr", 20 | "CqGQ7ADamq6rKKyU6BLHYTHQMpvf7PcaQKRA5pzckCwK", 21 | "HEm6nxtQjhRgTseNVikwieMfHMERagGbvMawR69BbYzJ", 22 | "GUdA1sLkkwPPrtxeVK3Krndgu7qJThyFopJv6LxzoVwN", 23 | "ArzWfhQ2nRWpmAHuXcVjrvxpMJFvUDSJRY7zXZdCo79p", 24 | "B38NjDDrAjQLDAJAFEfnt14m75Dkg46vqTUgnmB3vFBV", 25 | "6jcBXGPBCC21qhAL9zugmBttzKwBhBrQ9SyKsynf4P2q", 26 | "2ks5LTa7bhh2ZT31BZUjcGQVycjmZkx8Z8CuBBWZKKHY", 27 | "8HBHCLvUUHsMP36EwA2ey67MA2s4aJhUMfmadyrhZLRr", 28 | "G2zn7nKuZvbTNmzigeSaz8sLhLyMDYzP79yoGtqZMt4C", 29 | "JAzYeATyRMEBSm34NXocSBGvLLG3xD1FtLtYH3fCkhWf", 30 | "35zGrNRZDLb1vrDExejQBCsKsweXsRihYZhfam5vvYfG", 31 | "Fji2MVVFpCQcxLSXRPgmVAorGoHtDaQnDtfwfvfCf7Wv", 32 | "EtVoP8teT9hizSxfE2zcLo5HNTue48Pv6fejx1L8JK4R", 33 | "7BrcwKY9KSMNsZbUriSP7YcNuC3xfLv4V2XCF9r4XcBU", 34 | "bLFirKg7tjPSQwEFSixgJ5PuNnFHKsb8yUWVEEfPMgv", 35 | "FXeVU9Ust2DApu4U7dfftBSMtwsZm8TKJmmy7buJ35zD", 36 | "GxceSANEY9hnqK3NWA992wk2i4Qj42MoubribSZrLGcG", 37 | "5B34Atcyv7gqGvPR9BRgqYqfGFkbSdydt1Vu8z14d1Xp", 38 | "3EC3Ckbc2KHtDZF3z7KnqS1GGXhW1sBuYgCCfjJUPa9U", 39 | "JEBnKvYUgpGXNEEJPTr51mjLiNU5Hse1rfc5h11zz8qP", 40 | "BAMeVWyovTDYuSMSn2aWk8b8eY8vsNdqa8URXjsHUekw", 41 | "HQKRiZCreXr8WsgDUPqQfftrrHK4XLhacy4yyjnowYx" 42 | ] --------------------------------------------------------------------------------