├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── accounts ├── bonding_curve.rs ├── global.rs └── mod.rs ├── common ├── logs_data.rs ├── logs_events.rs ├── logs_filters.rs ├── logs_parser.rs ├── logs_subscribe.rs ├── mod.rs └── types.rs ├── constants └── mod.rs ├── error └── mod.rs ├── grpc └── mod.rs ├── instruction └── mod.rs ├── ipfs └── mod.rs ├── lib.rs ├── main.rs ├── pumpfun ├── buy.rs ├── common.rs ├── mod.rs └── sell.rs └── swqos ├── api.rs ├── common.rs ├── jito_grpc ├── bundle.rs ├── convert.rs ├── mod.rs ├── packet.rs ├── searcher.rs └── shared.rs ├── mod.rs └── searcher_client.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # RustRover 17 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 18 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 | # and can be added to the global gitignore or merged into this file. For a more nuclear 20 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 | #.idea/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pumpfun-sdk" 3 | version = "2.4.3" 4 | edition = "2021" 5 | authors = ["William "] 6 | repository = "https://github.com/MiracleAI-Labs/pumpfun-sdk" 7 | description = "Rust SDK to interact with the Pump.fun Solana program." 8 | license = "MIT" 9 | keywords = ["solana", "memecoins", "pumpfun", "pumpfun-sdk", "pumpbot"] 10 | readme = "README.md" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [dependencies] 16 | solana-sdk = "2.1.16" 17 | solana-client = "2.1.16" 18 | solana-program = "2.1.16" 19 | solana-rpc-client = "2.1.16" 20 | solana-rpc-client-api = "2.1.16" 21 | solana-transaction-status = "2.1.16" 22 | solana-account-decoder = "2.1.16" 23 | solana-hash = "2.1.16" 24 | solana-perf = "2.1.16" 25 | solana-security-txt = "1.1.1" 26 | 27 | spl-token = "8.0.0" 28 | spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] } 29 | spl-associated-token-account = "6.0.0" 30 | mpl-token-metadata = "5.1.0" 31 | 32 | borsh = { version = "1.5.3", features = ["derive"] } 33 | isahc = "1.7.2" 34 | serde = { version = "1.0.215", features = ["derive"] } 35 | serde_json = "1.0.134" 36 | futures = "0.3.31" 37 | futures-util = "0.3.31" 38 | base64 = "0.22.1" 39 | bs58 = "0.5.1" 40 | rand = "0.9.0" 41 | bincode = "1.3.3" 42 | anyhow = "1.0.90" 43 | yellowstone-grpc-client = { version = "6.0.0" } 44 | yellowstone-grpc-proto = { version = "6.0.0" } 45 | reqwest = { version = "0.12.12", features = ["json", "multipart"] } 46 | tokio = { version = "1.42.0" , features = ["full", "rt-multi-thread"]} 47 | tonic = { version = "0.12.3", features = ["tls", "tls-roots", "tls-webpki-roots"] } 48 | rustls = { version = "0.23.23", features = ["ring"] } 49 | rustls-native-certs = "0.8.1" 50 | tokio-rustls = "0.26.1" 51 | 52 | bytes = "1.4.0" 53 | dotenvy = "0.15.7" 54 | pretty_env_logger = "0.5.0" 55 | log = "0.4.22" 56 | chrono = "0.4.39" 57 | regex = "1" 58 | tracing = "0.1.41" 59 | thiserror = "2.0.11" 60 | async-trait = "0.1.86" 61 | lazy_static = "1.5.0" 62 | once_cell = "1.20.3" 63 | prost = "0.13.5" 64 | prost-types = "0.13.5" 65 | arrform = { git = "https://github.com/raydium-io/arrform" } 66 | num_enum = "0.7.3" 67 | num-derive = "0.4.2" 68 | num-traits = "0.2.19" 69 | uint = "0.10.0" 70 | clap = { version = "4.5.31", features = ["derive"] } 71 | 72 | hex = "0.4.3" 73 | bytemuck = { version = "1.4.0" } 74 | safe-transmute = "0.11.0" 75 | enumflags2 = "0.6.4" 76 | static_assertions = "1.1.0" 77 | demand = "1.2.2" 78 | arrayref = "0.3.6" 79 | default-env = "0.1.1" 80 | 81 | borsh-derive = "1.5.5" 82 | axum = { version = "0.8.1", features = ["macros"] } 83 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 84 | tokio-tungstenite = { version = "0.26.1", features = ["native-tls"] } 85 | indicatif = "0.17.11" 86 | toml = "0.8.20" 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 MiracleAI-Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PumpFun Rust SDK 2 | 3 | A comprehensive Rust SDK for seamless interaction with the PumpFun Solana program. This SDK provides a robust set of tools and interfaces to integrate PumpFun functionality into your applications. 4 | 5 | 6 | # Explanation 7 | 1. Add `create, buy, sell` for pump.fun. 8 | 2. Add `logs_subscribe` to subscribe the logs of the PumpFun program. 9 | 3. Add `yellowstone grpc` to subscribe the logs of the PumpFun program. 10 | 4. Add `jito` to send transaction with Jito. 11 | 5. Add `nextblock` to send transaction with nextblock. 12 | 6. Add `0slot` to send transaction with 0slot. 13 | 7. Submit a transaction using Jito, Nextblock, and 0slot simultaneously; the fastest one will succeed, while the others will fail. 14 | 15 | ## Usage 16 | ```shell 17 | cd `your project root directory` 18 | git clone https://github.com/0xfnzero/pumpfun-sdk 19 | ``` 20 | 21 | ```toml 22 | # add to your Cargo.toml 23 | pumpfun-sdk = { path = "./pumpfun-sdk", version = "2.4.3" } 24 | ``` 25 | 26 | ### logs subscription for token create and trade transaction 27 | ```rust 28 | use pumpfun_sdk::{common::logs_events::PumpfunEvent, grpc::YellowstoneGrpc}; 29 | 30 | // create grpc client 31 | let grpc_url = "http://127.0.0.1:10000"; 32 | let client = YellowstoneGrpc::new(grpc_url); 33 | 34 | // Define callback function 35 | let callback = |event: PumpfunEvent| { 36 | match event { 37 | PumpfunEvent::NewToken(token_info) => { 38 | println!("Received new token event: {:?}", token_info); 39 | }, 40 | PumpfunEvent::NewDevTrade(trade_info) => { 41 | println!("Received dev trade event: {:?}", trade_info); 42 | }, 43 | PumpfunEvent::NewUserTrade(trade_info) => { 44 | println!("Received new trade event: {:?}", trade_info); 45 | }, 46 | PumpfunEvent::NewBotTrade(trade_info) => { 47 | println!("Received new bot trade event: {:?}", trade_info); 48 | } 49 | PumpfunEvent::Error(err) => { 50 | println!("Received error: {}", err); 51 | } 52 | } 53 | }; 54 | 55 | let payer_keypair = Keypair::from_base58_string("your private key"); 56 | client.subscribe_pumpfun(callback, Some(payer_keypair.pubkey())).await?; 57 | ``` 58 | 59 | ### Init pumpfun instance for configs 60 | ```rust 61 | use std::sync::Arc; 62 | use pumpfun_sdk::{common::{Cluster, PriorityFee}, PumpFun}; 63 | use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair, signer::Signer}; 64 | 65 | let priority_fee = PriorityFee{ 66 | unit_limit: 190000, 67 | unit_price: 1000000, 68 | buy_tip_fee: 0.001, 69 | sell_tip_fee: 0.0001, 70 | }; 71 | 72 | let cluster = Cluster { 73 | rpc_url: "https://api.mainnet-beta.solana.com".to_string(), 74 | block_engine_url: "https://block-engine.example.com".to_string(), 75 | nextblock_url: "https://nextblock.example.com".to_string(), 76 | nextblock_auth_token: "nextblock_api_token".to_string(), 77 | zeroslot_url: "https://zeroslot.example.com".to_string(), 78 | zeroslot_auth_token: "zeroslot_api_token".to_string(), 79 | use_jito: true, 80 | use_nextblock: false, 81 | use_zeroslot: false, 82 | priority_fee, 83 | commitment: CommitmentConfig::processed(), 84 | }; 85 | 86 | // create pumpfun instance 87 | let payer = Keypair::from_base58_string("your private key"); 88 | let pumpfun = PumpFun::new( 89 | Arc::new(payer), 90 | &cluster, 91 | ).await; 92 | ``` 93 | 94 | ### pumpfun buy token 95 | ```rust 96 | use pumpfun_sdk::PumpFun; 97 | use solana_sdk::{native_token::sol_to_lamports, signature::Keypair, signer::Signer}; 98 | 99 | // create pumpfun instance 100 | let pumpfun = PumpFun::new(Arc::new(payer), &cluster).await; 101 | 102 | // Mint keypair 103 | let mint_pubkey: Keypair = Keypair::new(); 104 | 105 | // buy token with tip 106 | pumpfun.buy_with_tip(mint_pubkey, 10000, None).await?; 107 | 108 | ``` 109 | 110 | ### pumpfun sell token 111 | ```rust 112 | use pumpfun_sdk::PumpFun; 113 | use solana_sdk::{native_token::sol_to_lamports, signature::Keypair, signer::Signer}; 114 | 115 | // create pumpfun instance 116 | let pumpfun = PumpFun::new(Arc::new(payer), &cluster).await; 117 | 118 | // sell token with tip 119 | pumpfun.sell_with_tip(mint_pubkey, 100000, None).await?; 120 | 121 | // sell token by percent with tip 122 | pumpfun.sell_by_percent_with_tip(mint_pubkey, 100, None).await?; 123 | 124 | ``` 125 | 126 | ### Telegram group: 127 | https://t.me/fnzero_group 128 | -------------------------------------------------------------------------------- /src/accounts/bonding_curve.rs: -------------------------------------------------------------------------------- 1 | //! Bonding curve account for the Pump.fun Solana Program 2 | //! 3 | //! This module contains the definition for the bonding curve account. 4 | //! 5 | //! # Bonding Curve Account 6 | //! 7 | //! The bonding curve account is used to manage token pricing and liquidity. 8 | //! 9 | //! # Fields 10 | //! 11 | //! - `discriminator`: Unique identifier for the bonding curve 12 | //! - `virtual_token_reserves`: Virtual token reserves used for price calculations 13 | //! - `virtual_sol_reserves`: Virtual SOL reserves used for price calculations 14 | //! - `real_token_reserves`: Actual token reserves available for trading 15 | //! - `real_sol_reserves`: Actual SOL reserves available for trading 16 | //! - `token_total_supply`: Total supply of tokens 17 | //! - `complete`: Whether the bonding curve is complete/finalized 18 | //! 19 | //! # Methods 20 | //! 21 | //! - `new`: Creates a new bonding curve instance 22 | //! - `get_buy_price`: Calculates the amount of tokens received for a given SOL amount 23 | //! - `get_sell_price`: Calculates the amount of SOL received for selling tokens 24 | //! - `get_market_cap_sol`: Calculates the current market cap in SOL 25 | //! - `get_final_market_cap_sol`: Calculates the final market cap in SOL after all tokens are sold 26 | //! - `get_buy_out_price`: Calculates the price to buy out all remaining tokens 27 | 28 | use serde::{Serialize, Deserialize}; 29 | use solana_sdk::pubkey::Pubkey; 30 | 31 | /// Represents the global configuration account for token pricing and fees 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub struct BondingCurveAccount { 34 | /// Unique identifier for the bonding curve 35 | pub discriminator: u64, 36 | /// Virtual token reserves used for price calculations 37 | pub virtual_token_reserves: u64, 38 | /// Virtual SOL reserves used for price calculations 39 | pub virtual_sol_reserves: u64, 40 | /// Actual token reserves available for trading 41 | pub real_token_reserves: u64, 42 | /// Actual SOL reserves available for trading 43 | pub real_sol_reserves: u64, 44 | /// Total supply of tokens 45 | pub token_total_supply: u64, 46 | /// Whether the bonding curve is complete/finalized 47 | pub complete: bool, 48 | /// Creator of the bonding curve 49 | pub creator: Pubkey, 50 | } 51 | 52 | impl BondingCurveAccount { 53 | /// Creates a new bonding curve instance 54 | /// 55 | /// # Arguments 56 | /// * `discriminator` - Unique identifier for the curve 57 | /// * `virtual_token_reserves` - Virtual token reserves for price calculations 58 | /// * `virtual_sol_reserves` - Virtual SOL reserves for price calculations 59 | /// * `real_token_reserves` - Actual token reserves available 60 | /// * `real_sol_reserves` - Actual SOL reserves available 61 | /// * `token_total_supply` - Total supply of tokens 62 | /// * `complete` - Whether the curve is complete 63 | // pub fn new(mint: &Pubkey, dev_buy_token_amount: u64, dev_buy_sol_amount: u64) -> Self { 64 | // Self { 65 | // // account: get_bonding_curve_pda(mint).unwrap(), 66 | // virtual_token_reserves: INITIAL_VIRTUAL_TOKEN_RESERVES - dev_buy_token_amount, 67 | // virtual_sol_reserves: INITIAL_VIRTUAL_SOL_RESERVES + dev_buy_sol_amount, 68 | // real_token_reserves: INITIAL_REAL_TOKEN_RESERVES - dev_buy_token_amount, 69 | // real_sol_reserves: dev_buy_sol_amount, 70 | // token_total_supply: TOKEN_TOTAL_SUPPLY, 71 | // complete: false, 72 | // } 73 | // } 74 | 75 | /// Calculates the amount of tokens received for a given SOL amount 76 | /// 77 | /// # Arguments 78 | /// * `amount` - Amount of SOL to spend 79 | /// 80 | /// # Returns 81 | /// * `Ok(u64)` - Amount of tokens that would be received 82 | /// * `Err(&str)` - Error message if curve is complete 83 | pub fn get_buy_price(&self, amount: u64) -> Result { 84 | if self.complete { 85 | return Err("Curve is complete"); 86 | } 87 | 88 | if amount == 0 { 89 | return Ok(0); 90 | } 91 | 92 | // Calculate the product of virtual reserves using u128 to avoid overflow 93 | let n: u128 = (self.virtual_sol_reserves as u128) * (self.virtual_token_reserves as u128); 94 | 95 | // Calculate the new virtual sol reserves after the purchase 96 | let i: u128 = (self.virtual_sol_reserves as u128) + (amount as u128); 97 | 98 | // Calculate the new virtual token reserves after the purchase 99 | let r: u128 = n / i + 1; 100 | 101 | // Calculate the amount of tokens to be purchased 102 | let s: u128 = (self.virtual_token_reserves as u128) - r; 103 | 104 | // Convert back to u64 and return the minimum of calculated tokens and real reserves 105 | let s_u64 = s as u64; 106 | Ok(if s_u64 < self.real_token_reserves { 107 | s_u64 108 | } else { 109 | self.real_token_reserves 110 | }) 111 | } 112 | 113 | /// Calculates the amount of SOL received for selling tokens 114 | /// 115 | /// # Arguments 116 | /// * `amount` - Amount of tokens to sell 117 | /// * `fee_basis_points` - Fee in basis points (1/100th of a percent) 118 | /// 119 | /// # Returns 120 | /// * `Ok(u64)` - Amount of SOL that would be received after fees 121 | /// * `Err(&str)` - Error message if curve is complete 122 | pub fn get_sell_price(&self, amount: u64, fee_basis_points: u64) -> Result { 123 | if self.complete { 124 | return Err("Curve is complete"); 125 | } 126 | 127 | if amount == 0 { 128 | return Ok(0); 129 | } 130 | 131 | // Calculate the proportional amount of virtual sol reserves to be received using u128 132 | let n: u128 = ((amount as u128) * (self.virtual_sol_reserves as u128)) 133 | / ((self.virtual_token_reserves as u128) + (amount as u128)); 134 | 135 | // Calculate the fee amount in the same units 136 | let a: u128 = (n * (fee_basis_points as u128)) / 10000; 137 | 138 | // Return the net amount after deducting the fee, converting back to u64 139 | Ok((n - a) as u64) 140 | } 141 | 142 | /// Calculates the current market cap in SOL 143 | pub fn get_market_cap_sol(&self) -> u64 { 144 | if self.virtual_token_reserves == 0 { 145 | return 0; 146 | } 147 | 148 | ((self.token_total_supply as u128) * (self.virtual_sol_reserves as u128) 149 | / (self.virtual_token_reserves as u128)) as u64 150 | } 151 | 152 | /// Calculates the final market cap in SOL after all tokens are sold 153 | /// 154 | /// # Arguments 155 | /// * `fee_basis_points` - Fee in basis points (1/100th of a percent) 156 | pub fn get_final_market_cap_sol(&self, fee_basis_points: u64) -> u64 { 157 | let total_sell_value: u128 = 158 | self.get_buy_out_price(self.real_token_reserves, fee_basis_points) as u128; 159 | let total_virtual_value: u128 = (self.virtual_sol_reserves as u128) + total_sell_value; 160 | let total_virtual_tokens: u128 = 161 | (self.virtual_token_reserves as u128) - (self.real_token_reserves as u128); 162 | 163 | if total_virtual_tokens == 0 { 164 | return 0; 165 | } 166 | 167 | ((self.token_total_supply as u128) * total_virtual_value / total_virtual_tokens) as u64 168 | } 169 | 170 | /// Calculates the price to buy out all remaining tokens 171 | /// 172 | /// # Arguments 173 | /// * `amount` - Amount of tokens to buy 174 | /// * `fee_basis_points` - Fee in basis points (1/100th of a percent) 175 | pub fn get_buy_out_price(&self, amount: u64, fee_basis_points: u64) -> u64 { 176 | // Get the effective amount of sol tokens 177 | let sol_tokens: u128 = if amount < self.real_sol_reserves { 178 | self.real_sol_reserves as u128 179 | } else { 180 | amount as u128 181 | }; 182 | 183 | // Calculate total sell value 184 | let total_sell_value: u128 = (sol_tokens * (self.virtual_sol_reserves as u128)) 185 | / ((self.virtual_token_reserves as u128) - sol_tokens) 186 | + 1; 187 | 188 | // Calculate fee 189 | let fee: u128 = (total_sell_value * (fee_basis_points as u128)) / 10000; 190 | 191 | // Return total including fee, converting back to u64 192 | (total_sell_value + fee) as u64 193 | } 194 | 195 | pub fn get_token_price(&self) -> f64 { 196 | let v_sol = self.virtual_sol_reserves as f64 / 100_000_000.0; 197 | let v_tokens = self.virtual_token_reserves as f64 / 100_000.0; 198 | let token_price = v_sol / v_tokens; 199 | token_price 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/accounts/global.rs: -------------------------------------------------------------------------------- 1 | //! Global account for the Pump.fun Solana Program 2 | //! 3 | //! This module contains the definition for the global configuration account. 4 | //! 5 | //! # Global Account 6 | //! 7 | //! The global account is used to store the global configuration for the Pump.fun program. 8 | //! 9 | //! # Fields 10 | //! 11 | //! - `discriminator`: Unique identifier for the global account 12 | //! - `initialized`: Whether the global account has been initialized 13 | //! - `authority`: Authority pubkey that can modify settings 14 | //! - `fee_recipient`: Account that receives fees 15 | //! - `initial_virtual_token_reserves`: Initial virtual token reserves for price calculations 16 | //! - `initial_virtual_sol_reserves`: Initial virtual SOL reserves for price calculations 17 | //! - `initial_real_token_reserves`: Initial actual token reserves available for trading 18 | //! - `token_total_supply`: Total supply of tokens 19 | //! - `fee_basis_points`: Fee in basis points (1/100th of a percent) 20 | //! 21 | //! # Methods 22 | //! 23 | //! - `new`: Creates a new global account instance 24 | //! - `get_initial_buy_price`: Calculates the initial amount of tokens received for a given SOL amount 25 | 26 | use solana_sdk::pubkey::Pubkey; 27 | use borsh::{BorshDeserialize, BorshSerialize}; 28 | use serde::{Serialize, Deserialize}; 29 | /// Represents the global configuration account for token pricing and fees 30 | #[derive(Debug, Clone, Serialize, Deserialize)] 31 | pub struct GlobalAccount { 32 | /// Unique identifier for the global account 33 | pub discriminator: u64, 34 | /// Whether the global account has been initialized 35 | pub initialized: bool, 36 | /// Authority that can modify global settings 37 | pub authority: Pubkey, 38 | /// Account that receives fees 39 | pub fee_recipient: Pubkey, 40 | /// Initial virtual token reserves for price calculations 41 | pub initial_virtual_token_reserves: u64, 42 | /// Initial virtual SOL reserves for price calculations 43 | pub initial_virtual_sol_reserves: u64, 44 | /// Initial actual token reserves available for trading 45 | pub initial_real_token_reserves: u64, 46 | /// Total supply of tokens 47 | pub token_total_supply: u64, 48 | /// Fee in basis points (1/100th of a percent) 49 | pub fee_basis_points: u64, 50 | } 51 | 52 | impl GlobalAccount { 53 | /// Creates a new global account instance 54 | /// 55 | /// # Arguments 56 | /// * `discriminator` - Unique identifier for the account 57 | /// * `initialized` - Whether the account is initialized 58 | /// * `authority` - Authority pubkey that can modify settings 59 | /// * `fee_recipient` - Account that receives fees 60 | /// * `initial_virtual_token_reserves` - Initial virtual token reserves 61 | /// * `initial_virtual_sol_reserves` - Initial virtual SOL reserves 62 | /// * `initial_real_token_reserves` - Initial actual token reserves 63 | /// * `token_total_supply` - Total supply of tokens 64 | /// * `fee_basis_points` - Fee in basis points 65 | #[allow(clippy::too_many_arguments)] 66 | pub fn new( 67 | discriminator: u64, 68 | initialized: bool, 69 | authority: Pubkey, 70 | fee_recipient: Pubkey, 71 | initial_virtual_token_reserves: u64, 72 | initial_virtual_sol_reserves: u64, 73 | initial_real_token_reserves: u64, 74 | token_total_supply: u64, 75 | fee_basis_points: u64, 76 | ) -> Self { 77 | Self { 78 | discriminator, 79 | initialized, 80 | authority, 81 | fee_recipient, 82 | initial_virtual_token_reserves, 83 | initial_virtual_sol_reserves, 84 | initial_real_token_reserves, 85 | token_total_supply, 86 | fee_basis_points, 87 | } 88 | } 89 | 90 | /// Calculates the initial amount of tokens received for a given SOL amount 91 | /// 92 | /// # Arguments 93 | /// * `amount` - Amount of SOL to spend 94 | /// 95 | /// # Returns 96 | /// Amount of tokens that would be received 97 | pub fn get_initial_buy_price(&self, amount: u64) -> u64 { 98 | if amount == 0 { 99 | return 0; 100 | } 101 | 102 | let n: u128 = (self.initial_virtual_sol_reserves as u128) 103 | * (self.initial_virtual_token_reserves as u128); 104 | let i: u128 = (self.initial_virtual_sol_reserves as u128) + (amount as u128); 105 | let r: u128 = n / i + 1; 106 | let s: u128 = (self.initial_virtual_token_reserves as u128) - r; 107 | 108 | if s < (self.initial_real_token_reserves as u128) { 109 | s as u64 110 | } else { 111 | self.initial_real_token_reserves 112 | } 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | use super::*; 119 | 120 | fn get_global() -> GlobalAccount { 121 | GlobalAccount::new( 122 | 1, 123 | true, 124 | Pubkey::new_unique(), 125 | Pubkey::new_unique(), 126 | 1000, 127 | 1000, 128 | 500, 129 | 1000, 130 | 250, 131 | ) 132 | } 133 | 134 | fn get_large_global() -> GlobalAccount { 135 | GlobalAccount::new( 136 | 1, 137 | true, 138 | Pubkey::new_unique(), 139 | Pubkey::new_unique(), 140 | u64::MAX, 141 | u64::MAX, 142 | u64::MAX / 2, 143 | u64::MAX, 144 | 250, 145 | ) 146 | } 147 | 148 | #[test] 149 | fn test_global_account() { 150 | let global: GlobalAccount = get_global(); 151 | 152 | // Test initial buy price calculation 153 | assert_eq!(global.get_initial_buy_price(0), 0); 154 | 155 | let price: u64 = global.get_initial_buy_price(100); 156 | assert!(price > 0); 157 | assert!(price <= global.initial_real_token_reserves); 158 | } 159 | 160 | #[test] 161 | fn test_global_account_max_reserves() { 162 | let mut global: GlobalAccount = get_global(); 163 | global.initial_real_token_reserves = 100; 164 | 165 | // Test that returned amount is capped by real_token_reserves 166 | let price: u64 = global.get_initial_buy_price(1000); 167 | assert_eq!(price, global.initial_real_token_reserves); 168 | } 169 | 170 | #[test] 171 | fn test_global_account_overflow() { 172 | let global: GlobalAccount = get_large_global(); 173 | 174 | // Test with maximum possible SOL amount 175 | let price: u64 = global.get_initial_buy_price(u64::MAX); 176 | assert!(price > 0); 177 | assert!(price <= global.initial_real_token_reserves); 178 | 179 | // Test with large but not maximum SOL amount 180 | let price: u64 = global.get_initial_buy_price(u64::MAX / 2); 181 | assert!(price > 0); 182 | assert!(price <= global.initial_real_token_reserves); 183 | } 184 | 185 | #[test] 186 | fn test_global_account_overflow_edge_cases() { 187 | let mut global: GlobalAccount = get_large_global(); 188 | global.initial_virtual_sol_reserves = u64::MAX - 1000; 189 | global.initial_virtual_token_reserves = u64::MAX - 1000; 190 | global.initial_real_token_reserves = u64::MAX / 4; 191 | 192 | // Test with amounts near u64::MAX 193 | let price: u64 = global.get_initial_buy_price(u64::MAX - 1); 194 | assert!(price > 0); 195 | assert!(price <= global.initial_real_token_reserves); 196 | 197 | let price: u64 = global.get_initial_buy_price(u64::MAX - 1000); 198 | assert!(price > 0); 199 | assert!(price <= global.initial_real_token_reserves); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/accounts/mod.rs: -------------------------------------------------------------------------------- 1 | //! Accounts for the Pump.fun Solana Program 2 | //! 3 | //! This module contains the definitions for the accounts used by the Pump.fun program. 4 | //! 5 | //! # Accounts 6 | //! 7 | //! - `BondingCurve`: Represents a bonding curve account. 8 | //! - `Global`: Represents the global configuration account. 9 | 10 | mod bonding_curve; 11 | mod global; 12 | 13 | pub use bonding_curve::*; 14 | pub use global::*; 15 | -------------------------------------------------------------------------------- /src/common/logs_data.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_sdk::pubkey::Pubkey; 3 | 4 | use crate::error::{ClientError, ClientResult}; 5 | 6 | #[derive(Debug)] 7 | pub enum DexInstruction { 8 | CreateToken(CreateTokenInfo), 9 | UserTrade(TradeInfo), 10 | BotTrade(TradeInfo), 11 | Other, 12 | } 13 | 14 | #[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] 15 | pub struct CreateTokenInfo { 16 | pub slot: u64, 17 | pub name: String, 18 | pub symbol: String, 19 | pub uri: String, 20 | pub mint: Pubkey, 21 | pub bonding_curve: Pubkey, 22 | pub user: Pubkey, 23 | } 24 | 25 | #[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] 26 | pub struct TradeInfo { 27 | pub slot: u64, 28 | pub mint: Pubkey, 29 | pub sol_amount: u64, 30 | pub token_amount: u64, 31 | pub is_buy: bool, 32 | pub user: Pubkey, 33 | pub timestamp: i64, 34 | pub virtual_sol_reserves: u64, 35 | pub virtual_token_reserves: u64, 36 | pub real_sol_reserves: u64, 37 | pub real_token_reserves: u64, 38 | } 39 | 40 | #[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] 41 | pub struct CompleteInfo { 42 | pub user: Pubkey, 43 | pub mint: Pubkey, 44 | pub bonding_curve: Pubkey, 45 | pub timestamp: u64, 46 | } 47 | 48 | #[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] 49 | pub struct SwapBaseInLog { 50 | pub log_type: u8, 51 | // input 52 | pub amount_in: u64, 53 | pub minimum_out: u64, 54 | pub direction: u64, 55 | // user info 56 | pub user_source: u64, 57 | // pool info 58 | pub pool_coin: u64, 59 | pub pool_pc: u64, 60 | // calc result 61 | pub out_amount: u64, 62 | } 63 | 64 | pub trait EventTrait: Sized + std::fmt::Debug { 65 | fn from_bytes(bytes: &[u8]) -> ClientResult; 66 | } 67 | 68 | impl EventTrait for CreateTokenInfo { 69 | fn from_bytes(bytes: &[u8]) -> ClientResult { 70 | CreateTokenInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) 71 | } 72 | } 73 | 74 | impl EventTrait for TradeInfo { 75 | fn from_bytes(bytes: &[u8]) -> ClientResult { 76 | TradeInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) 77 | } 78 | } 79 | 80 | impl EventTrait for CompleteInfo { 81 | fn from_bytes(bytes: &[u8]) -> ClientResult { 82 | CompleteInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) 83 | } 84 | } 85 | 86 | impl EventTrait for SwapBaseInLog { 87 | fn from_bytes(bytes: &[u8]) -> ClientResult { 88 | SwapBaseInLog::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) 89 | } 90 | } -------------------------------------------------------------------------------- /src/common/logs_events.rs: -------------------------------------------------------------------------------- 1 | use base64::engine::general_purpose; 2 | use base64::Engine; 3 | use regex::Regex; 4 | use crate::common::logs_data::{CreateTokenInfo, TradeInfo, EventTrait}; 5 | 6 | pub const PROGRAM_DATA: &str = "Program data: "; 7 | 8 | #[derive(Debug)] 9 | pub enum PumpfunEvent { 10 | NewToken(CreateTokenInfo), 11 | NewDevTrade(TradeInfo), 12 | NewUserTrade(TradeInfo), 13 | NewBotTrade(TradeInfo), 14 | Error(String), 15 | } 16 | 17 | 18 | #[derive(Debug)] 19 | pub enum DexEvent { 20 | NewToken(CreateTokenInfo), 21 | NewUserTrade(TradeInfo), 22 | NewBotTrade(TradeInfo), 23 | Error(String), 24 | } 25 | 26 | // #[derive(Debug, Clone, Copy)] 27 | // pub struct PumpEvent {} 28 | 29 | impl PumpfunEvent { 30 | pub fn parse_logs(logs: &Vec) -> (Option, Option) { 31 | let mut create_info: Option = None; 32 | let mut trade_info: Option = None; 33 | 34 | if !logs.is_empty() { 35 | let logs_iter = logs.iter().peekable(); 36 | 37 | for l in logs_iter.rev() { 38 | if let Some(log) = l.strip_prefix(PROGRAM_DATA) { 39 | let borsh_bytes = general_purpose::STANDARD.decode(log).unwrap(); 40 | let slice: &[u8] = &borsh_bytes[8..]; 41 | 42 | if create_info.is_none() { 43 | if let Ok(e) = CreateTokenInfo::from_bytes(slice) { 44 | create_info = Some(e); 45 | continue; 46 | } 47 | } 48 | 49 | if trade_info.is_none() { 50 | if let Ok(e) = TradeInfo::from_bytes(slice) { 51 | trade_info = Some(e); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | (create_info, trade_info) 58 | } 59 | } 60 | 61 | #[derive(Debug, Clone, Copy)] 62 | pub struct RaydiumEvent {} 63 | 64 | impl RaydiumEvent { 65 | pub fn parse_logs(logs: &Vec) -> Option { 66 | let mut event: Option = None; 67 | 68 | if !logs.is_empty() { 69 | let logs_iter = logs.iter().peekable(); 70 | let re = Regex::new(r"ray_log: (?P[A-Za-z0-9+/=]+)").unwrap(); 71 | 72 | for l in logs_iter.rev() { 73 | if let Some(caps) = re.captures(l) { 74 | if let Some(base64) = caps.name("base64") { 75 | let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap(); 76 | 77 | if let Ok(e) = T::from_bytes(&bytes) { 78 | event = Some(e); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | event 86 | } 87 | } -------------------------------------------------------------------------------- /src/common/logs_filters.rs: -------------------------------------------------------------------------------- 1 | use crate::common::logs_data::DexInstruction; 2 | use crate::common::logs_parser::{parse_create_token_data, parse_trade_data}; 3 | use crate::error::ClientResult; 4 | use solana_sdk::pubkey::Pubkey; 5 | pub struct LogFilter; 6 | 7 | impl LogFilter { 8 | const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; 9 | 10 | /// Parse transaction logs and return instruction type and data 11 | pub fn parse_instruction(logs: &[String], bot_wallet: Option) -> ClientResult> { 12 | let mut current_instruction = None; 13 | let mut program_data = String::new(); 14 | let mut invoke_depth = 0; 15 | let mut last_data_len = 0; 16 | let mut instructions = Vec::new(); 17 | for log in logs { 18 | // Check program invocation 19 | if log.contains(&format!("Program {} invoke", Self::PROGRAM_ID)) { 20 | invoke_depth += 1; 21 | if invoke_depth == 1 { // Only reset state at top level call 22 | current_instruction = None; 23 | program_data.clear(); 24 | last_data_len = 0; 25 | } 26 | continue; 27 | } 28 | 29 | // Skip if not in our program 30 | if invoke_depth == 0 { 31 | continue; 32 | } 33 | 34 | // Identify instruction type (only at top level) 35 | if invoke_depth == 1 && log.contains("Program log: Instruction:") { 36 | if log.contains("Create") { 37 | current_instruction = Some("create"); 38 | } else if log.contains("Buy") || log.contains("Sell") { 39 | current_instruction = Some("trade"); 40 | } 41 | continue; 42 | } 43 | 44 | // Collect Program data 45 | if log.starts_with("Program data: ") { 46 | let data = log.trim_start_matches("Program data: "); 47 | if data.len() > last_data_len { 48 | program_data = data.to_string(); 49 | last_data_len = data.len(); 50 | } 51 | } 52 | 53 | // Check if program ends 54 | if log.contains(&format!("Program {} success", Self::PROGRAM_ID)) { 55 | invoke_depth -= 1; 56 | if invoke_depth == 0 { // Only process data when top level program ends 57 | if let Some(instruction_type) = current_instruction { 58 | if !program_data.is_empty() { 59 | match instruction_type { 60 | "create" => { 61 | if let Ok(token_info) = parse_create_token_data(&program_data) { 62 | instructions.push(DexInstruction::CreateToken(token_info)); 63 | } 64 | }, 65 | "trade" => { 66 | if let Ok(trade_info) = parse_trade_data(&program_data) { 67 | if let Some(bot_wallet_pubkey) = bot_wallet { 68 | if trade_info.user.to_string() == bot_wallet_pubkey.to_string() { 69 | instructions.push(DexInstruction::BotTrade(trade_info)); 70 | } else { 71 | instructions.push(DexInstruction::UserTrade(trade_info)); 72 | } 73 | } else { 74 | instructions.push(DexInstruction::UserTrade(trade_info)); 75 | } 76 | } 77 | }, 78 | _ => {} 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | Ok(instructions) 87 | } 88 | } -------------------------------------------------------------------------------- /src/common/logs_parser.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; 4 | 5 | use crate::error::{ClientError, ClientResult}; 6 | use crate::common::{ 7 | logs_data::{DexInstruction, CreateTokenInfo, TradeInfo}, 8 | logs_filters::LogFilter 9 | }; 10 | 11 | use solana_sdk::pubkey::Pubkey; 12 | 13 | pub async fn process_logs( 14 | signature: &str, 15 | logs: Vec, 16 | callback: F, 17 | payer: Option, 18 | ) -> ClientResult<()> 19 | where 20 | F: Fn(&str, DexInstruction) + Send + Sync, 21 | { 22 | let instructions = LogFilter::parse_instruction(&logs, payer)?; 23 | for instruction in instructions { 24 | callback(signature, instruction); 25 | } 26 | Ok(()) 27 | } 28 | 29 | // Add parsing function 30 | pub fn parse_create_token_data(data: &str) -> ClientResult { 31 | // First do base64 decoding 32 | let decoded = BASE64.decode(data) 33 | .map_err(|e| ClientError::Other(format!("Failed to decode base64: {}", e)))?; 34 | 35 | // Skip prefix bytes (if any) 36 | let mut cursor = if decoded.len() > 8 { 8 } else { 0 }; 37 | 38 | // Read name length and name 39 | if cursor + 4 > decoded.len() { 40 | return Err(ClientError::Other("Data too short for name length".to_string())); 41 | } 42 | let name_len = read_u32(&decoded[cursor..]) as usize; 43 | cursor += 4; 44 | 45 | if cursor + name_len > decoded.len() { 46 | return Err(ClientError::Other(format!("Data too short for name: need {} bytes", name_len))); 47 | } 48 | let name = String::from_utf8(decoded[cursor..cursor + name_len].to_vec()) 49 | .map_err(|e| ClientError::Other(format!("Invalid UTF-8 in name: {}", e)))?; 50 | cursor += name_len; 51 | 52 | // Read symbol length and symbol 53 | if cursor + 4 > decoded.len() { 54 | return Err(ClientError::Other("Data too short for symbol length".to_string())); 55 | } 56 | let symbol_len = read_u32(&decoded[cursor..]) as usize; 57 | cursor += 4; 58 | 59 | if cursor + symbol_len > decoded.len() { 60 | return Err(ClientError::Other(format!("Data too short for symbol: need {} bytes", symbol_len))); 61 | } 62 | let symbol = String::from_utf8(decoded[cursor..cursor + symbol_len].to_vec()) 63 | .map_err(|e| ClientError::Other(format!("Invalid UTF-8 in symbol: {}", e)))?; 64 | cursor += symbol_len; 65 | 66 | // Read URI length and URI 67 | if cursor + 4 > decoded.len() { 68 | return Err(ClientError::Other("Data too short for URI length".to_string())); 69 | } 70 | let uri_len = read_u32(&decoded[cursor..]) as usize; 71 | cursor += 4; 72 | 73 | if cursor + uri_len > decoded.len() { 74 | return Err(ClientError::Other(format!("Data too short for URI: need {} bytes", uri_len))); 75 | } 76 | let uri = String::from_utf8(decoded[cursor..cursor + uri_len].to_vec()) 77 | .map_err(|e| ClientError::Other(format!("Invalid UTF-8 in uri: {}", e)))?; 78 | cursor += uri_len; 79 | 80 | // Make sure there is enough data to read public keys 81 | if cursor + 32 * 3 > decoded.len() { 82 | return Err(ClientError::Other("Data too short for public keys".to_string())); 83 | } 84 | 85 | // Parse Mint Public Key 86 | let mint = bs58::encode(&decoded[cursor..cursor+32]).into_string(); 87 | cursor += 32; 88 | 89 | // Parse Bonding Curve Public Key 90 | let bonding_curve = bs58::encode(&decoded[cursor..cursor+32]).into_string(); 91 | cursor += 32; 92 | 93 | // Parse User Public Key 94 | let user = bs58::encode(&decoded[cursor..cursor+32]).into_string(); 95 | 96 | Ok(CreateTokenInfo { 97 | slot: 0, 98 | name, 99 | symbol, 100 | uri, 101 | mint: Pubkey::from_str(&mint).unwrap(), 102 | bonding_curve: Pubkey::from_str(&bonding_curve).unwrap(), 103 | user: Pubkey::from_str(&user).unwrap(), 104 | }) 105 | } 106 | 107 | fn read_u32(data: &[u8]) -> u32 { 108 | let mut bytes = [0u8; 4]; 109 | bytes.copy_from_slice(&data[..4]); 110 | u32::from_le_bytes(bytes) 111 | } 112 | 113 | pub fn parse_trade_data(data: &str) -> ClientResult { 114 | let engine = base64::engine::general_purpose::STANDARD; 115 | let decoded = engine.decode(data).map_err(|e| 116 | ClientError::Parse( 117 | "Failed to decode base64".to_string(), 118 | e.to_string() 119 | ) 120 | )?; 121 | 122 | let mut cursor = 8; // Skip prefix 123 | 124 | // 1. Mint (32 bytes) 125 | let mint = bs58::encode(&decoded[cursor..cursor + 32]).into_string(); 126 | cursor += 32; 127 | 128 | // 2. Sol Amount (8 bytes) 129 | let sol_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); 130 | cursor += 8; 131 | 132 | // 3. Token Amount (8 bytes) 133 | let token_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); 134 | cursor += 8; 135 | 136 | // 4. Is Buy (1 byte) 137 | let is_buy = decoded[cursor] != 0; 138 | cursor += 1; 139 | 140 | // 5. User (32 bytes) 141 | let user = bs58::encode(&decoded[cursor..cursor + 32]).into_string(); 142 | cursor += 32; 143 | 144 | // 6. Timestamp (8 bytes) 145 | let timestamp = i64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); 146 | cursor += 8; 147 | 148 | // 7. Virtual Sol Reserves (8 bytes) 149 | let virtual_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); 150 | cursor += 8; 151 | 152 | // 8. Virtual Token Reserves (8 bytes) 153 | let virtual_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); 154 | cursor += 8; 155 | 156 | let real_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); 157 | cursor += 8; 158 | 159 | let real_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); 160 | 161 | Ok(TradeInfo { 162 | slot: 0, 163 | mint: Pubkey::from_str(&mint).unwrap(), 164 | sol_amount, 165 | token_amount, 166 | is_buy, 167 | user: Pubkey::from_str(&user).unwrap(), 168 | timestamp, 169 | virtual_sol_reserves, 170 | virtual_token_reserves, 171 | real_sol_reserves, 172 | real_token_reserves, 173 | }) 174 | } -------------------------------------------------------------------------------- /src/common/logs_subscribe.rs: -------------------------------------------------------------------------------- 1 | use solana_client::{ 2 | nonblocking::pubsub_client::PubsubClient, 3 | rpc_config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter} 4 | }; 5 | 6 | use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; 7 | use std::sync::Arc; 8 | use tokio::sync::mpsc; 9 | use tokio::task::JoinHandle; 10 | use futures::StreamExt; 11 | use crate::{constants, common::{ 12 | logs_data::DexInstruction, logs_events::DexEvent, logs_filters::LogFilter 13 | }}; 14 | 15 | use super::logs_events::PumpfunEvent; 16 | 17 | /// Subscription handle containing task and unsubscribe logic 18 | pub struct SubscriptionHandle { 19 | pub task: JoinHandle<()>, 20 | pub unsub_fn: Box, 21 | } 22 | 23 | impl SubscriptionHandle { 24 | pub async fn shutdown(self) { 25 | (self.unsub_fn)(); 26 | self.task.abort(); 27 | } 28 | } 29 | 30 | pub async fn create_pubsub_client(ws_url: &str) -> PubsubClient { 31 | PubsubClient::new(ws_url).await.unwrap() 32 | } 33 | 34 | /// 启动订阅 35 | pub async fn tokens_subscription( 36 | ws_url: &str, 37 | commitment: CommitmentConfig, 38 | callback: F, 39 | bot_wallet: Option, 40 | ) -> Result> 41 | where 42 | F: Fn(PumpfunEvent) + Send + Sync + 'static, 43 | { 44 | let program_address = constants::accounts::PUMPFUN.to_string(); 45 | let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]); 46 | 47 | let logs_config = RpcTransactionLogsConfig { 48 | commitment: Some(commitment), 49 | }; 50 | 51 | // Create PubsubClient 52 | let sub_client = Arc::new(PubsubClient::new(ws_url).await.unwrap()); 53 | 54 | let sub_client_clone = Arc::clone(&sub_client); 55 | 56 | // Create channel for unsubscribe 57 | let (unsub_tx, _) = mpsc::channel(1); 58 | 59 | // Start subscription task 60 | let task = tokio::spawn(async move { 61 | let (mut stream, _) = sub_client_clone.logs_subscribe(logs_filter, logs_config).await.unwrap(); 62 | 63 | loop { 64 | let msg = stream.next().await; 65 | match msg { 66 | Some(msg) => { 67 | if let Some(_err) = msg.value.err { 68 | continue; 69 | } 70 | 71 | let instructions = LogFilter::parse_instruction(&msg.value.logs, bot_wallet).unwrap(); 72 | for instruction in instructions { 73 | match instruction { 74 | DexInstruction::CreateToken(token_info) => { 75 | callback(PumpfunEvent::NewToken(token_info)); 76 | } 77 | DexInstruction::UserTrade(trade_info) => { 78 | callback(PumpfunEvent::NewUserTrade(trade_info)); 79 | } 80 | DexInstruction::BotTrade(trade_info) => { 81 | callback(PumpfunEvent::NewBotTrade(trade_info)); 82 | } 83 | _ => {} 84 | } 85 | } 86 | } 87 | None => { 88 | println!("Token subscription stream ended"); 89 | } 90 | } 91 | } 92 | }); 93 | 94 | // Return subscription handle and unsubscribe logic 95 | Ok(SubscriptionHandle { 96 | task, 97 | unsub_fn: Box::new(move || { 98 | let _ = unsub_tx.try_send(()); 99 | }), 100 | }) 101 | } 102 | 103 | pub async fn stop_subscription(handle: SubscriptionHandle) { 104 | handle.shutdown().await; 105 | } 106 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod logs_data; 2 | pub mod logs_parser; 3 | pub mod logs_filters; 4 | pub mod logs_subscribe; 5 | pub mod logs_events; 6 | pub mod types; 7 | 8 | pub use types::*; 9 | -------------------------------------------------------------------------------- /src/common/types.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use solana_client::rpc_client::RpcClient; 4 | use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair}; 5 | use serde::Deserialize; 6 | use crate::{constants::trade::{DEFAULT_BUY_TIP_FEE, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SELL_TIP_FEE}, swqos::FeeClient}; 7 | 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub enum FeeType { 10 | Jito, 11 | NextBlock, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct Cluster { 16 | pub rpc_url: String, 17 | pub block_engine_url: String, 18 | pub nextblock_url: String, 19 | pub nextblock_auth_token: String, 20 | pub zeroslot_url: String, 21 | pub zeroslot_auth_token: String, 22 | pub use_jito: bool, 23 | pub use_nextblock: bool, 24 | pub use_zeroslot: bool, 25 | pub priority_fee: PriorityFee, 26 | pub commitment: CommitmentConfig, 27 | } 28 | 29 | impl Cluster { 30 | pub fn new( 31 | rpc_url: String, 32 | block_engine_url: 33 | String, nextblock_url: 34 | String, nextblock_auth_token: 35 | String, zeroslot_url: String, 36 | zeroslot_auth_token: String, 37 | priority_fee: PriorityFee, 38 | commitment: CommitmentConfig, 39 | use_jito: bool, 40 | use_nextblock: bool, 41 | use_zeroslot: bool 42 | ) -> Self { 43 | Self { 44 | rpc_url, 45 | block_engine_url, 46 | nextblock_url, 47 | nextblock_auth_token, 48 | zeroslot_url, 49 | zeroslot_auth_token, 50 | priority_fee, 51 | commitment, 52 | use_jito, 53 | use_nextblock, 54 | use_zeroslot 55 | } 56 | } 57 | } 58 | 59 | #[derive(Debug, Deserialize, Clone, Copy, PartialEq)] 60 | 61 | pub struct PriorityFee { 62 | pub unit_limit: u32, 63 | pub unit_price: u64, 64 | pub buy_tip_fee: f64, 65 | pub sell_tip_fee: f64, 66 | } 67 | 68 | impl Default for PriorityFee { 69 | fn default() -> Self { 70 | Self { 71 | unit_limit: DEFAULT_COMPUTE_UNIT_LIMIT, 72 | unit_price: DEFAULT_COMPUTE_UNIT_PRICE, 73 | buy_tip_fee: DEFAULT_BUY_TIP_FEE, 74 | sell_tip_fee: DEFAULT_SELL_TIP_FEE 75 | } 76 | } 77 | } 78 | 79 | pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient; 80 | 81 | pub struct MethodArgs { 82 | pub payer: Arc, 83 | pub rpc: Arc, 84 | pub nonblocking_rpc: Arc, 85 | pub jito_client: Arc, 86 | } 87 | 88 | impl MethodArgs { 89 | pub fn new(payer: Arc, rpc: Arc, nonblocking_rpc: Arc, jito_client: Arc) -> Self { 90 | Self { payer, rpc, nonblocking_rpc, jito_client } 91 | } 92 | } 93 | 94 | pub type AnyResult = anyhow::Result; 95 | 96 | -------------------------------------------------------------------------------- /src/constants/mod.rs: -------------------------------------------------------------------------------- 1 | //! Constants used by the crate. 2 | //! 3 | //! This module contains various constants used throughout the crate, including: 4 | //! 5 | //! - Seeds for deriving Program Derived Addresses (PDAs) 6 | //! - Program account addresses and public keys 7 | //! 8 | //! The constants are organized into submodules for better organization: 9 | //! 10 | //! - `seeds`: Contains seed values used for PDA derivation 11 | //! - `accounts`: Contains important program account addresses 12 | 13 | /// Constants used as seeds for deriving PDAs (Program Derived Addresses) 14 | pub mod seeds { 15 | /// Seed for the global state PDA 16 | pub const GLOBAL_SEED: &[u8] = b"global"; 17 | 18 | /// Seed for the mint authority PDA 19 | pub const MINT_AUTHORITY_SEED: &[u8] = b"mint-authority"; 20 | 21 | /// Seed for bonding curve PDAs 22 | pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve"; 23 | 24 | /// Seed for creator vault PDAs 25 | pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault"; 26 | 27 | /// Seed for metadata PDAs 28 | pub const METADATA_SEED: &[u8] = b"metadata"; 29 | } 30 | 31 | pub mod global_constants { 32 | use solana_sdk::{pubkey, pubkey::Pubkey}; 33 | 34 | pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000; 35 | 36 | pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000; 37 | 38 | pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000; 39 | 40 | pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000; 41 | 42 | pub const FEE_BASIS_POINTS: u64 = 95; 43 | 44 | pub const ENABLE_MIGRATE: bool = false; 45 | 46 | pub const POOL_MIGRATION_FEE: u64 = 15_000_001; 47 | 48 | pub const CREATOR_FEE: u64 = 5; 49 | 50 | pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals 51 | 52 | pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports 53 | 54 | pub const TOTAL_SUPPLY: u64 = 1_000_000_000 * SCALE; // 1 billion tokens 55 | 56 | pub const BONDING_CURVE_SUPPLY: u64 = 793_100_000 * SCALE; // total supply of bonding curve tokens 57 | 58 | pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL 59 | 60 | /// Public key for the fee recipient 61 | pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); 62 | 63 | /// Public key for the global PDA 64 | pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); 65 | 66 | /// Public key for the authority 67 | pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF"); 68 | 69 | /// Public key for the withdraw authority 70 | pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"); 71 | 72 | pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1 73 | pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2 74 | pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3 75 | pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4 76 | pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5 77 | pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6 78 | pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"); // Pump.fun AMM: Protocol Fee 7 79 | 80 | } 81 | 82 | /// Constants related to program accounts and authorities 83 | pub mod accounts { 84 | use solana_sdk::{pubkey, pubkey::Pubkey}; 85 | 86 | /// Public key for the Pump.fun program 87 | pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); 88 | 89 | /// Public key for the MPL Token Metadata program 90 | pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 91 | 92 | /// Authority for program events 93 | pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"); 94 | 95 | /// System Program ID 96 | pub const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111"); 97 | 98 | /// Token Program ID 99 | pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); 100 | 101 | /// Associated Token Program ID 102 | pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); 103 | 104 | /// Rent Sysvar ID 105 | pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111"); 106 | 107 | pub const JITO_TIP_ACCOUNTS: [&str; 8] = [ 108 | "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", 109 | "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", 110 | "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", 111 | "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", 112 | "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", 113 | "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", 114 | "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", 115 | "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", 116 | ]; 117 | 118 | /// Tip accounts 119 | pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[ 120 | "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE", 121 | "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2", 122 | "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X", 123 | "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb", 124 | "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At", 125 | "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG", 126 | "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid", 127 | "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc" 128 | ]; 129 | 130 | pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[ 131 | "Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3", 132 | "FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe", 133 | "ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13", 134 | "6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK", 135 | "Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr", 136 | ]; 137 | 138 | pub const NOZOMI_TIP_ACCOUNTS: &[&str] = &[ 139 | "TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq", 140 | "noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4", 141 | "noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE", 142 | "noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo", 143 | "noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ", 144 | "nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L", 145 | "nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z", 146 | "nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu", 147 | "noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7", 148 | "nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP", 149 | "nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P", 150 | "nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge", 151 | "nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3", 152 | "nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ", 153 | "nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk", 154 | "nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne", 155 | "nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb" 156 | ]; 157 | 158 | pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); 159 | } 160 | 161 | pub mod trade { 162 | pub const TRADER_TIP_AMOUNT: f64 = 0.0001; 163 | pub const DEFAULT_SLIPPAGE: u64 = 1000; // 10% 164 | pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000; 165 | pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000; 166 | pub const DEFAULT_BUY_TIP_FEE: f64 = 0.0006; 167 | pub const DEFAULT_SELL_TIP_FEE: f64 = 0.0001; 168 | } 169 | 170 | pub struct Symbol; 171 | 172 | impl Symbol { 173 | pub const SOLANA: &'static str = "solana"; 174 | } 175 | -------------------------------------------------------------------------------- /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 solana_client::{ 22 | client_error::ClientError as SolanaClientError, 23 | pubsub_client::PubsubClientError 24 | }; 25 | use solana_sdk::pubkey::ParsePubkeyError; 26 | 27 | // #[derive(Debug)] 28 | // #[allow(dead_code)] 29 | // pub struct AppError(anyhow::Error); 30 | 31 | // impl From for AppError 32 | // where 33 | // E: Into, 34 | // { 35 | // fn from(err: E) -> Self { 36 | // Self(err.into()) 37 | // } 38 | // } 39 | 40 | #[derive(Debug)] 41 | pub enum ClientError { 42 | /// Bonding curve account was not found 43 | BondingCurveNotFound, 44 | /// Error related to bonding curve operations 45 | BondingCurveError(&'static str), 46 | /// Error deserializing data using Borsh 47 | BorshError(std::io::Error), 48 | /// Error from Solana RPC client 49 | SolanaClientError(solana_client::client_error::ClientError), 50 | /// Error uploading metadata 51 | UploadMetadataError(Box), 52 | /// Invalid input parameters 53 | InvalidInput(&'static str), 54 | /// Insufficient funds for transaction 55 | InsufficientFunds, 56 | /// Transaction simulation failed 57 | SimulationError(String), 58 | /// Rate limit exceeded 59 | RateLimitExceeded, 60 | 61 | OrderLimitExceeded, 62 | 63 | ExternalService(String), 64 | 65 | Redis(String, String), 66 | 67 | Solana(String, String), 68 | 69 | Parse(String, String), 70 | 71 | Pubkey(String, String), 72 | 73 | Jito(String, String), 74 | 75 | Join(String), 76 | 77 | Subscribe(String, String), 78 | 79 | Send(String, String), 80 | 81 | Other(String), 82 | 83 | Anyhow(&'static str), 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::Anyhow(msg) => write!(f, "Anyhow error: {}", msg), 115 | Self::Solana(msg, details) => write!(f, "Solana error: {}, details: {}", msg, details), 116 | Self::Parse(msg, details) => write!(f, "Parse error: {}, details: {}", msg, details), 117 | Self::Jito(msg, details) => write!(f, "Jito error: {}, details: {}", msg, details), 118 | Self::Redis(msg, details) => write!(f, "Redis error: {}, details: {}", msg, details), 119 | Self::Join(msg) => write!(f, "Task join error: {}", msg), 120 | Self::Pubkey(msg, details) => write!(f, "Pubkey error: {}, details: {}", msg, details), 121 | Self::Subscribe(msg, details) => write!(f, "Subscribe error: {}, details: {}", msg, details), 122 | Self::Send(msg, details) => write!(f, "Send error: {}, details: {}", msg, details), 123 | Self::Other(msg) => write!(f, "Other error: {}", msg), 124 | Self::PumpFunBuy(msg) => write!(f, "PumpFun buy error: {}", msg), 125 | Self::PumpFunSell(msg) => write!(f, "PumpFun sell error: {}", msg), 126 | Self::InvalidData(msg) => write!(f, "Invalid data: {}", msg), 127 | Self::Timeout(msg, details) => write!(f, "Operation timed out: {}, details: {}", msg, details), 128 | Self::Duplicate(msg) => write!(f, "Duplicate event: {}", msg), 129 | Self::InvalidEventType => write!(f, "Invalid event type"), 130 | Self::ChannelClosed => write!(f, "Channel closed"), 131 | } 132 | } 133 | } 134 | impl std::error::Error for ClientError { 135 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 136 | match self { 137 | Self::BorshError(err) => Some(err), 138 | Self::SolanaClientError(err) => Some(err), 139 | Self::UploadMetadataError(err) => Some(err.as_ref()), 140 | Self::ExternalService(_) => None, 141 | Self::Redis(_, _) => None, 142 | Self::Solana(_, _) => None, 143 | Self::Parse(_, _) => None, 144 | Self::Jito(_, _) => None, 145 | Self::Join(_) => None, 146 | Self::Pubkey(_, _) => None, 147 | Self::Subscribe(_, _) => None, 148 | Self::Send(_, _) => None, 149 | Self::Other(_) => None, 150 | Self::PumpFunBuy(_) => None, 151 | Self::PumpFunSell(_) => None, 152 | Self::Timeout(_, _) => None, 153 | Self::Duplicate(_) => None, 154 | Self::InvalidEventType => None, 155 | Self::ChannelClosed => None, 156 | _ => None, 157 | } 158 | } 159 | } 160 | 161 | impl From for ClientError { 162 | fn from(error: SolanaClientError) -> Self { 163 | ClientError::Solana( 164 | "Solana client error".to_string(), 165 | error.to_string(), 166 | ) 167 | } 168 | } 169 | 170 | impl From for ClientError { 171 | fn from(error: PubsubClientError) -> Self { 172 | ClientError::Solana( 173 | "PubSub client error".to_string(), 174 | error.to_string(), 175 | ) 176 | } 177 | } 178 | 179 | impl From for ClientError { 180 | fn from(error: ParsePubkeyError) -> Self { 181 | ClientError::Pubkey( 182 | "Pubkey error".to_string(), 183 | error.to_string(), 184 | ) 185 | } 186 | } 187 | 188 | impl From for ClientError { 189 | fn from(err: Error) -> Self { 190 | ClientError::Parse( 191 | "JSON serialization error".to_string(), 192 | err.to_string() 193 | ) 194 | } 195 | } 196 | 197 | pub type ClientResult = Result; 198 | -------------------------------------------------------------------------------- /src/grpc/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fmt, time::Duration}; 2 | 3 | use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt}; 4 | use rustls::crypto::{ring::default_provider, CryptoProvider}; 5 | use tonic::{transport::channel::ClientTlsConfig, Status}; 6 | use yellowstone_grpc_client::{GeyserGrpcClient, GeyserGrpcClientResult}; 7 | use yellowstone_grpc_proto::geyser::{ 8 | CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate, 9 | SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing, 10 | }; 11 | use log::{error, info}; 12 | use chrono::Local; 13 | use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature}; 14 | use solana_transaction_status::{ 15 | option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding, 16 | }; 17 | 18 | use crate::common::logs_data::DexInstruction; 19 | use crate::common::logs_events::PumpfunEvent; 20 | use crate::common::logs_filters::LogFilter; 21 | use crate::error::{ClientError, ClientResult}; 22 | 23 | type TransactionsFilterMap = HashMap; 24 | 25 | const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); 26 | const CONNECT_TIMEOUT: u64 = 10; 27 | const REQUEST_TIMEOUT: u64 = 60; 28 | const CHANNEL_SIZE: usize = 1000; 29 | 30 | #[derive(Clone)] 31 | pub struct TransactionPretty { 32 | pub slot: u64, 33 | pub signature: Signature, 34 | pub is_vote: bool, 35 | pub tx: EncodedTransactionWithStatusMeta, 36 | // pub transaction: Option, 37 | } 38 | 39 | impl fmt::Debug for TransactionPretty { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta); 42 | impl<'a> fmt::Debug for TxWrap<'a> { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | let serialized = serde_json::to_string(self.0).expect("failed to serialize"); 45 | fmt::Display::fmt(&serialized, f) 46 | } 47 | } 48 | 49 | f.debug_struct("TransactionPretty") 50 | .field("slot", &self.slot) 51 | .field("signature", &self.signature) 52 | .field("is_vote", &self.is_vote) 53 | .field("tx", &TxWrap(&self.tx)) 54 | .finish() 55 | } 56 | } 57 | 58 | impl From for TransactionPretty { 59 | fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self { 60 | let tx = transaction.expect("should be defined"); 61 | // let transaction_info = tx.transaction.clone().unwrap(); 62 | Self { 63 | slot, 64 | signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"), 65 | is_vote: tx.is_vote, 66 | tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx) 67 | .expect("valid tx with meta") 68 | .encode(UiTransactionEncoding::Base64, Some(u8::MAX), true) 69 | .expect("failed to encode"), 70 | // transaction: Some(transaction_info), 71 | } 72 | } 73 | } 74 | 75 | #[derive(Clone)] 76 | pub struct YellowstoneGrpc { 77 | endpoint: String, 78 | } 79 | 80 | impl YellowstoneGrpc { 81 | pub fn new(endpoint: String) -> Self { 82 | Self { endpoint } 83 | } 84 | 85 | pub async fn connect( 86 | &self, 87 | transactions: TransactionsFilterMap, 88 | ) -> ClientResult< 89 | GeyserGrpcClientResult<( 90 | impl Sink, 91 | impl Stream>, 92 | )> 93 | > { 94 | if CryptoProvider::get_default().is_none() { 95 | default_provider() 96 | .install_default() 97 | .map_err(|e| ClientError::Other(format!("Failed to install crypto provider: {:?}", e)))?; 98 | } 99 | 100 | let mut client = GeyserGrpcClient::build_from_shared(self.endpoint.clone()) 101 | .map_err(|e| ClientError::Other(format!("Failed to build client: {:?}", e)))? 102 | .tls_config(ClientTlsConfig::new().with_native_roots()) 103 | .map_err(|e| ClientError::Other(format!("Failed to build client: {:?}", e)))? 104 | .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT)) 105 | .timeout(Duration::from_secs(REQUEST_TIMEOUT)) 106 | .connect() 107 | .await 108 | .map_err(|e| ClientError::Other(format!("Failed to connect: {:?}", e)))?; 109 | 110 | let subscribe_request = SubscribeRequest { 111 | transactions, 112 | commitment: Some(CommitmentLevel::Processed.into()), 113 | ..Default::default() 114 | }; 115 | 116 | Ok(client.subscribe_with_request(Some(subscribe_request)).await) 117 | } 118 | 119 | pub fn get_subscribe_request_filter( 120 | &self, 121 | account_include: Vec, 122 | account_exclude: Vec, 123 | account_required: Vec, 124 | ) -> TransactionsFilterMap { 125 | let mut transactions = HashMap::new(); 126 | transactions.insert( 127 | "client".to_string(), 128 | SubscribeRequestFilterTransactions { 129 | vote: Some(false), 130 | failed: Some(false), 131 | signature: None, 132 | account_include, 133 | account_exclude, 134 | account_required, 135 | }, 136 | ); 137 | transactions 138 | } 139 | 140 | async fn handle_stream_message( 141 | msg: SubscribeUpdate, 142 | tx: &mut mpsc::Sender, 143 | subscribe_tx: &mut (impl Sink + Unpin), 144 | ) -> ClientResult<()> { 145 | match msg.update_oneof { 146 | Some(UpdateOneof::Transaction(sut)) => { 147 | let transaction_pretty = TransactionPretty::from(sut); 148 | tx.try_send(transaction_pretty).map_err(|e| ClientError::Other(format!("Send error: {:?}", e)))?; 149 | } 150 | Some(UpdateOneof::Ping(_)) => { 151 | subscribe_tx 152 | .send(SubscribeRequest { 153 | ping: Some(SubscribeRequestPing { id: 1 }), 154 | ..Default::default() 155 | }) 156 | .await 157 | .map_err(|e| ClientError::Other(format!("Ping error: {:?}", e)))?; 158 | info!("service is ping: {}", Local::now()); 159 | } 160 | Some(UpdateOneof::Pong(_)) => { 161 | info!("service is pong: {}", Local::now()); 162 | } 163 | _ => {} 164 | } 165 | Ok(()) 166 | } 167 | 168 | pub async fn subscribe_pumpfun(&self, callback: F, bot_wallet: Option) -> ClientResult<()> 169 | where 170 | F: Fn(PumpfunEvent) + Send + Sync + 'static, 171 | { 172 | let addrs = vec![PUMP_PROGRAM_ID.to_string()]; 173 | let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]); 174 | let (mut subscribe_tx, mut stream) = self.connect(transactions).await? 175 | .map_err(|e| ClientError::Other(format!("Failed to subscribe: {:?}", e)))?; 176 | let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); 177 | 178 | let callback = Box::new(callback); 179 | 180 | tokio::spawn(async move { 181 | while let Some(message) = stream.next().await { 182 | match message { 183 | Ok(msg) => { 184 | if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { 185 | error!("Error handling message: {:?}", e); 186 | break; 187 | } 188 | } 189 | Err(error) => { 190 | error!("Stream error: {error:?}"); 191 | break; 192 | } 193 | } 194 | } 195 | }); 196 | 197 | while let Some(transaction_pretty) = rx.next().await { 198 | if let Err(e) = Self::process_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { 199 | error!("Error processing transaction: {:?}", e); 200 | } 201 | } 202 | Ok(()) 203 | } 204 | 205 | async fn process_pumpfun_transaction(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option) -> ClientResult<()> 206 | where 207 | F: Fn(PumpfunEvent) + Send + Sync, 208 | { 209 | let slot = transaction_pretty.slot; 210 | let trade_raw = transaction_pretty.tx; 211 | let meta = trade_raw.meta.as_ref() 212 | .ok_or_else(|| ClientError::Other("Missing transaction metadata".to_string()))?; 213 | 214 | if meta.err.is_some() { 215 | return Ok(()); 216 | } 217 | 218 | let logs = if let OptionSerializer::Some(logs) = &meta.log_messages { 219 | logs 220 | } else { 221 | &vec![] 222 | }; 223 | 224 | let mut dev_address: Option = None; 225 | let instructions = LogFilter::parse_instruction(logs, bot_wallet).unwrap(); 226 | for instruction in instructions { 227 | match instruction { 228 | DexInstruction::CreateToken(mut token_info) => { 229 | token_info.slot = slot; 230 | dev_address = Some(token_info.user); 231 | callback(PumpfunEvent::NewToken(token_info)); 232 | } 233 | DexInstruction::UserTrade(mut trade_info) => { 234 | trade_info.slot = slot; 235 | if Some(trade_info.user) == dev_address { 236 | callback(PumpfunEvent::NewDevTrade(trade_info)); 237 | } else { 238 | callback(PumpfunEvent::NewUserTrade(trade_info)); 239 | } 240 | } 241 | DexInstruction::BotTrade(mut trade_info) => { 242 | trade_info.slot = slot; 243 | callback(PumpfunEvent::NewBotTrade(trade_info)); 244 | } 245 | _ => {} 246 | } 247 | } 248 | 249 | Ok(()) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/instruction/mod.rs: -------------------------------------------------------------------------------- 1 | //! Instructions for interacting with the Pump.fun program. 2 | //! 3 | //! This module contains instruction builders for creating Solana instructions to interact with the 4 | //! Pump.fun program. Each function takes the required accounts and instruction data and returns a 5 | //! properly formatted Solana instruction. 6 | //! 7 | //! # Instructions 8 | //! 9 | //! - `create`: Instruction to create a new token with an associated bonding curve. 10 | //! - `buy`: Instruction to buy tokens from a bonding curve by providing SOL. 11 | //! - `sell`: Instruction to sell tokens back to the bonding curve in exchange for SOL. 12 | use crate::{ 13 | constants, 14 | pumpfun::common::{ 15 | get_bonding_curve_pda, get_global_pda, get_metadata_pda, get_mint_authority_pda 16 | }, 17 | }; 18 | use spl_associated_token_account::get_associated_token_address; 19 | 20 | use solana_sdk::{ 21 | instruction::{AccountMeta, Instruction}, 22 | pubkey::Pubkey, 23 | signature::Keypair, 24 | signer::Signer, 25 | }; 26 | 27 | pub struct Buy { 28 | pub _amount: u64, 29 | pub _max_sol_cost: u64, 30 | } 31 | 32 | impl Buy { 33 | pub fn data(&self) -> Vec { 34 | let mut data = Vec::with_capacity(8 + 8 + 8); 35 | data.extend_from_slice(&[102, 6, 61, 18, 1, 218, 235, 234]); // discriminator 36 | data.extend_from_slice(&self._amount.to_le_bytes()); 37 | data.extend_from_slice(&self._max_sol_cost.to_le_bytes()); 38 | data 39 | } 40 | } 41 | 42 | pub struct Sell { 43 | pub _amount: u64, 44 | pub _min_sol_output: u64, 45 | } 46 | 47 | impl Sell { 48 | pub fn data(&self) -> Vec { 49 | let mut data = Vec::with_capacity(8 + 8 + 8); 50 | data.extend_from_slice(&[51, 230, 133, 164, 1, 127, 131, 173]); // discriminator 51 | data.extend_from_slice(&self._amount.to_le_bytes()); 52 | data.extend_from_slice(&self._min_sol_output.to_le_bytes()); 53 | data 54 | } 55 | } 56 | 57 | /// Creates an instruction to buy tokens from a bonding curve 58 | /// 59 | /// Buys tokens by providing SOL. The amount of tokens received is calculated based on 60 | /// the bonding curve formula. A portion of the SOL is taken as a fee and sent to the 61 | /// fee recipient account. 62 | /// 63 | /// # Arguments 64 | /// 65 | /// * `payer` - Keypair that will provide the SOL to buy tokens 66 | /// * `mint` - Public key of the token mint to buy 67 | /// * `fee_recipient` - Public key of the account that will receive the transaction fee 68 | /// * `args` - Buy instruction data containing the SOL amount and maximum acceptable token price 69 | /// 70 | /// # Returns 71 | /// 72 | /// Returns a Solana instruction that when executed will buy tokens from the bonding curve 73 | pub fn buy( 74 | payer: &Keypair, 75 | mint: &Pubkey, 76 | bonding_curve: &Pubkey, 77 | creator_vault: &Pubkey, 78 | fee_recipient: &Pubkey, 79 | args: Buy, 80 | ) -> Instruction { 81 | Instruction::new_with_bytes( 82 | constants::accounts::PUMPFUN, 83 | &args.data(), 84 | vec![ 85 | AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false), 86 | AccountMeta::new(*fee_recipient, false), 87 | AccountMeta::new_readonly(*mint, false), 88 | AccountMeta::new(*bonding_curve, false), 89 | AccountMeta::new(get_associated_token_address(bonding_curve, mint), false), 90 | AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), 91 | AccountMeta::new(payer.pubkey(), true), 92 | AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false), 93 | AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false), 94 | AccountMeta::new(*creator_vault, false), 95 | AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false), 96 | AccountMeta::new_readonly(constants::accounts::PUMPFUN, false), 97 | ], 98 | ) 99 | } 100 | 101 | /// Creates an instruction to sell tokens back to a bonding curve 102 | /// 103 | /// Sells tokens back to the bonding curve in exchange for SOL. The amount of SOL received 104 | /// is calculated based on the bonding curve formula. A portion of the SOL is taken as 105 | /// a fee and sent to the fee recipient account. 106 | /// 107 | /// # Arguments 108 | /// 109 | /// * `payer` - Keypair that owns the tokens to sell 110 | /// * `mint` - Public key of the token mint to sell 111 | /// * `fee_recipient` - Public key of the account that will receive the transaction fee 112 | /// * `args` - Sell instruction data containing token amount and minimum acceptable SOL output 113 | /// 114 | /// # Returns 115 | /// 116 | /// Returns a Solana instruction that when executed will sell tokens to the bonding curve 117 | pub fn sell( 118 | payer: &Keypair, 119 | mint: &Pubkey, 120 | bonding_curve: &Pubkey, 121 | creator_vault: &Pubkey, 122 | fee_recipient: &Pubkey, 123 | args: Sell, 124 | ) -> Instruction { 125 | Instruction::new_with_bytes( 126 | constants::accounts::PUMPFUN, 127 | &args.data(), 128 | vec![ 129 | AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false), 130 | AccountMeta::new(*fee_recipient, false), 131 | AccountMeta::new_readonly(*mint, false), 132 | AccountMeta::new(*bonding_curve, false), 133 | AccountMeta::new(get_associated_token_address(&bonding_curve, mint), false), 134 | AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), 135 | AccountMeta::new(payer.pubkey(), true), 136 | AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false), 137 | AccountMeta::new(*creator_vault, false), 138 | AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false), 139 | AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false), 140 | AccountMeta::new_readonly(constants::accounts::PUMPFUN, false), 141 | ], 142 | ) 143 | } 144 | 145 | -------------------------------------------------------------------------------- /src/ipfs/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use serde_json::Value; 4 | use tokio::fs::File; 5 | use tokio::io::AsyncReadExt; 6 | use reqwest::Client; 7 | use reqwest::multipart::{Form, Part}; 8 | use base64::{Engine as _, engine::general_purpose}; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | /// Metadata structure for a token, matching the format expected by Pump.fun. 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct TokenMetadata { 15 | /// Name of the token 16 | pub name: String, 17 | /// Token symbol (e.g. "BTC") 18 | pub symbol: String, 19 | /// Description of the token 20 | pub description: String, 21 | /// IPFS URL of the token's image 22 | pub image: String, 23 | /// Whether to display the token's name 24 | pub show_name: bool, 25 | /// Creation timestamp/source 26 | pub created_on: String, 27 | /// Twitter handle 28 | pub twitter: Option, 29 | /// Telegram handle 30 | pub telegram: Option, 31 | /// Website URL 32 | pub website: Option, 33 | } 34 | 35 | /// Response received after successfully uploading token metadata. 36 | #[derive(Debug, Clone, Serialize, Deserialize)] 37 | #[serde(rename_all = "camelCase")] 38 | pub struct TokenMetadataIPFS { 39 | /// The uploaded token metadata 40 | pub metadata: TokenMetadata, 41 | /// IPFS URI where the metadata is stored 42 | pub metadata_uri: String, 43 | } 44 | 45 | /// Parameters for creating new token metadata. 46 | #[derive(Debug, Clone)] 47 | pub struct CreateTokenMetadata { 48 | /// Name of the token 49 | pub name: String, 50 | /// Token symbol (e.g. "BTC") 51 | pub symbol: String, 52 | /// Description of the token 53 | pub description: String, 54 | /// Path to the token's image file 55 | pub file: String, 56 | /// Optional Twitter handle 57 | pub twitter: Option, 58 | /// Optional Telegram group 59 | pub telegram: Option, 60 | /// Optional website URL 61 | pub website: Option, 62 | 63 | pub metadata_uri: Option, 64 | } 65 | 66 | pub async fn create_token_metadata(metadata: CreateTokenMetadata, jwt_token: &str) -> Result { 67 | let ipfs_url = if metadata.file.starts_with("http") || metadata.metadata_uri.is_some() { 68 | metadata.file 69 | } else { 70 | let base64_string = file_to_base64(&metadata.file).await?; 71 | upload_base64_file(&base64_string, jwt_token).await? 72 | }; 73 | 74 | let token_metadata = TokenMetadata { 75 | name: metadata.name, 76 | symbol: metadata.symbol, 77 | description: metadata.description, 78 | image: ipfs_url, 79 | show_name: true, 80 | created_on: "https://pump.fun".to_string(), 81 | twitter: metadata.twitter, 82 | telegram: metadata.telegram, 83 | website: metadata.website, 84 | }; 85 | 86 | if metadata.metadata_uri.is_some() { 87 | let token_metadata_ipfs = TokenMetadataIPFS { 88 | metadata: token_metadata, 89 | metadata_uri: metadata.metadata_uri.unwrap(), 90 | }; 91 | Ok(token_metadata_ipfs) 92 | } else { 93 | let client = Client::new(); 94 | let response = client 95 | .post("https://api.pinata.cloud/pinning/pinJSONToIPFS") 96 | .header("Content-Type", "application/json") 97 | .header("Authorization", format!("Bearer {}", jwt_token)) 98 | .json(&token_metadata) 99 | .send() 100 | .await?; 101 | 102 | // 确保请求成功 103 | if response.status().is_success() { 104 | let res_data: serde_json::Value = response.json().await?; 105 | let ipfs_hash = res_data["IpfsHash"].as_str().unwrap(); 106 | let ipfs_url = format!("https://ipfs.io/ipfs/{}", ipfs_hash); 107 | let token_metadata_ipfs = TokenMetadataIPFS { 108 | metadata: token_metadata, 109 | metadata_uri: ipfs_url, 110 | }; 111 | Ok(token_metadata_ipfs) 112 | } else { 113 | eprintln!("Error: {:?}", response.status()); 114 | Err(anyhow::anyhow!("Failed to create token metadata")) 115 | } 116 | } 117 | } 118 | 119 | pub async fn upload_base64_file(base64_string: &str, jwt_token: &str) -> Result { 120 | let decoded_bytes = general_purpose::STANDARD.decode(base64_string)?; 121 | 122 | let client = Client::builder() 123 | .timeout(Duration::from_secs(120)) // 增加超时时间到120秒 124 | .pool_max_idle_per_host(0) // 禁用连接池 125 | .pool_idle_timeout(None) // 禁用空闲超时 126 | .build()?; 127 | 128 | let part = Part::bytes(decoded_bytes) 129 | .file_name("file.png") // 添加文件扩展名 130 | .mime_str("image/png")?; // 指定正确的MIME类型 131 | 132 | let form = Form::new().part("file", part); 133 | 134 | let response = client 135 | .post("https://api.pinata.cloud/pinning/pinFileToIPFS") 136 | .header("Authorization", format!("Bearer {}", jwt_token)) 137 | .header("Accept", "application/json") 138 | .multipart(form) 139 | .send() 140 | .await?; 141 | 142 | if response.status().is_success() { 143 | let response_json: Value = response.json().await.map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}", e))?; 144 | println!("{:#?}", response_json); 145 | let ipfs_hash = response_json["IpfsHash"].as_str().unwrap(); 146 | let ipfs_url = format!("https://ipfs.io/ipfs/{}", ipfs_hash); 147 | Ok(ipfs_url) 148 | } else { 149 | let error_text = response.text().await?; 150 | eprintln!("Error: {:?}", error_text); 151 | Err(anyhow::anyhow!("Failed to upload file to IPFS: {}", error_text)) 152 | } 153 | } 154 | 155 | async fn file_to_base64(file_path: &str) -> Result { 156 | let mut file = File::open(file_path).await?; 157 | let mut buffer = Vec::new(); 158 | file.read_to_end(&mut buffer).await?; 159 | let base64_string = general_purpose::STANDARD.encode(&buffer); 160 | Ok(base64_string) 161 | } 162 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod accounts; 2 | pub mod constants; 3 | pub mod error; 4 | pub mod instruction; 5 | pub mod grpc; 6 | pub mod common; 7 | pub mod ipfs; 8 | pub mod swqos; 9 | pub mod pumpfun; 10 | 11 | use std::sync::Arc; 12 | 13 | use swqos::{FeeClient, JitoClient, NextBlockClient, ZeroSlotClient}; 14 | use rustls::crypto::{ring::default_provider, CryptoProvider}; 15 | use solana_sdk::{ 16 | commitment_config::CommitmentConfig, 17 | pubkey::Pubkey, 18 | signature::{Keypair, Signer}, 19 | }; 20 | 21 | use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe, Cluster, PriorityFee, SolanaRpcClient}; 22 | use common::logs_subscribe::SubscriptionHandle; 23 | use ipfs::TokenMetadataIPFS; 24 | 25 | pub struct PumpFun { 26 | pub payer: Arc, 27 | pub rpc: Arc, 28 | pub fee_clients: Vec>, 29 | pub priority_fee: PriorityFee, 30 | pub cluster: Cluster, 31 | } 32 | 33 | impl Clone for PumpFun { 34 | fn clone(&self) -> Self { 35 | Self { 36 | payer: self.payer.clone(), 37 | rpc: self.rpc.clone(), 38 | fee_clients: self.fee_clients.clone(), 39 | priority_fee: self.priority_fee.clone(), 40 | cluster: self.cluster.clone(), 41 | } 42 | } 43 | } 44 | 45 | impl PumpFun { 46 | #[inline] 47 | pub async fn new( 48 | payer: Arc, 49 | cluster: &Cluster, 50 | ) -> Self { 51 | if CryptoProvider::get_default().is_none() { 52 | let _ = default_provider() 53 | .install_default() 54 | .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e)); 55 | } 56 | 57 | let rpc = SolanaRpcClient::new_with_commitment( 58 | cluster.clone().rpc_url, 59 | cluster.clone().commitment 60 | ); 61 | 62 | let mut fee_clients: Vec> = vec![]; 63 | if cluster.clone().use_jito { 64 | let jito_client = JitoClient::new( 65 | cluster.clone().rpc_url, 66 | cluster.clone().block_engine_url 67 | ).await.expect("Failed to create Jito client"); 68 | 69 | fee_clients.push(Arc::new(jito_client)); 70 | } 71 | 72 | if cluster.clone().use_zeroslot { 73 | let zeroslot_client = ZeroSlotClient::new( 74 | cluster.clone().rpc_url, 75 | cluster.clone().zeroslot_url, 76 | cluster.clone().zeroslot_auth_token 77 | ); 78 | 79 | fee_clients.push(Arc::new(zeroslot_client)); 80 | } 81 | 82 | if cluster.clone().use_nextblock { 83 | let nextblock_client = NextBlockClient::new( 84 | cluster.clone().rpc_url, 85 | cluster.clone().nextblock_url, 86 | cluster.clone().nextblock_auth_token 87 | ); 88 | 89 | fee_clients.push(Arc::new(nextblock_client)); 90 | } 91 | 92 | Self { 93 | payer, 94 | rpc: Arc::new(rpc), 95 | fee_clients, 96 | priority_fee: cluster.clone().priority_fee, 97 | cluster: cluster.clone(), 98 | } 99 | } 100 | 101 | /// Buy tokens 102 | pub async fn buy( 103 | &self, 104 | mint: Pubkey, 105 | amount_sol: u64, 106 | slippage_basis_points: Option, 107 | ) -> Result<(), anyhow::Error> { 108 | pumpfun::buy::buy( 109 | self.rpc.clone(), 110 | self.payer.clone(), 111 | mint, 112 | amount_sol, 113 | slippage_basis_points, 114 | self.priority_fee.clone(), 115 | ).await 116 | } 117 | 118 | /// Buy tokens using Jito 119 | pub async fn buy_with_tip( 120 | &self, 121 | mint: Pubkey, 122 | amount_sol: u64, 123 | slippage_basis_points: Option, 124 | ) -> Result<(), anyhow::Error> { 125 | pumpfun::buy::buy_with_tip( 126 | self.rpc.clone(), 127 | self.fee_clients.clone(), 128 | self.payer.clone(), 129 | mint, 130 | amount_sol, 131 | slippage_basis_points, 132 | self.priority_fee.clone(), 133 | ).await 134 | } 135 | 136 | /// Sell tokens 137 | pub async fn sell( 138 | &self, 139 | mint: Pubkey, 140 | amount_token: Option, 141 | ) -> Result<(), anyhow::Error> { 142 | pumpfun::sell::sell( 143 | self.rpc.clone(), 144 | self.payer.clone(), 145 | mint.clone(), 146 | amount_token, 147 | self.priority_fee.clone(), 148 | ).await 149 | } 150 | 151 | /// Sell tokens by percentage 152 | pub async fn sell_by_percent( 153 | &self, 154 | mint: Pubkey, 155 | percent: u64, 156 | ) -> Result<(), anyhow::Error> { 157 | pumpfun::sell::sell_by_percent( 158 | self.rpc.clone(), 159 | self.payer.clone(), 160 | mint.clone(), 161 | percent, 162 | self.priority_fee.clone(), 163 | ).await 164 | } 165 | 166 | pub async fn sell_by_percent_with_tip( 167 | &self, 168 | mint: Pubkey, 169 | percent: u64, 170 | ) -> Result<(), anyhow::Error> { 171 | pumpfun::sell::sell_by_percent_with_tip( 172 | self.rpc.clone(), 173 | self.fee_clients.clone(), 174 | self.payer.clone(), 175 | mint, 176 | percent, 177 | self.priority_fee.clone(), 178 | ).await 179 | } 180 | 181 | /// Sell tokens using Jito 182 | pub async fn sell_with_tip( 183 | &self, 184 | mint: Pubkey, 185 | amount_token: Option, 186 | ) -> Result<(), anyhow::Error> { 187 | pumpfun::sell::sell_with_tip( 188 | self.rpc.clone(), 189 | self.fee_clients.clone(), 190 | self.payer.clone(), 191 | mint, 192 | amount_token, 193 | self.priority_fee.clone(), 194 | ).await 195 | } 196 | 197 | #[inline] 198 | pub async fn tokens_subscription( 199 | &self, 200 | ws_url: &str, 201 | commitment: CommitmentConfig, 202 | callback: F, 203 | bot_wallet: Option, 204 | ) -> Result> 205 | where 206 | F: Fn(PumpfunEvent) + Send + Sync + 'static, 207 | { 208 | logs_subscribe::tokens_subscription(ws_url, commitment, callback, bot_wallet).await 209 | } 210 | 211 | #[inline] 212 | pub async fn stop_subscription(&self, subscription_handle: SubscriptionHandle) { 213 | subscription_handle.shutdown().await; 214 | } 215 | 216 | #[inline] 217 | pub async fn get_sol_balance(&self, payer: &Pubkey) -> Result { 218 | pumpfun::common::get_sol_balance(&self.rpc, payer).await 219 | } 220 | 221 | #[inline] 222 | pub async fn get_payer_sol_balance(&self) -> Result { 223 | pumpfun::common::get_sol_balance(&self.rpc, &self.payer.pubkey()).await 224 | } 225 | 226 | #[inline] 227 | pub async fn get_token_balance(&self, payer: &Pubkey, mint: &Pubkey) -> Result { 228 | println!("get_token_balance payer: {}, mint: {}, cluster: {}", payer, mint, self.cluster.rpc_url); 229 | pumpfun::common::get_token_balance(&self.rpc, payer, mint).await 230 | } 231 | 232 | #[inline] 233 | pub async fn get_payer_token_balance(&self, mint: &Pubkey) -> Result { 234 | pumpfun::common::get_token_balance(&self.rpc, &self.payer.pubkey(), mint).await 235 | } 236 | 237 | #[inline] 238 | pub fn get_payer_pubkey(&self) -> Pubkey { 239 | self.payer.pubkey() 240 | } 241 | 242 | #[inline] 243 | pub fn get_payer(&self) -> &Keypair { 244 | self.payer.as_ref() 245 | } 246 | 247 | #[inline] 248 | pub fn get_token_price(&self,virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { 249 | pumpfun::common::get_token_price(virtual_sol_reserves, virtual_token_reserves) 250 | } 251 | 252 | #[inline] 253 | pub fn get_buy_price(&self, amount: u64, trade_info: &TradeInfo) -> u64 { 254 | pumpfun::common::get_buy_price(amount, trade_info) 255 | } 256 | 257 | #[inline] 258 | pub async fn transfer_sol(&self, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { 259 | pumpfun::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use pumpfun_sdk::{common::{logs_events::PumpfunEvent, Cluster, PriorityFee}, grpc::YellowstoneGrpc, ipfs, PumpFun}; 3 | use solana_sdk::{commitment_config::CommitmentConfig, native_token::sol_to_lamports, signature::Keypair, signer::Signer}; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<(), Box> { 7 | // create grpc client 8 | let grpc_url = "http://127.0.0.1:10000"; 9 | let client = YellowstoneGrpc::new(grpc_url.to_string()); 10 | 11 | // Define callback function 12 | let callback = |event: PumpfunEvent| { 13 | match event { 14 | PumpfunEvent::NewToken(token_info) => { 15 | println!("Received new token event: {:?}", token_info); 16 | }, 17 | PumpfunEvent::NewDevTrade(trade_info) => { 18 | println!("Received dev trade event: {:?}", trade_info); 19 | }, 20 | PumpfunEvent::NewUserTrade(trade_info) => { 21 | println!("Received new trade event: {:?}", trade_info); 22 | }, 23 | PumpfunEvent::NewBotTrade(trade_info) => { 24 | println!("Received new bot trade event: {:?}", trade_info); 25 | } 26 | PumpfunEvent::Error(err) => { 27 | println!("Received error: {}", err); 28 | } 29 | } 30 | }; 31 | 32 | let payer_keypair = Keypair::from_base58_string("your private key"); 33 | client.subscribe_pumpfun(callback, Some(payer_keypair.pubkey())).await?; 34 | 35 | Ok(()) 36 | } -------------------------------------------------------------------------------- /src/pumpfun/buy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use solana_sdk::{ 3 | compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} 4 | }; 5 | use solana_hash::Hash; 6 | use spl_associated_token_account::instruction::create_associated_token_account; 7 | use tokio::task::JoinHandle; 8 | use std::{str::FromStr, time::Instant, sync::Arc}; 9 | 10 | use crate::{common::{PriorityFee, SolanaRpcClient}, constants::{self, global_constants::FEE_RECIPIENT}, instruction, swqos::FeeClient}; 11 | 12 | const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 250000; 13 | 14 | use super::common::{calculate_with_slippage_buy, get_bonding_curve_account, get_buy_token_amount_from_sol_amount, get_creator_vault_pda}; 15 | 16 | pub async fn buy( 17 | rpc: Arc, 18 | payer: Arc, 19 | mint: Pubkey, 20 | amount_sol: u64, 21 | slippage_basis_points: Option, 22 | priority_fee: PriorityFee, 23 | ) -> Result<(), anyhow::Error> { 24 | let transaction = build_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points, priority_fee.clone()).await?; 25 | rpc.send_and_confirm_transaction(&transaction).await?; 26 | Ok(()) 27 | } 28 | 29 | /// Buy tokens using Jito 30 | pub async fn buy_with_tip( 31 | rpc: Arc, 32 | fee_clients: Vec>, 33 | payer: Arc, 34 | mint: Pubkey, 35 | amount_sol: u64, 36 | slippage_basis_points: Option, 37 | priority_fee: PriorityFee, 38 | ) -> Result<(), anyhow::Error> { 39 | let start_time = Instant::now(); 40 | 41 | let mint = Arc::new(mint.clone()); 42 | let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?; 43 | 44 | let mut transactions = vec![]; 45 | let recent_blockhash = rpc.get_latest_blockhash().await?; 46 | for fee_client in fee_clients.clone() { 47 | let payer = payer.clone(); 48 | let priority_fee = priority_fee.clone(); 49 | let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?; 50 | let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); 51 | 52 | let transaction = build_buy_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?; 53 | transactions.push(transaction); 54 | } 55 | 56 | let mut handles: Vec>> = vec![]; 57 | for i in 0..fee_clients.len() { 58 | let fee_client = fee_clients[i].clone(); 59 | let transactions = transactions.clone(); 60 | let start_time = start_time.clone(); 61 | let transaction = transactions[i].clone(); 62 | let handle = tokio::spawn(async move { 63 | fee_client.send_transaction(&transaction).await?; 64 | println!("index: {}, Total Jito buy operation time: {:?}ms", i, start_time.elapsed().as_millis()); 65 | Ok::<(), anyhow::Error>(()) 66 | }); 67 | 68 | handles.push(handle); 69 | } 70 | 71 | for handle in handles { 72 | match handle.await { 73 | Ok(Ok(_)) => (), 74 | Ok(Err(e)) => println!("Error in task: {}", e), 75 | Err(e) => println!("Task join error: {}", e), 76 | } 77 | } 78 | 79 | Ok(()) 80 | } 81 | 82 | pub async fn build_buy_transaction( 83 | rpc: Arc, 84 | payer: Arc, 85 | mint: Pubkey, 86 | amount_sol: u64, 87 | slippage_basis_points: Option, 88 | priority_fee: PriorityFee, 89 | ) -> Result { 90 | let mut instructions = vec![ 91 | ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), 92 | ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), 93 | ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), 94 | ]; 95 | 96 | let build_instructions = build_buy_instructions(rpc.clone(), payer.clone(), Arc::new(mint), amount_sol, slippage_basis_points).await?; 97 | instructions.extend(build_instructions); 98 | 99 | let recent_blockhash = rpc.get_latest_blockhash().await?; 100 | let transaction = Transaction::new_signed_with_payer( 101 | &instructions, 102 | Some(&payer.pubkey()), 103 | &[payer], 104 | recent_blockhash, 105 | ); 106 | 107 | Ok(transaction) 108 | } 109 | 110 | pub async fn build_buy_transaction_with_tip( 111 | tip_account: Arc, 112 | payer: Arc, 113 | priority_fee: PriorityFee, 114 | build_instructions: Vec, 115 | blockhash: Hash, 116 | ) -> Result { 117 | let mut instructions = vec![ 118 | ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), 119 | ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), 120 | ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), 121 | system_instruction::transfer( 122 | &payer.pubkey(), 123 | &tip_account, 124 | sol_to_lamports(priority_fee.buy_tip_fee), 125 | ), 126 | ]; 127 | 128 | instructions.extend(build_instructions); 129 | 130 | let v0_message: v0::Message = 131 | v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?; 132 | let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); 133 | let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; 134 | 135 | Ok(transaction) 136 | } 137 | 138 | pub async fn build_buy_instructions( 139 | rpc: Arc, 140 | payer: Arc, 141 | mint: Arc, 142 | buy_sol_cost: u64, 143 | slippage_basis_points: Option, 144 | ) -> Result, anyhow::Error> { 145 | if buy_sol_cost == 0 { 146 | return Err(anyhow!("Amount cannot be zero")); 147 | } 148 | 149 | let (bonding_curve, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint).await?; 150 | let creator_vault_pda = get_creator_vault_pda(&bonding_curve.creator).unwrap(); 151 | let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(100)); 152 | 153 | let mut buy_token_amount = get_buy_token_amount_from_sol_amount(&bonding_curve, buy_sol_cost); 154 | if buy_token_amount <= 100 * 1_000_000_u64 { 155 | buy_token_amount = if max_sol_cost > sol_to_lamports(0.01) { 156 | 25547619 * 1_000_000_u64 157 | } else { 158 | 255476 * 1_000_000_u64 159 | }; 160 | } 161 | 162 | let mut instructions = vec![]; 163 | instructions.push(create_associated_token_account( 164 | &payer.pubkey(), 165 | &payer.pubkey(), 166 | &mint, 167 | &constants::accounts::TOKEN_PROGRAM, 168 | )); 169 | 170 | instructions.push(instruction::buy( 171 | payer.as_ref(), 172 | &mint, 173 | &bonding_curve_pda, 174 | &creator_vault_pda, 175 | &FEE_RECIPIENT, 176 | instruction::Buy { 177 | _amount: buy_token_amount, 178 | _max_sol_cost: max_sol_cost, 179 | }, 180 | )); 181 | 182 | Ok(instructions) 183 | } -------------------------------------------------------------------------------- /src/pumpfun/common.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use spl_token::state::Account; 3 | use tokio::sync::RwLock; 4 | use std::{collections::HashMap, sync::Arc}; 5 | use solana_sdk::{ 6 | commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction 7 | }; 8 | use spl_associated_token_account::get_associated_token_address; 9 | use crate::{accounts::{self, BondingCurveAccount}, common::{logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}; 10 | use borsh::BorshDeserialize; 11 | 12 | lazy_static::lazy_static! { 13 | static ref ACCOUNT_CACHE: RwLock>> = RwLock::new(HashMap::new()); 14 | } 15 | 16 | pub async fn transfer_sol(rpc: &SolanaRpcClient, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { 17 | if amount == 0 { 18 | return Err(anyhow!("transfer_sol: Amount cannot be zero")); 19 | } 20 | 21 | let balance = get_sol_balance(rpc, &payer.pubkey()).await?; 22 | if balance < amount { 23 | return Err(anyhow!("Insufficient balance")); 24 | } 25 | 26 | let transfer_instruction = system_instruction::transfer( 27 | &payer.pubkey(), 28 | receive_wallet, 29 | amount, 30 | ); 31 | 32 | let recent_blockhash = rpc.get_latest_blockhash().await?; 33 | 34 | let transaction = Transaction::new_signed_with_payer( 35 | &[transfer_instruction], 36 | Some(&payer.pubkey()), 37 | &[payer], 38 | recent_blockhash, 39 | ); 40 | 41 | rpc.send_and_confirm_transaction(&transaction).await?; 42 | 43 | Ok(()) 44 | } 45 | 46 | #[inline] 47 | pub fn create_priority_fee_instructions(priority_fee: PriorityFee) -> Vec { 48 | let mut instructions = Vec::with_capacity(2); 49 | instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit)); 50 | instructions.push(ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price)); 51 | 52 | instructions 53 | } 54 | 55 | // #[inline] 56 | pub async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Pubkey, mint: &Pubkey) -> Result { 57 | let ata = get_associated_token_address(payer, mint); 58 | // let account_data = rpc.get_account_data(&ata).await?; 59 | // let token_account = Account::unpack(&account_data.as_slice())?; 60 | 61 | // Ok(token_account.amount) 62 | 63 | // println!("get_token_balance ata: {}", ata); 64 | let balance = rpc.get_token_account_balance(&ata).await?; 65 | let balance_u64 = balance.amount.parse::() 66 | .map_err(|_| anyhow!("Failed to parse token balance"))?; 67 | Ok(balance_u64) 68 | } 69 | 70 | #[inline] 71 | pub async fn get_token_balance_and_ata(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> { 72 | let ata = get_associated_token_address(&payer.pubkey(), mint); 73 | // let account_data = rpc.get_account_data(&ata).await?; 74 | // let token_account = Account::unpack(&account_data)?; 75 | 76 | // Ok((token_account.amount, ata)) 77 | 78 | let balance = rpc.get_token_account_balance(&ata).await?; 79 | let balance_u64 = balance.amount.parse::() 80 | .map_err(|_| anyhow!("Failed to parse token balance"))?; 81 | 82 | if balance_u64 == 0 { 83 | return Err(anyhow!("Balance is 0")); 84 | } 85 | 86 | Ok((balance_u64, ata)) 87 | } 88 | 89 | #[inline] 90 | pub async fn get_sol_balance(rpc: &SolanaRpcClient, account: &Pubkey) -> Result { 91 | let balance = rpc.get_balance(account).await?; 92 | Ok(balance) 93 | } 94 | 95 | #[inline] 96 | pub fn get_global_pda() -> Pubkey { 97 | static GLOBAL_PDA: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { 98 | Pubkey::find_program_address(&[constants::seeds::GLOBAL_SEED], &constants::accounts::PUMPFUN).0 99 | }); 100 | *GLOBAL_PDA 101 | } 102 | 103 | #[inline] 104 | pub fn get_mint_authority_pda() -> Pubkey { 105 | static MINT_AUTHORITY_PDA: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { 106 | Pubkey::find_program_address(&[constants::seeds::MINT_AUTHORITY_SEED], &constants::accounts::PUMPFUN).0 107 | }); 108 | *MINT_AUTHORITY_PDA 109 | } 110 | 111 | #[inline] 112 | pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option { 113 | let seeds: &[&[u8]; 2] = &[constants::seeds::BONDING_CURVE_SEED, mint.as_ref()]; 114 | let program_id: &Pubkey = &constants::accounts::PUMPFUN; 115 | let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); 116 | pda.map(|pubkey| pubkey.0) 117 | } 118 | 119 | #[inline] 120 | pub fn get_creator_vault_pda(creator: &Pubkey) -> Option { 121 | let seeds: &[&[u8]; 2] = &[constants::seeds::CREATOR_VAULT_SEED, creator.as_ref()]; 122 | let program_id: &Pubkey = &constants::accounts::PUMPFUN; 123 | let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); 124 | pda.map(|pubkey| pubkey.0) 125 | } 126 | 127 | #[inline] 128 | pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey { 129 | Pubkey::find_program_address( 130 | &[ 131 | constants::seeds::METADATA_SEED, 132 | constants::accounts::MPL_TOKEN_METADATA.as_ref(), 133 | mint.as_ref(), 134 | ], 135 | &constants::accounts::MPL_TOKEN_METADATA 136 | ).0 137 | } 138 | 139 | #[inline] 140 | pub async fn get_global_account(rpc: &SolanaRpcClient) -> Result, anyhow::Error> { 141 | let global = get_global_pda(); 142 | if let Some(account) = ACCOUNT_CACHE.read().await.get(&global) { 143 | return Ok(account.clone()); 144 | } 145 | 146 | let account = rpc.get_account(&global).await?; 147 | let global_account = bincode::deserialize::(&account.data)?; 148 | let global_account = Arc::new(global_account); 149 | 150 | ACCOUNT_CACHE.write().await.insert(global, global_account.clone()); 151 | Ok(global_account) 152 | } 153 | 154 | #[inline] 155 | pub async fn get_initial_buy_price(global_account: &Arc, amount_sol: u64) -> Result { 156 | let buy_amount = global_account.get_initial_buy_price(amount_sol); 157 | Ok(buy_amount) 158 | } 159 | 160 | #[inline] 161 | pub async fn get_bonding_curve_account( 162 | rpc: &SolanaRpcClient, 163 | mint: &Pubkey, 164 | ) -> Result<(Arc, Pubkey), anyhow::Error> { 165 | let bonding_curve_pda = get_bonding_curve_pda(mint) 166 | .ok_or(anyhow!("Bonding curve not found"))?; 167 | 168 | let account = rpc.get_account(&bonding_curve_pda).await?; 169 | if account.data.is_empty() { 170 | return Err(anyhow!("Bonding curve not found")); 171 | } 172 | 173 | let bonding_curve = Arc::new(bincode::deserialize::(&account.data)?); 174 | 175 | Ok((bonding_curve, bonding_curve_pda)) 176 | } 177 | 178 | #[inline] 179 | pub fn get_buy_token_amount( 180 | bonding_curve_account: &Arc, 181 | buy_sol_cost: u64, 182 | slippage_basis_points: Option, 183 | ) -> anyhow::Result<(u64, u64)> { 184 | let buy_token = bonding_curve_account.get_buy_price(buy_sol_cost).map_err(|e| anyhow!(e))?; 185 | 186 | let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); 187 | 188 | Ok((buy_token, max_sol_cost)) 189 | } 190 | 191 | pub fn get_buy_token_amount_from_sol_amount( 192 | bonding_curve: &BondingCurveAccount, 193 | amount: u64, 194 | ) -> u64 { 195 | if amount == 0 { 196 | return 0; 197 | } 198 | 199 | if bonding_curve.virtual_token_reserves == 0 { 200 | return 0; 201 | } 202 | 203 | let total_fee_basis_points = FEE_BASIS_POINTS 204 | + if bonding_curve.creator != Pubkey::default() { 205 | CREATOR_FEE 206 | } else { 207 | 0 208 | }; 209 | 210 | // 转为 u128 防止溢出 211 | let amount_128 = amount as u128; 212 | let total_fee_basis_points_128 = total_fee_basis_points as u128; 213 | let input_amount = amount_128 214 | .checked_mul(10_000) 215 | .unwrap() 216 | .checked_div(total_fee_basis_points_128 + 10_000) 217 | .unwrap(); 218 | 219 | let virtual_token_reserves = bonding_curve.virtual_token_reserves as u128; 220 | let virtual_sol_reserves = bonding_curve.virtual_sol_reserves as u128; 221 | let real_token_reserves = bonding_curve.real_token_reserves as u128; 222 | 223 | let denominator = virtual_sol_reserves + input_amount; 224 | 225 | let tokens_received = input_amount 226 | .checked_mul(virtual_token_reserves) 227 | .unwrap() 228 | .checked_div(denominator) 229 | .unwrap(); 230 | 231 | tokens_received.min(real_token_reserves) as u64 232 | } 233 | 234 | #[inline] 235 | pub fn get_buy_amount_with_slippage(amount_sol: u64, slippage_basis_points: Option) -> u64 { 236 | let slippage = slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE); 237 | amount_sol + (amount_sol * slippage / 10000) 238 | } 239 | 240 | #[inline] 241 | pub fn get_token_price(virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { 242 | let v_sol = virtual_sol_reserves as f64 / 100_000_000.0; 243 | let v_tokens = virtual_token_reserves as f64 / 100_000.0; 244 | v_sol / v_tokens 245 | } 246 | 247 | #[inline] 248 | pub fn get_buy_price(amount: u64, trade_info: &TradeInfo) -> u64 { 249 | if amount == 0 { 250 | return 0; 251 | } 252 | 253 | let n: u128 = (trade_info.virtual_sol_reserves as u128) * (trade_info.virtual_token_reserves as u128); 254 | let i: u128 = (trade_info.virtual_sol_reserves as u128) + (amount as u128); 255 | let r: u128 = n / i + 1; 256 | let s: u128 = (trade_info.virtual_token_reserves as u128) - r; 257 | let s_u64 = s as u64; 258 | 259 | s_u64.min(trade_info.real_token_reserves) 260 | } 261 | 262 | #[inline] 263 | pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 { 264 | amount + (amount * basis_points) / 10000 265 | } 266 | 267 | #[inline] 268 | pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 { 269 | amount - (amount * basis_points) / 10000 270 | } 271 | -------------------------------------------------------------------------------- /src/pumpfun/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod buy; 2 | pub mod sell; 3 | pub mod common; -------------------------------------------------------------------------------- /src/pumpfun/sell.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use solana_sdk::{ 3 | compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} 4 | }; 5 | use solana_hash::Hash; 6 | use spl_associated_token_account::get_associated_token_address; 7 | use spl_token::instruction::close_account; 8 | use tokio::task::JoinHandle; 9 | 10 | use std::{str::FromStr, time::Instant, sync::Arc}; 11 | 12 | use crate::{common::{PriorityFee, SolanaRpcClient}, instruction, swqos::FeeClient}; 13 | 14 | use super::common::{get_bonding_curve_account, get_creator_vault_pda, get_global_account}; 15 | 16 | async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> { 17 | let ata = get_associated_token_address(&payer.pubkey(), mint); 18 | let balance = rpc.get_token_account_balance(&ata).await?; 19 | let balance_u64 = balance.amount.parse::() 20 | .map_err(|_| anyhow!("Failed to parse token balance"))?; 21 | 22 | if balance_u64 == 0 { 23 | return Err(anyhow!("Balance is 0")); 24 | } 25 | 26 | Ok((balance_u64, ata)) 27 | } 28 | 29 | pub async fn sell( 30 | rpc: Arc, 31 | payer: Arc, 32 | mint: Pubkey, 33 | amount_token: Option, 34 | priority_fee: PriorityFee, 35 | ) -> Result<(), anyhow::Error> { 36 | let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token).await?; 37 | let transaction = build_sell_transaction(rpc.clone(), payer.clone(), priority_fee, instructions).await?; 38 | rpc.send_and_confirm_transaction(&transaction).await?; 39 | 40 | Ok(()) 41 | } 42 | 43 | /// Sell tokens by percentage 44 | pub async fn sell_by_percent( 45 | rpc: Arc, 46 | payer: Arc, 47 | mint: Pubkey, 48 | percent: u64, 49 | priority_fee: PriorityFee, 50 | ) -> Result<(), anyhow::Error> { 51 | if percent == 0 || percent > 100 { 52 | return Err(anyhow!("Percentage must be between 1 and 100")); 53 | } 54 | 55 | let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; 56 | let amount = balance_u64 * percent / 100; 57 | sell(rpc, payer, mint, Some(amount), priority_fee).await 58 | } 59 | 60 | pub async fn sell_by_percent_with_tip( 61 | rpc: Arc, 62 | fee_clients: Vec>, 63 | payer: Arc, 64 | mint: Pubkey, 65 | percent: u64, 66 | priority_fee: PriorityFee, 67 | ) -> Result<(), anyhow::Error> { 68 | if percent == 0 || percent > 100 { 69 | return Err(anyhow!("Percentage must be between 1 and 100")); 70 | } 71 | 72 | let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; 73 | let amount = balance_u64 * percent / 100; 74 | sell_with_tip(rpc, fee_clients, payer, mint, Some(amount), priority_fee).await 75 | } 76 | 77 | /// Sell tokens using Jito 78 | pub async fn sell_with_tip( 79 | rpc: Arc, 80 | fee_clients: Vec>, 81 | payer: Arc, 82 | mint: Pubkey, 83 | amount_token: Option, 84 | priority_fee: PriorityFee, 85 | ) -> Result<(), anyhow::Error> { 86 | let start_time = Instant::now(); 87 | 88 | let mut transactions = vec![]; 89 | let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token).await?; 90 | 91 | let recent_blockhash = rpc.get_latest_blockhash().await?; 92 | for fee_client in fee_clients.clone() { 93 | let payer = payer.clone(); 94 | let priority_fee = priority_fee.clone(); 95 | let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?; 96 | let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); 97 | 98 | let transaction = build_sell_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?; 99 | transactions.push(transaction); 100 | } 101 | 102 | let mut handles = vec![]; 103 | for i in 0..fee_clients.len() { 104 | let fee_client = fee_clients[i].clone(); 105 | let transaction = transactions[i].clone(); 106 | let handle: JoinHandle> = tokio::spawn(async move { 107 | fee_client.send_transaction(&transaction).await?; 108 | println!("index: {}, Total Jito sell operation time: {:?}ms", i, start_time.elapsed().as_millis()); 109 | Ok(()) 110 | }); 111 | 112 | handles.push(handle); 113 | } 114 | 115 | for handle in handles { 116 | match handle.await { 117 | Ok(Ok(_)) => (), 118 | Ok(Err(e)) => println!("Error in task: {}", e), 119 | Err(e) => println!("Task join error: {}", e), 120 | } 121 | } 122 | 123 | println!("Total Jito sell operation time: {:?}ms", start_time.elapsed().as_millis()); 124 | Ok(()) 125 | } 126 | 127 | pub async fn build_sell_transaction( 128 | rpc: Arc, 129 | payer: Arc, 130 | priority_fee: PriorityFee, 131 | build_instructions: Vec 132 | ) -> Result { 133 | let mut instructions = vec![ 134 | ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), 135 | ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), 136 | ]; 137 | 138 | instructions.extend(build_instructions); 139 | 140 | let recent_blockhash = rpc.get_latest_blockhash().await?; 141 | let transaction = Transaction::new_signed_with_payer( 142 | &instructions, 143 | Some(&payer.pubkey()), 144 | &[payer.as_ref()], 145 | recent_blockhash, 146 | ); 147 | 148 | Ok(transaction) 149 | } 150 | 151 | pub async fn build_sell_transaction_with_tip( 152 | tip_account: Arc, 153 | payer: Arc, 154 | priority_fee: PriorityFee, 155 | build_instructions: Vec, 156 | blockhash: Hash, 157 | ) -> Result { 158 | let mut instructions = vec![ 159 | ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), 160 | ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), 161 | system_instruction::transfer( 162 | &payer.pubkey(), 163 | &tip_account, 164 | sol_to_lamports(priority_fee.sell_tip_fee), 165 | ), 166 | ]; 167 | 168 | instructions.extend(build_instructions); 169 | 170 | let v0_message: v0::Message = 171 | v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?; 172 | let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); 173 | 174 | let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; 175 | 176 | Ok(transaction) 177 | } 178 | 179 | pub async fn build_sell_instructions( 180 | rpc: Arc, 181 | payer: Arc, 182 | mint: Pubkey, 183 | amount_token: Option, 184 | ) -> Result, anyhow::Error> { 185 | let (balance_u64, ata) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; 186 | let amount = amount_token.unwrap_or(balance_u64); 187 | 188 | if amount == 0 { 189 | return Err(anyhow!("Amount cannot be zero")); 190 | } 191 | 192 | let global_account = get_global_account(rpc.as_ref()).await?; 193 | let (bonding_curve_account, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint).await?; 194 | let creator_vault_pda = get_creator_vault_pda(&bonding_curve_account.creator).unwrap(); 195 | 196 | let instructions = vec![ 197 | instruction::sell( 198 | payer.as_ref(), 199 | &mint, 200 | &bonding_curve_pda, 201 | &creator_vault_pda, 202 | &global_account.fee_recipient, 203 | instruction::Sell { 204 | _amount: amount, 205 | _min_sol_output: 0, 206 | }, 207 | ), 208 | 209 | close_account( 210 | &spl_token::ID, 211 | &ata, 212 | &payer.pubkey(), 213 | &payer.pubkey(), 214 | &[&payer.pubkey()], 215 | )? 216 | ]; 217 | 218 | Ok(instructions) 219 | } 220 | -------------------------------------------------------------------------------- /src/swqos/api.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by prost-build. 2 | #[derive(Clone, PartialEq, ::prost::Message)] 3 | pub struct PostSubmitRequest { 4 | #[prost(message, optional, tag = "1")] 5 | pub transaction: ::core::option::Option, 6 | #[prost(bool, tag = "2")] 7 | pub skip_pre_flight: bool, 8 | #[prost(bool, optional, tag = "3")] 9 | pub front_running_protection: ::core::option::Option, 10 | #[prost(bool, optional, tag = "8")] 11 | pub experimental_front_running_protection: ::core::option::Option, 12 | #[prost(bool, optional, tag = "9")] 13 | pub snipe_transaction: ::core::option::Option, 14 | } 15 | #[derive(Clone, PartialEq, ::prost::Message)] 16 | pub struct PostSubmitRequestEntry { 17 | #[prost(message, optional, tag = "1")] 18 | pub transaction: ::core::option::Option, 19 | #[prost(bool, tag = "2")] 20 | pub skip_pre_flight: bool, 21 | } 22 | #[derive(Clone, PartialEq, ::prost::Message)] 23 | pub struct PostSubmitBatchRequest { 24 | #[prost(message, repeated, tag = "1")] 25 | pub entries: ::prost::alloc::vec::Vec, 26 | #[prost(enumeration = "SubmitStrategy", tag = "2")] 27 | pub submit_strategy: i32, 28 | #[prost(bool, optional, tag = "3")] 29 | pub use_bundle: ::core::option::Option, 30 | #[prost(bool, optional, tag = "4")] 31 | pub front_running_protection: ::core::option::Option, 32 | } 33 | #[derive(Clone, PartialEq, ::prost::Message)] 34 | pub struct PostSubmitBatchResponseEntry { 35 | #[prost(string, tag = "1")] 36 | pub signature: ::prost::alloc::string::String, 37 | #[prost(string, tag = "2")] 38 | pub error: ::prost::alloc::string::String, 39 | #[prost(bool, tag = "3")] 40 | pub submitted: bool, 41 | } 42 | #[derive(Clone, PartialEq, ::prost::Message)] 43 | pub struct PostSubmitBatchResponse { 44 | #[prost(message, repeated, tag = "1")] 45 | pub transactions: ::prost::alloc::vec::Vec, 46 | } 47 | #[derive(Clone, PartialEq, ::prost::Message)] 48 | pub struct PostSubmitResponse { 49 | #[prost(string, tag = "1")] 50 | pub signature: ::prost::alloc::string::String, 51 | } 52 | #[derive(Clone, PartialEq, ::prost::Message)] 53 | pub struct TransactionMessage { 54 | #[prost(string, tag = "1")] 55 | pub content: ::prost::alloc::string::String, 56 | #[prost(bool, tag = "2")] 57 | pub is_cleanup: bool, 58 | } 59 | #[derive(Clone, PartialEq, ::prost::Message)] 60 | pub struct TransactionMessageV2 { 61 | #[prost(string, tag = "1")] 62 | pub content: ::prost::alloc::string::String, 63 | } 64 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] 65 | #[repr(i32)] 66 | pub enum SubmitStrategy { 67 | PUknown = 0, 68 | PSubmitAll = 1, 69 | PAbortOnFirstError = 2, 70 | PWaitForConfirmation = 3, 71 | } 72 | impl SubmitStrategy { 73 | /// String value of the enum field names used in the ProtoBuf definition. 74 | /// 75 | /// The values are not transformed in any way and thus are considered stable 76 | /// (if the ProtoBuf definition does not change) and safe for programmatic use. 77 | pub fn as_str_name(&self) -> &'static str { 78 | match self { 79 | Self::PUknown => "P_UKNOWN", 80 | Self::PSubmitAll => "P_SUBMIT_ALL", 81 | Self::PAbortOnFirstError => "P_ABORT_ON_FIRST_ERROR", 82 | Self::PWaitForConfirmation => "P_WAIT_FOR_CONFIRMATION", 83 | } 84 | } 85 | /// Creates an enum from field names used in the ProtoBuf definition. 86 | pub fn from_str_name(value: &str) -> ::core::option::Option { 87 | match value { 88 | "P_UKNOWN" => Some(Self::PUknown), 89 | "P_SUBMIT_ALL" => Some(Self::PSubmitAll), 90 | "P_ABORT_ON_FIRST_ERROR" => Some(Self::PAbortOnFirstError), 91 | "P_WAIT_FOR_CONFIRMATION" => Some(Self::PWaitForConfirmation), 92 | _ => None, 93 | } 94 | } 95 | } 96 | /// Generated client implementations. 97 | pub mod api_client { 98 | #![allow( 99 | unused_variables, 100 | dead_code, 101 | missing_docs, 102 | clippy::wildcard_imports, 103 | clippy::let_unit_value, 104 | )] 105 | use tonic::codegen::*; 106 | use tonic::codegen::http::Uri; 107 | #[derive(Debug, Clone)] 108 | pub struct ApiClient { 109 | inner: tonic::client::Grpc, 110 | } 111 | impl ApiClient { 112 | /// Attempt to create a new client by connecting to a given endpoint. 113 | pub async fn connect(dst: D) -> Result 114 | where 115 | D: std::convert::TryInto, 116 | D::Error: Into, 117 | { 118 | let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; 119 | Ok(Self::new(conn)) 120 | } 121 | } 122 | impl ApiClient 123 | where 124 | T: tonic::client::GrpcService, 125 | T::Error: Into, 126 | T::ResponseBody: Body + std::marker::Send + 'static, 127 | ::Error: Into + std::marker::Send, 128 | { 129 | pub fn new(inner: T) -> Self { 130 | let inner = tonic::client::Grpc::new(inner); 131 | Self { inner } 132 | } 133 | pub fn with_origin(inner: T, origin: Uri) -> Self { 134 | let inner = tonic::client::Grpc::with_origin(inner, origin); 135 | Self { inner } 136 | } 137 | pub fn with_interceptor( 138 | inner: T, 139 | interceptor: F, 140 | ) -> ApiClient> 141 | where 142 | F: tonic::service::Interceptor, 143 | T::ResponseBody: Default, 144 | T: tonic::codegen::Service< 145 | http::Request, 146 | Response = http::Response< 147 | >::ResponseBody, 148 | >, 149 | >, 150 | , 152 | >>::Error: Into + std::marker::Send + std::marker::Sync, 153 | { 154 | ApiClient::new(InterceptedService::new(inner, interceptor)) 155 | } 156 | /// Compress requests with the given encoding. 157 | /// 158 | /// This requires the server to support it otherwise it might respond with an 159 | /// error. 160 | #[must_use] 161 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 162 | self.inner = self.inner.send_compressed(encoding); 163 | self 164 | } 165 | /// Enable decompressing responses. 166 | #[must_use] 167 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 168 | self.inner = self.inner.accept_compressed(encoding); 169 | self 170 | } 171 | /// Limits the maximum size of a decoded message. 172 | /// 173 | /// Default: `4MB` 174 | #[must_use] 175 | pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 176 | self.inner = self.inner.max_decoding_message_size(limit); 177 | self 178 | } 179 | /// Limits the maximum size of an encoded message. 180 | /// 181 | /// Default: `usize::MAX` 182 | #[must_use] 183 | pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 184 | self.inner = self.inner.max_encoding_message_size(limit); 185 | self 186 | } 187 | pub async fn post_submit_v2( 188 | &mut self, 189 | request: impl tonic::IntoRequest, 190 | ) -> std::result::Result< 191 | tonic::Response, 192 | tonic::Status, 193 | > { 194 | self.inner 195 | .ready() 196 | .await 197 | .map_err(|e| { 198 | tonic::Status::unknown( 199 | format!("Service was not ready: {}", e.into()), 200 | ) 201 | })?; 202 | let codec = tonic::codec::ProstCodec::default(); 203 | let path = http::uri::PathAndQuery::from_static("/api.Api/PostSubmitV2"); 204 | let mut req = request.into_request(); 205 | req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitV2")); 206 | self.inner.unary(req, path, codec).await 207 | } 208 | pub async fn post_submit_batch_v2( 209 | &mut self, 210 | request: impl tonic::IntoRequest, 211 | ) -> std::result::Result< 212 | tonic::Response, 213 | tonic::Status, 214 | > { 215 | self.inner 216 | .ready() 217 | .await 218 | .map_err(|e| { 219 | tonic::Status::unknown( 220 | format!("Service was not ready: {}", e.into()), 221 | ) 222 | })?; 223 | let codec = tonic::codec::ProstCodec::default(); 224 | let path = http::uri::PathAndQuery::from_static( 225 | "/api.Api/PostSubmitBatchV2", 226 | ); 227 | let mut req = request.into_request(); 228 | req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitBatchV2")); 229 | self.inner.unary(req, path, codec).await 230 | } 231 | } 232 | } 233 | /// Generated server implementations. 234 | pub mod api_server { 235 | #![allow( 236 | unused_variables, 237 | dead_code, 238 | missing_docs, 239 | clippy::wildcard_imports, 240 | clippy::let_unit_value, 241 | )] 242 | use tonic::codegen::*; 243 | /// Generated trait containing gRPC methods that should be implemented for use with ApiServer. 244 | #[async_trait] 245 | pub trait Api: std::marker::Send + std::marker::Sync + 'static { 246 | async fn post_submit_v2( 247 | &self, 248 | request: tonic::Request, 249 | ) -> std::result::Result< 250 | tonic::Response, 251 | tonic::Status, 252 | >; 253 | async fn post_submit_batch_v2( 254 | &self, 255 | request: tonic::Request, 256 | ) -> std::result::Result< 257 | tonic::Response, 258 | tonic::Status, 259 | >; 260 | } 261 | #[derive(Debug)] 262 | pub struct ApiServer { 263 | inner: Arc, 264 | accept_compression_encodings: EnabledCompressionEncodings, 265 | send_compression_encodings: EnabledCompressionEncodings, 266 | max_decoding_message_size: Option, 267 | max_encoding_message_size: Option, 268 | } 269 | impl ApiServer { 270 | pub fn new(inner: T) -> Self { 271 | Self::from_arc(Arc::new(inner)) 272 | } 273 | pub fn from_arc(inner: Arc) -> Self { 274 | Self { 275 | inner, 276 | accept_compression_encodings: Default::default(), 277 | send_compression_encodings: Default::default(), 278 | max_decoding_message_size: None, 279 | max_encoding_message_size: None, 280 | } 281 | } 282 | pub fn with_interceptor( 283 | inner: T, 284 | interceptor: F, 285 | ) -> InterceptedService 286 | where 287 | F: tonic::service::Interceptor, 288 | { 289 | InterceptedService::new(Self::new(inner), interceptor) 290 | } 291 | /// Enable decompressing requests with the given encoding. 292 | #[must_use] 293 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 294 | self.accept_compression_encodings.enable(encoding); 295 | self 296 | } 297 | /// Compress responses with the given encoding, if the client supports it. 298 | #[must_use] 299 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 300 | self.send_compression_encodings.enable(encoding); 301 | self 302 | } 303 | /// Limits the maximum size of a decoded message. 304 | /// 305 | /// Default: `4MB` 306 | #[must_use] 307 | pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 308 | self.max_decoding_message_size = Some(limit); 309 | self 310 | } 311 | /// Limits the maximum size of an encoded message. 312 | /// 313 | /// Default: `usize::MAX` 314 | #[must_use] 315 | pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 316 | self.max_encoding_message_size = Some(limit); 317 | self 318 | } 319 | } 320 | impl tonic::codegen::Service> for ApiServer 321 | where 322 | T: Api, 323 | B: Body + std::marker::Send + 'static, 324 | B::Error: Into + std::marker::Send + 'static, 325 | { 326 | type Response = http::Response; 327 | type Error = std::convert::Infallible; 328 | type Future = BoxFuture; 329 | fn poll_ready( 330 | &mut self, 331 | _cx: &mut Context<'_>, 332 | ) -> Poll> { 333 | Poll::Ready(Ok(())) 334 | } 335 | fn call(&mut self, req: http::Request) -> Self::Future { 336 | match req.uri().path() { 337 | "/api.Api/PostSubmitV2" => { 338 | #[allow(non_camel_case_types)] 339 | struct PostSubmitV2Svc(pub Arc); 340 | impl tonic::server::UnaryService 341 | for PostSubmitV2Svc { 342 | type Response = super::PostSubmitResponse; 343 | type Future = BoxFuture< 344 | tonic::Response, 345 | tonic::Status, 346 | >; 347 | fn call( 348 | &mut self, 349 | request: tonic::Request, 350 | ) -> Self::Future { 351 | let inner = Arc::clone(&self.0); 352 | let fut = async move { 353 | ::post_submit_v2(&inner, request).await 354 | }; 355 | Box::pin(fut) 356 | } 357 | } 358 | let accept_compression_encodings = self.accept_compression_encodings; 359 | let send_compression_encodings = self.send_compression_encodings; 360 | let max_decoding_message_size = self.max_decoding_message_size; 361 | let max_encoding_message_size = self.max_encoding_message_size; 362 | let inner = self.inner.clone(); 363 | let fut = async move { 364 | let method = PostSubmitV2Svc(inner); 365 | let codec = tonic::codec::ProstCodec::default(); 366 | let mut grpc = tonic::server::Grpc::new(codec) 367 | .apply_compression_config( 368 | accept_compression_encodings, 369 | send_compression_encodings, 370 | ) 371 | .apply_max_message_size_config( 372 | max_decoding_message_size, 373 | max_encoding_message_size, 374 | ); 375 | let res = grpc.unary(method, req).await; 376 | Ok(res) 377 | }; 378 | Box::pin(fut) 379 | } 380 | "/api.Api/PostSubmitBatchV2" => { 381 | #[allow(non_camel_case_types)] 382 | struct PostSubmitBatchV2Svc(pub Arc); 383 | impl< 384 | T: Api, 385 | > tonic::server::UnaryService 386 | for PostSubmitBatchV2Svc { 387 | type Response = super::PostSubmitBatchResponse; 388 | type Future = BoxFuture< 389 | tonic::Response, 390 | tonic::Status, 391 | >; 392 | fn call( 393 | &mut self, 394 | request: tonic::Request, 395 | ) -> Self::Future { 396 | let inner = Arc::clone(&self.0); 397 | let fut = async move { 398 | ::post_submit_batch_v2(&inner, request).await 399 | }; 400 | Box::pin(fut) 401 | } 402 | } 403 | let accept_compression_encodings = self.accept_compression_encodings; 404 | let send_compression_encodings = self.send_compression_encodings; 405 | let max_decoding_message_size = self.max_decoding_message_size; 406 | let max_encoding_message_size = self.max_encoding_message_size; 407 | let inner = self.inner.clone(); 408 | let fut = async move { 409 | let method = PostSubmitBatchV2Svc(inner); 410 | let codec = tonic::codec::ProstCodec::default(); 411 | let mut grpc = tonic::server::Grpc::new(codec) 412 | .apply_compression_config( 413 | accept_compression_encodings, 414 | send_compression_encodings, 415 | ) 416 | .apply_max_message_size_config( 417 | max_decoding_message_size, 418 | max_encoding_message_size, 419 | ); 420 | let res = grpc.unary(method, req).await; 421 | Ok(res) 422 | }; 423 | Box::pin(fut) 424 | } 425 | _ => { 426 | Box::pin(async move { 427 | let mut response = http::Response::new(empty_body()); 428 | let headers = response.headers_mut(); 429 | headers 430 | .insert( 431 | tonic::Status::GRPC_STATUS, 432 | (tonic::Code::Unimplemented as i32).into(), 433 | ); 434 | headers 435 | .insert( 436 | http::header::CONTENT_TYPE, 437 | tonic::metadata::GRPC_CONTENT_TYPE, 438 | ); 439 | Ok(response) 440 | }) 441 | } 442 | } 443 | } 444 | } 445 | impl Clone for ApiServer { 446 | fn clone(&self) -> Self { 447 | let inner = self.inner.clone(); 448 | Self { 449 | inner, 450 | accept_compression_encodings: self.accept_compression_encodings, 451 | send_compression_encodings: self.send_compression_encodings, 452 | max_decoding_message_size: self.max_decoding_message_size, 453 | max_encoding_message_size: self.max_encoding_message_size, 454 | } 455 | } 456 | } 457 | /// Generated gRPC service name 458 | pub const SERVICE_NAME: &str = "api.Api"; 459 | impl tonic::server::NamedService for ApiServer { 460 | const NAME: &'static str = SERVICE_NAME; 461 | } 462 | } -------------------------------------------------------------------------------- /src/swqos/common.rs: -------------------------------------------------------------------------------- 1 | use bincode::serialize; 2 | use serde_json::json; 3 | use solana_client::rpc_client::SerializableTransaction; 4 | use solana_sdk::signature::Signature; 5 | use solana_sdk::transaction::Transaction; 6 | use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding}; 7 | use std::str::FromStr; 8 | use std::time::{Duration, Instant}; 9 | use tokio::time::sleep; 10 | use crate::common::types::SolanaRpcClient; 11 | use anyhow::Result; 12 | use base64::Engine; 13 | use base64::engine::general_purpose::STANDARD; 14 | use reqwest::Client; 15 | 16 | pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signature) -> Result { 17 | // 15 second timeout 18 | let timeout: Duration = Duration::from_secs(5); 19 | // 5 second retry interval 20 | let interval: Duration = Duration::from_millis(300); 21 | let start: Instant = Instant::now(); 22 | 23 | loop { 24 | if start.elapsed() >= timeout { 25 | return Err(anyhow::anyhow!("Transaction {}'s confirmation timed out", txt_sig)); 26 | } 27 | 28 | let status = rpc.get_signature_statuses(&[txt_sig]).await?; 29 | 30 | match status.value[0].clone() { 31 | Some(status) => { 32 | if status.err.is_none() 33 | && (status.confirmation_status == Some(TransactionConfirmationStatus::Confirmed) 34 | || status.confirmation_status == Some(TransactionConfirmationStatus::Finalized)) 35 | { 36 | return Ok(txt_sig); 37 | } 38 | if status.err.is_some() { 39 | return Err(anyhow::anyhow!(status.err.unwrap())); 40 | } 41 | } 42 | None => { 43 | sleep(interval).await; 44 | } 45 | } 46 | } 47 | } 48 | 49 | pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result { 50 | // 序列化交易 51 | let serialized = bincode::serialize(transaction) 52 | .map_err(|e| anyhow::anyhow!("序列化交易失败: {}", e))?; 53 | 54 | // Base64编码 55 | let encoded = STANDARD.encode(serialized); 56 | 57 | let request_data = json!({ 58 | "transaction": { 59 | "content": encoded 60 | }, 61 | "frontRunningProtection": true 62 | }); 63 | 64 | let url = format!("{}/api/v2/submit", endpoint); 65 | let response = client 66 | .post(url) 67 | .header("Authorization", auth_token) 68 | .header("Content-Type", "application/json") 69 | .json(&request_data) 70 | .send() 71 | .await 72 | .map_err(|e| anyhow::anyhow!("请求失败: {}", e))?; 73 | 74 | let resp = response.json::().await 75 | .map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?; 76 | 77 | if let Some(reason) = resp["reason"].as_str() { 78 | return Err(anyhow::anyhow!(reason.to_string())); 79 | } 80 | 81 | let signature = resp["signature"].as_str() 82 | .ok_or_else(|| anyhow::anyhow!("响应中缺少signature字段"))?; 83 | 84 | let signature = Signature::from_str(signature) 85 | .map_err(|e| anyhow::anyhow!("无效的签名: {}", e))?; 86 | 87 | Ok(signature) 88 | } 89 | 90 | pub async fn serialize_and_encode( 91 | transaction: &Vec, 92 | encoding: UiTransactionEncoding, 93 | ) -> Result { 94 | let serialized = match encoding { 95 | UiTransactionEncoding::Base58 => bs58::encode(transaction).into_string(), 96 | UiTransactionEncoding::Base64 => STANDARD.encode(transaction), 97 | _ => return Err(anyhow::anyhow!("Unsupported encoding")), 98 | }; 99 | Ok(serialized) 100 | } 101 | 102 | pub async fn serialize_transaction_and_encode( 103 | transaction: &impl SerializableTransaction, 104 | encoding: UiTransactionEncoding, 105 | ) -> Result { 106 | let serialized_tx = serialize(transaction)?; 107 | let serialized = match encoding { 108 | UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(), 109 | UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx), 110 | _ => return Err(anyhow::anyhow!("Unsupported encoding")), 111 | }; 112 | Ok(serialized) 113 | } 114 | 115 | pub async fn serialize_smart_transaction_and_encode( 116 | transaction: &impl SerializableTransaction, 117 | encoding: UiTransactionEncoding, 118 | ) -> Result<(String, Signature)> { 119 | let signature = transaction.get_signature(); 120 | let serialized_tx = serialize(transaction)?; 121 | let serialized = match encoding { 122 | UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(), 123 | UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx), 124 | _ => return Err(anyhow::anyhow!("Unsupported encoding")), 125 | }; 126 | Ok((serialized, *signature)) 127 | } -------------------------------------------------------------------------------- /src/swqos/jito_grpc/bundle.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by prost-build. 2 | #[derive(Clone, PartialEq, ::prost::Message)] 3 | pub struct Bundle { 4 | #[prost(message, optional, tag = "2")] 5 | pub header: ::core::option::Option, 6 | #[prost(message, repeated, tag = "3")] 7 | pub packets: ::prost::alloc::vec::Vec, 8 | } 9 | #[derive(Clone, PartialEq, ::prost::Message)] 10 | pub struct BundleUuid { 11 | #[prost(message, optional, tag = "1")] 12 | pub bundle: ::core::option::Option, 13 | #[prost(string, tag = "2")] 14 | pub uuid: ::prost::alloc::string::String, 15 | } 16 | /// Indicates the bundle was accepted and forwarded to a validator. 17 | /// NOTE: A single bundle may have multiple events emitted if forwarded to many validators. 18 | #[derive(Clone, PartialEq, ::prost::Message)] 19 | pub struct Accepted { 20 | /// Slot at which bundle was forwarded. 21 | #[prost(uint64, tag = "1")] 22 | pub slot: u64, 23 | /// Validator identity bundle was forwarded to. 24 | #[prost(string, tag = "2")] 25 | pub validator_identity: ::prost::alloc::string::String, 26 | } 27 | /// Indicates the bundle was dropped and therefore not forwarded to any validator. 28 | #[derive(Clone, PartialEq, ::prost::Message)] 29 | pub struct Rejected { 30 | #[prost(oneof = "rejected::Reason", tags = "1, 2, 3, 4, 5")] 31 | pub reason: ::core::option::Option, 32 | } 33 | /// Nested message and enum types in `Rejected`. 34 | pub mod rejected { 35 | #[derive(Clone, PartialEq, ::prost::Oneof)] 36 | pub enum Reason { 37 | #[prost(message, tag = "1")] 38 | StateAuctionBidRejected(super::StateAuctionBidRejected), 39 | #[prost(message, tag = "2")] 40 | WinningBatchBidRejected(super::WinningBatchBidRejected), 41 | #[prost(message, tag = "3")] 42 | SimulationFailure(super::SimulationFailure), 43 | #[prost(message, tag = "4")] 44 | InternalError(super::InternalError), 45 | #[prost(message, tag = "5")] 46 | DroppedBundle(super::DroppedBundle), 47 | } 48 | } 49 | /// Indicates the bundle's bid was high enough to win its state auction. 50 | /// However, not high enough relative to other state auction winners and therefore excluded from being forwarded. 51 | #[derive(Clone, PartialEq, ::prost::Message)] 52 | pub struct WinningBatchBidRejected { 53 | /// Auction's unique identifier. 54 | #[prost(string, tag = "1")] 55 | pub auction_id: ::prost::alloc::string::String, 56 | /// Bundle's simulated bid. 57 | #[prost(uint64, tag = "2")] 58 | pub simulated_bid_lamports: u64, 59 | #[prost(string, optional, tag = "3")] 60 | pub msg: ::core::option::Option<::prost::alloc::string::String>, 61 | } 62 | /// Indicates the bundle's bid was __not__ high enough to be included in its state auction's set of winners. 63 | #[derive(Clone, PartialEq, ::prost::Message)] 64 | pub struct StateAuctionBidRejected { 65 | /// Auction's unique identifier. 66 | #[prost(string, tag = "1")] 67 | pub auction_id: ::prost::alloc::string::String, 68 | /// Bundle's simulated bid. 69 | #[prost(uint64, tag = "2")] 70 | pub simulated_bid_lamports: u64, 71 | #[prost(string, optional, tag = "3")] 72 | pub msg: ::core::option::Option<::prost::alloc::string::String>, 73 | } 74 | /// Bundle dropped due to simulation failure. 75 | #[derive(Clone, PartialEq, ::prost::Message)] 76 | pub struct SimulationFailure { 77 | /// Signature of the offending transaction. 78 | #[prost(string, tag = "1")] 79 | pub tx_signature: ::prost::alloc::string::String, 80 | #[prost(string, optional, tag = "2")] 81 | pub msg: ::core::option::Option<::prost::alloc::string::String>, 82 | } 83 | /// Bundle dropped due to an internal error. 84 | #[derive(Clone, PartialEq, ::prost::Message)] 85 | pub struct InternalError { 86 | #[prost(string, tag = "1")] 87 | pub msg: ::prost::alloc::string::String, 88 | } 89 | /// Bundle dropped (e.g. because no leader upcoming) 90 | #[derive(Clone, PartialEq, ::prost::Message)] 91 | pub struct DroppedBundle { 92 | #[prost(string, tag = "1")] 93 | pub msg: ::prost::alloc::string::String, 94 | } 95 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 96 | pub struct Finalized {} 97 | #[derive(Clone, PartialEq, ::prost::Message)] 98 | pub struct Processed { 99 | #[prost(string, tag = "1")] 100 | pub validator_identity: ::prost::alloc::string::String, 101 | #[prost(uint64, tag = "2")] 102 | pub slot: u64, 103 | /// / Index within the block. 104 | #[prost(uint64, tag = "3")] 105 | pub bundle_index: u64, 106 | } 107 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 108 | pub struct Dropped { 109 | #[prost(enumeration = "DroppedReason", tag = "1")] 110 | pub reason: i32, 111 | } 112 | #[derive(Clone, PartialEq, ::prost::Message)] 113 | pub struct BundleResult { 114 | /// Bundle's Uuid. 115 | #[prost(string, tag = "1")] 116 | pub bundle_id: ::prost::alloc::string::String, 117 | #[prost(oneof = "bundle_result::Result", tags = "2, 3, 4, 5, 6")] 118 | pub result: ::core::option::Option, 119 | } 120 | /// Nested message and enum types in `BundleResult`. 121 | pub mod bundle_result { 122 | #[derive(Clone, PartialEq, ::prost::Oneof)] 123 | pub enum Result { 124 | /// Indicated accepted by the block-engine and forwarded to a jito-solana validator. 125 | #[prost(message, tag = "2")] 126 | Accepted(super::Accepted), 127 | /// Rejected by the block-engine. 128 | #[prost(message, tag = "3")] 129 | Rejected(super::Rejected), 130 | /// Reached finalized commitment level. 131 | #[prost(message, tag = "4")] 132 | Finalized(super::Finalized), 133 | /// Reached a processed commitment level. 134 | #[prost(message, tag = "5")] 135 | Processed(super::Processed), 136 | /// Was accepted and forwarded by the block-engine but never landed on-chain. 137 | #[prost(message, tag = "6")] 138 | Dropped(super::Dropped), 139 | } 140 | } 141 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] 142 | #[repr(i32)] 143 | pub enum DroppedReason { 144 | BlockhashExpired = 0, 145 | /// One or more transactions in the bundle landed on-chain, invalidating the bundle. 146 | PartiallyProcessed = 1, 147 | /// This indicates bundle was processed but not finalized. This could occur during forks. 148 | NotFinalized = 2, 149 | } 150 | impl DroppedReason { 151 | /// String value of the enum field names used in the ProtoBuf definition. 152 | /// 153 | /// The values are not transformed in any way and thus are considered stable 154 | /// (if the ProtoBuf definition does not change) and safe for programmatic use. 155 | pub fn as_str_name(&self) -> &'static str { 156 | match self { 157 | Self::BlockhashExpired => "BlockhashExpired", 158 | Self::PartiallyProcessed => "PartiallyProcessed", 159 | Self::NotFinalized => "NotFinalized", 160 | } 161 | } 162 | /// Creates an enum from field names used in the ProtoBuf definition. 163 | pub fn from_str_name(value: &str) -> ::core::option::Option { 164 | match value { 165 | "BlockhashExpired" => Some(Self::BlockhashExpired), 166 | "PartiallyProcessed" => Some(Self::PartiallyProcessed), 167 | "NotFinalized" => Some(Self::NotFinalized), 168 | _ => None, 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/swqos/jito_grpc/convert.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::min, 3 | net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr}, 4 | str::FromStr, 5 | }; 6 | 7 | use bincode::serialize; 8 | use solana_perf::packet::{Packet, PacketBatch, PACKET_DATA_SIZE}; 9 | use solana_sdk::{ 10 | packet::{Meta, PacketFlags}, 11 | transaction::VersionedTransaction, 12 | }; 13 | 14 | use crate::swqos::jito_grpc::packet::{ 15 | Meta as ProtoMeta, Packet as ProtoPacket, PacketBatch as ProtoPacketBatch, 16 | PacketFlags as ProtoPacketFlags, 17 | }; 18 | use crate::swqos::jito_grpc::shared::Socket; 19 | 20 | /// Converts a Solana packet to a protobuf packet 21 | /// NOTE: the packet.data() function will filter packets marked for discard 22 | pub fn packet_to_proto_packet(p: &Packet) -> Option { 23 | Some(ProtoPacket { 24 | data: p.data(..)?.to_vec(), 25 | meta: Some(ProtoMeta { 26 | size: p.meta().size as u64, 27 | addr: p.meta().addr.to_string(), 28 | port: p.meta().port as u32, 29 | flags: Some(ProtoPacketFlags { 30 | discard: p.meta().discard(), 31 | forwarded: p.meta().forwarded(), 32 | repair: p.meta().repair(), 33 | simple_vote_tx: p.meta().is_simple_vote_tx(), 34 | tracer_packet: p.meta().is_perf_track_packet(), 35 | from_staked_node: p.meta().is_from_staked_node(), 36 | }), 37 | sender_stake: 0, 38 | }), 39 | }) 40 | } 41 | 42 | pub fn packet_batches_to_proto_packets( 43 | batches: &[PacketBatch], 44 | ) -> impl Iterator + '_ { 45 | batches 46 | .iter() 47 | .flat_map(|b| b.iter().filter_map(packet_to_proto_packet)) 48 | } 49 | 50 | /// converts from a protobuf packet to packet 51 | pub fn proto_packet_to_packet(p: &ProtoPacket) -> Packet { 52 | let mut data = [0u8; PACKET_DATA_SIZE]; 53 | let copy_len = min(data.len(), p.data.len()); 54 | data[..copy_len].copy_from_slice(&p.data[..copy_len]); 55 | let mut packet = Packet::new(data, Meta::default()); 56 | if let Some(meta) = &p.meta { 57 | packet.meta_mut().size = meta.size as usize; 58 | packet.meta_mut().addr = meta 59 | .addr 60 | .parse() 61 | .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); 62 | packet.meta_mut().port = meta.port as u16; 63 | if let Some(flags) = &meta.flags { 64 | if flags.simple_vote_tx { 65 | packet.meta_mut().flags.insert(PacketFlags::SIMPLE_VOTE_TX); 66 | } 67 | if flags.forwarded { 68 | packet.meta_mut().flags.insert(PacketFlags::FORWARDED); 69 | } 70 | if flags.tracer_packet { 71 | packet.meta_mut().flags.insert(PacketFlags::PERF_TRACK_PACKET); 72 | } 73 | if flags.repair { 74 | packet.meta_mut().flags.insert(PacketFlags::REPAIR); 75 | } 76 | if flags.discard { 77 | packet.meta_mut().flags.insert(PacketFlags::DISCARD); 78 | } 79 | } 80 | } 81 | packet 82 | } 83 | 84 | pub fn proto_packet_batch_to_packets( 85 | packet_batch: ProtoPacketBatch, 86 | ) -> impl Iterator { 87 | packet_batch 88 | .packets 89 | .into_iter() 90 | .map(|proto_packet| proto_packet_to_packet(&proto_packet)) 91 | } 92 | 93 | /// Converts a protobuf packet to a VersionedTransaction 94 | pub fn versioned_tx_from_packet(p: &ProtoPacket) -> Option { 95 | let mut data = [0; PACKET_DATA_SIZE]; 96 | let copy_len = min(data.len(), p.data.len()); 97 | data[..copy_len].copy_from_slice(&p.data[..copy_len]); 98 | let mut packet = Packet::new(data, Default::default()); 99 | if let Some(meta) = &p.meta { 100 | packet.meta_mut().size = meta.size as usize; 101 | } 102 | packet.deserialize_slice(..).ok() 103 | } 104 | 105 | /// Coverts a VersionedTransaction to packet 106 | pub fn packet_from_versioned_tx(tx: VersionedTransaction) -> Packet { 107 | let tx_data = serialize(&tx).expect("serializes"); 108 | let mut data = [0; PACKET_DATA_SIZE]; 109 | let copy_len = min(tx_data.len(), data.len()); 110 | data[..copy_len].copy_from_slice(&tx_data[..copy_len]); 111 | let mut packet = Packet::new(data, Default::default()); 112 | packet.meta_mut().size = copy_len; 113 | packet 114 | } 115 | 116 | /// Converts a VersionedTransaction to a protobuf packet 117 | pub fn proto_packet_from_versioned_tx(tx: &VersionedTransaction) -> ProtoPacket { 118 | let data = serialize(tx).expect("serializes"); 119 | let size = data.len() as u64; 120 | ProtoPacket { 121 | data, 122 | meta: Some(ProtoMeta { 123 | size, 124 | addr: "".to_string(), 125 | port: 0, 126 | flags: None, 127 | sender_stake: 0, 128 | }), 129 | } 130 | } 131 | 132 | /// Converts a GRPC Socket to stdlib SocketAddr 133 | impl TryFrom<&Socket> for SocketAddr { 134 | type Error = AddrParseError; 135 | 136 | fn try_from(value: &Socket) -> Result { 137 | IpAddr::from_str(&value.ip).map(|ip| SocketAddr::new(ip, value.port as u16)) 138 | } 139 | } 140 | 141 | // #[cfg(test)] 142 | // mod tests { 143 | // use solana_perf::test_tx::test_tx; 144 | // use solana_sdk::transaction::VersionedTransaction; 145 | 146 | // use crate::convert::{proto_packet_from_versioned_tx, versioned_tx_from_packet}; 147 | 148 | // #[test] 149 | // fn test_proto_to_packet() { 150 | // let tx_before = VersionedTransaction::from(test_tx()); 151 | // let tx_after = versioned_tx_from_packet(&proto_packet_from_versioned_tx(&tx_before)) 152 | // .expect("tx_after"); 153 | 154 | // assert_eq!(tx_before, tx_after); 155 | // } 156 | // } 157 | -------------------------------------------------------------------------------- /src/swqos/jito_grpc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bundle; 2 | pub mod packet; 3 | pub mod searcher; 4 | pub mod shared; 5 | pub mod convert; -------------------------------------------------------------------------------- /src/swqos/jito_grpc/packet.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by prost-build. 2 | #[derive(Clone, PartialEq, ::prost::Message)] 3 | pub struct PacketBatch { 4 | #[prost(message, repeated, tag = "1")] 5 | pub packets: ::prost::alloc::vec::Vec, 6 | } 7 | #[derive(Clone, PartialEq, ::prost::Message)] 8 | pub struct Packet { 9 | #[prost(bytes = "vec", tag = "1")] 10 | pub data: ::prost::alloc::vec::Vec, 11 | #[prost(message, optional, tag = "2")] 12 | pub meta: ::core::option::Option, 13 | } 14 | #[derive(Clone, PartialEq, ::prost::Message)] 15 | pub struct Meta { 16 | #[prost(uint64, tag = "1")] 17 | pub size: u64, 18 | #[prost(string, tag = "2")] 19 | pub addr: ::prost::alloc::string::String, 20 | #[prost(uint32, tag = "3")] 21 | pub port: u32, 22 | #[prost(message, optional, tag = "4")] 23 | pub flags: ::core::option::Option, 24 | #[prost(uint64, tag = "5")] 25 | pub sender_stake: u64, 26 | } 27 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 28 | pub struct PacketFlags { 29 | #[prost(bool, tag = "1")] 30 | pub discard: bool, 31 | #[prost(bool, tag = "2")] 32 | pub forwarded: bool, 33 | #[prost(bool, tag = "3")] 34 | pub repair: bool, 35 | #[prost(bool, tag = "4")] 36 | pub simple_vote_tx: bool, 37 | #[prost(bool, tag = "5")] 38 | pub tracer_packet: bool, 39 | #[prost(bool, tag = "6")] 40 | pub from_staked_node: bool, 41 | } 42 | -------------------------------------------------------------------------------- /src/swqos/jito_grpc/searcher.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by prost-build. 2 | #[derive(Clone, PartialEq, ::prost::Message)] 3 | pub struct SlotList { 4 | #[prost(uint64, repeated, tag = "1")] 5 | pub slots: ::prost::alloc::vec::Vec, 6 | } 7 | #[derive(Clone, PartialEq, ::prost::Message)] 8 | pub struct ConnectedLeadersResponse { 9 | /// Mapping of validator pubkey to leader slots for the current epoch. 10 | #[prost(map = "string, message", tag = "1")] 11 | pub connected_validators: ::std::collections::HashMap< 12 | ::prost::alloc::string::String, 13 | SlotList, 14 | >, 15 | } 16 | #[derive(Clone, PartialEq, ::prost::Message)] 17 | pub struct SendBundleRequest { 18 | #[prost(message, optional, tag = "1")] 19 | pub bundle: ::core::option::Option, 20 | } 21 | #[derive(Clone, PartialEq, ::prost::Message)] 22 | pub struct SendBundleResponse { 23 | /// server uuid for the bundle 24 | #[prost(string, tag = "1")] 25 | pub uuid: ::prost::alloc::string::String, 26 | } 27 | #[derive(Clone, PartialEq, ::prost::Message)] 28 | pub struct NextScheduledLeaderRequest { 29 | /// Defaults to the currently connected region if no region provided. 30 | #[prost(string, repeated, tag = "1")] 31 | pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 32 | } 33 | #[derive(Clone, PartialEq, ::prost::Message)] 34 | pub struct NextScheduledLeaderResponse { 35 | /// the current slot the backend is on 36 | #[prost(uint64, tag = "1")] 37 | pub current_slot: u64, 38 | /// the slot of the next leader 39 | #[prost(uint64, tag = "2")] 40 | pub next_leader_slot: u64, 41 | /// the identity pubkey (base58) of the next leader 42 | #[prost(string, tag = "3")] 43 | pub next_leader_identity: ::prost::alloc::string::String, 44 | /// the block engine region of the next leader 45 | #[prost(string, tag = "4")] 46 | pub next_leader_region: ::prost::alloc::string::String, 47 | } 48 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 49 | pub struct ConnectedLeadersRequest {} 50 | #[derive(Clone, PartialEq, ::prost::Message)] 51 | pub struct ConnectedLeadersRegionedRequest { 52 | /// Defaults to the currently connected region if no region provided. 53 | #[prost(string, repeated, tag = "1")] 54 | pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 55 | } 56 | #[derive(Clone, PartialEq, ::prost::Message)] 57 | pub struct ConnectedLeadersRegionedResponse { 58 | #[prost(map = "string, message", tag = "1")] 59 | pub connected_validators: ::std::collections::HashMap< 60 | ::prost::alloc::string::String, 61 | ConnectedLeadersResponse, 62 | >, 63 | } 64 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 65 | pub struct GetTipAccountsRequest {} 66 | #[derive(Clone, PartialEq, ::prost::Message)] 67 | pub struct GetTipAccountsResponse { 68 | #[prost(string, repeated, tag = "1")] 69 | pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 70 | } 71 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 72 | pub struct SubscribeBundleResultsRequest {} 73 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 74 | pub struct GetRegionsRequest {} 75 | #[derive(Clone, PartialEq, ::prost::Message)] 76 | pub struct GetRegionsResponse { 77 | /// The region the client is currently connected to 78 | #[prost(string, tag = "1")] 79 | pub current_region: ::prost::alloc::string::String, 80 | /// Regions that are online and ready for connections 81 | /// All regions: 82 | #[prost(string, repeated, tag = "2")] 83 | pub available_regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 84 | } 85 | /// Generated client implementations. 86 | pub mod searcher_service_client { 87 | #![allow( 88 | unused_variables, 89 | dead_code, 90 | missing_docs, 91 | clippy::wildcard_imports, 92 | clippy::let_unit_value, 93 | )] 94 | use tonic::codegen::*; 95 | use tonic::codegen::http::Uri; 96 | #[derive(Debug, Clone)] 97 | pub struct SearcherServiceClient { 98 | inner: tonic::client::Grpc, 99 | } 100 | impl SearcherServiceClient { 101 | /// Attempt to create a new client by connecting to a given endpoint. 102 | pub async fn connect(dst: D) -> Result 103 | where 104 | D: TryInto, 105 | D::Error: Into, 106 | { 107 | let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; 108 | Ok(Self::new(conn)) 109 | } 110 | } 111 | impl SearcherServiceClient 112 | where 113 | T: tonic::client::GrpcService, 114 | T::Error: Into, 115 | T::ResponseBody: Body + std::marker::Send + 'static, 116 | ::Error: Into + std::marker::Send, 117 | { 118 | pub fn new(inner: T) -> Self { 119 | let inner = tonic::client::Grpc::new(inner); 120 | Self { inner } 121 | } 122 | pub fn with_origin(inner: T, origin: Uri) -> Self { 123 | let inner = tonic::client::Grpc::with_origin(inner, origin); 124 | Self { inner } 125 | } 126 | pub fn with_interceptor( 127 | inner: T, 128 | interceptor: F, 129 | ) -> SearcherServiceClient> 130 | where 131 | F: tonic::service::Interceptor, 132 | T::ResponseBody: Default, 133 | T: tonic::codegen::Service< 134 | http::Request, 135 | Response = http::Response< 136 | >::ResponseBody, 137 | >, 138 | >, 139 | , 141 | >>::Error: Into + std::marker::Send + std::marker::Sync, 142 | { 143 | SearcherServiceClient::new(InterceptedService::new(inner, interceptor)) 144 | } 145 | /// Compress requests with the given encoding. 146 | /// 147 | /// This requires the server to support it otherwise it might respond with an 148 | /// error. 149 | #[must_use] 150 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 151 | self.inner = self.inner.send_compressed(encoding); 152 | self 153 | } 154 | /// Enable decompressing responses. 155 | #[must_use] 156 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 157 | self.inner = self.inner.accept_compressed(encoding); 158 | self 159 | } 160 | /// Limits the maximum size of a decoded message. 161 | /// 162 | /// Default: `4MB` 163 | #[must_use] 164 | pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 165 | self.inner = self.inner.max_decoding_message_size(limit); 166 | self 167 | } 168 | /// Limits the maximum size of an encoded message. 169 | /// 170 | /// Default: `usize::MAX` 171 | #[must_use] 172 | pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 173 | self.inner = self.inner.max_encoding_message_size(limit); 174 | self 175 | } 176 | /// Searchers can invoke this endpoint to subscribe to their respective bundle results. 177 | /// A success result would indicate the bundle won its state auction and was submitted to the validator. 178 | pub async fn subscribe_bundle_results( 179 | &mut self, 180 | request: impl tonic::IntoRequest, 181 | ) -> std::result::Result< 182 | tonic::Response>, 183 | tonic::Status, 184 | > { 185 | self.inner 186 | .ready() 187 | .await 188 | .map_err(|e| { 189 | tonic::Status::unknown( 190 | format!("Service was not ready: {}", e.into()), 191 | ) 192 | })?; 193 | let codec = tonic::codec::ProstCodec::default(); 194 | let path = http::uri::PathAndQuery::from_static( 195 | "/searcher.SearcherService/SubscribeBundleResults", 196 | ); 197 | let mut req = request.into_request(); 198 | req.extensions_mut() 199 | .insert( 200 | GrpcMethod::new("searcher.SearcherService", "SubscribeBundleResults"), 201 | ); 202 | self.inner.server_streaming(req, path, codec).await 203 | } 204 | pub async fn send_bundle( 205 | &mut self, 206 | request: impl tonic::IntoRequest, 207 | ) -> std::result::Result< 208 | tonic::Response, 209 | tonic::Status, 210 | > { 211 | self.inner 212 | .ready() 213 | .await 214 | .map_err(|e| { 215 | tonic::Status::unknown( 216 | format!("Service was not ready: {}", e.into()), 217 | ) 218 | })?; 219 | let codec = tonic::codec::ProstCodec::default(); 220 | let path = http::uri::PathAndQuery::from_static( 221 | "/searcher.SearcherService/SendBundle", 222 | ); 223 | let mut req = request.into_request(); 224 | req.extensions_mut() 225 | .insert(GrpcMethod::new("searcher.SearcherService", "SendBundle")); 226 | self.inner.unary(req, path, codec).await 227 | } 228 | /// Returns the next scheduled leader connected to the block engine. 229 | pub async fn get_next_scheduled_leader( 230 | &mut self, 231 | request: impl tonic::IntoRequest, 232 | ) -> std::result::Result< 233 | tonic::Response, 234 | tonic::Status, 235 | > { 236 | self.inner 237 | .ready() 238 | .await 239 | .map_err(|e| { 240 | tonic::Status::unknown( 241 | format!("Service was not ready: {}", e.into()), 242 | ) 243 | })?; 244 | let codec = tonic::codec::ProstCodec::default(); 245 | let path = http::uri::PathAndQuery::from_static( 246 | "/searcher.SearcherService/GetNextScheduledLeader", 247 | ); 248 | let mut req = request.into_request(); 249 | req.extensions_mut() 250 | .insert( 251 | GrpcMethod::new("searcher.SearcherService", "GetNextScheduledLeader"), 252 | ); 253 | self.inner.unary(req, path, codec).await 254 | } 255 | /// Returns leader slots for connected jito validators during the current epoch. Only returns data for this region. 256 | pub async fn get_connected_leaders( 257 | &mut self, 258 | request: impl tonic::IntoRequest, 259 | ) -> std::result::Result< 260 | tonic::Response, 261 | tonic::Status, 262 | > { 263 | self.inner 264 | .ready() 265 | .await 266 | .map_err(|e| { 267 | tonic::Status::unknown( 268 | format!("Service was not ready: {}", e.into()), 269 | ) 270 | })?; 271 | let codec = tonic::codec::ProstCodec::default(); 272 | let path = http::uri::PathAndQuery::from_static( 273 | "/searcher.SearcherService/GetConnectedLeaders", 274 | ); 275 | let mut req = request.into_request(); 276 | req.extensions_mut() 277 | .insert( 278 | GrpcMethod::new("searcher.SearcherService", "GetConnectedLeaders"), 279 | ); 280 | self.inner.unary(req, path, codec).await 281 | } 282 | /// Returns leader slots for connected jito validators during the current epoch. 283 | pub async fn get_connected_leaders_regioned( 284 | &mut self, 285 | request: impl tonic::IntoRequest, 286 | ) -> std::result::Result< 287 | tonic::Response, 288 | tonic::Status, 289 | > { 290 | self.inner 291 | .ready() 292 | .await 293 | .map_err(|e| { 294 | tonic::Status::unknown( 295 | format!("Service was not ready: {}", e.into()), 296 | ) 297 | })?; 298 | let codec = tonic::codec::ProstCodec::default(); 299 | let path = http::uri::PathAndQuery::from_static( 300 | "/searcher.SearcherService/GetConnectedLeadersRegioned", 301 | ); 302 | let mut req = request.into_request(); 303 | req.extensions_mut() 304 | .insert( 305 | GrpcMethod::new( 306 | "searcher.SearcherService", 307 | "GetConnectedLeadersRegioned", 308 | ), 309 | ); 310 | self.inner.unary(req, path, codec).await 311 | } 312 | /// Returns the tip accounts searchers shall transfer funds to for the leader to claim. 313 | pub async fn get_tip_accounts( 314 | &mut self, 315 | request: impl tonic::IntoRequest, 316 | ) -> std::result::Result< 317 | tonic::Response, 318 | tonic::Status, 319 | > { 320 | self.inner 321 | .ready() 322 | .await 323 | .map_err(|e| { 324 | tonic::Status::unknown( 325 | format!("Service was not ready: {}", e.into()), 326 | ) 327 | })?; 328 | let codec = tonic::codec::ProstCodec::default(); 329 | let path = http::uri::PathAndQuery::from_static( 330 | "/searcher.SearcherService/GetTipAccounts", 331 | ); 332 | let mut req = request.into_request(); 333 | req.extensions_mut() 334 | .insert(GrpcMethod::new("searcher.SearcherService", "GetTipAccounts")); 335 | self.inner.unary(req, path, codec).await 336 | } 337 | /// Returns region the client is directly connected to, along with all available regions 338 | pub async fn get_regions( 339 | &mut self, 340 | request: impl tonic::IntoRequest, 341 | ) -> std::result::Result< 342 | tonic::Response, 343 | tonic::Status, 344 | > { 345 | self.inner 346 | .ready() 347 | .await 348 | .map_err(|e| { 349 | tonic::Status::unknown( 350 | format!("Service was not ready: {}", e.into()), 351 | ) 352 | })?; 353 | let codec = tonic::codec::ProstCodec::default(); 354 | let path = http::uri::PathAndQuery::from_static( 355 | "/searcher.SearcherService/GetRegions", 356 | ); 357 | let mut req = request.into_request(); 358 | req.extensions_mut() 359 | .insert(GrpcMethod::new("searcher.SearcherService", "GetRegions")); 360 | self.inner.unary(req, path, codec).await 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/swqos/jito_grpc/shared.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by prost-build. 2 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 3 | pub struct Header { 4 | #[prost(message, optional, tag = "1")] 5 | pub ts: ::core::option::Option<::prost_types::Timestamp>, 6 | } 7 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 8 | pub struct Heartbeat { 9 | #[prost(uint64, tag = "1")] 10 | pub count: u64, 11 | } 12 | #[derive(Clone, PartialEq, ::prost::Message)] 13 | pub struct Socket { 14 | #[prost(string, tag = "1")] 15 | pub ip: ::prost::alloc::string::String, 16 | #[prost(int64, tag = "2")] 17 | pub port: i64, 18 | } 19 | -------------------------------------------------------------------------------- /src/swqos/mod.rs: -------------------------------------------------------------------------------- 1 | use api::api_client::ApiClient; 2 | use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode}; 3 | use crate::swqos::jito_grpc::searcher::searcher_service_client::SearcherServiceClient; 4 | use reqwest::Client; 5 | use searcher_client::{get_searcher_client_no_auth, send_bundle_with_confirmation}; 6 | use serde_json::json; 7 | use tonic::transport::Channel; 8 | use yellowstone_grpc_client::Interceptor; 9 | use std::{sync::Arc, time::Instant}; 10 | use tokio::sync::{Mutex, RwLock}; 11 | 12 | use solana_sdk::signature::Signature; 13 | 14 | use std::str::FromStr; 15 | use rustls::crypto::{ring::default_provider, CryptoProvider}; 16 | 17 | use tonic::{service::interceptor::InterceptedService, transport::Uri, Status}; 18 | use std::time::Duration; 19 | use solana_transaction_status::UiTransactionEncoding; 20 | use tonic::transport::ClientTlsConfig; 21 | 22 | use anyhow::{anyhow, Result}; 23 | use rand::{rng, seq::{IndexedRandom, IteratorRandom}}; 24 | use solana_sdk::transaction::VersionedTransaction; 25 | 26 | use crate::{common::SolanaRpcClient, constants::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS}}; 27 | 28 | pub mod common; 29 | pub mod searcher_client; 30 | pub mod api; 31 | pub mod jito_grpc; 32 | 33 | lazy_static::lazy_static! { 34 | static ref TIP_ACCOUNT_CACHE: RwLock> = RwLock::new(Vec::new()); 35 | } 36 | 37 | #[derive(Debug, Clone, Copy)] 38 | pub enum ClientType { 39 | Jito, 40 | NextBlock, 41 | ZeroSlot, 42 | } 43 | 44 | pub type FeeClient = dyn FeeClientTrait + Send + Sync + 'static; 45 | 46 | #[async_trait::async_trait] 47 | pub trait FeeClientTrait { 48 | async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result; 49 | async fn send_transactions(&self, transactions: &Vec) -> Result>; 50 | async fn get_tip_account(&self) -> Result; 51 | async fn get_client_type(&self) -> ClientType; 52 | } 53 | 54 | pub struct JitoClient { 55 | pub rpc_client: Arc, 56 | pub searcher_client: Arc>>, 57 | } 58 | 59 | #[async_trait::async_trait] 60 | impl FeeClientTrait for JitoClient { 61 | async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { 62 | self.send_bundle_with_confirmation(&vec![transaction.clone()]).await?.first().cloned().ok_or(anyhow!("Failed to send transaction")) 63 | } 64 | 65 | async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { 66 | self.send_bundle_with_confirmation(transactions).await 67 | } 68 | 69 | async fn get_tip_account(&self) -> Result { 70 | if let Some(acc) = JITO_TIP_ACCOUNTS.iter().choose(&mut rng()) { 71 | Ok(acc.to_string()) 72 | } else { 73 | Err(anyhow!("no valid tip accounts found")) 74 | } 75 | } 76 | 77 | async fn get_client_type(&self) -> ClientType { 78 | ClientType::Jito 79 | } 80 | } 81 | 82 | impl JitoClient { 83 | pub async fn new(rpc_url: String, block_engine_url: String) -> Result { 84 | let rpc_client = SolanaRpcClient::new(rpc_url); 85 | let searcher_client = get_searcher_client_no_auth(block_engine_url.as_str()).await?; 86 | Ok(Self { rpc_client: Arc::new(rpc_client), searcher_client: Arc::new(Mutex::new(searcher_client)) }) 87 | } 88 | 89 | pub async fn send_bundle_with_confirmation( 90 | &self, 91 | transactions: &Vec, 92 | ) -> Result, anyhow::Error> { 93 | send_bundle_with_confirmation(self.rpc_client.clone(), &transactions, self.searcher_client.clone()).await 94 | } 95 | 96 | pub async fn send_bundle_no_wait( 97 | &self, 98 | transactions: &Vec, 99 | ) -> Result, anyhow::Error> { 100 | searcher_client::send_bundle_no_wait(&transactions, self.searcher_client.clone()).await 101 | } 102 | } 103 | 104 | #[derive(Clone)] 105 | pub struct MyInterceptor { 106 | auth_token: String, 107 | } 108 | 109 | impl MyInterceptor { 110 | pub fn new(auth_token: String) -> Self { 111 | Self { auth_token } 112 | } 113 | } 114 | 115 | impl Interceptor for MyInterceptor { 116 | fn call(&mut self, mut request: tonic::Request<()>) -> Result, Status> { 117 | request.metadata_mut().insert( 118 | "authorization", 119 | tonic::metadata::MetadataValue::from_str(&self.auth_token) 120 | .map_err(|_| Status::invalid_argument("Invalid auth token"))? 121 | ); 122 | Ok(request) 123 | } 124 | } 125 | 126 | #[derive(Clone)] 127 | pub struct NextBlockClient { 128 | pub rpc_client: Arc, 129 | pub client: ApiClient>, 130 | } 131 | 132 | #[async_trait::async_trait] 133 | impl FeeClientTrait for NextBlockClient { 134 | async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { 135 | self.send_transaction(transaction).await 136 | } 137 | 138 | async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { 139 | self.send_transactions(transactions).await 140 | } 141 | 142 | async fn get_tip_account(&self) -> Result { 143 | let tip_account = self.get_tip_account().await?; 144 | Ok(tip_account) 145 | } 146 | 147 | async fn get_client_type(&self) -> ClientType { 148 | ClientType::NextBlock 149 | } 150 | } 151 | 152 | impl NextBlockClient { 153 | pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { 154 | if CryptoProvider::get_default().is_none() { 155 | let _ = default_provider() 156 | .install_default() 157 | .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e)); 158 | } 159 | 160 | let endpoint = endpoint.parse::().unwrap(); 161 | let tls = ClientTlsConfig::new().with_native_roots(); 162 | let channel = Channel::builder(endpoint) 163 | .tls_config(tls).expect("Failed to create TLS config") 164 | .tcp_keepalive(Some(Duration::from_secs(60))) 165 | .http2_keep_alive_interval(Duration::from_secs(30)) 166 | .keep_alive_while_idle(true) 167 | .timeout(Duration::from_secs(30)) 168 | .connect_timeout(Duration::from_secs(10)) 169 | .connect_lazy(); 170 | 171 | let client = ApiClient::with_interceptor(channel, MyInterceptor::new(auth_token)); 172 | let rpc_client = SolanaRpcClient::new(rpc_url); 173 | Self { rpc_client: Arc::new(rpc_client), client } 174 | } 175 | 176 | pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { 177 | let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; 178 | 179 | self.client.clone().post_submit_v2(api::PostSubmitRequest { 180 | transaction: Some(api::TransactionMessage { 181 | content, 182 | is_cleanup: false, 183 | }), 184 | skip_pre_flight: true, 185 | front_running_protection: Some(true), 186 | experimental_front_running_protection: Some(true), 187 | snipe_transaction: Some(true), 188 | }).await?; 189 | 190 | let timeout: Duration = Duration::from_secs(10); 191 | let start_time: Instant = Instant::now(); 192 | while Instant::now().duration_since(start_time) < timeout { 193 | match poll_transaction_confirmation(&self.rpc_client, signature).await { 194 | Ok(sig) => return Ok(sig), 195 | Err(_) => continue, 196 | } 197 | } 198 | 199 | Ok(signature) 200 | } 201 | 202 | pub async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { 203 | let mut entries = Vec::new(); 204 | let encoding = UiTransactionEncoding::Base64; 205 | 206 | let mut signatures = Vec::new(); 207 | for transaction in transactions { 208 | let (content, signature) = serialize_smart_transaction_and_encode(transaction, encoding).await?; 209 | entries.push(api::PostSubmitRequestEntry { 210 | transaction: Some(api::TransactionMessage { 211 | content, 212 | is_cleanup: false, 213 | }), 214 | skip_pre_flight: true, 215 | }); 216 | signatures.push(signature); 217 | } 218 | 219 | self.client.clone().post_submit_batch_v2(api::PostSubmitBatchRequest { 220 | entries, 221 | submit_strategy: api::SubmitStrategy::PSubmitAll as i32, 222 | use_bundle: Some(true), 223 | front_running_protection: Some(true), 224 | }).await?; 225 | 226 | let timeout: Duration = Duration::from_secs(10); 227 | let start_time: Instant = Instant::now(); 228 | while Instant::now().duration_since(start_time) < timeout { 229 | for signature in signatures.clone() { 230 | match poll_transaction_confirmation(&self.rpc_client, signature).await { 231 | Ok(sig) => signatures.push(sig), 232 | Err(_) => continue, 233 | } 234 | } 235 | } 236 | 237 | Ok(signatures) 238 | } 239 | 240 | async fn get_tip_account(&self) -> Result { 241 | let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap(); 242 | Ok(tip_account.to_string()) 243 | } 244 | } 245 | 246 | #[derive(Clone)] 247 | pub struct ZeroSlotClient { 248 | pub endpoint: String, 249 | pub auth_token: String, 250 | pub rpc_client: Arc, 251 | } 252 | 253 | #[async_trait::async_trait] 254 | impl FeeClientTrait for ZeroSlotClient { 255 | async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { 256 | self.send_transaction(transaction).await 257 | } 258 | 259 | async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { 260 | self.send_transactions(transactions).await 261 | } 262 | 263 | async fn get_tip_account(&self) -> Result { 264 | let tip_account = self.get_tip_account().await?; 265 | Ok(tip_account) 266 | } 267 | 268 | async fn get_client_type(&self) -> ClientType { 269 | ClientType::ZeroSlot 270 | } 271 | } 272 | 273 | impl ZeroSlotClient { 274 | pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { 275 | let rpc_client = SolanaRpcClient::new(rpc_url); 276 | Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token } 277 | } 278 | 279 | pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { 280 | let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; 281 | 282 | let client = Client::new(); 283 | let request_body = json!({ 284 | "jsonrpc": "2.0", 285 | "id": 1, 286 | "method": "sendTransaction", 287 | "params": [ 288 | content, 289 | { 290 | "encoding": "base64", 291 | "skipPreflight": true, 292 | } 293 | ] 294 | }); 295 | 296 | // Send the request 297 | let response = client.post(format!("{}/?api-key={}", self.endpoint, self.auth_token)) 298 | .json(&request_body) 299 | .send() 300 | .await?; 301 | 302 | // Parse the response 303 | let response_json: serde_json::Value = response.json().await?; 304 | if let Some(result) = response_json.get("result") { 305 | println!("Transaction sent successfully: {}", result); 306 | } else if let Some(error) = response_json.get("error") { 307 | eprintln!("Failed to send transaction: {}", error); 308 | } 309 | 310 | let timeout: Duration = Duration::from_secs(10); 311 | let start_time: Instant = Instant::now(); 312 | while Instant::now().duration_since(start_time) < timeout { 313 | match poll_transaction_confirmation(&self.rpc_client, signature).await { 314 | Ok(sig) => return Ok(sig), 315 | Err(_) => continue, 316 | } 317 | } 318 | 319 | Ok(signature) 320 | } 321 | 322 | pub async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { 323 | let mut signatures = Vec::new(); 324 | for transaction in transactions { 325 | let signature = self.send_transaction(transaction).await?; 326 | signatures.push(signature); 327 | } 328 | Ok(signatures) 329 | } 330 | 331 | async fn get_tip_account(&self) -> Result { 332 | let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap(); 333 | Ok(tip_account.to_string()) 334 | } 335 | } -------------------------------------------------------------------------------- /src/swqos/searcher_client.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::Arc, 3 | time::{Duration, Instant}, 4 | }; 5 | 6 | use crate::swqos::jito_grpc::{ 7 | bundle::{ 8 | Bundle, BundleResult, 9 | }, 10 | convert::proto_packet_from_versioned_tx, 11 | searcher::{ 12 | searcher_service_client::SearcherServiceClient, SendBundleRequest, SubscribeBundleResultsRequest, 13 | }, 14 | }; 15 | use solana_sdk::{ 16 | signature::Signature, 17 | transaction::VersionedTransaction, 18 | }; 19 | use thiserror::Error; 20 | use tokio::sync::Mutex; 21 | use tonic::{transport::{self, Channel, Endpoint}, Status}; 22 | use yellowstone_grpc_client::ClientTlsConfig; 23 | 24 | use crate::swqos::common::poll_transaction_confirmation; 25 | use crate::common::SolanaRpcClient; 26 | 27 | #[derive(Debug, Error)] 28 | pub enum BlockEngineConnectionError { 29 | #[error("transport error {0}")] 30 | TransportError(#[from] transport::Error), 31 | #[error("client error {0}")] 32 | ClientError(#[from] Status), 33 | } 34 | 35 | #[derive(Debug, Error)] 36 | pub enum BundleRejectionError { 37 | #[error("bundle lost state auction, auction: {0}, tip {1} lamports")] 38 | StateAuctionBidRejected(String, u64), 39 | #[error("bundle won state auction but failed global auction, auction {0}, tip {1} lamports")] 40 | WinningBatchBidRejected(String, u64), 41 | #[error("bundle simulation failure on tx {0}, message: {1:?}")] 42 | SimulationFailure(String, Option), 43 | #[error("internal error {0}")] 44 | InternalError(String), 45 | } 46 | 47 | pub type BlockEngineConnectionResult = Result; 48 | 49 | pub async fn get_searcher_client_no_auth( 50 | block_engine_url: &str, 51 | ) -> BlockEngineConnectionResult> { 52 | let searcher_channel = create_grpc_channel(block_engine_url).await?; 53 | let searcher_client = SearcherServiceClient::new(searcher_channel); 54 | Ok(searcher_client) 55 | } 56 | 57 | pub async fn create_grpc_channel(url: &str) -> BlockEngineConnectionResult { 58 | let mut endpoint = Endpoint::from_shared(url.to_string()).expect("invalid url"); 59 | if url.starts_with("https") { 60 | endpoint = endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?; 61 | } 62 | 63 | endpoint = endpoint.tcp_nodelay(true); 64 | endpoint = endpoint.tcp_keepalive(Some(Duration::from_secs(10))); 65 | endpoint = endpoint.connect_timeout(Duration::from_secs(20)); 66 | endpoint = endpoint.http2_keep_alive_interval(Duration::from_secs(10)); 67 | 68 | Ok(endpoint.connect().await?) 69 | } 70 | 71 | pub async fn subscribe_bundle_results( 72 | searcher_client: Arc>>, 73 | request: impl tonic::IntoRequest, 74 | ) -> std::result::Result< 75 | tonic::Response>, 76 | tonic::Status, 77 | > { 78 | let mut searcher = searcher_client.lock().await; 79 | searcher.subscribe_bundle_results(request).await 80 | } 81 | 82 | pub async fn send_bundle_with_confirmation( 83 | rpc: Arc, 84 | transactions: &Vec, 85 | searcher_client: Arc>>, 86 | ) -> Result, anyhow::Error> { 87 | let mut signatures = send_bundle_no_wait(transactions, searcher_client).await?; 88 | 89 | let timeout: Duration = Duration::from_secs(10); 90 | let start_time: Instant = Instant::now(); 91 | while Instant::now().duration_since(start_time) < timeout { 92 | for signature in signatures.clone() { 93 | match poll_transaction_confirmation(&rpc, signature).await { 94 | Ok(sig) => signatures.push(sig), 95 | Err(_) => continue, 96 | } 97 | } 98 | } 99 | 100 | Ok(signatures) 101 | } 102 | 103 | pub async fn send_bundle_no_wait( 104 | transactions: &Vec, 105 | searcher_client: Arc>>, 106 | ) -> Result, anyhow::Error> { 107 | let mut packets = vec![]; 108 | let mut signatures = vec![]; 109 | for transaction in transactions { 110 | let packet = proto_packet_from_versioned_tx(transaction); 111 | packets.push(packet); 112 | signatures.push(transaction.signatures[0]); 113 | } 114 | 115 | let mut searcher = searcher_client.lock().await; 116 | searcher 117 | .send_bundle(SendBundleRequest { 118 | bundle: Some(Bundle { 119 | header: None, 120 | packets, 121 | }), 122 | }) 123 | .await?; 124 | 125 | Ok(signatures) 126 | } 127 | --------------------------------------------------------------------------------