├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml └── src ├── common ├── accounts.rs ├── keypair.rs ├── mod.rs ├── trading_client.rs └── trading_endpoint.rs ├── dex ├── amm_calc.rs ├── dex_traits.rs ├── mod.rs ├── pumpfun.rs ├── pumpfun_types.rs ├── pumpswap.rs └── types.rs ├── instruction ├── builder.rs └── mod.rs ├── ipfs ├── metadata.rs ├── mod.rs └── types.rs ├── lib.rs ├── main.rs └── swqos ├── blox.rs ├── default.rs ├── jito.rs ├── mod.rs ├── nextblock.rs ├── swqos_rpc.rs ├── temporal.rs └── zeroslot.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 = "solana-trading-sdk" 3 | version = "1.0.5" 4 | edition = "2021" 5 | authors = ["vFight "] 6 | repository = "https://github.com/LulzFi/solana-trading-sdk" 7 | description = "A Rust SDK for trading on Solana" 8 | license = "MIT" 9 | keywords = ["solana", "memecoins", "pumpfun", "trading", "sdk"] 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-transaction-status = "2.1.16" 21 | 22 | spl-token = "8.0.0" 23 | spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] } 24 | spl-associated-token-account = "6.0.0" 25 | mpl-token-metadata = "5.1.0" 26 | 27 | borsh = { version = "1.5.3", features = ["derive"] } 28 | serde = { version = "1.0.215", features = ["derive"] } 29 | serde_json = "1.0.134" 30 | futures = "0.3.31" 31 | rand = "0.9.0" 32 | bincode = "1.3.3" 33 | anyhow = "1.0.90" 34 | reqwest = { version = "0.12.12", features = ["json", "multipart", "rustls-tls"] } 35 | tokio = { version = "1.42.0", features = ["full", "rt-multi-thread"] } 36 | 37 | async-trait = "0.1.86" 38 | lazy_static = "1.5.0" 39 | once_cell = "1.20.3" 40 | solana-account-decoder = "2.2.7" 41 | base64 = "0.22.1" 42 | -------------------------------------------------------------------------------- /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 | # Solana Trading SDK 2 | 3 | A comprehensive Rust SDK for trading on the Solana blockchain, with support for multiple DEXs and advanced transaction submission strategies. 4 | 5 | ## Features 6 | 7 | - **Multi-DEX Support**: Trade on Pump.fun, PumpSwap, and other Solana DEXs 8 | - **Smart Transaction Routing**: Multiple SWQoS (Solana Web Quality of Service) providers for optimal transaction submission 9 | - **Token Creation**: Create and deploy new tokens with metadata on IPFS 10 | - **Priority Fees & MEV Protection**: Built-in support for priority fees and MEV protection through Jito bundles 11 | - **Comprehensive Trading**: Buy, sell, and create tokens with customizable slippage and fees 12 | 13 | ## Supported DEXs 14 | 15 | - **Pumpfun**: The popular meme coin launchpad 16 | - **PumpSwap**: Advanced trading features for pump tokens 17 | 18 | ## Supported SWQoS Providers 19 | 20 | - **Default RPC**: Standard Solana RPC endpoints 21 | - **Jito**: MEV protection and bundle submission 22 | - **NextBlock**: High-performance transaction processing 23 | - **Blox**: Advanced routing and execution 24 | - **ZeroSlot**: Fast transaction confirmation 25 | - **Temporal**: Time-based transaction optimization 26 | 27 | ## Installation 28 | 29 | Add this to your `Cargo.toml`: 30 | 31 | ```toml 32 | [dependencies] 33 | solana-trading-sdk = { path = "." } 34 | tokio = { version = "1.0", features = ["full"] } 35 | anyhow = "1.0" 36 | ``` 37 | 38 | ## Quick Start 39 | 40 | ### Basic Setup 41 | 42 | ```rust 43 | use solana_trading_sdk::{ 44 | common::{TradingClient, TradingConfig}, 45 | swqos::SWQoSType, 46 | }; 47 | 48 | #[tokio::main] 49 | async fn main() -> anyhow::Result<()> { 50 | let client = TradingClient::new(TradingConfig { 51 | rpc_url: "https://solana-rpc.publicnode.com".to_string(), 52 | swqos: vec![ 53 | SWQoSType::Default("https://solana-rpc.publicnode.com".to_string(), None), 54 | SWQoSType::Jito("https://mainnet.block-engine.jito.wtf".to_string()), 55 | ], 56 | })?; 57 | 58 | client.initialize().await?; 59 | Ok(()) 60 | } 61 | ``` 62 | 63 | ### Token Trading 64 | 65 | ```rust 66 | use solana_trading_sdk::{ 67 | dex::types::DexType, 68 | instruction::builder::PriorityFee, 69 | }; 70 | use solana_sdk::{native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair}; 71 | 72 | async fn buy_token() -> anyhow::Result<()> { 73 | let client = get_trading_client().await?; 74 | let payer = Keypair::from_base58_string("your_private_key"); 75 | let mint = Pubkey::from_str("token_mint_address")?; 76 | 77 | let sol_amount = sol_to_lamports(1.0); // 1 SOL 78 | let slippage_basis_points = 3000; // 30% 79 | let fee = PriorityFee { 80 | unit_limit: 100000, 81 | unit_price: 10000000, 82 | }; 83 | let tip = sol_to_lamports(0.001); // 0.001 SOL tip 84 | 85 | // Buy on Pump.fun 86 | client.dexs[&DexType::Pumpfun] 87 | .buy(&payer, &mint, sol_amount, slippage_basis_points, Some(fee), Some(tip)) 88 | .await?; 89 | 90 | Ok(()) 91 | } 92 | ``` 93 | 94 | ### Token Creation 95 | 96 | ```rust 97 | use solana_trading_sdk::{ 98 | ipfs::{metadata::create_token_metadata, types::CreateTokenMetadata}, 99 | dex::{pumpfun::Pumpfun, types::Create}, 100 | }; 101 | 102 | async fn create_token() -> anyhow::Result<()> { 103 | // 1. Create metadata 104 | let token_info = CreateTokenMetadata { 105 | name: "My Token".to_string(), 106 | symbol: "MTK".to_string(), 107 | description: "A revolutionary new token".to_string(), 108 | file: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==".to_string(), 109 | twitter: Some("@mytoken".to_string()), 110 | telegram: Some("@mytokengroup".to_string()), 111 | website: Some("https://mytoken.com".to_string()), 112 | metadata_uri: None, 113 | }; 114 | 115 | let metadata = create_token_metadata(token_info, "your_pinata_jwt_token").await?; 116 | 117 | // 2. Create token on Pump.fun 118 | let payer = Keypair::from_base58_string("your_private_key"); 119 | let mint = Keypair::new(); 120 | 121 | let create = Create { 122 | name: metadata.metadata.name, 123 | symbol: metadata.metadata.symbol, 124 | uri: metadata.metadata_uri, 125 | mint: mint.pubkey(), 126 | buy_sol_amount: Some(sol_to_lamports(0.1)), 127 | slippage_basis_points: Some(3000), 128 | }; 129 | 130 | let pumpfun_client = get_pumpfun_client().await?; 131 | pumpfun_client.create(payer, create, Some(fee), Some(tip)).await?; 132 | 133 | Ok(()) 134 | } 135 | ``` 136 | 137 | ### SOL and Token Transfers 138 | 139 | ```rust 140 | async fn transfer_sol() -> anyhow::Result<()> { 141 | let from = Keypair::from_base58_string("sender_private_key"); 142 | let to = Pubkey::from_str("recipient_address")?; 143 | let amount = sol_to_lamports(0.1); // 0.1 SOL 144 | 145 | let swqos_client = get_swqos_client(); 146 | swqos_client.transfer(&from, &to, amount, Some(fee)).await?; 147 | Ok(()) 148 | } 149 | 150 | async fn transfer_token() -> anyhow::Result<()> { 151 | let from = Keypair::from_base58_string("sender_private_key"); 152 | let to = Pubkey::from_str("recipient_address")?; 153 | let mint = Pubkey::from_str("token_mint_address")?; 154 | let amount = 1000; // Token amount in smallest units 155 | 156 | let swqos_client = get_swqos_client(); 157 | swqos_client.spl_transfer(&from, &to, &mint, amount, Some(fee)).await?; 158 | Ok(()) 159 | } 160 | ``` 161 | 162 | ## Configuration 163 | 164 | ### SWQoS Providers 165 | 166 | Configure multiple SWQoS providers for optimal transaction routing: 167 | 168 | ```rust 169 | let swqos = vec![ 170 | SWQoSType::Default("https://solana-rpc.publicnode.com".to_string(), None), 171 | SWQoSType::Jito("https://mainnet.block-engine.jito.wtf".to_string()), 172 | SWQoSType::NextBlock("https://fra.nextblock.io".to_string(), "your_api_key".to_string()), 173 | SWQoSType::Blox("https://fra.blox.so".to_string(), "your_api_key".to_string()), 174 | SWQoSType::ZeroSlot("https://fra.zeroslot.io".to_string(), "your_api_key".to_string()), 175 | SWQoSType::Temporal("https://fra.temporal.io".to_string(), "your_api_key".to_string()), 176 | ]; 177 | ``` 178 | 179 | ### Priority Fees 180 | 181 | Set custom priority fees for faster transaction confirmation: 182 | 183 | ```rust 184 | let fee = PriorityFee { 185 | unit_limit: 100000, // Compute unit limit 186 | unit_price: 10000000, // Micro-lamports per compute unit 187 | }; 188 | ``` 189 | 190 | ## Examples 191 | 192 | Check the [`main.rs`](src/main.rs) file for complete working examples of: 193 | 194 | - Setting up trading clients 195 | - Buying and selling tokens 196 | - Creating new tokens 197 | - Transferring SOL and SPL tokens 198 | - Using different SWQoS providers 199 | 200 | ## API Reference 201 | 202 | ### Core Components 203 | 204 | - [`TradingClient`](src/common/trading_client.rs) - Main client for trading operations 205 | - [`TradingEndpoint`](src/common/trading_endpoint.rs) - RPC and SWQoS endpoint management 206 | - [`DexTrait`](src/dex/dex_traits.rs) - Common interface for all DEX implementations 207 | 208 | ### DEX Implementations 209 | 210 | - [`Pumpfun`](src/dex/pumpfun.rs) - Pump.fun DEX implementation 211 | - [`PumpSwap`](src/dex/pumpswap.rs) - PumpSwap DEX implementation 212 | 213 | ### SWQoS Providers 214 | 215 | - [`DefaultSWQoSClient`](src/swqos/default.rs) - Standard RPC client 216 | - [`JitoClient`](src/swqos/jito.rs) - Jito MEV protection 217 | - [`NextBlockClient`](src/swqos/nextblock.rs) - NextBlock routing 218 | - [`BloxClient`](src/swqos/blox.rs) - Blox execution 219 | - [`ZeroSlotClient`](src/swqos/zeroslot.rs) - ZeroSlot confirmation 220 | - [`TemporalClient`](src/swqos/temporal.rs) - Temporal optimization 221 | 222 | ### IPFS Integration 223 | 224 | - [`create_token_metadata`](src/ipfs/metadata.rs) - Upload token metadata to IPFS 225 | - [`CreateTokenMetadata`](src/ipfs/types.rs) - Token metadata structure 226 | 227 | ## Environment Setup 228 | 229 | 1. **RPC Endpoint**: Use a reliable Solana RPC endpoint 230 | 2. **Pinata JWT Token**: Required for IPFS metadata uploads 231 | 3. **SWQoS API Keys**: Optional API keys for premium routing services 232 | 233 | ## Error Handling 234 | 235 | The SDK uses `anyhow::Result` for comprehensive error handling. All functions return detailed error information for debugging. 236 | 237 | ## Contributing 238 | 239 | Contributions are welcome! Please feel free to submit a Pull Request. 240 | 241 | ## License 242 | 243 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 244 | 245 | ## Disclaimer 246 | 247 | This SDK is for educational and development purposes. Always test thoroughly on devnet before using on mainnet. Trading cryptocurrencies involves risk of loss. -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 160 2 | -------------------------------------------------------------------------------- /src/common/accounts.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey; 2 | use solana_sdk::pubkey::Pubkey; 3 | 4 | pub const PUBKEY_WSOL: Pubkey = pubkey!("So11111111111111111111111111111111111111112"); 5 | -------------------------------------------------------------------------------- /src/common/keypair.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{bs58, signature::Keypair}; 2 | 3 | pub trait KeypairBase58 { 4 | fn from_base58(private_key: &str) -> anyhow::Result; 5 | } 6 | 7 | impl KeypairBase58 for Keypair { 8 | fn from_base58(private_key: &str) -> anyhow::Result { 9 | let buf = bs58::decode(private_key.to_string()).into_vec()?; 10 | let keypair = Keypair::from_bytes(&buf)?; 11 | Ok(keypair) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod accounts; 2 | pub mod keypair; 3 | pub mod trading_client; 4 | pub mod trading_endpoint; 5 | 6 | pub use trading_client::*; 7 | -------------------------------------------------------------------------------- /src/common/trading_client.rs: -------------------------------------------------------------------------------- 1 | use super::trading_endpoint::TradingEndpoint; 2 | use crate::{ 3 | dex::{dex_traits::DexTrait, types::DexType}, 4 | swqos::SWQoSType, 5 | }; 6 | use solana_client::nonblocking::rpc_client::RpcClient; 7 | use std::{collections::HashMap, sync::Arc}; 8 | 9 | pub struct TradingConfig { 10 | pub rpc_url: String, 11 | pub swqos: Vec, 12 | } 13 | 14 | pub struct TradingClient { 15 | pub endpoint: Arc, 16 | pub dexs: HashMap>, 17 | } 18 | 19 | impl TradingClient { 20 | pub fn new(config: TradingConfig) -> anyhow::Result { 21 | let rpc = Arc::new(RpcClient::new(config.rpc_url)); 22 | let swqos = config.swqos.into_iter().map(|swqos| swqos.instantiate(rpc.clone())).collect(); 23 | let endpoint = Arc::new(TradingEndpoint::new(rpc, swqos)); 24 | let dexs = DexType::all().into_iter().map(|dex| (dex, dex.instantiate(endpoint.clone()))).collect(); 25 | 26 | Ok(Self { endpoint, dexs }) 27 | } 28 | 29 | pub async fn initialize(&self) -> anyhow::Result<()> { 30 | for (_, dex) in &self.dexs { 31 | dex.initialize().await?; 32 | } 33 | Ok(()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/common/trading_endpoint.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | instruction::builder::{build_transaction, PriorityFee, TipFee}, 3 | swqos::SWQoSTrait, 4 | }; 5 | use solana_client::nonblocking::rpc_client::RpcClient; 6 | use solana_sdk::{ 7 | hash::Hash, 8 | instruction::Instruction, 9 | signature::{Keypair, Signature}, 10 | }; 11 | use std::sync::Arc; 12 | 13 | pub struct TradingEndpoint { 14 | pub rpc: Arc, 15 | pub swqos: Vec>, 16 | } 17 | 18 | pub struct BatchTxItem { 19 | pub payer: Keypair, 20 | pub instructions: Vec, 21 | } 22 | 23 | impl TradingEndpoint { 24 | pub fn new(rpc: Arc, swqos: Vec>) -> Self { 25 | Self { rpc, swqos } 26 | } 27 | 28 | pub async fn get_latest_blockhash(&self) -> anyhow::Result { 29 | let blockhash = self.rpc.get_latest_blockhash().await?; 30 | Ok(blockhash) 31 | } 32 | 33 | pub async fn build_and_broadcast_tx( 34 | &self, 35 | payer: &Keypair, 36 | instructions: Vec, 37 | blockhash: Hash, 38 | fee: Option, 39 | tip: Option, 40 | other_signers: Option>, 41 | ) -> anyhow::Result> { 42 | let mut tasks = vec![]; 43 | let mut signatures = vec![]; 44 | for swqos in &self.swqos { 45 | let tip = if let Some(tip_account) = swqos.get_tip_account() { 46 | if let Some(tip) = tip { 47 | Some(TipFee { 48 | tip_account, 49 | tip_lamports: tip, 50 | }) 51 | } else { 52 | // If no tip is provided, skip this Tip-SWQoS 53 | eprintln!("No tip provided for SWQoS: {}", swqos.get_name()); 54 | continue; 55 | } 56 | } else { 57 | None 58 | }; 59 | 60 | let tx = build_transaction(payer, instructions.clone(), blockhash, fee, tip, other_signers.clone())?; 61 | signatures.push(tx.signatures[0]); 62 | tasks.push(swqos.send_transaction(tx)); 63 | } 64 | 65 | let result = futures::future::join_all(tasks).await; 66 | let errors = result.into_iter().filter_map(|res| res.err()).collect::>(); 67 | if errors.len() > 0 { 68 | return Err(anyhow::anyhow!("{:?}", errors)); 69 | } 70 | 71 | Ok(signatures) 72 | } 73 | 74 | pub async fn build_and_broadcast_batch_txs(&self, items: Vec, blockhash: Hash, fee: PriorityFee, tip: u64) -> anyhow::Result> { 75 | let mut tasks = vec![]; 76 | let mut signatures = vec![]; 77 | for swqos in &self.swqos { 78 | let tip_account = swqos 79 | .get_tip_account() 80 | .ok_or(anyhow::anyhow!("No tip account provided for SWQoS: {}", swqos.get_name()))?; 81 | let mut tip = Some(TipFee { 82 | tip_account, 83 | tip_lamports: tip, 84 | }); 85 | 86 | let txs = items 87 | .iter() 88 | .map(|item| build_transaction(&item.payer, item.instructions.clone(), blockhash, Some(fee), tip.take(), None)) 89 | .collect::, _>>()?; 90 | 91 | signatures.extend(txs.iter().map(|tx| tx.signatures[0])); 92 | tasks.push(swqos.send_transactions(txs)); 93 | } 94 | 95 | let result = futures::future::join_all(tasks).await; 96 | let errors = result.into_iter().filter_map(|res| res.err()).collect::>(); 97 | if errors.len() > 0 { 98 | return Err(anyhow::anyhow!("{:?}", errors)); 99 | } 100 | 101 | Ok(signatures) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/dex/amm_calc.rs: -------------------------------------------------------------------------------- 1 | pub fn amm_buy_get_sol_in(sol_reserve: u64, token_reserve: u64, token_out: u64) -> u64 { 2 | if token_out == 0 || sol_reserve == 0 || token_reserve == 0 || token_out >= token_reserve { 3 | return 0; 4 | } 5 | 6 | let sol_reserve = sol_reserve as u128; 7 | let token_reserve = token_reserve as u128; 8 | let token_in = token_out as u128; 9 | let numerator = sol_reserve.checked_mul(token_in).unwrap(); 10 | let denominator = token_reserve.checked_sub(token_in).unwrap(); 11 | let sol_out = numerator.checked_div(denominator).unwrap(); 12 | 13 | sol_out as u64 14 | } 15 | 16 | pub fn amm_buy_get_token_out(sol_reserve: u64, token_reserve: u64, sol_in: u64) -> u64 { 17 | if sol_in == 0 || sol_reserve == 0 || token_reserve == 0 { 18 | return 0; 19 | } 20 | 21 | let invariant = sol_reserve as u128 * token_reserve as u128; 22 | let new_sol_reserve = sol_reserve as u128 + sol_in as u128; 23 | 24 | let new_token_reserve = invariant / new_sol_reserve; 25 | let token_out = token_reserve as u128 - new_token_reserve; 26 | 27 | token_out as u64 28 | } 29 | 30 | pub fn amm_sell_get_sol_out(sol_reserve: u64, token_reserve: u64, token_in: u64) -> u64 { 31 | if token_in == 0 || sol_reserve == 0 || token_reserve == 0 { 32 | return 0; 33 | } 34 | 35 | let sol_reserve = sol_reserve as u128; 36 | let token_reserve = token_reserve as u128; 37 | let token_in = token_in as u128; 38 | let numerator = sol_reserve.checked_mul(token_in).unwrap(); 39 | let denominator = token_reserve.checked_add(token_in).unwrap(); 40 | let sol_out = numerator.checked_div(denominator).unwrap(); 41 | 42 | sol_out as u64 43 | } 44 | 45 | pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 { 46 | amount + (amount * basis_points) / 10000 47 | } 48 | 49 | pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 { 50 | amount - (amount * basis_points) / 10000 51 | } 52 | -------------------------------------------------------------------------------- /src/dex/dex_traits.rs: -------------------------------------------------------------------------------- 1 | use super::types::{Create, TokenAmountType}; 2 | use crate::{dex::types::CreateATA, instruction::builder::PriorityFee}; 3 | use solana_sdk::{ 4 | hash::Hash, 5 | pubkey::Pubkey, 6 | signature::{Keypair, Signature}, 7 | }; 8 | use std::any::Any; 9 | 10 | #[async_trait::async_trait] 11 | pub trait DexTrait: Send + Sync + Any { 12 | async fn initialize(&self) -> anyhow::Result<()>; 13 | fn initialized(&self) -> anyhow::Result<()>; 14 | async fn create(&self, payer: Keypair, create: Create, fee: Option, tip: Option) -> anyhow::Result>; 15 | async fn buy( 16 | &self, 17 | payer: &Keypair, 18 | mint: &Pubkey, 19 | sol_amount: u64, 20 | slippage_basis_points: u64, 21 | fee: Option, 22 | tip: Option, 23 | ) -> anyhow::Result>; 24 | async fn buy_immediately( 25 | &self, 26 | payer: &Keypair, 27 | mint: &Pubkey, 28 | pool: Option<&Pubkey>, 29 | creator_vault: Option<&Pubkey>, 30 | sol_amount: u64, 31 | token_amount: u64, 32 | blockhash: Hash, 33 | create_ata: CreateATA, 34 | fee: Option, 35 | tip: Option, 36 | ) -> anyhow::Result>; 37 | async fn sell( 38 | &self, 39 | payer: &Keypair, 40 | mint: &Pubkey, 41 | token_amount: TokenAmountType, 42 | slippage_basis_points: u64, 43 | close_mint_ata: bool, 44 | fee: Option, 45 | tip: Option, 46 | ) -> anyhow::Result>; 47 | async fn sell_immediately( 48 | &self, 49 | payer: &Keypair, 50 | mint: &Pubkey, 51 | pool: Option<&Pubkey>, 52 | creator_vault: Option<&Pubkey>, 53 | token_amount: u64, 54 | sol_amount: u64, 55 | close_mint_ata: bool, 56 | blockhash: Hash, 57 | fee: Option, 58 | tip: Option, 59 | ) -> anyhow::Result>; 60 | async fn batch_buy( 61 | &self, 62 | mint: &Pubkey, 63 | slippage_basis_points: u64, 64 | fee: PriorityFee, 65 | tip: u64, 66 | items: Vec, 67 | ) -> anyhow::Result>; 68 | async fn batch_sell( 69 | &self, 70 | mint: &Pubkey, 71 | slippage_basis_points: u64, 72 | fee: PriorityFee, 73 | tip: u64, 74 | items: Vec, 75 | ) -> anyhow::Result>; 76 | } 77 | 78 | pub struct BatchBuyParam { 79 | pub payer: Keypair, 80 | pub sol_amount: u64, 81 | } 82 | 83 | pub struct BatchSellParam { 84 | pub payer: Keypair, 85 | pub token_amount: u64, 86 | pub close_mint_ata: bool, 87 | } 88 | -------------------------------------------------------------------------------- /src/dex/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod amm_calc; 2 | pub mod dex_traits; 3 | pub mod pumpfun; 4 | pub mod pumpswap; 5 | pub mod types; 6 | pub mod pumpfun_types; 7 | -------------------------------------------------------------------------------- /src/dex/pumpfun.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | amm_calc::{amm_buy_get_token_out, amm_sell_get_sol_out, calculate_with_slippage_buy, calculate_with_slippage_sell}, 3 | dex_traits::{BatchBuyParam, BatchSellParam, DexTrait}, 4 | pumpfun_types::{BuyInfo, SellInfo}, 5 | types::{Buy, Create, Sell, TokenAmountType}, 6 | }; 7 | use crate::{ 8 | common::trading_endpoint::{BatchTxItem, TradingEndpoint}, 9 | dex::types::CreateATA, 10 | instruction::builder::{build_sol_buy_instructions, build_sol_sell_instructions, PriorityFee}, 11 | }; 12 | use borsh::{BorshDeserialize, BorshSerialize}; 13 | use once_cell::sync::OnceCell; 14 | use serde::{Deserialize, Serialize}; 15 | use solana_sdk::{ 16 | hash::Hash, 17 | instruction::{AccountMeta, Instruction}, 18 | pubkey, 19 | pubkey::Pubkey, 20 | signature::{Keypair, Signature}, 21 | signer::Signer, 22 | }; 23 | use spl_associated_token_account::{get_associated_token_address, instruction::create_associated_token_account}; 24 | use std::sync::Arc; 25 | 26 | pub const PUBKEY_PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); 27 | pub const PUBKEY_GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); 28 | pub const PUBKEY_EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"); 29 | pub const PUBKEY_FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); 30 | 31 | pub const GLOBAL_SEED: &[u8] = b"global"; 32 | pub const MINT_AUTHORITY_SEED: &[u8] = b"mint-authority"; 33 | pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve"; 34 | pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault"; 35 | pub const METADATA_SEED: &[u8] = b"metadata"; 36 | 37 | pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000; 38 | pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000; 39 | 40 | lazy_static::lazy_static! { 41 | static ref PUBKEY_MINT_AUTHORITY_PDA: Pubkey = Pubkey::find_program_address(&[MINT_AUTHORITY_SEED], &PUBKEY_PUMPFUN).0; 42 | static ref PUBKEY_GLOBAL_PDA: Pubkey = Pubkey::find_program_address(&[GLOBAL_SEED], &PUBKEY_PUMPFUN).0; 43 | } 44 | 45 | #[derive(Debug, Clone, Serialize, Deserialize)] 46 | pub struct GlobalAccount { 47 | pub discriminator: u64, 48 | pub initialized: bool, 49 | pub authority: Pubkey, 50 | pub fee_recipient: Pubkey, 51 | pub initial_virtual_token_reserves: u64, 52 | pub initial_virtual_sol_reserves: u64, 53 | pub initial_real_token_reserves: u64, 54 | pub token_total_supply: u64, 55 | pub fee_basis_points: u64, 56 | } 57 | 58 | #[derive(Debug, Clone, Serialize, Deserialize)] 59 | pub struct BondingCurveAccount { 60 | pub discriminator: u64, 61 | pub virtual_token_reserves: u64, 62 | pub virtual_sol_reserves: u64, 63 | pub real_token_reserves: u64, 64 | pub real_sol_reserves: u64, 65 | pub token_total_supply: u64, 66 | pub complete: bool, 67 | pub creator: Pubkey, 68 | } 69 | 70 | #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] 71 | pub struct CreateInfo { 72 | pub discriminator: u64, 73 | pub name: String, 74 | pub symbol: String, 75 | pub uri: String, 76 | pub creator: Pubkey, 77 | } 78 | 79 | impl CreateInfo { 80 | pub fn from_create(create: &Create, creator: Pubkey) -> Self { 81 | Self { 82 | discriminator: 8576854823835016728, 83 | name: create.name.to_string(), 84 | symbol: create.symbol.to_string(), 85 | uri: create.uri.to_string(), 86 | creator, 87 | } 88 | } 89 | } 90 | 91 | pub struct Pumpfun { 92 | pub endpoint: Arc, 93 | pub global_account: OnceCell>, 94 | } 95 | 96 | #[async_trait::async_trait] 97 | impl DexTrait for Pumpfun { 98 | async fn initialize(&self) -> anyhow::Result<()> { 99 | let account = self.endpoint.rpc.get_account(&PUBKEY_GLOBAL_ACCOUNT).await?; 100 | let global_account = bincode::deserialize::(&account.data)?; 101 | let global_account = Arc::new(global_account); 102 | 103 | self.global_account.set(global_account).unwrap(); 104 | Ok(()) 105 | } 106 | 107 | fn initialized(&self) -> anyhow::Result<()> { 108 | if self.global_account.get().is_none() { 109 | return Err(anyhow::anyhow!("Pumpfun not initialized")); 110 | } 111 | Ok(()) 112 | } 113 | 114 | async fn create(&self, payer: Keypair, create: Create, fee: Option, tip: Option) -> anyhow::Result> { 115 | let mint = create.mint_private_key.pubkey(); 116 | let buy_sol_amount = create.buy_sol_amount; 117 | let slippage_basis_points = create.slippage_basis_points.unwrap_or(0); 118 | 119 | let create_info = CreateInfo::from_create(&create, payer.pubkey()); 120 | let mut buffer = Vec::new(); 121 | create_info.serialize(&mut buffer)?; 122 | 123 | let blockhash = self.endpoint.rpc.get_latest_blockhash().await?; 124 | let bonding_curve = Self::get_bonding_curve_pda(&mint).ok_or(anyhow::anyhow!("Bonding curve not found"))?; 125 | 126 | let mut instructions = vec![]; 127 | let create_instruction = Instruction::new_with_bytes( 128 | PUBKEY_PUMPFUN, 129 | &buffer, 130 | vec![ 131 | AccountMeta::new(mint, true), 132 | AccountMeta::new(*PUBKEY_MINT_AUTHORITY_PDA, false), 133 | AccountMeta::new(bonding_curve, false), 134 | AccountMeta::new(get_associated_token_address(&bonding_curve, &mint), false), 135 | AccountMeta::new_readonly(*PUBKEY_GLOBAL_PDA, false), 136 | AccountMeta::new_readonly(mpl_token_metadata::ID, false), 137 | AccountMeta::new(mpl_token_metadata::accounts::Metadata::find_pda(&mint).0, false), 138 | AccountMeta::new(payer.pubkey(), true), 139 | AccountMeta::new_readonly(solana_program::system_program::ID, false), 140 | AccountMeta::new_readonly(spl_token::ID, false), 141 | AccountMeta::new_readonly(spl_associated_token_account::ID, false), 142 | AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false), 143 | AccountMeta::new_readonly(PUBKEY_EVENT_AUTHORITY, false), 144 | AccountMeta::new_readonly(PUBKEY_PUMPFUN, false), 145 | ], 146 | ); 147 | 148 | instructions.push(create_instruction); 149 | 150 | if let Some(buy_sol_amount) = buy_sol_amount { 151 | let create_ata = create_associated_token_account(&payer.pubkey(), &payer.pubkey(), &mint, &spl_token::ID); 152 | instructions.push(create_ata); 153 | 154 | let buy_token_amount = amm_buy_get_token_out(INITIAL_VIRTUAL_SOL_RESERVES, INITIAL_VIRTUAL_TOKEN_RESERVES, buy_sol_amount); 155 | let sol_lamports_with_slippage = calculate_with_slippage_buy(buy_sol_amount, slippage_basis_points); 156 | let creator_vault = Self::get_creator_vault_pda(&payer.pubkey()).unwrap(); 157 | let buy_instruction = self.build_buy_instruction( 158 | &payer, 159 | &mint, 160 | &creator_vault, 161 | Buy { 162 | token_amount: buy_token_amount, 163 | sol_amount: sol_lamports_with_slippage, 164 | }, 165 | )?; 166 | instructions.push(buy_instruction); 167 | } 168 | 169 | let signatures = self 170 | .endpoint 171 | .build_and_broadcast_tx(&payer, instructions, blockhash, fee, tip, Some(vec![&create.mint_private_key])) 172 | .await?; 173 | 174 | Ok(signatures) 175 | } 176 | 177 | async fn buy( 178 | &self, 179 | payer: &Keypair, 180 | mint: &Pubkey, 181 | sol_lamports: u64, 182 | slippage_basis_points: u64, 183 | fee: Option, 184 | tip: Option, 185 | ) -> anyhow::Result> { 186 | let sol_lamports_with_slippage = calculate_with_slippage_buy(sol_lamports, slippage_basis_points); 187 | let ((_, pool_account), blockhash) = tokio::try_join!(self.get_pool(&mint), self.endpoint.get_latest_blockhash())?; 188 | let buy_token_amount = amm_buy_get_token_out(pool_account.virtual_sol_reserves, pool_account.virtual_token_reserves, sol_lamports); 189 | let creator_vault = Self::get_creator_vault_pda(&pool_account.creator).ok_or(anyhow::anyhow!("Creator vault not found: {}", mint.to_string()))?; 190 | 191 | self.buy_immediately( 192 | payer, 193 | mint, 194 | None, 195 | Some(&creator_vault), 196 | sol_lamports_with_slippage, 197 | buy_token_amount, 198 | blockhash, 199 | CreateATA::Idempotent, 200 | fee, 201 | tip, 202 | ) 203 | .await 204 | } 205 | 206 | async fn buy_immediately( 207 | &self, 208 | payer: &Keypair, 209 | mint: &Pubkey, 210 | _: Option<&Pubkey>, 211 | creator_vault: Option<&Pubkey>, 212 | sol_amount: u64, 213 | buy_token_amount: u64, 214 | blockhash: Hash, 215 | create_ata: CreateATA, 216 | fee: Option, 217 | tip: Option, 218 | ) -> anyhow::Result> { 219 | let creator_vault = creator_vault.ok_or(anyhow::anyhow!("creator vault not provided: {}", mint.to_string()))?; 220 | let instruction = self.build_buy_instruction( 221 | payer, 222 | mint, 223 | &creator_vault, 224 | Buy { 225 | token_amount: buy_token_amount, 226 | sol_amount, 227 | }, 228 | )?; 229 | let instructions = build_sol_buy_instructions(payer, mint, instruction, create_ata)?; 230 | let signatures = self.endpoint.build_and_broadcast_tx(payer, instructions, blockhash, fee, tip, None).await?; 231 | 232 | Ok(signatures) 233 | } 234 | 235 | async fn sell( 236 | &self, 237 | payer: &Keypair, 238 | mint: &Pubkey, 239 | token_amount: TokenAmountType, 240 | slippage_basis_points: u64, 241 | close_mint_ata: bool, 242 | fee: Option, 243 | tip: Option, 244 | ) -> anyhow::Result> { 245 | let payer_pubkey = payer.pubkey(); 246 | let ((_, pool_account), blockhash, token_amount) = tokio::try_join!( 247 | self.get_pool(&mint), 248 | self.endpoint.get_latest_blockhash(), 249 | token_amount.to_amount(self.endpoint.rpc.clone(), &payer_pubkey, mint) 250 | )?; 251 | let sol_lamports = amm_sell_get_sol_out(pool_account.virtual_sol_reserves, pool_account.virtual_token_reserves, token_amount); 252 | let sol_lamports_with_slippage = calculate_with_slippage_sell(sol_lamports, slippage_basis_points); 253 | let creator_vault = Self::get_creator_vault_pda(&pool_account.creator).ok_or(anyhow::anyhow!("Creator vault not found: {}", mint.to_string()))?; 254 | 255 | self.sell_immediately( 256 | payer, 257 | mint, 258 | None, 259 | Some(&creator_vault), 260 | token_amount, 261 | sol_lamports_with_slippage, 262 | close_mint_ata, 263 | blockhash, 264 | fee, 265 | tip, 266 | ) 267 | .await 268 | } 269 | 270 | async fn sell_immediately( 271 | &self, 272 | payer: &Keypair, 273 | mint: &Pubkey, 274 | _: Option<&Pubkey>, 275 | creator_vault: Option<&Pubkey>, 276 | token_amount: u64, 277 | sol_amount: u64, 278 | close_mint_ata: bool, 279 | blockhash: Hash, 280 | fee: Option, 281 | tip: Option, 282 | ) -> anyhow::Result> { 283 | let creator_vault = creator_vault.ok_or(anyhow::anyhow!("creator vault not provided: {}", mint.to_string()))?; 284 | let instruction = self.build_sell_instruction(payer, mint, creator_vault, Sell { token_amount, sol_amount })?; 285 | let instructions = build_sol_sell_instructions(payer, mint, instruction, close_mint_ata)?; 286 | let signatures = self.endpoint.build_and_broadcast_tx(payer, instructions, blockhash, fee, tip, None).await?; 287 | 288 | Ok(signatures) 289 | } 290 | 291 | async fn batch_buy( 292 | &self, 293 | mint: &Pubkey, 294 | slippage_basis_points: u64, 295 | fee: PriorityFee, 296 | tip: u64, 297 | items: Vec, 298 | ) -> anyhow::Result> { 299 | let ((_, pool_account), blockhash) = tokio::try_join!(self.get_pool(&mint), self.endpoint.get_latest_blockhash())?; 300 | let creator_vault = Self::get_creator_vault_pda(&pool_account.creator).ok_or(anyhow::anyhow!("Creator vault not found: {}", mint.to_string()))?; 301 | let mut pool_token_amount = pool_account.virtual_token_reserves; 302 | let mut pool_sol_amount = pool_account.virtual_sol_reserves; 303 | let mut batch_items = vec![]; 304 | 305 | for item in items { 306 | let sol_lamports_with_slippage = calculate_with_slippage_buy(item.sol_amount, slippage_basis_points); 307 | let buy_token_amount = amm_buy_get_token_out(pool_sol_amount, pool_token_amount, item.sol_amount); 308 | let instruction = self.build_buy_instruction( 309 | &item.payer, 310 | &mint, 311 | &creator_vault, 312 | Buy { 313 | token_amount: buy_token_amount, 314 | sol_amount: sol_lamports_with_slippage, 315 | }, 316 | )?; 317 | let instructions = build_sol_buy_instructions(&item.payer, mint, instruction, CreateATA::Idempotent)?; 318 | batch_items.push(BatchTxItem { 319 | payer: item.payer, 320 | instructions, 321 | }); 322 | pool_sol_amount += item.sol_amount; 323 | pool_token_amount -= buy_token_amount; 324 | } 325 | 326 | let signatures = self.endpoint.build_and_broadcast_batch_txs(batch_items, blockhash, fee, tip).await?; 327 | 328 | Ok(signatures) 329 | } 330 | 331 | async fn batch_sell( 332 | &self, 333 | mint: &Pubkey, 334 | slippage_basis_points: u64, 335 | fee: PriorityFee, 336 | tip: u64, 337 | items: Vec, 338 | ) -> anyhow::Result> { 339 | let ((_, pool_account), blockhash) = tokio::try_join!(self.get_pool(&mint), self.endpoint.get_latest_blockhash())?; 340 | let creator_vault = Self::get_creator_vault_pda(&pool_account.creator).ok_or(anyhow::anyhow!("Creator vault not found: {}", mint.to_string()))?; 341 | let mut pool_token_amount = pool_account.virtual_token_reserves; 342 | let mut pool_sol_amount = pool_account.virtual_sol_reserves; 343 | let mut batch_items = vec![]; 344 | 345 | for item in items { 346 | let sol_lamports = amm_sell_get_sol_out(pool_sol_amount, pool_token_amount, item.token_amount); 347 | let sol_lamports_with_slippage = calculate_with_slippage_sell(sol_lamports, slippage_basis_points); 348 | let instruction = self.build_sell_instruction( 349 | &item.payer, 350 | &mint, 351 | &creator_vault, 352 | Sell { 353 | token_amount: item.token_amount, 354 | sol_amount: sol_lamports_with_slippage, 355 | }, 356 | )?; 357 | let instructions = build_sol_sell_instructions(&item.payer, mint, instruction, item.close_mint_ata)?; 358 | batch_items.push(BatchTxItem { 359 | payer: item.payer, 360 | instructions, 361 | }); 362 | pool_sol_amount -= sol_lamports; 363 | pool_token_amount += item.token_amount; 364 | } 365 | 366 | let signatures = self.endpoint.build_and_broadcast_batch_txs(batch_items, blockhash, fee, tip).await?; 367 | 368 | Ok(signatures) 369 | } 370 | } 371 | 372 | impl Pumpfun { 373 | pub fn new(endpoint: Arc) -> Self { 374 | Self { 375 | endpoint, 376 | global_account: OnceCell::new(), 377 | } 378 | } 379 | 380 | pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option { 381 | let seeds: &[&[u8]; 2] = &[BONDING_CURVE_SEED, mint.as_ref()]; 382 | let program_id: &Pubkey = &PUBKEY_PUMPFUN; 383 | let pda = Pubkey::try_find_program_address(seeds, program_id)?; 384 | Some(pda.0) 385 | } 386 | 387 | pub fn get_creator_vault_pda(creator: &Pubkey) -> Option { 388 | let seeds: &[&[u8]; 2] = &[CREATOR_VAULT_SEED, creator.as_ref()]; 389 | let program_id: &Pubkey = &PUBKEY_PUMPFUN; 390 | let pda = Pubkey::try_find_program_address(seeds, program_id)?; 391 | Some(pda.0) 392 | } 393 | 394 | pub async fn get_pool(&self, mint: &Pubkey) -> anyhow::Result<(Pubkey, BondingCurveAccount)> { 395 | let bonding_curve_pda = Self::get_bonding_curve_pda(mint).unwrap(); 396 | let account = self.endpoint.rpc.get_account(&bonding_curve_pda).await?; 397 | if account.data.is_empty() { 398 | return Err(anyhow::anyhow!("Bonding curve not found: {}", mint.to_string())); 399 | } 400 | 401 | let bonding_curve = bincode::deserialize::(&account.data)?; 402 | 403 | Ok((bonding_curve_pda, bonding_curve)) 404 | } 405 | 406 | fn build_buy_instruction(&self, payer: &Keypair, mint: &Pubkey, creator_vault: &Pubkey, buy: Buy) -> anyhow::Result { 407 | self.initialized()?; 408 | 409 | let buy_info: BuyInfo = buy.into(); 410 | let buffer = buy_info.to_buffer()?; 411 | let bonding_curve = Self::get_bonding_curve_pda(mint).ok_or(anyhow::anyhow!("Bonding curve not found: {}", mint.to_string()))?; 412 | 413 | Ok(Instruction::new_with_bytes( 414 | PUBKEY_PUMPFUN, 415 | &buffer, 416 | vec![ 417 | AccountMeta::new_readonly(PUBKEY_GLOBAL_ACCOUNT, false), 418 | AccountMeta::new(PUBKEY_FEE_RECIPIENT, false), 419 | AccountMeta::new_readonly(*mint, false), 420 | AccountMeta::new(bonding_curve, false), 421 | AccountMeta::new(get_associated_token_address(&bonding_curve, mint), false), 422 | AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), 423 | AccountMeta::new(payer.pubkey(), true), 424 | AccountMeta::new_readonly(solana_program::system_program::ID, false), 425 | AccountMeta::new_readonly(spl_token::ID, false), 426 | AccountMeta::new(*creator_vault, false), 427 | AccountMeta::new_readonly(PUBKEY_EVENT_AUTHORITY, false), 428 | AccountMeta::new_readonly(PUBKEY_PUMPFUN, false), 429 | ], 430 | )) 431 | } 432 | 433 | pub fn build_sell_instruction(&self, payer: &Keypair, mint: &Pubkey, creator_vault: &Pubkey, sell: Sell) -> anyhow::Result { 434 | self.initialized()?; 435 | 436 | let sell_info: SellInfo = sell.into(); 437 | let buffer = sell_info.to_buffer()?; 438 | let bonding_curve = Self::get_bonding_curve_pda(mint).ok_or(anyhow::anyhow!("Bonding curve not found: {}", mint.to_string()))?; 439 | 440 | Ok(Instruction::new_with_bytes( 441 | PUBKEY_PUMPFUN, 442 | &buffer, 443 | vec![ 444 | AccountMeta::new_readonly(PUBKEY_GLOBAL_ACCOUNT, false), 445 | AccountMeta::new(PUBKEY_FEE_RECIPIENT, false), 446 | AccountMeta::new_readonly(*mint, false), 447 | AccountMeta::new(bonding_curve, false), 448 | AccountMeta::new(get_associated_token_address(&bonding_curve, mint), false), 449 | AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), 450 | AccountMeta::new(payer.pubkey(), true), 451 | AccountMeta::new_readonly(solana_program::system_program::ID, false), 452 | AccountMeta::new(*creator_vault, false), 453 | AccountMeta::new_readonly(spl_token::ID, false), 454 | AccountMeta::new_readonly(PUBKEY_EVENT_AUTHORITY, false), 455 | AccountMeta::new_readonly(PUBKEY_PUMPFUN, false), 456 | ], 457 | )) 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /src/dex/pumpfun_types.rs: -------------------------------------------------------------------------------- 1 | use super::types::{Buy, Sell}; 2 | use borsh::{BorshDeserialize, BorshSerialize}; 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] 5 | pub struct BuyInfo { 6 | pub discriminator: u64, 7 | pub token_amount: u64, 8 | pub sol_amount: u64, 9 | } 10 | 11 | #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] 12 | pub struct SellInfo { 13 | pub discriminator: u64, 14 | pub token_amount: u64, 15 | pub sol_amount: u64, 16 | } 17 | 18 | impl From for BuyInfo { 19 | fn from(buy: Buy) -> Self { 20 | Self { 21 | discriminator: 16927863322537952870, 22 | token_amount: buy.token_amount, 23 | sol_amount: buy.sol_amount, 24 | } 25 | } 26 | } 27 | 28 | impl From for SellInfo { 29 | fn from(sell: Sell) -> Self { 30 | Self { 31 | discriminator: 12502976635542562355, 32 | token_amount: sell.token_amount, 33 | sol_amount: sell.sol_amount, 34 | } 35 | } 36 | } 37 | 38 | impl BuyInfo { 39 | pub fn to_buffer(&self) -> anyhow::Result> { 40 | let mut buffer = Vec::new(); 41 | self.serialize(&mut buffer)?; 42 | Ok(buffer) 43 | } 44 | } 45 | 46 | impl SellInfo { 47 | pub fn to_buffer(&self) -> anyhow::Result> { 48 | let mut buffer = Vec::new(); 49 | self.serialize(&mut buffer)?; 50 | Ok(buffer) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/dex/pumpswap.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | amm_calc::{amm_buy_get_token_out, amm_sell_get_sol_out, calculate_with_slippage_buy, calculate_with_slippage_sell}, 3 | dex_traits::{BatchBuyParam, BatchSellParam, DexTrait}, 4 | pumpfun::PUBKEY_PUMPFUN, 5 | pumpfun_types::{BuyInfo, SellInfo}, 6 | types::{Buy, Create, Sell, TokenAmountType}, 7 | }; 8 | use crate::{ 9 | common::{ 10 | accounts::PUBKEY_WSOL, 11 | trading_endpoint::{BatchTxItem, TradingEndpoint}, 12 | }, 13 | dex::types::CreateATA, 14 | instruction::builder::{build_wsol_buy_instructions, build_wsol_sell_instructions, PriorityFee}, 15 | }; 16 | use borsh::{BorshDeserialize, BorshSerialize}; 17 | use once_cell::sync::OnceCell; 18 | use rand::seq::IndexedRandom; 19 | use serde::{Deserialize, Serialize}; 20 | use solana_sdk::{ 21 | hash::Hash, 22 | instruction::{AccountMeta, Instruction}, 23 | pubkey, 24 | pubkey::Pubkey, 25 | signature::{Keypair, Signature}, 26 | signer::Signer, 27 | }; 28 | use spl_associated_token_account::get_associated_token_address; 29 | use std::{str::FromStr, sync::Arc}; 30 | 31 | pub const PUBKEY_PUMPSWAP: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"); 32 | pub const PUBKEY_GLOBAL_ACCOUNT: Pubkey = pubkey!("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"); 33 | pub const PUBKEY_EVENT_AUTHORITY: Pubkey = pubkey!("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"); 34 | 35 | #[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] 36 | pub struct GlobalAccount { 37 | pub discriminator: u64, 38 | pub admin: Pubkey, 39 | pub lp_fee_basis_points: u64, 40 | pub protocol_fee_basis_points: u64, 41 | pub disable_flags: u8, 42 | pub protocol_fee_recipients: [Pubkey; 8], 43 | } 44 | 45 | #[derive(Debug, Clone, Serialize, Deserialize)] 46 | pub struct PoolAccount { 47 | pub discriminator: u64, 48 | pub pool_bump: u8, 49 | pub index: u16, 50 | pub creator: Pubkey, 51 | pub base_mint: Pubkey, 52 | pub quote_mint: Pubkey, 53 | pub lp_mint: Pubkey, 54 | pub pool_base_token_account: Pubkey, 55 | pub pool_quote_token_account: Pubkey, 56 | pub lp_supply: u64, 57 | pub coin_creator: Pubkey, 58 | } 59 | pub struct PoolInfo { 60 | pub pool_address: Pubkey, 61 | pub pool_account: PoolAccount, 62 | pub pool_base_reserve: u64, 63 | pub pool_quote_reserve: u64, 64 | } 65 | 66 | pub struct PumpSwap { 67 | pub endpoint: Arc, 68 | pub global_account: OnceCell>, 69 | } 70 | 71 | #[async_trait::async_trait] 72 | impl DexTrait for PumpSwap { 73 | async fn initialize(&self) -> anyhow::Result<()> { 74 | let account = self.endpoint.rpc.get_account(&PUBKEY_GLOBAL_ACCOUNT).await?; 75 | let global_account = bincode::deserialize::(&account.data)?; 76 | let global_account = Arc::new(global_account); 77 | 78 | self.global_account.set(global_account).unwrap(); 79 | Ok(()) 80 | } 81 | 82 | fn initialized(&self) -> anyhow::Result<()> { 83 | if self.global_account.get().is_none() { 84 | return Err(anyhow::anyhow!("PumpSwap not initialized")); 85 | } 86 | Ok(()) 87 | } 88 | 89 | async fn create(&self, _: Keypair, _: Create, _: Option, _: Option) -> anyhow::Result> { 90 | Err(anyhow::anyhow!("Not supported")) 91 | } 92 | 93 | async fn buy( 94 | &self, 95 | payer: &Keypair, 96 | mint: &Pubkey, 97 | sol_amount: u64, 98 | slippage_basis_points: u64, 99 | fee: Option, 100 | tip: Option, 101 | ) -> anyhow::Result> { 102 | let (pool_info, blockhash) = tokio::try_join!(self.get_pool(&mint), self.endpoint.get_latest_blockhash(),)?; 103 | let buy_token_amount = amm_buy_get_token_out(pool_info.pool_quote_reserve, pool_info.pool_base_reserve, sol_amount); 104 | let creator_vault = Self::get_creator_vault(&pool_info.pool_account.coin_creator); 105 | let sol_lamports_with_slippage = calculate_with_slippage_buy(sol_amount, slippage_basis_points); 106 | 107 | self.buy_immediately( 108 | payer, 109 | mint, 110 | None, 111 | Some(&creator_vault), 112 | sol_lamports_with_slippage, 113 | buy_token_amount, 114 | blockhash, 115 | CreateATA::Idempotent, 116 | fee, 117 | tip, 118 | ) 119 | .await 120 | } 121 | 122 | async fn buy_immediately( 123 | &self, 124 | payer: &Keypair, 125 | mint: &Pubkey, 126 | _: Option<&Pubkey>, 127 | creator_vault: Option<&Pubkey>, 128 | sol_amount: u64, 129 | token_amount: u64, 130 | blockhash: Hash, 131 | create_ata: CreateATA, 132 | fee: Option, 133 | tip: Option, 134 | ) -> anyhow::Result> { 135 | let creator_vault = creator_vault.ok_or(anyhow::anyhow!("creator vault not provided: {}", mint.to_string()))?; 136 | let instruction = self.build_buy_instruction(payer, mint, &creator_vault, Buy { token_amount, sol_amount })?; 137 | let instructions = build_wsol_buy_instructions(payer, mint, sol_amount, instruction, create_ata)?; 138 | let signatures = self.endpoint.build_and_broadcast_tx(payer, instructions, blockhash, fee, tip, None).await?; 139 | 140 | Ok(signatures) 141 | } 142 | 143 | async fn sell( 144 | &self, 145 | payer: &Keypair, 146 | mint: &Pubkey, 147 | token_amount: TokenAmountType, 148 | slippage_basis_points: u64, 149 | close_mint_ata: bool, 150 | fee: Option, 151 | tip: Option, 152 | ) -> anyhow::Result> { 153 | let payer_pubkey = payer.pubkey(); 154 | let (pool_info, blockhash, token_amount) = tokio::try_join!( 155 | self.get_pool(&mint), 156 | self.endpoint.get_latest_blockhash(), 157 | token_amount.to_amount(self.endpoint.rpc.clone(), &payer_pubkey, mint) 158 | )?; 159 | let creator_vault = Self::get_creator_vault(&pool_info.pool_account.coin_creator); 160 | let sol_lamports = amm_sell_get_sol_out(pool_info.pool_quote_reserve, pool_info.pool_base_reserve, token_amount); 161 | let sol_lamports_with_slippage = calculate_with_slippage_sell(sol_lamports, slippage_basis_points); 162 | 163 | self.sell_immediately( 164 | payer, 165 | mint, 166 | None, 167 | Some(&creator_vault), 168 | token_amount, 169 | sol_lamports_with_slippage, 170 | close_mint_ata, 171 | blockhash, 172 | fee, 173 | tip, 174 | ) 175 | .await 176 | } 177 | 178 | async fn sell_immediately( 179 | &self, 180 | payer: &Keypair, 181 | mint: &Pubkey, 182 | _: Option<&Pubkey>, 183 | creator_vault: Option<&Pubkey>, 184 | token_amount: u64, 185 | sol_amount: u64, 186 | close_mint_ata: bool, 187 | blockhash: Hash, 188 | fee: Option, 189 | tip: Option, 190 | ) -> anyhow::Result> { 191 | let creator_vault = creator_vault.ok_or(anyhow::anyhow!("creator vault not provided: {}", mint.to_string()))?; 192 | let instruction = self.build_sell_instruction(payer, mint, &creator_vault, Sell { token_amount, sol_amount })?; 193 | let instructions = build_wsol_sell_instructions(payer, mint, close_mint_ata, instruction)?; 194 | let signatures = self.endpoint.build_and_broadcast_tx(payer, instructions, blockhash, fee, tip, None).await?; 195 | 196 | Ok(signatures) 197 | } 198 | 199 | async fn batch_buy( 200 | &self, 201 | mint: &Pubkey, 202 | slippage_basis_points: u64, 203 | fee: PriorityFee, 204 | tip: u64, 205 | items: Vec, 206 | ) -> anyhow::Result> { 207 | let (pool_info, blockhash) = tokio::try_join!(self.get_pool(&mint), self.endpoint.get_latest_blockhash(),)?; 208 | let creator_vault = Self::get_creator_vault(&pool_info.pool_account.coin_creator); 209 | let mut pool_token_amount = pool_info.pool_base_reserve; 210 | let mut pool_sol_amount = pool_info.pool_quote_reserve; 211 | let mut batch_items = vec![]; 212 | 213 | for item in items { 214 | let sol_lamports_with_slippage = calculate_with_slippage_buy(item.sol_amount, slippage_basis_points); 215 | let buy_token_amount = amm_buy_get_token_out(pool_sol_amount, pool_token_amount, item.sol_amount); 216 | let instruction = self.build_buy_instruction( 217 | &item.payer, 218 | &mint, 219 | &creator_vault, 220 | Buy { 221 | token_amount: buy_token_amount, 222 | sol_amount: sol_lamports_with_slippage, 223 | }, 224 | )?; 225 | let instructions = build_wsol_buy_instructions(&item.payer, mint, sol_lamports_with_slippage, instruction, CreateATA::Idempotent)?; 226 | batch_items.push(BatchTxItem { 227 | payer: item.payer, 228 | instructions, 229 | }); 230 | pool_sol_amount += item.sol_amount; 231 | pool_token_amount -= buy_token_amount; 232 | } 233 | 234 | let signatures = self.endpoint.build_and_broadcast_batch_txs(batch_items, blockhash, fee, tip).await?; 235 | 236 | Ok(signatures) 237 | } 238 | 239 | async fn batch_sell( 240 | &self, 241 | mint: &Pubkey, 242 | slippage_basis_points: u64, 243 | fee: PriorityFee, 244 | tip: u64, 245 | items: Vec, 246 | ) -> anyhow::Result> { 247 | let (pool_info, blockhash) = tokio::try_join!(self.get_pool(&mint), self.endpoint.get_latest_blockhash(),)?; 248 | let creator_vault = Self::get_creator_vault(&pool_info.pool_account.coin_creator); 249 | let mut pool_token_amount = pool_info.pool_base_reserve; 250 | let mut pool_sol_amount = pool_info.pool_quote_reserve; 251 | let mut batch_items = vec![]; 252 | 253 | for item in items { 254 | let sol_amount = amm_sell_get_sol_out(pool_sol_amount, pool_token_amount, item.token_amount); 255 | let sol_lamports_with_slippage = calculate_with_slippage_sell(sol_amount, slippage_basis_points); 256 | let instruction = self.build_sell_instruction( 257 | &item.payer, 258 | &mint, 259 | &creator_vault, 260 | Sell { 261 | token_amount: sol_amount, 262 | sol_amount: sol_lamports_with_slippage, 263 | }, 264 | )?; 265 | let instructions = build_wsol_sell_instructions(&item.payer, mint, item.close_mint_ata, instruction)?; 266 | batch_items.push(BatchTxItem { 267 | payer: item.payer, 268 | instructions, 269 | }); 270 | pool_sol_amount -= sol_amount; 271 | pool_token_amount += item.token_amount; 272 | } 273 | 274 | let signatures = self.endpoint.build_and_broadcast_batch_txs(batch_items, blockhash, fee, tip).await?; 275 | 276 | Ok(signatures) 277 | } 278 | } 279 | 280 | impl PumpSwap { 281 | pub fn new(endpoint: Arc) -> Self { 282 | Self { 283 | endpoint, 284 | global_account: OnceCell::new(), 285 | } 286 | } 287 | 288 | pub fn get_creator_vault(creator: &Pubkey) -> Pubkey { 289 | Pubkey::find_program_address(&[b"creator_vault", creator.as_ref()], &PUBKEY_PUMPSWAP).0 290 | } 291 | 292 | pub fn get_pool_authority_pda(mint: &Pubkey) -> Pubkey { 293 | Pubkey::find_program_address(&[b"pool-authority", mint.as_ref()], &PUBKEY_PUMPFUN).0 294 | } 295 | 296 | pub fn get_pool_address(mint: &Pubkey) -> Pubkey { 297 | Pubkey::find_program_address( 298 | &[ 299 | b"pool", 300 | &0u16.to_le_bytes(), 301 | Self::get_pool_authority_pda(mint).as_ref(), 302 | mint.as_ref(), 303 | PUBKEY_WSOL.as_ref(), 304 | ], 305 | &PUBKEY_PUMPSWAP, 306 | ) 307 | .0 308 | } 309 | 310 | pub async fn get_pool(&self, mint: &Pubkey) -> anyhow::Result { 311 | let pool = Self::get_pool_address(&mint); 312 | 313 | let pool_base = get_associated_token_address(&pool, &mint); 314 | let pool_quote = get_associated_token_address(&pool, &PUBKEY_WSOL); 315 | let (pool_account, pool_base_account, pool_quote_account) = tokio::try_join!( 316 | self.endpoint.rpc.get_account(&pool), 317 | self.endpoint.rpc.get_token_account(&pool_base), 318 | self.endpoint.rpc.get_token_account(&pool_quote), 319 | )?; 320 | 321 | if pool_account.data.is_empty() { 322 | return Err(anyhow::anyhow!("Pool account not found: {}", mint.to_string())); 323 | } 324 | 325 | let pool_account = bincode::deserialize::(&pool_account.data)?; 326 | let pool_base_account = pool_base_account.ok_or_else(|| anyhow::anyhow!("Pool base account not found: {}", mint.to_string()))?; 327 | let pool_quote_account = pool_quote_account.ok_or_else(|| anyhow::anyhow!("Pool quote account not found: {}", mint.to_string()))?; 328 | 329 | let pool_base_reserve = u64::from_str(&pool_base_account.token_amount.amount)?; 330 | let pool_quote_reserve = u64::from_str(&pool_quote_account.token_amount.amount)?; 331 | 332 | Ok(PoolInfo { 333 | pool_address: pool, 334 | pool_account, 335 | pool_base_reserve, 336 | pool_quote_reserve, 337 | }) 338 | } 339 | 340 | fn build_buy_instruction(&self, payer: &Keypair, mint: &Pubkey, creator_vault: &Pubkey, buy: Buy) -> anyhow::Result { 341 | self.initialized()?; 342 | 343 | let buy_info: BuyInfo = buy.into(); 344 | let buffer = buy_info.to_buffer()?; 345 | let pool = Self::get_pool_address(&mint); 346 | let creator_vault_ata = get_associated_token_address(creator_vault, &PUBKEY_WSOL); 347 | let fee_recipient = self.global_account.get().unwrap().protocol_fee_recipients.choose(&mut rand::rng()).unwrap(); 348 | 349 | Ok(Instruction::new_with_bytes( 350 | PUBKEY_PUMPSWAP, 351 | &buffer, 352 | vec![ 353 | AccountMeta::new_readonly(pool, false), 354 | AccountMeta::new(payer.pubkey(), true), 355 | AccountMeta::new_readonly(PUBKEY_GLOBAL_ACCOUNT, false), 356 | AccountMeta::new_readonly(*mint, false), 357 | AccountMeta::new_readonly(PUBKEY_WSOL, false), 358 | AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), 359 | AccountMeta::new(get_associated_token_address(&payer.pubkey(), &PUBKEY_WSOL), false), 360 | AccountMeta::new(get_associated_token_address(&pool, mint), false), 361 | AccountMeta::new(get_associated_token_address(&pool, &PUBKEY_WSOL), false), 362 | AccountMeta::new_readonly(*fee_recipient, false), 363 | AccountMeta::new(get_associated_token_address(fee_recipient, &PUBKEY_WSOL), false), 364 | AccountMeta::new_readonly(spl_token::ID, false), 365 | AccountMeta::new_readonly(spl_token::ID, false), 366 | AccountMeta::new_readonly(solana_program::system_program::ID, false), 367 | AccountMeta::new_readonly(spl_associated_token_account::ID, false), 368 | AccountMeta::new_readonly(PUBKEY_EVENT_AUTHORITY, false), 369 | AccountMeta::new_readonly(PUBKEY_PUMPSWAP, false), 370 | AccountMeta::new(creator_vault_ata, false), 371 | AccountMeta::new_readonly(*creator_vault, false), 372 | ], 373 | )) 374 | } 375 | 376 | pub fn build_sell_instruction(&self, payer: &Keypair, mint: &Pubkey, creator_vault: &Pubkey, sell: Sell) -> anyhow::Result { 377 | self.initialized()?; 378 | 379 | let sell_info: SellInfo = sell.into(); 380 | let buffer = sell_info.to_buffer()?; 381 | let pool = Self::get_pool_address(&mint); 382 | let creator_vault_ata = get_associated_token_address(creator_vault, &PUBKEY_WSOL); 383 | let fee_recipient = self.global_account.get().unwrap().protocol_fee_recipients.choose(&mut rand::rng()).unwrap(); 384 | 385 | Ok(Instruction::new_with_bytes( 386 | PUBKEY_PUMPSWAP, 387 | &buffer, 388 | vec![ 389 | AccountMeta::new_readonly(pool, false), 390 | AccountMeta::new(payer.pubkey(), true), 391 | AccountMeta::new_readonly(PUBKEY_GLOBAL_ACCOUNT, false), 392 | AccountMeta::new_readonly(*mint, false), 393 | AccountMeta::new_readonly(PUBKEY_WSOL, false), 394 | AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), 395 | AccountMeta::new(get_associated_token_address(&payer.pubkey(), &PUBKEY_WSOL), false), 396 | AccountMeta::new(get_associated_token_address(&pool, mint), false), 397 | AccountMeta::new(get_associated_token_address(&pool, &PUBKEY_WSOL), false), 398 | AccountMeta::new_readonly(*fee_recipient, false), 399 | AccountMeta::new(get_associated_token_address(fee_recipient, &PUBKEY_WSOL), false), 400 | AccountMeta::new_readonly(spl_token::ID, false), 401 | AccountMeta::new_readonly(spl_token::ID, false), 402 | AccountMeta::new_readonly(solana_program::system_program::ID, false), 403 | AccountMeta::new_readonly(spl_associated_token_account::ID, false), 404 | AccountMeta::new_readonly(PUBKEY_EVENT_AUTHORITY, false), 405 | AccountMeta::new_readonly(PUBKEY_PUMPSWAP, false), 406 | AccountMeta::new(creator_vault_ata, false), 407 | AccountMeta::new_readonly(*creator_vault, false), 408 | ], 409 | )) 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/dex/types.rs: -------------------------------------------------------------------------------- 1 | use super::{dex_traits::DexTrait, pumpfun, pumpswap}; 2 | use crate::common::trading_endpoint::TradingEndpoint; 3 | use serde::{Deserialize, Serialize}; 4 | use solana_client::nonblocking::rpc_client::RpcClient; 5 | use solana_sdk::{pubkey::Pubkey, signature::Keypair}; 6 | use spl_associated_token_account::get_associated_token_address; 7 | use std::sync::Arc; 8 | 9 | pub struct Buy { 10 | pub token_amount: u64, 11 | pub sol_amount: u64, 12 | } 13 | 14 | pub struct Sell { 15 | pub token_amount: u64, 16 | pub sol_amount: u64, 17 | } 18 | 19 | pub struct Create { 20 | pub name: String, 21 | pub symbol: String, 22 | pub uri: String, 23 | pub mint_private_key: Keypair, 24 | pub buy_sol_amount: Option, 25 | pub slippage_basis_points: Option, 26 | } 27 | 28 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 29 | pub enum DexType { 30 | Pumpfun, 31 | PumpSwap, 32 | } 33 | 34 | impl DexType { 35 | pub fn all() -> Vec { 36 | vec![DexType::Pumpfun, DexType::PumpSwap] 37 | } 38 | 39 | pub fn instantiate(&self, endpoint: Arc) -> Arc { 40 | match self { 41 | DexType::Pumpfun => Arc::new(pumpfun::Pumpfun::new(endpoint)), 42 | DexType::PumpSwap => Arc::new(pumpswap::PumpSwap::new(endpoint)), 43 | } 44 | } 45 | } 46 | 47 | pub enum TokenAmountType { 48 | Percent(u64), 49 | Amount(u64), 50 | } 51 | 52 | impl TokenAmountType { 53 | pub async fn to_amount(&self, rpc: Arc, payer: &Pubkey, mint: &Pubkey) -> anyhow::Result { 54 | match self { 55 | TokenAmountType::Percent(percent) => { 56 | let ata = get_associated_token_address(payer, mint); 57 | let balance = rpc.get_token_account_balance(&ata).await?; 58 | let balance_u64 = balance.amount.parse::()?; 59 | Ok((balance_u64 * percent) / 100) 60 | } 61 | TokenAmountType::Amount(amount) => Ok(*amount), 62 | } 63 | } 64 | } 65 | 66 | pub enum CreateATA { 67 | Create, 68 | None, 69 | Idempotent, 70 | } 71 | -------------------------------------------------------------------------------- /src/instruction/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{common::accounts::PUBKEY_WSOL, dex::types::CreateATA}; 2 | use serde::{Deserialize, Serialize}; 3 | use solana_sdk::{ 4 | compute_budget::ComputeBudgetInstruction, 5 | hash::Hash, 6 | instruction::Instruction, 7 | message::{v0, VersionedMessage}, 8 | pubkey::Pubkey, 9 | signature::Keypair, 10 | signer::Signer, 11 | system_instruction, 12 | transaction::VersionedTransaction, 13 | }; 14 | use spl_associated_token_account::{ 15 | get_associated_token_address, 16 | instruction::{create_associated_token_account, create_associated_token_account_idempotent}, 17 | }; 18 | use spl_token::instruction::{close_account, sync_native}; 19 | 20 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 21 | pub struct PriorityFee { 22 | pub unit_limit: u32, 23 | pub unit_price: u64, 24 | } 25 | 26 | #[derive(Debug, Clone, Copy)] 27 | pub struct TipFee { 28 | pub tip_account: Pubkey, 29 | pub tip_lamports: u64, 30 | } 31 | 32 | pub fn build_transaction( 33 | payer: &Keypair, 34 | instructions: Vec, 35 | blockhash: Hash, 36 | fee: Option, 37 | tip: Option, 38 | other_signers: Option>, 39 | ) -> anyhow::Result { 40 | let mut insts = vec![]; 41 | if let Some(fee) = fee { 42 | insts.push(ComputeBudgetInstruction::set_compute_unit_price(fee.unit_price)); 43 | insts.push(ComputeBudgetInstruction::set_compute_unit_limit(fee.unit_limit)); 44 | } 45 | 46 | if let Some(tip) = tip { 47 | insts.push(system_instruction::transfer(&payer.pubkey(), &tip.tip_account, tip.tip_lamports)); 48 | } 49 | 50 | insts.extend(instructions); 51 | 52 | let v0_message: v0::Message = v0::Message::try_compile(&payer.pubkey(), &insts, &[], blockhash)?; 53 | let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); 54 | let signers = vec![payer].into_iter().chain(other_signers.unwrap_or_default().into_iter()).collect::>(); 55 | let transaction = VersionedTransaction::try_new(versioned_message, &signers)?; 56 | 57 | Ok(transaction) 58 | } 59 | 60 | pub fn build_sol_buy_instructions(payer: &Keypair, mint: &Pubkey, buy_instruction: Instruction, crate_ata: CreateATA) -> anyhow::Result> { 61 | let mut instructions = vec![]; 62 | 63 | match crate_ata { 64 | CreateATA::Create => { 65 | instructions.push(create_associated_token_account(&payer.pubkey(), &payer.pubkey(), &mint, &spl_token::ID)); 66 | } 67 | CreateATA::Idempotent => { 68 | instructions.push(create_associated_token_account_idempotent( 69 | &payer.pubkey(), 70 | &payer.pubkey(), 71 | &mint, 72 | &spl_token::ID, 73 | )); 74 | } 75 | CreateATA::None => {} 76 | } 77 | 78 | instructions.push(buy_instruction); 79 | 80 | Ok(instructions) 81 | } 82 | 83 | pub fn build_sol_sell_instructions(payer: &Keypair, mint: &Pubkey, sell_instruction: Instruction, close_ata: bool) -> Result, anyhow::Error> { 84 | let mut instructions = vec![sell_instruction]; 85 | 86 | if close_ata { 87 | let mint_ata = get_associated_token_address(&payer.pubkey(), &mint); 88 | instructions.push(close_account(&spl_token::ID, &mint_ata, &payer.pubkey(), &payer.pubkey(), &[&payer.pubkey()])?); 89 | } 90 | 91 | Ok(instructions) 92 | } 93 | 94 | pub fn build_wsol_buy_instructions( 95 | payer: &Keypair, 96 | mint: &Pubkey, 97 | amount_sol: u64, 98 | buy_instruction: Instruction, 99 | crate_ata: CreateATA, 100 | ) -> anyhow::Result> { 101 | let mut instructions = vec![]; 102 | 103 | match crate_ata { 104 | CreateATA::Create => { 105 | instructions.push(create_associated_token_account(&payer.pubkey(), &payer.pubkey(), &mint, &spl_token::ID)); 106 | } 107 | CreateATA::Idempotent => { 108 | instructions.push(create_associated_token_account_idempotent( 109 | &payer.pubkey(), 110 | &payer.pubkey(), 111 | &mint, 112 | &spl_token::ID, 113 | )); 114 | } 115 | CreateATA::None => {} 116 | } 117 | 118 | instructions.push(create_associated_token_account_idempotent( 119 | &payer.pubkey(), 120 | &payer.pubkey(), 121 | &PUBKEY_WSOL, 122 | &spl_token::ID, 123 | )); 124 | 125 | let wsol_ata = get_associated_token_address(&payer.pubkey(), &PUBKEY_WSOL); 126 | instructions.push(system_instruction::transfer(&payer.pubkey(), &wsol_ata, amount_sol)); 127 | 128 | instructions.push(sync_native(&spl_token::ID, &wsol_ata).unwrap()); 129 | 130 | instructions.push(buy_instruction); 131 | 132 | instructions.push(close_account(&spl_token::ID, &wsol_ata, &payer.pubkey(), &payer.pubkey(), &[&payer.pubkey()]).unwrap()); 133 | 134 | Ok(instructions) 135 | } 136 | 137 | pub fn build_wsol_sell_instructions(payer: &Keypair, mint: &Pubkey, close_mint_ata: bool, sell_instruction: Instruction) -> anyhow::Result> { 138 | let mint_ata = get_associated_token_address(&payer.pubkey(), &mint); 139 | let wsol_ata = get_associated_token_address(&payer.pubkey(), &PUBKEY_WSOL); 140 | 141 | let mut instructions = vec![]; 142 | instructions.push(create_associated_token_account_idempotent( 143 | &payer.pubkey(), 144 | &payer.pubkey(), 145 | &PUBKEY_WSOL, 146 | &spl_token::ID, 147 | )); 148 | 149 | instructions.push(sell_instruction); 150 | 151 | instructions.push(close_account(&spl_token::ID, &wsol_ata, &payer.pubkey(), &payer.pubkey(), &[&payer.pubkey()]).unwrap()); 152 | 153 | if close_mint_ata { 154 | instructions.push(close_account(&spl_token::ID, &mint_ata, &payer.pubkey(), &payer.pubkey(), &[&payer.pubkey()]).unwrap()); 155 | } 156 | 157 | Ok(instructions) 158 | } 159 | -------------------------------------------------------------------------------- /src/instruction/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod builder; -------------------------------------------------------------------------------- /src/ipfs/metadata.rs: -------------------------------------------------------------------------------- 1 | use super::types::{CreateTokenMetadata, TokenMetadata, TokenMetadataIPFS}; 2 | use base64::{engine::general_purpose, Engine as _}; 3 | use reqwest::multipart::{Form, Part}; 4 | use reqwest::Client; 5 | use serde_json::Value; 6 | use std::time::Duration; 7 | use tokio::fs::File; 8 | use tokio::io::AsyncReadExt; 9 | 10 | pub async fn create_token_metadata(metadata: CreateTokenMetadata, jwt_token: &str) -> anyhow::Result { 11 | let ipfs_url = if metadata.file.starts_with("http") || metadata.metadata_uri.is_some() { 12 | metadata.file 13 | } else if metadata.file.starts_with("data:image/png;base64,") { 14 | upload_base64_file(&metadata.file[22..], jwt_token).await? 15 | } else { 16 | let base64_string = file_to_base64(&metadata.file).await?; 17 | upload_base64_file(&base64_string, jwt_token).await? 18 | }; 19 | 20 | let token_metadata = TokenMetadata { 21 | name: metadata.name, 22 | symbol: metadata.symbol, 23 | description: metadata.description, 24 | image: ipfs_url, 25 | show_name: true, 26 | created_on: "https://pump.fun".to_string(), 27 | twitter: metadata.twitter, 28 | telegram: metadata.telegram, 29 | website: metadata.website, 30 | }; 31 | 32 | if let Some(metadata_uri) = metadata.metadata_uri { 33 | let token_metadata_ipfs = TokenMetadataIPFS { 34 | metadata: token_metadata, 35 | metadata_uri: metadata_uri, 36 | }; 37 | Ok(token_metadata_ipfs) 38 | } else { 39 | let client = Client::new(); 40 | let response = client 41 | .post("https://api.pinata.cloud/pinning/pinJSONToIPFS") 42 | .header("Content-Type", "application/json") 43 | .header("Authorization", format!("Bearer {}", jwt_token)) 44 | .json(&token_metadata) 45 | .send() 46 | .await?; 47 | 48 | if response.status().is_success() { 49 | let res_data: serde_json::Value = response.json().await?; 50 | let ipfs_hash = res_data["IpfsHash"].as_str().unwrap(); 51 | let ipfs_url = format!("https://ipfs.io/ipfs/{}", ipfs_hash); 52 | let token_metadata_ipfs = TokenMetadataIPFS { 53 | metadata: token_metadata, 54 | metadata_uri: ipfs_url, 55 | }; 56 | Ok(token_metadata_ipfs) 57 | } else { 58 | eprintln!("create_token_metadata error: {:?}", response.status()); 59 | Err(anyhow::anyhow!("create_token_metadata error: {:?}", response.status())) 60 | } 61 | } 62 | } 63 | 64 | pub async fn upload_base64_file(base64_string: &str, jwt_token: &str) -> Result { 65 | let decoded_bytes = general_purpose::STANDARD.decode(base64_string)?; 66 | 67 | let client = Client::builder() 68 | .timeout(Duration::from_secs(120)) // 增加超时时间到120秒 69 | .pool_max_idle_per_host(0) // 禁用连接池 70 | .pool_idle_timeout(None) // 禁用空闲超时 71 | .build()?; 72 | 73 | let part = Part::bytes(decoded_bytes) 74 | .file_name("file.png") // 添加文件扩展名 75 | .mime_str("image/png")?; // 指定正确的MIME类型 76 | 77 | let form = Form::new().part("file", part); 78 | 79 | let response = client 80 | .post("https://api.pinata.cloud/pinning/pinFileToIPFS") 81 | .header("Authorization", format!("Bearer {}", jwt_token)) 82 | .header("Accept", "application/json") 83 | .multipart(form) 84 | .send() 85 | .await?; 86 | 87 | if response.status().is_success() { 88 | let response_json: Value = response.json().await.map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}", e))?; 89 | println!("{:#?}", response_json); 90 | let ipfs_hash = response_json["IpfsHash"].as_str().unwrap(); 91 | let ipfs_url = format!("https://ipfs.io/ipfs/{}", ipfs_hash); 92 | Ok(ipfs_url) 93 | } else { 94 | let error_text = response.text().await?; 95 | eprintln!("Error: {:?}", error_text); 96 | Err(anyhow::anyhow!("Failed to upload file to IPFS: {}", error_text)) 97 | } 98 | } 99 | 100 | async fn file_to_base64(file_path: &str) -> Result { 101 | let mut file = File::open(file_path).await?; 102 | let mut buffer = Vec::new(); 103 | file.read_to_end(&mut buffer).await?; 104 | let base64_string = general_purpose::STANDARD.encode(&buffer); 105 | Ok(base64_string) 106 | } 107 | -------------------------------------------------------------------------------- /src/ipfs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod types; 2 | pub mod metadata; -------------------------------------------------------------------------------- /src/ipfs/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Metadata structure for a token, matching the format expected by Pump.fun. 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct TokenMetadata { 7 | /// Name of the token 8 | pub name: String, 9 | /// Token symbol (e.g. "BTC") 10 | pub symbol: String, 11 | /// Description of the token 12 | pub description: String, 13 | /// IPFS URL of the token's image 14 | pub image: String, 15 | /// Whether to display the token's name 16 | pub show_name: bool, 17 | /// Creation timestamp/source 18 | pub created_on: String, 19 | /// Twitter handle 20 | pub twitter: Option, 21 | /// Telegram handle 22 | pub telegram: Option, 23 | /// Website URL 24 | pub website: Option, 25 | } 26 | 27 | /// Response received after successfully uploading token metadata. 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct TokenMetadataIPFS { 31 | /// The uploaded token metadata 32 | pub metadata: TokenMetadata, 33 | /// IPFS URI where the metadata is stored 34 | pub metadata_uri: String, 35 | } 36 | 37 | /// Parameters for creating new token metadata. 38 | #[derive(Debug, Clone)] 39 | pub struct CreateTokenMetadata { 40 | /// Name of the token 41 | pub name: String, 42 | /// Token symbol (e.g. "BTC") 43 | pub symbol: String, 44 | /// Description of the token 45 | pub description: String, 46 | /// Path or base64 to the token's image file 47 | pub file: String, 48 | /// Optional Twitter handle 49 | pub twitter: Option, 50 | /// Optional Telegram group 51 | pub telegram: Option, 52 | /// Optional website URL 53 | pub website: Option, 54 | 55 | pub metadata_uri: Option, 56 | } 57 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod instruction; 2 | pub mod ipfs; 3 | pub mod dex; 4 | pub mod swqos; 5 | pub mod common; -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Ok; 2 | use solana_client::nonblocking::rpc_client::RpcClient; 3 | use solana_sdk::{native_token::sol_str_to_lamports, pubkey::Pubkey, signature::Keypair}; 4 | use solana_trading_sdk::{ 5 | common::{trading_endpoint::TradingEndpoint, TradingClient, TradingConfig}, 6 | dex::{ 7 | dex_traits::DexTrait, 8 | pumpfun::Pumpfun, 9 | types::{Create, DexType}, 10 | }, 11 | instruction::builder::PriorityFee, 12 | ipfs::{metadata::create_token_metadata, types::CreateTokenMetadata}, 13 | swqos::{ 14 | blox::BLOX_ENDPOINT_FRA, default::DefaultSWQoSClient, jito::JITO_ENDPOINT_MAINNET, nextblock::NEXTBLOCK_ENDPOINT_FRA, temporal::TEMPORAL_ENDPOINT_FRA, 15 | zeroslot::ZEROSLOT_ENDPOINT_FRA, SWQoSType, 16 | }, 17 | }; 18 | use std::{str::FromStr, sync::Arc}; 19 | 20 | const RPC_ENDPOINT: &str = "https://solana-rpc.publicnode.com"; 21 | 22 | #[tokio::main] 23 | async fn main() -> anyhow::Result<()> { 24 | Ok(()) 25 | } 26 | 27 | pub fn get_solana_client() -> Arc { 28 | Arc::new(RpcClient::new(RPC_ENDPOINT.to_string())) 29 | } 30 | 31 | pub fn get_swqos_client() -> DefaultSWQoSClient { 32 | let swqos_client = DefaultSWQoSClient::new("default", get_solana_client(), RPC_ENDPOINT.to_string(), None, vec![]); 33 | swqos_client 34 | } 35 | 36 | pub async fn transfer_sol() -> anyhow::Result<()> { 37 | let rpc_url = "https://solana-rpc.publicnode.com".to_string(); 38 | let from = Keypair::from_base58_string("your_payer_pubkey"); 39 | let to = Pubkey::from_str("recipient_pubkey")?; 40 | let amount = sol_str_to_lamports("0.1").unwrap(); 41 | let fee = PriorityFee { 42 | unit_limit: 100000, 43 | unit_price: 10000000, 44 | }; 45 | let swqos_client = DefaultSWQoSClient::new("default", Arc::new(RpcClient::new(rpc_url.clone())), rpc_url.to_string(), None, vec![]); 46 | swqos_client.transfer(&from, &to, amount, Some(fee)).await?; 47 | Ok(()) 48 | } 49 | 50 | pub async fn transfer_token() -> anyhow::Result<()> { 51 | let from = Keypair::from_base58_string("your_payer_pubkey"); 52 | let to = Pubkey::from_str("recipient_pubkey")?; 53 | let mint = Pubkey::from_str("token_mint_pubkey")?; 54 | let amount = 1000; 55 | let fee = PriorityFee { 56 | unit_limit: 100000, 57 | unit_price: 10000000, 58 | }; 59 | let swqos_client = get_swqos_client(); 60 | swqos_client.spl_transfer(&from, &to, &mint, amount, Some(fee)).await?; 61 | Ok(()) 62 | } 63 | 64 | pub async fn get_trading_client() -> anyhow::Result { 65 | let rpc_url = "https://solana-rpc.publicnode.com".to_string(); 66 | let client = TradingClient::new(TradingConfig { 67 | rpc_url: rpc_url.to_string(), 68 | swqos: vec![ 69 | SWQoSType::Default("https://solana-rpc.publicnode.com".to_string(), None), 70 | SWQoSType::Jito(JITO_ENDPOINT_MAINNET.to_string()), 71 | SWQoSType::NextBlock(NEXTBLOCK_ENDPOINT_FRA.to_string(), "your_api_key".to_string()), 72 | SWQoSType::Blox(BLOX_ENDPOINT_FRA.to_string(), "your_api_key".to_string()), 73 | SWQoSType::ZeroSlot(ZEROSLOT_ENDPOINT_FRA.to_string(), "your_api_key".to_string()), 74 | SWQoSType::Temporal(TEMPORAL_ENDPOINT_FRA.to_string(), "your_api_key".to_string()), 75 | ], 76 | })?; 77 | 78 | client.initialize().await?; 79 | Ok(client) 80 | } 81 | 82 | pub async fn swap() -> anyhow::Result<()> { 83 | let client = get_trading_client().await?; 84 | let payer = Keypair::from_base58_string("your_payer_pubkey"); 85 | let mint = Pubkey::from_str("token_mint_pubkey")?; 86 | let sol_amount = sol_str_to_lamports("1.0").unwrap(); 87 | let slippage_basis_points = 3000; // 30% 88 | let fee = PriorityFee { 89 | unit_limit: 100000, 90 | unit_price: 10000000, 91 | }; 92 | let tip = sol_str_to_lamports("0.001").unwrap(); 93 | 94 | client.dexs[&DexType::Pumpfun] 95 | .buy(&payer, &mint, sol_amount, slippage_basis_points, Some(fee), Some(tip)) 96 | .await?; 97 | 98 | client.dexs[&DexType::PumpSwap] 99 | .buy(&payer, &mint, sol_amount, slippage_basis_points, Some(fee), Some(tip)) 100 | .await?; 101 | 102 | Ok(()) 103 | } 104 | 105 | pub async fn create() -> anyhow::Result<()> { 106 | let jwt_token = "jpinata.cloud jwt_token"; 107 | let token_info = CreateTokenMetadata { 108 | name: "TokenName".to_string(), 109 | symbol: "TOKEN".to_string(), 110 | description: "Token description".to_string(), 111 | file: "data:image/png;base64,base64_image_string".to_string(), 112 | twitter: Some("twitter".to_string()), 113 | telegram: Some("telegram".to_string()), 114 | website: Some("https://example.com".to_string()), 115 | metadata_uri: None, 116 | }; 117 | let metadata = create_token_metadata(token_info, jwt_token).await?; 118 | 119 | let swqos_client = get_swqos_client(); 120 | let trading_endpoint = TradingEndpoint::new(get_solana_client(), vec![Arc::new(swqos_client)]); 121 | let pumpfun_client = Pumpfun::new(Arc::new(trading_endpoint)); 122 | 123 | let payer = Keypair::from_base58_string("your_payer_keypair"); 124 | let mint_key = Keypair::from_base58_string("your_mint_keypair"); 125 | let buy_sol_amount = Some(sol_str_to_lamports("0.1").unwrap()); 126 | let slippage_basis_points = 3000; // 30% 127 | let fee = PriorityFee { 128 | unit_limit: 100000, 129 | unit_price: 10000000, 130 | }; 131 | let tip = sol_str_to_lamports("0.001").unwrap(); 132 | 133 | let create = Create { 134 | mint_private_key: mint_key, 135 | name: metadata.metadata.name, 136 | symbol: metadata.metadata.symbol, 137 | uri: metadata.metadata_uri, 138 | buy_sol_amount, 139 | slippage_basis_points: Some(slippage_basis_points), 140 | }; 141 | pumpfun_client.create(payer, create, Some(fee), Some(tip)).await?; 142 | 143 | Ok(()) 144 | } 145 | -------------------------------------------------------------------------------- /src/swqos/blox.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | swqos_rpc::{SWQoSClientTrait, SWQoSRequest}, 3 | SWQoSTrait, 4 | }; 5 | use crate::swqos::swqos_rpc::FormatBase64VersionedTransaction; 6 | use rand::seq::IndexedRandom; 7 | use solana_client::nonblocking::rpc_client::RpcClient; 8 | use solana_sdk::{pubkey, pubkey::Pubkey, transaction::VersionedTransaction}; 9 | use std::sync::Arc; 10 | 11 | pub const BLOX_TIP_ACCOUNTS: &[Pubkey] = &[ 12 | pubkey!("HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY"), 13 | pubkey!("95cfoy472fcQHaw4tPGBTKpn6ZQnfEPfBgDQx6gcRmRg"), 14 | pubkey!("3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd"), 15 | pubkey!("FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF"), 16 | ]; 17 | 18 | pub const BLOX_ENDPOINT_FRA: &str = "https://germany.solana.dex.blxrbdn.com"; 19 | pub const BLOX_ENDPOINT_AMS: &str = "https://amsterdam.solana.dex.blxrbdn.com"; 20 | pub const BLOX_ENDPOINT_NY: &str = "https://ny.solana.dex.blxrbdn.com"; 21 | pub const BLOX_ENDPOINT_UK: &str = "https://uk.solana.dex.blxrbdn.com"; 22 | pub const BLOX_ENDPOINT_LA: &str = "https://la.solana.dex.blxrbdn.com"; 23 | pub const BLOX_ENDPOINT_TOKYO: &str = "https://tokyo.solana.dex.blxrbdn.com"; 24 | 25 | #[derive(Clone)] 26 | pub struct BloxClient { 27 | pub rpc_client: Arc, 28 | pub swqos_endpoint: String, 29 | pub swqos_header: Option<(String, String)>, 30 | pub swqos_client: Arc, 31 | } 32 | 33 | #[async_trait::async_trait] 34 | impl SWQoSTrait for BloxClient { 35 | async fn send_transaction(&self, transaction: VersionedTransaction) -> anyhow::Result<()> { 36 | let body = serde_json::json!({ 37 | "transaction": { 38 | "content": transaction.to_base64_string(), 39 | }, 40 | "frontRunningProtection": false, 41 | "useStakedRPCs": true, 42 | }); 43 | 44 | self.swqos_client 45 | .swqos_json_post( 46 | SWQoSRequest { 47 | name: self.get_name().to_string(), 48 | url: format!("{}/api/v2/submit", self.swqos_endpoint), 49 | auth_header: self.swqos_header.clone(), 50 | transactions: vec![transaction], 51 | }, 52 | body, 53 | ) 54 | .await 55 | } 56 | 57 | async fn send_transactions(&self, transactions: Vec) -> anyhow::Result<()> { 58 | let body = serde_json::json!({ 59 | "entries": transactions 60 | .iter() 61 | .map(|tx| { 62 | serde_json::json!({ 63 | "transaction": { 64 | "content": tx.to_base64_string(), 65 | }, 66 | }) 67 | }) 68 | .collect::>(), 69 | }); 70 | 71 | self.swqos_client 72 | .swqos_json_post( 73 | SWQoSRequest { 74 | name: self.get_name().to_string(), 75 | url: format!("{}/api/v2/submit-batch", self.swqos_endpoint), 76 | auth_header: self.swqos_header.clone(), 77 | transactions, 78 | }, 79 | body, 80 | ) 81 | .await 82 | } 83 | 84 | fn get_tip_account(&self) -> Option { 85 | Some(*BLOX_TIP_ACCOUNTS.choose(&mut rand::rng())?) 86 | } 87 | 88 | fn get_name(&self) -> &str { 89 | "blox" 90 | } 91 | } 92 | 93 | impl BloxClient { 94 | pub fn new(rpc_client: Arc, endpoint: String, auth_token: String) -> Self { 95 | let swqos_client = reqwest::Client::new_swqos_client(); 96 | 97 | Self { 98 | rpc_client, 99 | swqos_endpoint: endpoint, 100 | swqos_header: Some(("Authorization".to_string(), auth_token)), 101 | swqos_client: Arc::new(swqos_client), 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/swqos/default.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | swqos_rpc::{SWQoSClientTrait, SWQoSRequest}, 3 | SWQoSTrait, 4 | }; 5 | use crate::instruction::builder::{build_transaction, PriorityFee}; 6 | use rand::seq::IndexedRandom; 7 | use solana_client::{nonblocking::rpc_client::RpcClient, rpc_config::RpcTransactionConfig}; 8 | use solana_sdk::{ 9 | commitment_config::CommitmentConfig, 10 | pubkey::Pubkey, 11 | signature::{Keypair, Signature}, 12 | signer::Signer, 13 | transaction::VersionedTransaction, 14 | }; 15 | use solana_transaction_status::UiTransactionEncoding; 16 | use spl_associated_token_account::{get_associated_token_address, instruction::create_associated_token_account_idempotent}; 17 | use std::sync::Arc; 18 | 19 | #[derive(Clone)] 20 | pub struct DefaultSWQoSClient { 21 | pub name: String, 22 | pub rpc_client: Arc, 23 | pub tip_accounts: Vec, 24 | pub swqos_endpoint: String, 25 | pub swqos_header: Option<(String, String)>, 26 | pub swqos_client: Arc, 27 | } 28 | 29 | #[async_trait::async_trait] 30 | impl SWQoSTrait for DefaultSWQoSClient { 31 | async fn send_transaction(&self, transaction: VersionedTransaction) -> anyhow::Result<()> { 32 | self.swqos_client 33 | .swqos_send_transaction(SWQoSRequest { 34 | name: self.name.clone(), 35 | url: self.swqos_endpoint.clone(), 36 | auth_header: self.swqos_header.clone(), 37 | transactions: vec![transaction], 38 | }) 39 | .await 40 | } 41 | 42 | async fn send_transactions(&self, transactions: Vec) -> anyhow::Result<()> { 43 | self.swqos_client 44 | .swqos_send_transactions(SWQoSRequest { 45 | name: self.name.clone(), 46 | url: self.swqos_endpoint.clone(), 47 | auth_header: self.swqos_header.clone(), 48 | transactions, 49 | }) 50 | .await 51 | } 52 | 53 | fn get_tip_account(&self) -> Option { 54 | Some(*self.tip_accounts.choose(&mut rand::rng())?) 55 | } 56 | 57 | fn get_name(&self) -> &str { 58 | &self.name 59 | } 60 | } 61 | 62 | pub struct TransferInfo { 63 | pub to: Pubkey, 64 | pub amount: u64, 65 | } 66 | 67 | impl DefaultSWQoSClient { 68 | pub fn new(name: &str, rpc_client: Arc, endpoint: String, header: Option<(String, String)>, tip_accounts: Vec) -> Self { 69 | let swqos_client = reqwest::Client::new_swqos_client(); 70 | 71 | Self { 72 | name: name.to_string(), 73 | rpc_client, 74 | tip_accounts, 75 | swqos_endpoint: endpoint, 76 | swqos_header: header, 77 | swqos_client: Arc::new(swqos_client), 78 | } 79 | } 80 | 81 | pub async fn transfer(&self, from: &Keypair, to: &Pubkey, amount: u64, fee: Option) -> anyhow::Result { 82 | let blockhash = self.rpc_client.get_latest_blockhash().await?; 83 | let instruction = solana_sdk::system_instruction::transfer(&from.pubkey(), to, amount); 84 | let transaction = build_transaction(from, vec![instruction], blockhash, fee, None, None)?; 85 | let signature = transaction.signatures[0]; 86 | self.send_transaction(transaction).await?; 87 | Ok(signature) 88 | } 89 | 90 | pub async fn batch_transfer(&self, from: &Keypair, to: Vec, fee: Option) -> anyhow::Result { 91 | let blockhash = self.rpc_client.get_latest_blockhash().await?; 92 | let instructions = to 93 | .iter() 94 | .map(|transfer| solana_sdk::system_instruction::transfer(&from.pubkey(), &transfer.to, transfer.amount)) 95 | .collect::>(); 96 | let transaction = build_transaction(from, instructions, blockhash, fee, None, None)?; 97 | let signature = transaction.signatures[0]; 98 | self.send_transaction(transaction).await?; 99 | Ok(signature) 100 | } 101 | 102 | pub async fn spl_transfer(&self, from: &Keypair, to: &Pubkey, mint: &Pubkey, amount: u64, fee: Option) -> anyhow::Result { 103 | let blockhash = self.rpc_client.get_latest_blockhash().await?; 104 | let from_ata = get_associated_token_address(&from.pubkey(), mint); 105 | let to_ata = get_associated_token_address(to, mint); 106 | let create_ata = create_associated_token_account_idempotent(&from.pubkey(), to, &mint, &spl_token::ID); 107 | let instruction = spl_token::instruction::transfer(&spl_token::ID, &from_ata, &to_ata, &from.pubkey(), &[], amount)?; 108 | let transaction = build_transaction(from, vec![create_ata, instruction], blockhash, fee, None, None)?; 109 | let signature = transaction.signatures[0]; 110 | self.send_transaction(transaction).await?; 111 | Ok(signature) 112 | } 113 | 114 | pub async fn spl_batch_transfer(&self, from: &Keypair, to: Vec, mint: &Pubkey, fee: Option) -> anyhow::Result { 115 | let blockhash = self.rpc_client.get_latest_blockhash().await?; 116 | let from_ata = get_associated_token_address(&from.pubkey(), mint); 117 | let mut instructions = Vec::new(); 118 | 119 | for transfer in &to { 120 | let to_ata = get_associated_token_address(&transfer.to, mint); 121 | let create_ata = create_associated_token_account_idempotent(&from.pubkey(), &transfer.to, &mint, &spl_token::ID); 122 | let instruction = spl_token::instruction::transfer(&spl_token::ID, &from_ata, &to_ata, &from.pubkey(), &[], transfer.amount)?; 123 | instructions.push(create_ata); 124 | instructions.push(instruction); 125 | } 126 | 127 | let transaction = build_transaction(from, instructions, blockhash, fee, None, None)?; 128 | let signature = transaction.signatures[0]; 129 | self.send_transaction(transaction).await?; 130 | Ok(signature) 131 | } 132 | 133 | pub async fn wait_for_confirm(&self, signature: &Signature) -> anyhow::Result<()> { 134 | const MAX_WAIT_SECONDS: u64 = 10; 135 | let ts = std::time::SystemTime::now(); 136 | loop { 137 | if let Ok(tx) = self 138 | .rpc_client 139 | .get_transaction_with_config( 140 | &signature, 141 | RpcTransactionConfig { 142 | encoding: Some(UiTransactionEncoding::Json), 143 | commitment: Some(CommitmentConfig::confirmed()), 144 | max_supported_transaction_version: Some(0), 145 | }, 146 | ) 147 | .await 148 | { 149 | if tx.slot > 0 { 150 | break; 151 | } 152 | } 153 | if ts.elapsed().unwrap().as_secs() > MAX_WAIT_SECONDS { 154 | return Err(anyhow::anyhow!("Transaction confirmation timedout: {:?}", signature)); 155 | } 156 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 157 | } 158 | Ok(()) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/swqos/jito.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | swqos_rpc::{SWQoSClientTrait, SWQoSRequest}, 3 | SWQoSTrait, 4 | }; 5 | use crate::swqos::swqos_rpc::FormatBase64VersionedTransaction; 6 | use rand::seq::IndexedRandom; 7 | use solana_client::nonblocking::rpc_client::RpcClient; 8 | use solana_sdk::{pubkey, pubkey::Pubkey, transaction::VersionedTransaction}; 9 | use std::sync::Arc; 10 | 11 | pub const JITO_TIP_ACCOUNTS: &[Pubkey] = &[ 12 | pubkey!("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5"), 13 | pubkey!("HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe"), 14 | pubkey!("Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY"), 15 | pubkey!("ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49"), 16 | pubkey!("DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh"), 17 | pubkey!("ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt"), 18 | pubkey!("DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL"), 19 | pubkey!("3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT"), 20 | ]; 21 | 22 | pub const JITO_ENDPOINT_MAINNET: &str = "https://mainnet.block-engine.jito.wtf"; 23 | pub const JITO_ENDPOINT_MAS: &str = "https://amsterdam.mainnet.block-engine.jito.wtf"; 24 | pub const JITO_ENDPOINT_FRA: &str = "https://frankfurt.mainnet.block-engine.jito.wtf"; 25 | pub const JITO_ENDPOINT_LONDON: &str = "https://london.mainnet.block-engine.jito.wtf"; 26 | pub const JITO_ENDPOINT_NY: &str = "https://ny.mainnet.block-engine.jito.wtf"; 27 | pub const JITO_ENDPOINT_TOKYO: &str = "https://tokyo.mainnet.block-engine.jito.wtf"; 28 | pub const JITO_ENDPOINT_SLC: &str = "https://slc.mainnet.block-engine.jito.wtf"; 29 | 30 | pub const JITO_RELAYER_AMS: &str = "http://amsterdam.mainnet.relayer.jito.wtf:8100"; 31 | pub const JITO_RELAYER_TOKYO: &str = "http://tokyo.mainnet.relayer.jito.wtf:8100"; 32 | pub const JITO_RELAYER_NY: &str = "http://ny.mainnet.relayer.jito.wtf:8100"; 33 | pub const JITO_RELAYER_FRA: &str = "http://frankfurt.mainnet.relayer.jito.wtf:8100"; 34 | pub const JITO_RELAYER_LONDON: &str = "http://london.mainnet.relayer.jito.wtf:8100"; 35 | 36 | #[derive(Clone)] 37 | pub struct JitoClient { 38 | pub rpc_client: Arc, 39 | pub swqos_endpoint: String, 40 | pub swqos_client: Arc, 41 | } 42 | 43 | #[async_trait::async_trait] 44 | impl SWQoSTrait for JitoClient { 45 | async fn send_transaction(&self, transaction: VersionedTransaction) -> anyhow::Result<()> { 46 | self.swqos_client 47 | .swqos_send_transaction(SWQoSRequest { 48 | name: self.get_name().to_string(), 49 | url: format!("{}/api/v1/transactions", self.swqos_endpoint), 50 | auth_header: None, 51 | transactions: vec![transaction], 52 | }) 53 | .await 54 | } 55 | 56 | async fn send_transactions(&self, transactions: Vec) -> anyhow::Result<()> { 57 | let txs_base64 = transactions.iter().map(|tx| tx.to_base64_string()).collect::>(); 58 | let body = serde_json::json!({ 59 | "jsonrpc": "2.0", 60 | "method": "sendBundle", 61 | "params": [ 62 | txs_base64, 63 | { "encoding": "base64" } 64 | ], 65 | "id": 1, 66 | }); 67 | 68 | self.swqos_client 69 | .swqos_json_post( 70 | SWQoSRequest { 71 | name: self.get_name().to_string(), 72 | url: format!("{}/api/v1/bundles", self.swqos_endpoint), 73 | auth_header: None, 74 | transactions: transactions, 75 | }, 76 | body, 77 | ) 78 | .await 79 | } 80 | 81 | fn get_tip_account(&self) -> Option { 82 | Some(*JITO_TIP_ACCOUNTS.choose(&mut rand::rng())?) 83 | } 84 | 85 | fn get_name(&self) -> &str { 86 | "jito" 87 | } 88 | } 89 | 90 | impl JitoClient { 91 | pub fn new(rpc_client: Arc, endpoint: String) -> Self { 92 | let swqos_client = reqwest::Client::new_swqos_client(); 93 | 94 | Self { 95 | rpc_client, 96 | swqos_endpoint: endpoint, 97 | swqos_client: Arc::new(swqos_client), 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/swqos/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blox; 2 | pub mod default; 3 | pub mod jito; 4 | pub mod nextblock; 5 | pub mod swqos_rpc; 6 | pub mod temporal; 7 | pub mod zeroslot; 8 | 9 | use blox::BloxClient; 10 | use default::DefaultSWQoSClient; 11 | use jito::JitoClient; 12 | use nextblock::NextBlockClient; 13 | use solana_client::nonblocking::rpc_client::RpcClient; 14 | use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction}; 15 | use std::{any::Any, sync::Arc}; 16 | use temporal::TEMPORAL_TIP_ACCOUNTS; 17 | use zeroslot::ZEROSLOT_TIP_ACCOUNTS; 18 | 19 | // (endpoint, auth_token) 20 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 21 | pub enum SWQoSType { 22 | Default(String, Option<(String, String)>), 23 | Jito(String), 24 | NextBlock(String, String), 25 | Blox(String, String), 26 | Temporal(String, String), 27 | ZeroSlot(String, String), 28 | } 29 | 30 | #[async_trait::async_trait] 31 | pub trait SWQoSTrait: Send + Sync + Any { 32 | async fn send_transaction(&self, transaction: VersionedTransaction) -> anyhow::Result<()>; 33 | async fn send_transactions(&self, transactions: Vec) -> anyhow::Result<()>; 34 | fn get_tip_account(&self) -> Option; 35 | fn get_name(&self) -> &str; 36 | } 37 | 38 | impl SWQoSType { 39 | pub fn instantiate(&self, rpc_client: Arc) -> Arc { 40 | match self { 41 | SWQoSType::Default(endpoint, header) => Arc::new(DefaultSWQoSClient::new("default", rpc_client, endpoint.to_string(), header.clone(), vec![])), 42 | SWQoSType::Jito(endpoint) => Arc::new(JitoClient::new(rpc_client, endpoint.to_string())), 43 | SWQoSType::NextBlock(endpoint, auth_token) => Arc::new(NextBlockClient::new(rpc_client, endpoint.to_string(), auth_token.to_string())), 44 | SWQoSType::Blox(endpoint, auth_token) => Arc::new(BloxClient::new(rpc_client, endpoint.to_string(), auth_token.to_string())), 45 | SWQoSType::ZeroSlot(endpoint, auth_token) => Arc::new(DefaultSWQoSClient::new( 46 | "0slot", 47 | rpc_client, 48 | format!("{}?api-key={}", endpoint, auth_token), 49 | None, 50 | ZEROSLOT_TIP_ACCOUNTS.into(), 51 | )), 52 | 53 | SWQoSType::Temporal(endpoint, auth_token) => Arc::new(DefaultSWQoSClient::new( 54 | "temporal", 55 | rpc_client, 56 | format!("{}?c={}", endpoint, auth_token), 57 | None, 58 | TEMPORAL_TIP_ACCOUNTS.into(), 59 | )), 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/swqos/nextblock.rs: -------------------------------------------------------------------------------- 1 | use super::{swqos_rpc::SWQoSRequest, SWQoSTrait}; 2 | use crate::swqos::swqos_rpc::SWQoSClientTrait; 3 | use base64::{engine::general_purpose, Engine}; 4 | use rand::seq::IndexedRandom; 5 | use solana_client::nonblocking::rpc_client::RpcClient; 6 | use solana_sdk::{pubkey, pubkey::Pubkey, transaction::VersionedTransaction}; 7 | use std::sync::Arc; 8 | 9 | pub const NEXTBLOCK_TIP_ACCOUNTS: &[Pubkey] = &[ 10 | pubkey!("NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE"), 11 | pubkey!("NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2"), 12 | pubkey!("NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X"), 13 | pubkey!("NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb"), 14 | pubkey!("neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At"), 15 | pubkey!("nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG"), 16 | pubkey!("NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid"), 17 | pubkey!("nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc"), 18 | ]; 19 | 20 | pub const NEXTBLOCK_ENDPOINT_FRA: &str = "https://fra.nextblock.io"; 21 | pub const NEXTBLOCK_ENDPOINT_NY: &str = "https://ny.nextblock.io"; 22 | 23 | #[derive(Clone)] 24 | pub struct NextBlockClient { 25 | pub rpc_client: Arc, 26 | pub swqos_endpoint: String, 27 | pub swqos_header: Option<(String, String)>, 28 | pub swqos_client: Arc, 29 | } 30 | 31 | #[async_trait::async_trait] 32 | impl SWQoSTrait for NextBlockClient { 33 | async fn send_transaction(&self, transaction: VersionedTransaction) -> anyhow::Result<()> { 34 | let tx_bytes = bincode::serialize(&transaction)?; 35 | let tx_base64 = general_purpose::STANDARD.encode(tx_bytes); 36 | let body = serde_json::json!({ 37 | "transaction": { 38 | "content": tx_base64, 39 | }, 40 | "frontRunningProtection": false, 41 | }); 42 | 43 | let url = format!("{}/api/v2/submit", self.swqos_endpoint); 44 | self.swqos_client 45 | .swqos_json_post( 46 | SWQoSRequest { 47 | name: self.get_name().to_string(), 48 | url: url.clone(), 49 | auth_header: self.swqos_header.clone(), 50 | transactions: vec![transaction], 51 | }, 52 | body, 53 | ) 54 | .await 55 | } 56 | 57 | async fn send_transactions(&self, transactions: Vec) -> anyhow::Result<()> { 58 | let body = serde_json::json!({ 59 | "entries": transactions 60 | .iter() 61 | .map(|tx| { 62 | let tx_bytes = bincode::serialize(tx).unwrap(); 63 | let tx_base64 = general_purpose::STANDARD.encode(tx_bytes); 64 | serde_json::json!({ 65 | "transaction": { 66 | "content": tx_base64, 67 | }, 68 | }) 69 | }) 70 | .collect::>(), 71 | }); 72 | 73 | let url = format!("{}/api/v2/submit-batch", self.swqos_endpoint); 74 | self.swqos_client 75 | .swqos_json_post( 76 | SWQoSRequest { 77 | name: self.get_name().to_string(), 78 | url: url.clone(), 79 | auth_header: self.swqos_header.clone(), 80 | transactions, 81 | }, 82 | body, 83 | ) 84 | .await 85 | } 86 | 87 | fn get_tip_account(&self) -> Option { 88 | Some(*NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng())?) 89 | } 90 | 91 | fn get_name(&self) -> &str { 92 | "nextblock" 93 | } 94 | } 95 | 96 | impl NextBlockClient { 97 | pub fn new(rpc_client: Arc, endpoint: String, auth_token: String) -> Self { 98 | let swqos_client = reqwest::Client::new_swqos_client(); 99 | 100 | Self { 101 | rpc_client, 102 | swqos_endpoint: endpoint, 103 | swqos_header: Some(("Authorization".to_string(), auth_token)), 104 | swqos_client: Arc::new(swqos_client), 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/swqos/swqos_rpc.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose, Engine}; 2 | use solana_sdk::transaction::VersionedTransaction; 3 | use std::{str::FromStr, time::Duration}; 4 | use tokio::time::timeout; 5 | 6 | pub const SWQOS_RPC_TIMEOUT: std::time::Duration = Duration::from_secs(10); 7 | 8 | pub struct SWQoSRequest { 9 | pub name: String, 10 | pub url: String, 11 | pub auth_header: Option<(String, String)>, 12 | pub transactions: Vec, 13 | } 14 | 15 | pub trait FormatBase64VersionedTransaction { 16 | fn to_base64_string(&self) -> String; 17 | } 18 | 19 | impl FormatBase64VersionedTransaction for VersionedTransaction { 20 | fn to_base64_string(&self) -> String { 21 | let tx_bytes = bincode::serialize(self).unwrap(); 22 | general_purpose::STANDARD.encode(tx_bytes) 23 | } 24 | } 25 | 26 | #[async_trait::async_trait] 27 | pub trait SWQoSClientTrait { 28 | fn new_swqos_client() -> reqwest::Client { 29 | reqwest::Client::builder().timeout(SWQOS_RPC_TIMEOUT).build().unwrap() 30 | } 31 | async fn swqos_send_transaction(&self, request: SWQoSRequest) -> anyhow::Result<()>; 32 | async fn swqos_send_transactions(&self, request: SWQoSRequest) -> anyhow::Result<()>; 33 | async fn swqos_json_post(&self, request: SWQoSRequest, body: serde_json::Value) -> anyhow::Result<()>; 34 | } 35 | 36 | #[async_trait::async_trait] 37 | impl SWQoSClientTrait for reqwest::Client { 38 | async fn swqos_send_transaction(&self, request: SWQoSRequest) -> anyhow::Result<()> { 39 | let body = serde_json::json!({ 40 | "jsonrpc": "2.0", 41 | "method": "sendTransaction", 42 | "params": [ 43 | request.transactions[0].to_base64_string(), 44 | { "encoding": "base64" } 45 | ], 46 | "id": 1, 47 | }); 48 | 49 | self.swqos_json_post(request, body).await 50 | } 51 | 52 | async fn swqos_send_transactions(&self, request: SWQoSRequest) -> anyhow::Result<()> { 53 | let txs_base64 = request.transactions.iter().map(|tx| tx.to_base64_string()).collect::>(); 54 | let body = serde_json::json!({ 55 | "jsonrpc": "2.0", 56 | "method": "sendTransactions", 57 | "params": [ 58 | txs_base64, 59 | { "encoding": "base64" } 60 | ], 61 | "id": 1, 62 | }); 63 | 64 | self.swqos_json_post(request, body).await 65 | } 66 | 67 | async fn swqos_json_post(&self, request: SWQoSRequest, body: serde_json::Value) -> anyhow::Result<()> { 68 | let txs_hash = request 69 | .transactions 70 | .iter() 71 | .map(|tx| tx.signatures[0].to_string()) 72 | .collect::>() 73 | .join(", "); 74 | let response = if let Some((key, value)) = request.auth_header { 75 | timeout(SWQOS_RPC_TIMEOUT, self.post(request.url).header(key, value).json(&body).send()).await?? 76 | } else { 77 | timeout(SWQOS_RPC_TIMEOUT, self.post(request.url).json(&body).send()).await?? 78 | }; 79 | let http_status = response.status(); 80 | let response_body = timeout(SWQOS_RPC_TIMEOUT, response.text()).await??; 81 | 82 | if !http_status.is_success() { 83 | let error = format!("swqos_json_post error: {} {} {} {}", request.name, txs_hash, http_status, response_body); 84 | eprintln!("{}", error); 85 | return Err(anyhow::anyhow!(error)); 86 | } 87 | 88 | let response_json = serde_json::Value::from_str(&response_body)?; 89 | if let Some(error) = response_json.get("error") { 90 | let error = format!("swqos_json_post error: {} {} {} {}", request.name, txs_hash, http_status, error.to_string()); 91 | eprintln!("{}", error); 92 | return Err(anyhow::anyhow!(error)); 93 | } 94 | 95 | println!("swqos_json_post success: {} {} {:#?}", request.name, txs_hash, response_json); 96 | 97 | Ok(()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/swqos/temporal.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{pubkey, pubkey::Pubkey}; 2 | 3 | pub const TEMPORAL_TIP_ACCOUNTS: &[Pubkey] = &[ 4 | pubkey!("TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq"), 5 | pubkey!("noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4"), 6 | pubkey!("noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE"), 7 | pubkey!("noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo"), 8 | pubkey!("noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ"), 9 | pubkey!("nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L"), 10 | pubkey!("nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z"), 11 | pubkey!("nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu"), 12 | pubkey!("noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7"), 13 | pubkey!("nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP"), 14 | pubkey!("nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P"), 15 | pubkey!("nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge"), 16 | pubkey!("nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3"), 17 | pubkey!("nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ"), 18 | pubkey!("nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk"), 19 | pubkey!("nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne"), 20 | pubkey!("nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb"), 21 | ]; 22 | 23 | pub const TEMPORAL_ENDPOINT_FRA: &str = "http://fra2.nozomi.temporal.xyz"; 24 | pub const TEMPORAL_ENDPOINT_EWR: &str = "http://ewr1.nozomi.temporal.xyz"; 25 | pub const TEMPORAL_ENDPOINT_PITT: &str = "http://pit1.nozomi.temporal.xyz"; 26 | pub const TEMPORAL_ENDPOINT_AMS: &str = "http://ams1.nozomi.temporal.xyz"; 27 | -------------------------------------------------------------------------------- /src/swqos/zeroslot.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{pubkey, pubkey::Pubkey}; 2 | 3 | pub const ZEROSLOT_TIP_ACCOUNTS: &[Pubkey] = &[ 4 | pubkey!("Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3"), 5 | pubkey!("FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe"), 6 | pubkey!("ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13"), 7 | pubkey!("6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK"), 8 | pubkey!("Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr"), 9 | ]; 10 | 11 | pub const ZEROSLOT_ENDPOINT_FRA: &str = "https://de.0slot.trade"; 12 | pub const ZEROSLOT_ENDPOINT_NY: &str = "https://ny.0slot.trade"; 13 | pub const ZEROSLOT_ENDPOINT_AMS: &str = "https://ams.0slot.trade"; 14 | pub const ZEROSLOT_ENDPOINT_TOKYO: &str = "https://jp.0slot.trade"; 15 | pub const ZEROSLOT_ENDPOINT_LA: &str = "https://la.0slot.trade"; 16 | --------------------------------------------------------------------------------