diff --git a/Cargo.toml b/Cargo.toml index 60ccbe8..1f57d60 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "pumpfun-sdk" +name = "grpc-parsed" version = "2.4.3" edition = "2021" -authors = ["William "] -repository = "https://github.com/MiracleAI-Labs/pumpfun-sdk" +authors = [] +repository = "" description = "Rust SDK to interact with the Pump.fun Solana program." license = "MIT" -keywords = ["solana", "memecoins", "pumpfun", "pumpfun-sdk", "pumpbot"] +keywords = ["solana", "memecoins", "pumpfun", "grpc-parsed", "pumpbot"] readme = "README.md" [lib] @@ -21,8 +21,10 @@ solana-rpc-client-api = "2.1.16" solana-transaction-status = "2.1.16" solana-account-decoder = "2.1.16" solana-hash = "2.1.16" -solana-perf = "2.1.16" solana-security-txt = "1.1.1" +solana-entry = "2.1.16" +solana-rpc-client-nonce-utils = "2.1.16" +solana-perf = "2.1.16" spl-token = "8.0.0" spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] } @@ -48,8 +50,8 @@ tonic = { version = "0.12.3", features = ["tls", "tls-roots", "tls-webpki-roots" rustls = { version = "0.23.23", features = ["ring"] } rustls-native-certs = "0.8.1" tokio-rustls = "0.26.1" +core_affinity = "0.8" -bytes = "1.4.0" dotenvy = "0.15.7" pretty_env_logger = "0.5.0" log = "0.4.22" @@ -85,6 +87,8 @@ tokio-tungstenite = { version = "0.26.1", features = ["native-tls"] } indicatif = "0.17.11" toml = "0.8.20" +pumpfun = "4.2.0" + diff --git a/README.md b/README.md index 5d32780..5cd7690 100755 --- a/README.md +++ b/README.md @@ -1,127 +1,108 @@ -# PumpFun Rust SDK +# GrpcParsed -A comprehensive Rust SDK for seamless interaction with the PumpFun Solana program. This SDK provides a robust set of tools and interfaces to integrate PumpFun functionality into your applications. +A gRPC client implementation for subscribing to and processing Solana transaction data. +## Features -# Explanation -1. Add `create, buy, sell` for pump.fun. -2. Add `logs_subscribe` to subscribe the logs of the PumpFun program. -3. Add `yellowstone grpc` to subscribe the logs of the PumpFun program. -4. Add `jito` to send transaction with Jito. -5. Add `nextblock` to send transaction with nextblock. -6. Add `0slot` to send transaction with 0slot. -7. Submit a transaction using Jito, Nextblock, and 0slot simultaneously; the fastest one will succeed, while the others will fail. +- Real-time subscription to Solana transaction data via gRPC +- Support for processing transaction entries and transactions +- Asynchronous transaction data processing +- Custom callback function support for transaction events +- Built-in error handling mechanism -## Usage -```shell -cd `your project root directory` -git clone https://github.com/0xfnzero/pumpfun-sdk -``` +## Installation + +Add the following to your `Cargo.toml`: ```toml -# add to your Cargo.toml -pumpfun-sdk = { path = "./pumpfun-sdk", version = "2.4.3" } +[dependencies] +grpc-parsed = { path = ".", version = "0.1.0" } ``` -### logs subscription for token create and trade transaction +## Usage Examples + +### 1. Initializing the Client + ```rust -use pumpfun_sdk::{common::logs_events::PumpfunEvent, grpc::YellowstoneGrpc}; +use grpc_parsed::grpc::YellowstoneGrpc; -// create grpc client -let grpc_url = "http://127.0.0.1:10000"; -let client = YellowstoneGrpc::new(grpc_url); +async fn setup_client() -> Result> { + let endpoint = "https://solana-yellowstone-grpc.publicnode.com:443"; + let client = YellowstoneGrpc::new(endpoint.to_string(), None)?; + Ok(client) +} +``` -// Define callback function -let callback = |event: PumpfunEvent| { - match event { - PumpfunEvent::NewToken(token_info) => { - println!("Received new token event: {:?}", token_info); - }, - PumpfunEvent::NewDevTrade(trade_info) => { - println!("Received dev trade event: {:?}", trade_info); - }, - PumpfunEvent::NewUserTrade(trade_info) => { - println!("Received new trade event: {:?}", trade_info); - }, - PumpfunEvent::NewBotTrade(trade_info) => { - println!("Received new bot trade event: {:?}", trade_info); +### 2. Subscribing to Transaction Data + +```rust +use grpc_parsed::common::logs_events::PumpfunEvent; +use solana_sdk::pubkey::Pubkey; + +async fn subscribe_to_transactions() -> Result<(), Box> { + let client = YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + )?; + + let callback = |event: PumpfunEvent| { + match event { + PumpfunEvent::NewToken(token_info) => { + println!("Received new token event: {:?}", token_info); + }, + PumpfunEvent::NewDevTrade(trade_info) => { + println!("Received new dev trade event: {:?}", trade_info); + }, + PumpfunEvent::NewUserTrade(trade_info) => { + println!("Received new trade event: {:?}", trade_info); + }, + PumpfunEvent::NewBotTrade(trade_info) => { + println!("Received new bot trade event: {:?}", trade_info); + }, + PumpfunEvent::Error(err) => { + println!("Received error: {}", err); + } } - PumpfunEvent::Error(err) => { - println!("Received error: {}", err); + }; + + // Optional: Specify bot wallet address for filtering + let bot_wallet = None; + client.subscribe_pumpfun(callback, bot_wallet).await?; + + Ok(()) +} +``` + +## Error Handling + +```rust +use grpc_parsed::grpc::YellowstoneGrpc; + +async fn handle_errors() { + match YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + ) { + Ok(client) => { + println!("Client initialized successfully"); + }, + Err(e) => { + eprintln!("Failed to initialize client: {}", e); } } -}; - -let payer_keypair = Keypair::from_base58_string("your private key"); -client.subscribe_pumpfun(callback, Some(payer_keypair.pubkey())).await?; +} ``` -### Init pumpfun instance for configs -```rust -use std::sync::Arc; -use pumpfun_sdk::{common::{Cluster, PriorityFee}, PumpFun}; -use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair, signer::Signer}; +## Important Notes -let priority_fee = PriorityFee{ - unit_limit: 190000, - unit_price: 1000000, - buy_tip_fee: 0.001, - sell_tip_fee: 0.0001, -}; +- Ensure the gRPC server address is correct and accessible +- Callback functions should be thread-safe (Send + Sync) +- Implement appropriate error retry mechanisms in production environments +- Be mindful of memory usage when processing large volumes of transaction data -let cluster = Cluster { - rpc_url: "https://api.mainnet-beta.solana.com".to_string(), - block_engine_url: "https://block-engine.example.com".to_string(), - nextblock_url: "https://nextblock.example.com".to_string(), - nextblock_auth_token: "nextblock_api_token".to_string(), - zeroslot_url: "https://zeroslot.example.com".to_string(), - zeroslot_auth_token: "zeroslot_api_token".to_string(), - use_jito: true, - use_nextblock: false, - use_zeroslot: false, - priority_fee, - commitment: CommitmentConfig::processed(), -}; +## License -// create pumpfun instance -let payer = Keypair::from_base58_string("your private key"); -let pumpfun = PumpFun::new( - Arc::new(payer), - &cluster, -).await; -``` - -### pumpfun buy token -```rust -use pumpfun_sdk::PumpFun; -use solana_sdk::{native_token::sol_to_lamports, signature::Keypair, signer::Signer}; - -// create pumpfun instance -let pumpfun = PumpFun::new(Arc::new(payer), &cluster).await; - -// Mint keypair -let mint_pubkey: Keypair = Keypair::new(); - -// buy token with tip -pumpfun.buy_with_tip(mint_pubkey, 10000, None).await?; - -``` - -### pumpfun sell token -```rust -use pumpfun_sdk::PumpFun; -use solana_sdk::{native_token::sol_to_lamports, signature::Keypair, signer::Signer}; - -// create pumpfun instance -let pumpfun = PumpFun::new(Arc::new(payer), &cluster).await; - -// sell token with tip -pumpfun.sell_with_tip(mint_pubkey, 100000, None).await?; - -// sell token by percent with tip -pumpfun.sell_by_percent_with_tip(mint_pubkey, 100, None).await?; - -``` +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ### Telegram group: https://t.me/fnzero_group diff --git a/src/accounts/bonding_curve.rs b/src/accounts/bonding_curve.rs deleted file mode 100755 index 1b4ae53..0000000 --- a/src/accounts/bonding_curve.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Bonding curve account for the Pump.fun Solana Program -//! -//! This module contains the definition for the bonding curve account. -//! -//! # Bonding Curve Account -//! -//! The bonding curve account is used to manage token pricing and liquidity. -//! -//! # Fields -//! -//! - `discriminator`: Unique identifier for the bonding curve -//! - `virtual_token_reserves`: Virtual token reserves used for price calculations -//! - `virtual_sol_reserves`: Virtual SOL reserves used for price calculations -//! - `real_token_reserves`: Actual token reserves available for trading -//! - `real_sol_reserves`: Actual SOL reserves available for trading -//! - `token_total_supply`: Total supply of tokens -//! - `complete`: Whether the bonding curve is complete/finalized -//! -//! # Methods -//! -//! - `new`: Creates a new bonding curve instance -//! - `get_buy_price`: Calculates the amount of tokens received for a given SOL amount -//! - `get_sell_price`: Calculates the amount of SOL received for selling tokens -//! - `get_market_cap_sol`: Calculates the current market cap in SOL -//! - `get_final_market_cap_sol`: Calculates the final market cap in SOL after all tokens are sold -//! - `get_buy_out_price`: Calculates the price to buy out all remaining tokens - -use serde::{Serialize, Deserialize}; -use solana_sdk::pubkey::Pubkey; - -/// Represents the global configuration account for token pricing and fees -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BondingCurveAccount { - /// Unique identifier for the bonding curve - pub discriminator: u64, - /// Virtual token reserves used for price calculations - pub virtual_token_reserves: u64, - /// Virtual SOL reserves used for price calculations - pub virtual_sol_reserves: u64, - /// Actual token reserves available for trading - pub real_token_reserves: u64, - /// Actual SOL reserves available for trading - pub real_sol_reserves: u64, - /// Total supply of tokens - pub token_total_supply: u64, - /// Whether the bonding curve is complete/finalized - pub complete: bool, - /// Creator of the bonding curve - pub creator: Pubkey, -} - -impl BondingCurveAccount { - /// Creates a new bonding curve instance - /// - /// # Arguments - /// * `discriminator` - Unique identifier for the curve - /// * `virtual_token_reserves` - Virtual token reserves for price calculations - /// * `virtual_sol_reserves` - Virtual SOL reserves for price calculations - /// * `real_token_reserves` - Actual token reserves available - /// * `real_sol_reserves` - Actual SOL reserves available - /// * `token_total_supply` - Total supply of tokens - /// * `complete` - Whether the curve is complete - // pub fn new(mint: &Pubkey, dev_buy_token_amount: u64, dev_buy_sol_amount: u64) -> Self { - // Self { - // // account: get_bonding_curve_pda(mint).unwrap(), - // virtual_token_reserves: INITIAL_VIRTUAL_TOKEN_RESERVES - dev_buy_token_amount, - // virtual_sol_reserves: INITIAL_VIRTUAL_SOL_RESERVES + dev_buy_sol_amount, - // real_token_reserves: INITIAL_REAL_TOKEN_RESERVES - dev_buy_token_amount, - // real_sol_reserves: dev_buy_sol_amount, - // token_total_supply: TOKEN_TOTAL_SUPPLY, - // complete: false, - // } - // } - - /// Calculates the amount of tokens received for a given SOL amount - /// - /// # Arguments - /// * `amount` - Amount of SOL to spend - /// - /// # Returns - /// * `Ok(u64)` - Amount of tokens that would be received - /// * `Err(&str)` - Error message if curve is complete - pub fn get_buy_price(&self, amount: u64) -> Result { - if self.complete { - return Err("Curve is complete"); - } - - if amount == 0 { - return Ok(0); - } - - // Calculate the product of virtual reserves using u128 to avoid overflow - let n: u128 = (self.virtual_sol_reserves as u128) * (self.virtual_token_reserves as u128); - - // Calculate the new virtual sol reserves after the purchase - let i: u128 = (self.virtual_sol_reserves as u128) + (amount as u128); - - // Calculate the new virtual token reserves after the purchase - let r: u128 = n / i + 1; - - // Calculate the amount of tokens to be purchased - let s: u128 = (self.virtual_token_reserves as u128) - r; - - // Convert back to u64 and return the minimum of calculated tokens and real reserves - let s_u64 = s as u64; - Ok(if s_u64 < self.real_token_reserves { - s_u64 - } else { - self.real_token_reserves - }) - } - - /// Calculates the amount of SOL received for selling tokens - /// - /// # Arguments - /// * `amount` - Amount of tokens to sell - /// * `fee_basis_points` - Fee in basis points (1/100th of a percent) - /// - /// # Returns - /// * `Ok(u64)` - Amount of SOL that would be received after fees - /// * `Err(&str)` - Error message if curve is complete - pub fn get_sell_price(&self, amount: u64, fee_basis_points: u64) -> Result { - if self.complete { - return Err("Curve is complete"); - } - - if amount == 0 { - return Ok(0); - } - - // Calculate the proportional amount of virtual sol reserves to be received using u128 - let n: u128 = ((amount as u128) * (self.virtual_sol_reserves as u128)) - / ((self.virtual_token_reserves as u128) + (amount as u128)); - - // Calculate the fee amount in the same units - let a: u128 = (n * (fee_basis_points as u128)) / 10000; - - // Return the net amount after deducting the fee, converting back to u64 - Ok((n - a) as u64) - } - - /// Calculates the current market cap in SOL - pub fn get_market_cap_sol(&self) -> u64 { - if self.virtual_token_reserves == 0 { - return 0; - } - - ((self.token_total_supply as u128) * (self.virtual_sol_reserves as u128) - / (self.virtual_token_reserves as u128)) as u64 - } - - /// Calculates the final market cap in SOL after all tokens are sold - /// - /// # Arguments - /// * `fee_basis_points` - Fee in basis points (1/100th of a percent) - pub fn get_final_market_cap_sol(&self, fee_basis_points: u64) -> u64 { - let total_sell_value: u128 = - self.get_buy_out_price(self.real_token_reserves, fee_basis_points) as u128; - let total_virtual_value: u128 = (self.virtual_sol_reserves as u128) + total_sell_value; - let total_virtual_tokens: u128 = - (self.virtual_token_reserves as u128) - (self.real_token_reserves as u128); - - if total_virtual_tokens == 0 { - return 0; - } - - ((self.token_total_supply as u128) * total_virtual_value / total_virtual_tokens) as u64 - } - - /// Calculates the price to buy out all remaining tokens - /// - /// # Arguments - /// * `amount` - Amount of tokens to buy - /// * `fee_basis_points` - Fee in basis points (1/100th of a percent) - pub fn get_buy_out_price(&self, amount: u64, fee_basis_points: u64) -> u64 { - // Get the effective amount of sol tokens - let sol_tokens: u128 = if amount < self.real_sol_reserves { - self.real_sol_reserves as u128 - } else { - amount as u128 - }; - - // Calculate total sell value - let total_sell_value: u128 = (sol_tokens * (self.virtual_sol_reserves as u128)) - / ((self.virtual_token_reserves as u128) - sol_tokens) - + 1; - - // Calculate fee - let fee: u128 = (total_sell_value * (fee_basis_points as u128)) / 10000; - - // Return total including fee, converting back to u64 - (total_sell_value + fee) as u64 - } - - pub fn get_token_price(&self) -> f64 { - let v_sol = self.virtual_sol_reserves as f64 / 100_000_000.0; - let v_tokens = self.virtual_token_reserves as f64 / 100_000.0; - let token_price = v_sol / v_tokens; - token_price - } -} diff --git a/src/accounts/global.rs b/src/accounts/global.rs deleted file mode 100755 index 91aa052..0000000 --- a/src/accounts/global.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Global account for the Pump.fun Solana Program -//! -//! This module contains the definition for the global configuration account. -//! -//! # Global Account -//! -//! The global account is used to store the global configuration for the Pump.fun program. -//! -//! # Fields -//! -//! - `discriminator`: Unique identifier for the global account -//! - `initialized`: Whether the global account has been initialized -//! - `authority`: Authority pubkey that can modify settings -//! - `fee_recipient`: Account that receives fees -//! - `initial_virtual_token_reserves`: Initial virtual token reserves for price calculations -//! - `initial_virtual_sol_reserves`: Initial virtual SOL reserves for price calculations -//! - `initial_real_token_reserves`: Initial actual token reserves available for trading -//! - `token_total_supply`: Total supply of tokens -//! - `fee_basis_points`: Fee in basis points (1/100th of a percent) -//! -//! # Methods -//! -//! - `new`: Creates a new global account instance -//! - `get_initial_buy_price`: Calculates the initial amount of tokens received for a given SOL amount - -use solana_sdk::pubkey::Pubkey; -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Serialize, Deserialize}; -/// Represents the global configuration account for token pricing and fees -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GlobalAccount { - /// Unique identifier for the global account - pub discriminator: u64, - /// Whether the global account has been initialized - pub initialized: bool, - /// Authority that can modify global settings - pub authority: Pubkey, - /// Account that receives fees - pub fee_recipient: Pubkey, - /// Initial virtual token reserves for price calculations - pub initial_virtual_token_reserves: u64, - /// Initial virtual SOL reserves for price calculations - pub initial_virtual_sol_reserves: u64, - /// Initial actual token reserves available for trading - pub initial_real_token_reserves: u64, - /// Total supply of tokens - pub token_total_supply: u64, - /// Fee in basis points (1/100th of a percent) - pub fee_basis_points: u64, -} - -impl GlobalAccount { - /// Creates a new global account instance - /// - /// # Arguments - /// * `discriminator` - Unique identifier for the account - /// * `initialized` - Whether the account is initialized - /// * `authority` - Authority pubkey that can modify settings - /// * `fee_recipient` - Account that receives fees - /// * `initial_virtual_token_reserves` - Initial virtual token reserves - /// * `initial_virtual_sol_reserves` - Initial virtual SOL reserves - /// * `initial_real_token_reserves` - Initial actual token reserves - /// * `token_total_supply` - Total supply of tokens - /// * `fee_basis_points` - Fee in basis points - #[allow(clippy::too_many_arguments)] - pub fn new( - discriminator: u64, - initialized: bool, - authority: Pubkey, - fee_recipient: Pubkey, - initial_virtual_token_reserves: u64, - initial_virtual_sol_reserves: u64, - initial_real_token_reserves: u64, - token_total_supply: u64, - fee_basis_points: u64, - ) -> Self { - Self { - discriminator, - initialized, - authority, - fee_recipient, - initial_virtual_token_reserves, - initial_virtual_sol_reserves, - initial_real_token_reserves, - token_total_supply, - fee_basis_points, - } - } - - /// Calculates the initial amount of tokens received for a given SOL amount - /// - /// # Arguments - /// * `amount` - Amount of SOL to spend - /// - /// # Returns - /// Amount of tokens that would be received - pub fn get_initial_buy_price(&self, amount: u64) -> u64 { - if amount == 0 { - return 0; - } - - let n: u128 = (self.initial_virtual_sol_reserves as u128) - * (self.initial_virtual_token_reserves as u128); - let i: u128 = (self.initial_virtual_sol_reserves as u128) + (amount as u128); - let r: u128 = n / i + 1; - let s: u128 = (self.initial_virtual_token_reserves as u128) - r; - - if s < (self.initial_real_token_reserves as u128) { - s as u64 - } else { - self.initial_real_token_reserves - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn get_global() -> GlobalAccount { - GlobalAccount::new( - 1, - true, - Pubkey::new_unique(), - Pubkey::new_unique(), - 1000, - 1000, - 500, - 1000, - 250, - ) - } - - fn get_large_global() -> GlobalAccount { - GlobalAccount::new( - 1, - true, - Pubkey::new_unique(), - Pubkey::new_unique(), - u64::MAX, - u64::MAX, - u64::MAX / 2, - u64::MAX, - 250, - ) - } - - #[test] - fn test_global_account() { - let global: GlobalAccount = get_global(); - - // Test initial buy price calculation - assert_eq!(global.get_initial_buy_price(0), 0); - - let price: u64 = global.get_initial_buy_price(100); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - } - - #[test] - fn test_global_account_max_reserves() { - let mut global: GlobalAccount = get_global(); - global.initial_real_token_reserves = 100; - - // Test that returned amount is capped by real_token_reserves - let price: u64 = global.get_initial_buy_price(1000); - assert_eq!(price, global.initial_real_token_reserves); - } - - #[test] - fn test_global_account_overflow() { - let global: GlobalAccount = get_large_global(); - - // Test with maximum possible SOL amount - let price: u64 = global.get_initial_buy_price(u64::MAX); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - - // Test with large but not maximum SOL amount - let price: u64 = global.get_initial_buy_price(u64::MAX / 2); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - } - - #[test] - fn test_global_account_overflow_edge_cases() { - let mut global: GlobalAccount = get_large_global(); - global.initial_virtual_sol_reserves = u64::MAX - 1000; - global.initial_virtual_token_reserves = u64::MAX - 1000; - global.initial_real_token_reserves = u64::MAX / 4; - - // Test with amounts near u64::MAX - let price: u64 = global.get_initial_buy_price(u64::MAX - 1); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - - let price: u64 = global.get_initial_buy_price(u64::MAX - 1000); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - } -} diff --git a/src/accounts/mod.rs b/src/accounts/mod.rs deleted file mode 100755 index f77567c..0000000 --- a/src/accounts/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Accounts for the Pump.fun Solana Program -//! -//! This module contains the definitions for the accounts used by the Pump.fun program. -//! -//! # Accounts -//! -//! - `BondingCurve`: Represents a bonding curve account. -//! - `Global`: Represents the global configuration account. - -mod bonding_curve; -mod global; - -pub use bonding_curve::*; -pub use global::*; diff --git a/src/common/logs_data.rs b/src/common/logs_data.rs index 05ae2d6..c8f5c5b 100755 --- a/src/common/logs_data.rs +++ b/src/common/logs_data.rs @@ -1,5 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use solana_sdk::pubkey::Pubkey; +use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction}; use crate::error::{ClientError, ClientResult}; @@ -61,6 +61,13 @@ pub struct SwapBaseInLog { pub out_amount: u64, } +#[derive(Clone, Debug, Default, PartialEq)] +pub struct TransferInfo { + pub slot: u64, + pub signature: String, + pub tx: Option, +} + pub trait EventTrait: Sized + std::fmt::Debug { fn from_bytes(bytes: &[u8]) -> ClientResult; } diff --git a/src/common/logs_events.rs b/src/common/logs_events.rs index a5799e5..5477b66 100755 --- a/src/common/logs_events.rs +++ b/src/common/logs_events.rs @@ -1,7 +1,7 @@ use base64::engine::general_purpose; use base64::Engine; use regex::Regex; -use crate::common::logs_data::{CreateTokenInfo, TradeInfo, EventTrait}; +use crate::common::logs_data::{CreateTokenInfo, TradeInfo, EventTrait, TransferInfo}; pub const PROGRAM_DATA: &str = "Program data: "; @@ -23,6 +23,12 @@ pub enum DexEvent { Error(String), } +#[derive(Debug)] +pub enum SystemEvent { + NewTransfer(TransferInfo), + Error(String), +} + // #[derive(Debug, Clone, Copy)] // pub struct PumpEvent {} @@ -67,9 +73,10 @@ impl RaydiumEvent { if !logs.is_empty() { let logs_iter = logs.iter().peekable(); - let re = Regex::new(r"ray_log: (?P[A-Za-z0-9+/=]+)").unwrap(); for l in logs_iter.rev() { + let re = Regex::new(r"ray_log: (?P[A-Za-z0-9+/=]+)").unwrap(); + if let Some(caps) = re.captures(l) { if let Some(base64) = caps.name("base64") { let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap(); diff --git a/src/common/logs_filters.rs b/src/common/logs_filters.rs index 4ac4963..d0b6c96 100755 --- a/src/common/logs_filters.rs +++ b/src/common/logs_filters.rs @@ -1,11 +1,75 @@ use crate::common::logs_data::DexInstruction; -use crate::common::logs_parser::{parse_create_token_data, parse_trade_data}; +use crate::common::logs_parser::{parse_create_token_data, parse_trade_data, parse_instruction_create_token_data, parse_instruction_trade_data}; use crate::error::ClientResult; -use solana_sdk::pubkey::Pubkey; pub struct LogFilter; +use solana_sdk::pubkey::Pubkey; +use std::str::FromStr; + +use solana_sdk::transaction::VersionedTransaction; impl LogFilter { const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; + + /// Parse transaction logs and return instruction type and data + pub fn parse_compiled_instruction( + versioned_tx: VersionedTransaction, + bot_wallet: Option) -> ClientResult> { + let compiled_instructions = versioned_tx.message.instructions(); + let accounts = versioned_tx.message.static_account_keys(); + let program_id = Pubkey::from_str(Self::PROGRAM_ID).unwrap_or_default(); + let pump_index = accounts.iter().position(|key| key == &program_id); + let mut instructions: Vec = Vec::new(); + if let Some(index) = pump_index { + for instruction in compiled_instructions { + if instruction.program_id_index as usize == index { + let all_accounts_valid = instruction.accounts.iter() + .all(|&acc_idx| (acc_idx as usize) < accounts.len()); + if !all_accounts_valid { + continue; + } + match instruction.data.first() { + // create + Some(&24) => { + if let Ok(token_info) = parse_instruction_create_token_data(instruction, accounts) { + instructions.push(DexInstruction::CreateToken(token_info)); + }; + } + // buy + Some(&102) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => { + if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, true) { + if let Some(bot_wallet_pubkey) = bot_wallet { + if trade_info.user.to_string() == bot_wallet_pubkey.to_string() { + instructions.push(DexInstruction::BotTrade(trade_info)); + } else { + instructions.push(DexInstruction::UserTrade(trade_info)); + } + } else { + instructions.push(DexInstruction::UserTrade(trade_info)); + } + }; + } + // sell + Some(&51) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => { + if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, false) { + if let Some(bot_wallet_pubkey) = bot_wallet { + if trade_info.user.to_string() == bot_wallet_pubkey.to_string() { + instructions.push(DexInstruction::BotTrade(trade_info)); + } else { + instructions.push(DexInstruction::UserTrade(trade_info)); + } + } else { + instructions.push(DexInstruction::UserTrade(trade_info)); + } + }; + } + _ => {} + } + } + } + } + Ok(instructions) + } + /// Parse transaction logs and return instruction type and data pub fn parse_instruction(logs: &[String], bot_wallet: Option) -> ClientResult> { diff --git a/src/common/logs_parser.rs b/src/common/logs_parser.rs index 7da29d6..7832f74 100755 --- a/src/common/logs_parser.rs +++ b/src/common/logs_parser.rs @@ -9,6 +9,8 @@ use crate::common::{ }; use solana_sdk::pubkey::Pubkey; +use solana_sdk::instruction::CompiledInstruction; +use std::time::{SystemTime, UNIX_EPOCH}; pub async fn process_logs( signature: &str, @@ -171,4 +173,64 @@ pub fn parse_trade_data(data: &str) -> ClientResult { real_sol_reserves, real_token_reserves, }) +} + +fn current_timestamp_millis() -> i64 { + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + + duration.as_millis() as i64 +} + +pub fn parse_instruction_create_token_data(instruction: &CompiledInstruction, accounts: &[Pubkey]) -> ClientResult { + let data = instruction.data.clone(); + let mut offset = 0; + offset += 8; + let len1 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; + offset += 4; + let name = String::from_utf8_lossy(&data[offset..offset + len1]); + offset += len1; + let len2 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; + offset += 4; + let symbol = String::from_utf8_lossy(&data[offset..offset + len2]); + offset += len2; + let _flag = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()); + offset += 4; + let hash_start = data.len() - 32; + let ipfs_bytes = &data[offset..hash_start]; + let uri = String::from_utf8_lossy(ipfs_bytes); + let mint = accounts[instruction.accounts[0] as usize]; + let user = accounts[instruction.accounts[7] as usize]; + let bonding_curve= accounts[instruction.accounts[2] as usize]; + Ok(CreateTokenInfo { + slot: 0, + name: name.to_string(), + symbol: symbol.to_string(), + uri: uri.to_string(), + mint, + bonding_curve, + user, + }) +} + +pub fn parse_instruction_trade_data(instruction: &CompiledInstruction, accounts: &[Pubkey], is_buy: bool) -> ClientResult { + let data = instruction.data.clone(); + let amount = u64::from_le_bytes(data[8..16].try_into().unwrap()); + let max_sol_cost_or_min_sol_output = u64::from_le_bytes(data[16..24].try_into().unwrap()); + let user = accounts[instruction.accounts[6] as usize]; + let mint = accounts[instruction.accounts[2] as usize]; + Ok(TradeInfo { + slot: 0, + mint, + sol_amount: max_sol_cost_or_min_sol_output, + token_amount: amount, + is_buy, + user, + timestamp: current_timestamp_millis(), + virtual_sol_reserves: 0, + virtual_token_reserves: 0, + real_sol_reserves: 0, + real_token_reserves: 0, + }) } \ No newline at end of file diff --git a/src/common/logs_subscribe.rs b/src/common/logs_subscribe.rs index 8578e7b..2b0d97a 100755 --- a/src/common/logs_subscribe.rs +++ b/src/common/logs_subscribe.rs @@ -8,9 +8,9 @@ use std::sync::Arc; use tokio::sync::mpsc; use tokio::task::JoinHandle; use futures::StreamExt; -use crate::{constants, common::{ - logs_data::DexInstruction, logs_events::DexEvent, logs_filters::LogFilter -}}; +use crate::common::{ + logs_data::DexInstruction, logs_filters::LogFilter +}; use super::logs_events::PumpfunEvent; @@ -41,7 +41,7 @@ pub async fn tokens_subscription( where F: Fn(PumpfunEvent) + Send + Sync + 'static, { - let program_address = constants::accounts::PUMPFUN.to_string(); + let program_address = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P".to_string(); let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]); let logs_config = RpcTransactionLogsConfig { diff --git a/src/common/mod.rs b/src/common/mod.rs index af56e04..7b13f32 100755 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -3,6 +3,4 @@ pub mod logs_parser; pub mod logs_filters; pub mod logs_subscribe; pub mod logs_events; -pub mod types; -pub use types::*; diff --git a/src/common/types.rs b/src/common/types.rs deleted file mode 100755 index 7b1de0d..0000000 --- a/src/common/types.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::sync::Arc; - -use solana_client::rpc_client::RpcClient; -use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair}; -use serde::Deserialize; -use crate::{constants::trade::{DEFAULT_BUY_TIP_FEE, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SELL_TIP_FEE}, swqos::FeeClient}; - -#[derive(Debug, Clone, PartialEq)] -pub enum FeeType { - Jito, - NextBlock, -} - -#[derive(Debug, Clone)] -pub struct Cluster { - pub rpc_url: String, - pub block_engine_url: String, - pub nextblock_url: String, - pub nextblock_auth_token: String, - pub zeroslot_url: String, - pub zeroslot_auth_token: String, - pub use_jito: bool, - pub use_nextblock: bool, - pub use_zeroslot: bool, - pub priority_fee: PriorityFee, - pub commitment: CommitmentConfig, -} - -impl Cluster { - pub fn new( - rpc_url: String, - block_engine_url: - String, nextblock_url: - String, nextblock_auth_token: - String, zeroslot_url: String, - zeroslot_auth_token: String, - priority_fee: PriorityFee, - commitment: CommitmentConfig, - use_jito: bool, - use_nextblock: bool, - use_zeroslot: bool - ) -> Self { - Self { - rpc_url, - block_engine_url, - nextblock_url, - nextblock_auth_token, - zeroslot_url, - zeroslot_auth_token, - priority_fee, - commitment, - use_jito, - use_nextblock, - use_zeroslot - } - } -} - -#[derive(Debug, Deserialize, Clone, Copy, PartialEq)] - -pub struct PriorityFee { - pub unit_limit: u32, - pub unit_price: u64, - pub buy_tip_fee: f64, - pub sell_tip_fee: f64, -} - -impl Default for PriorityFee { - fn default() -> Self { - Self { - unit_limit: DEFAULT_COMPUTE_UNIT_LIMIT, - unit_price: DEFAULT_COMPUTE_UNIT_PRICE, - buy_tip_fee: DEFAULT_BUY_TIP_FEE, - sell_tip_fee: DEFAULT_SELL_TIP_FEE - } - } -} - -pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient; - -pub struct MethodArgs { - pub payer: Arc, - pub rpc: Arc, - pub nonblocking_rpc: Arc, - pub jito_client: Arc, -} - -impl MethodArgs { - pub fn new(payer: Arc, rpc: Arc, nonblocking_rpc: Arc, jito_client: Arc) -> Self { - Self { payer, rpc, nonblocking_rpc, jito_client } - } -} - -pub type AnyResult = anyhow::Result; - diff --git a/src/constants/mod.rs b/src/constants/mod.rs deleted file mode 100755 index 762b3be..0000000 --- a/src/constants/mod.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Constants used by the crate. -//! -//! This module contains various constants used throughout the crate, including: -//! -//! - Seeds for deriving Program Derived Addresses (PDAs) -//! - Program account addresses and public keys -//! -//! The constants are organized into submodules for better organization: -//! -//! - `seeds`: Contains seed values used for PDA derivation -//! - `accounts`: Contains important program account addresses - -/// Constants used as seeds for deriving PDAs (Program Derived Addresses) -pub mod seeds { - /// Seed for the global state PDA - pub const GLOBAL_SEED: &[u8] = b"global"; - - /// Seed for the mint authority PDA - pub const MINT_AUTHORITY_SEED: &[u8] = b"mint-authority"; - - /// Seed for bonding curve PDAs - pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve"; - - /// Seed for creator vault PDAs - pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault"; - - /// Seed for metadata PDAs - pub const METADATA_SEED: &[u8] = b"metadata"; -} - -pub mod global_constants { - use solana_sdk::{pubkey, pubkey::Pubkey}; - - pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000; - - pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000; - - pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000; - - pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000; - - pub const FEE_BASIS_POINTS: u64 = 95; - - pub const ENABLE_MIGRATE: bool = false; - - pub const POOL_MIGRATION_FEE: u64 = 15_000_001; - - pub const CREATOR_FEE: u64 = 5; - - pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals - - pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports - - pub const TOTAL_SUPPLY: u64 = 1_000_000_000 * SCALE; // 1 billion tokens - - pub const BONDING_CURVE_SUPPLY: u64 = 793_100_000 * SCALE; // total supply of bonding curve tokens - - pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL - - /// Public key for the fee recipient - pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); - - /// Public key for the global PDA - pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); - - /// Public key for the authority - pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF"); - - /// Public key for the withdraw authority - pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"); - - pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1 - pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2 - pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3 - pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4 - pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5 - pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6 - pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"); // Pump.fun AMM: Protocol Fee 7 - -} - -/// Constants related to program accounts and authorities -pub mod accounts { - use solana_sdk::{pubkey, pubkey::Pubkey}; - - /// Public key for the Pump.fun program - pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); - - /// Public key for the MPL Token Metadata program - pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); - - /// Authority for program events - pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"); - - /// System Program ID - pub const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111"); - - /// Token Program ID - pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); - - /// Associated Token Program ID - pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); - - /// Rent Sysvar ID - pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111"); - - pub const JITO_TIP_ACCOUNTS: [&str; 8] = [ - "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", - "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", - "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", - "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", - "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", - "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", - "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", - "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", - ]; - - /// Tip accounts - pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[ - "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE", - "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2", - "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X", - "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb", - "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At", - "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG", - "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid", - "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc" - ]; - - pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[ - "Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3", - "FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe", - "ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13", - "6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK", - "Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr", - ]; - - pub const NOZOMI_TIP_ACCOUNTS: &[&str] = &[ - "TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq", - "noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4", - "noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE", - "noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo", - "noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ", - "nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L", - "nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z", - "nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu", - "noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7", - "nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP", - "nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P", - "nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge", - "nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3", - "nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ", - "nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk", - "nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne", - "nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb" - ]; - - pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); -} - -pub mod trade { - pub const TRADER_TIP_AMOUNT: f64 = 0.0001; - pub const DEFAULT_SLIPPAGE: u64 = 1000; // 10% - pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000; - pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000; - pub const DEFAULT_BUY_TIP_FEE: f64 = 0.0006; - pub const DEFAULT_SELL_TIP_FEE: f64 = 0.0001; -} - -pub struct Symbol; - -impl Symbol { - pub const SOLANA: &'static str = "solana"; -} diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 52067a0..228048f 100755 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -1,251 +1,3 @@ -use std::{collections::HashMap, fmt, time::Duration}; +pub mod yellow_stone; -use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt}; -use rustls::crypto::{ring::default_provider, CryptoProvider}; -use tonic::{transport::channel::ClientTlsConfig, Status}; -use yellowstone_grpc_client::{GeyserGrpcClient, GeyserGrpcClientResult}; -use yellowstone_grpc_proto::geyser::{ - CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate, - SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing, -}; -use log::{error, info}; -use chrono::Local; -use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature}; -use solana_transaction_status::{ - option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding, -}; - -use crate::common::logs_data::DexInstruction; -use crate::common::logs_events::PumpfunEvent; -use crate::common::logs_filters::LogFilter; -use crate::error::{ClientError, ClientResult}; - -type TransactionsFilterMap = HashMap; - -const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); -const CONNECT_TIMEOUT: u64 = 10; -const REQUEST_TIMEOUT: u64 = 60; -const CHANNEL_SIZE: usize = 1000; - -#[derive(Clone)] -pub struct TransactionPretty { - pub slot: u64, - pub signature: Signature, - pub is_vote: bool, - pub tx: EncodedTransactionWithStatusMeta, - // pub transaction: Option, -} - -impl fmt::Debug for TransactionPretty { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta); - impl<'a> fmt::Debug for TxWrap<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let serialized = serde_json::to_string(self.0).expect("failed to serialize"); - fmt::Display::fmt(&serialized, f) - } - } - - f.debug_struct("TransactionPretty") - .field("slot", &self.slot) - .field("signature", &self.signature) - .field("is_vote", &self.is_vote) - .field("tx", &TxWrap(&self.tx)) - .finish() - } -} - -impl From for TransactionPretty { - fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self { - let tx = transaction.expect("should be defined"); - // let transaction_info = tx.transaction.clone().unwrap(); - Self { - slot, - signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"), - is_vote: tx.is_vote, - tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx) - .expect("valid tx with meta") - .encode(UiTransactionEncoding::Base64, Some(u8::MAX), true) - .expect("failed to encode"), - // transaction: Some(transaction_info), - } - } -} - -#[derive(Clone)] -pub struct YellowstoneGrpc { - endpoint: String, -} - -impl YellowstoneGrpc { - pub fn new(endpoint: String) -> Self { - Self { endpoint } - } - - pub async fn connect( - &self, - transactions: TransactionsFilterMap, - ) -> ClientResult< - GeyserGrpcClientResult<( - impl Sink, - impl Stream>, - )> - > { - if CryptoProvider::get_default().is_none() { - default_provider() - .install_default() - .map_err(|e| ClientError::Other(format!("Failed to install crypto provider: {:?}", e)))?; - } - - let mut client = GeyserGrpcClient::build_from_shared(self.endpoint.clone()) - .map_err(|e| ClientError::Other(format!("Failed to build client: {:?}", e)))? - .tls_config(ClientTlsConfig::new().with_native_roots()) - .map_err(|e| ClientError::Other(format!("Failed to build client: {:?}", e)))? - .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT)) - .timeout(Duration::from_secs(REQUEST_TIMEOUT)) - .connect() - .await - .map_err(|e| ClientError::Other(format!("Failed to connect: {:?}", e)))?; - - let subscribe_request = SubscribeRequest { - transactions, - commitment: Some(CommitmentLevel::Processed.into()), - ..Default::default() - }; - - Ok(client.subscribe_with_request(Some(subscribe_request)).await) - } - - pub fn get_subscribe_request_filter( - &self, - account_include: Vec, - account_exclude: Vec, - account_required: Vec, - ) -> TransactionsFilterMap { - let mut transactions = HashMap::new(); - transactions.insert( - "client".to_string(), - SubscribeRequestFilterTransactions { - vote: Some(false), - failed: Some(false), - signature: None, - account_include, - account_exclude, - account_required, - }, - ); - transactions - } - - async fn handle_stream_message( - msg: SubscribeUpdate, - tx: &mut mpsc::Sender, - subscribe_tx: &mut (impl Sink + Unpin), - ) -> ClientResult<()> { - match msg.update_oneof { - Some(UpdateOneof::Transaction(sut)) => { - let transaction_pretty = TransactionPretty::from(sut); - tx.try_send(transaction_pretty).map_err(|e| ClientError::Other(format!("Send error: {:?}", e)))?; - } - Some(UpdateOneof::Ping(_)) => { - subscribe_tx - .send(SubscribeRequest { - ping: Some(SubscribeRequestPing { id: 1 }), - ..Default::default() - }) - .await - .map_err(|e| ClientError::Other(format!("Ping error: {:?}", e)))?; - info!("service is ping: {}", Local::now()); - } - Some(UpdateOneof::Pong(_)) => { - info!("service is pong: {}", Local::now()); - } - _ => {} - } - Ok(()) - } - - pub async fn subscribe_pumpfun(&self, callback: F, bot_wallet: Option) -> ClientResult<()> - where - F: Fn(PumpfunEvent) + Send + Sync + 'static, - { - let addrs = vec![PUMP_PROGRAM_ID.to_string()]; - let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]); - let (mut subscribe_tx, mut stream) = self.connect(transactions).await? - .map_err(|e| ClientError::Other(format!("Failed to subscribe: {:?}", e)))?; - let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); - - let callback = Box::new(callback); - - tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { - error!("Error handling message: {:?}", e); - break; - } - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } - } - } - }); - - while let Some(transaction_pretty) = rx.next().await { - if let Err(e) = Self::process_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { - error!("Error processing transaction: {:?}", e); - } - } - Ok(()) - } - - async fn process_pumpfun_transaction(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option) -> ClientResult<()> - where - F: Fn(PumpfunEvent) + Send + Sync, - { - let slot = transaction_pretty.slot; - let trade_raw = transaction_pretty.tx; - let meta = trade_raw.meta.as_ref() - .ok_or_else(|| ClientError::Other("Missing transaction metadata".to_string()))?; - - if meta.err.is_some() { - return Ok(()); - } - - let logs = if let OptionSerializer::Some(logs) = &meta.log_messages { - logs - } else { - &vec![] - }; - - let mut dev_address: Option = None; - let instructions = LogFilter::parse_instruction(logs, bot_wallet).unwrap(); - for instruction in instructions { - match instruction { - DexInstruction::CreateToken(mut token_info) => { - token_info.slot = slot; - dev_address = Some(token_info.user); - callback(PumpfunEvent::NewToken(token_info)); - } - DexInstruction::UserTrade(mut trade_info) => { - trade_info.slot = slot; - if Some(trade_info.user) == dev_address { - callback(PumpfunEvent::NewDevTrade(trade_info)); - } else { - callback(PumpfunEvent::NewUserTrade(trade_info)); - } - } - DexInstruction::BotTrade(mut trade_info) => { - trade_info.slot = slot; - callback(PumpfunEvent::NewBotTrade(trade_info)); - } - _ => {} - } - } - - Ok(()) - } -} +pub use yellow_stone::YellowstoneGrpc; \ No newline at end of file diff --git a/src/grpc/yellow_stone.rs b/src/grpc/yellow_stone.rs new file mode 100755 index 0000000..4423092 --- /dev/null +++ b/src/grpc/yellow_stone.rs @@ -0,0 +1,352 @@ +use std::{collections::HashMap, fmt, time::Duration}; + +use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt}; +use rustls::crypto::{ring::default_provider, CryptoProvider}; +use tonic::{transport::channel::ClientTlsConfig, Status}; +use yellowstone_grpc_client::{GeyserGrpcClient, Interceptor}; +use yellowstone_grpc_proto::geyser::{ + CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate, + SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing, +}; +use log::{error, info}; +use chrono::Local; +use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature}; +use solana_transaction_status::{ + option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding, +}; + +use crate::common::logs_data::{DexInstruction, TransferInfo}; +use crate::common::logs_events::{PumpfunEvent, SystemEvent}; +use crate::common::logs_filters::LogFilter; +pub type AnyResult = anyhow::Result; + +type TransactionsFilterMap = HashMap; + +const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); +const SYSTEM_PROGRAM_ID: Pubkey = pubkey!("11111111111111111111111111111111"); +const CONNECT_TIMEOUT: u64 = 10; +const REQUEST_TIMEOUT: u64 = 60; +const CHANNEL_SIZE: usize = 1000; +const MAX_DECODING_MESSAGE_SIZE: usize = 1024 * 1024 * 10; + +#[derive(Clone)] +pub struct TransactionPretty { + pub slot: u64, + pub signature: Signature, + pub is_vote: bool, + pub tx: EncodedTransactionWithStatusMeta, +} + +impl fmt::Debug for TransactionPretty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta); + impl<'a> fmt::Debug for TxWrap<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let serialized = serde_json::to_string(self.0).expect("failed to serialize"); + fmt::Display::fmt(&serialized, f) + } + } + + f.debug_struct("TransactionPretty") + .field("slot", &self.slot) + .field("signature", &self.signature) + .field("is_vote", &self.is_vote) + .field("tx", &TxWrap(&self.tx)) + .finish() + } +} + +impl From for TransactionPretty { + fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self { + let tx = transaction.expect("should be defined"); + Self { + slot, + signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"), + is_vote: tx.is_vote, + tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx) + .expect("valid tx with meta") + .encode(UiTransactionEncoding::Base64, Some(u8::MAX), true) + .expect("failed to encode"), + } + } +} + +pub struct YellowstoneGrpc { + endpoint: String, + x_token: Option, +} + +impl YellowstoneGrpc { + pub fn new(endpoint: String, x_token: Option) -> AnyResult { + if CryptoProvider::get_default().is_none() { + default_provider() + .install_default() + .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?; + } + + Ok(Self { + endpoint, + x_token, + }) + } + + pub async fn connect( + &self, + ) -> AnyResult> + { + let builder = GeyserGrpcClient::build_from_shared(self.endpoint.clone())? + .x_token(self.x_token.clone())? + .tls_config(ClientTlsConfig::new().with_native_roots())? + .max_decoding_message_size(MAX_DECODING_MESSAGE_SIZE) + .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT)) + .timeout(Duration::from_secs(REQUEST_TIMEOUT)); + + Ok(builder.connect().await?) + } + + pub async fn subscribe_with_request( + &self, + transactions: TransactionsFilterMap, + ) -> AnyResult<( + impl Sink, + impl Stream>, + )> { + let subscribe_request = SubscribeRequest { + transactions, + commitment: Some(CommitmentLevel::Processed.into()), + ..Default::default() + }; + + let mut client = self.connect().await?; + let (sink, stream) = client.subscribe_with_request(Some(subscribe_request)).await?; + Ok((sink, stream)) + } + + pub fn get_subscribe_request_filter( + &self, + account_include: Vec, + account_exclude: Vec, + account_required: Vec, + ) -> TransactionsFilterMap { + let mut transactions = HashMap::new(); + transactions.insert( + "client".to_string(), + SubscribeRequestFilterTransactions { + vote: Some(false), + failed: Some(false), + signature: None, + account_include, + account_exclude, + account_required, + }, + ); + transactions + } + + async fn handle_stream_message( + msg: SubscribeUpdate, + tx: &mut mpsc::Sender, + subscribe_tx: &mut (impl Sink + Unpin), + ) -> AnyResult<()> { + match msg.update_oneof { + Some(UpdateOneof::Transaction(sut)) => { + let transaction_pretty = TransactionPretty::from(sut); + tx.try_send(transaction_pretty)?; + } + Some(UpdateOneof::Ping(_)) => { + subscribe_tx + .send(SubscribeRequest { + ping: Some(SubscribeRequestPing { id: 1 }), + ..Default::default() + }) + .await?; + info!("service is ping: {}", Local::now()); + } + Some(UpdateOneof::Pong(_)) => { + info!("service is pong: {}", Local::now()); + } + _ => {} + } + Ok(()) + } + + pub async fn subscribe_pumpfun(&self, callback: F, bot_wallet: Option) -> AnyResult<()> + where + F: Fn(PumpfunEvent) + Send + Sync + 'static, + { + let addrs = vec![PUMP_PROGRAM_ID.to_string()]; + let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]); + let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?; + let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); + + let callback = Box::new(callback); + + tokio::spawn(async move { + while let Some(message) = stream.next().await { + match message { + Ok(msg) => { + if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { + error!("Error handling message: {:?}", e); + break; + } + } + Err(error) => { + error!("Stream error: {error:?}"); + break; + } + } + } + }); + + while let Some(transaction_pretty) = rx.next().await { + if let Err(e) = Self::process_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { + error!("Error processing transaction: {:?}", e); + } + } + Ok(()) + } + + pub async fn subscribe_pumpfun_with_filter(&self, callback: F, bot_wallet: Option, account_include: Option>, account_exclude: Option>) -> AnyResult<()> + where + F: Fn(PumpfunEvent) + Send + Sync + 'static, + { + let addrs = vec![PUMP_PROGRAM_ID.to_string()]; + let account_include = account_include.unwrap_or_default(); + let account_exclude = account_exclude.unwrap_or_default(); + let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs); + let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?; + let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); + + let callback = Box::new(callback); + + tokio::spawn(async move { + while let Some(message) = stream.next().await { + match message { + Ok(msg) => { + if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { + error!("Error handling message: {:?}", e); + break; + } + } + Err(error) => { + error!("Stream error: {error:?}"); + break; + } + } + } + }); + + while let Some(transaction_pretty) = rx.next().await { + if let Err(e) = Self::process_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { + error!("Error processing transaction: {:?}", e); + } + } + Ok(()) + } + + async fn process_pumpfun_transaction(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option) -> AnyResult<()> + where + F: Fn(PumpfunEvent) + Send + Sync, + { + let slot = transaction_pretty.slot; + let trade_raw: EncodedTransactionWithStatusMeta = transaction_pretty.tx; + let meta = trade_raw.meta.as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?; + + if meta.err.is_some() { + return Ok(()); + } + + let logs = if let OptionSerializer::Some(logs) = &meta.log_messages { + logs + } else { + &vec![] + }; + + let mut dev_address: Option = None; + let instructions = LogFilter::parse_instruction(logs, bot_wallet).unwrap(); + for instruction in instructions { + match instruction { + DexInstruction::CreateToken(mut token_info) => { + token_info.slot = slot; + dev_address = Some(token_info.user); + callback(PumpfunEvent::NewToken(token_info)); + } + DexInstruction::UserTrade(mut trade_info) => { + trade_info.slot = slot; + if Some(trade_info.user) == dev_address { + callback(PumpfunEvent::NewDevTrade(trade_info)); + } else { + callback(PumpfunEvent::NewUserTrade(trade_info)); + } + } + DexInstruction::BotTrade(mut trade_info) => { + trade_info.slot = slot; + callback(PumpfunEvent::NewBotTrade(trade_info)); + } + _ => {} + } + } + + Ok(()) + } + + pub async fn subscribe_system(&self, callback: F, account_include: Option>, account_exclude: Option>) -> AnyResult<()> + where + F: Fn(SystemEvent) + Send + Sync + 'static, + { + let addrs = vec![SYSTEM_PROGRAM_ID.to_string()]; + let account_include = account_include.unwrap_or_default(); + let account_exclude = account_exclude.unwrap_or_default(); + let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs); + let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?; + let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); + + let callback = Box::new(callback); + + tokio::spawn(async move { + while let Some(message) = stream.next().await { + match message { + Ok(msg) => { + if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { + error!("Error handling message: {:?}", e); + break; + } + } + Err(error) => { + error!("Stream error: {error:?}"); + break; + } + } + } + }); + + while let Some(transaction_pretty) = rx.next().await { + if let Err(e) = Self::process_system_transaction(transaction_pretty, &*callback).await { + error!("Error processing transaction: {:?}", e); + } + } + Ok(()) + } + + async fn process_system_transaction(transaction_pretty: TransactionPretty, callback: &F) -> AnyResult<()> + where + F: Fn(SystemEvent) + Send + Sync, + { + let trade_raw: EncodedTransactionWithStatusMeta = transaction_pretty.tx; + let meta = trade_raw.meta.as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?; + + if meta.err.is_some() { + return Ok(()); + } + + callback(SystemEvent::NewTransfer(TransferInfo { + slot: transaction_pretty.slot, + signature: transaction_pretty.signature.to_string(), + tx: trade_raw.transaction.decode(), + })); + + Ok(()) + } +} diff --git a/src/instruction/mod.rs b/src/instruction/mod.rs deleted file mode 100755 index 2dadbf8..0000000 --- a/src/instruction/mod.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Instructions for interacting with the Pump.fun program. -//! -//! This module contains instruction builders for creating Solana instructions to interact with the -//! Pump.fun program. Each function takes the required accounts and instruction data and returns a -//! properly formatted Solana instruction. -//! -//! # Instructions -//! -//! - `create`: Instruction to create a new token with an associated bonding curve. -//! - `buy`: Instruction to buy tokens from a bonding curve by providing SOL. -//! - `sell`: Instruction to sell tokens back to the bonding curve in exchange for SOL. -use crate::{ - constants, - pumpfun::common::{ - get_bonding_curve_pda, get_global_pda, get_metadata_pda, get_mint_authority_pda - }, -}; -use spl_associated_token_account::get_associated_token_address; - -use solana_sdk::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - signature::Keypair, - signer::Signer, -}; - -pub struct Buy { - pub _amount: u64, - pub _max_sol_cost: u64, -} - -impl Buy { - pub fn data(&self) -> Vec { - let mut data = Vec::with_capacity(8 + 8 + 8); - data.extend_from_slice(&[102, 6, 61, 18, 1, 218, 235, 234]); // discriminator - data.extend_from_slice(&self._amount.to_le_bytes()); - data.extend_from_slice(&self._max_sol_cost.to_le_bytes()); - data - } -} - -pub struct Sell { - pub _amount: u64, - pub _min_sol_output: u64, -} - -impl Sell { - pub fn data(&self) -> Vec { - let mut data = Vec::with_capacity(8 + 8 + 8); - data.extend_from_slice(&[51, 230, 133, 164, 1, 127, 131, 173]); // discriminator - data.extend_from_slice(&self._amount.to_le_bytes()); - data.extend_from_slice(&self._min_sol_output.to_le_bytes()); - data - } -} - -/// Creates an instruction to buy tokens from a bonding curve -/// -/// Buys tokens by providing SOL. The amount of tokens received is calculated based on -/// the bonding curve formula. A portion of the SOL is taken as a fee and sent to the -/// fee recipient account. -/// -/// # Arguments -/// -/// * `payer` - Keypair that will provide the SOL to buy tokens -/// * `mint` - Public key of the token mint to buy -/// * `fee_recipient` - Public key of the account that will receive the transaction fee -/// * `args` - Buy instruction data containing the SOL amount and maximum acceptable token price -/// -/// # Returns -/// -/// Returns a Solana instruction that when executed will buy tokens from the bonding curve -pub fn buy( - payer: &Keypair, - mint: &Pubkey, - bonding_curve: &Pubkey, - creator_vault: &Pubkey, - fee_recipient: &Pubkey, - args: Buy, -) -> Instruction { - Instruction::new_with_bytes( - constants::accounts::PUMPFUN, - &args.data(), - vec![ - AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false), - AccountMeta::new(*fee_recipient, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new(*bonding_curve, false), - AccountMeta::new(get_associated_token_address(bonding_curve, mint), false), - AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), - AccountMeta::new(payer.pubkey(), true), - AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false), - AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false), - AccountMeta::new(*creator_vault, false), - AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false), - AccountMeta::new_readonly(constants::accounts::PUMPFUN, false), - ], - ) -} - -/// Creates an instruction to sell tokens back to a bonding curve -/// -/// Sells tokens back to the bonding curve in exchange for SOL. The amount of SOL received -/// is calculated based on the bonding curve formula. A portion of the SOL is taken as -/// a fee and sent to the fee recipient account. -/// -/// # Arguments -/// -/// * `payer` - Keypair that owns the tokens to sell -/// * `mint` - Public key of the token mint to sell -/// * `fee_recipient` - Public key of the account that will receive the transaction fee -/// * `args` - Sell instruction data containing token amount and minimum acceptable SOL output -/// -/// # Returns -/// -/// Returns a Solana instruction that when executed will sell tokens to the bonding curve -pub fn sell( - payer: &Keypair, - mint: &Pubkey, - bonding_curve: &Pubkey, - creator_vault: &Pubkey, - fee_recipient: &Pubkey, - args: Sell, -) -> Instruction { - Instruction::new_with_bytes( - constants::accounts::PUMPFUN, - &args.data(), - vec![ - AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false), - AccountMeta::new(*fee_recipient, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new(*bonding_curve, false), - AccountMeta::new(get_associated_token_address(&bonding_curve, mint), false), - AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), - AccountMeta::new(payer.pubkey(), true), - AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false), - AccountMeta::new(*creator_vault, false), - AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false), - AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false), - AccountMeta::new_readonly(constants::accounts::PUMPFUN, false), - ], - ) -} - diff --git a/src/ipfs/mod.rs b/src/ipfs/mod.rs deleted file mode 100755 index 9a5e538..0000000 --- a/src/ipfs/mod.rs +++ /dev/null @@ -1,161 +0,0 @@ -use std::time::Duration; - -use serde_json::Value; -use tokio::fs::File; -use tokio::io::AsyncReadExt; -use reqwest::Client; -use reqwest::multipart::{Form, Part}; -use base64::{Engine as _, engine::general_purpose}; -use serde::{Deserialize, Serialize}; - -/// Metadata structure for a token, matching the format expected by Pump.fun. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TokenMetadata { - /// Name of the token - pub name: String, - /// Token symbol (e.g. "BTC") - pub symbol: String, - /// Description of the token - pub description: String, - /// IPFS URL of the token's image - pub image: String, - /// Whether to display the token's name - pub show_name: bool, - /// Creation timestamp/source - pub created_on: String, - /// Twitter handle - pub twitter: Option, - /// Telegram handle - pub telegram: Option, - /// Website URL - pub website: Option, -} - -/// Response received after successfully uploading token metadata. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TokenMetadataIPFS { - /// The uploaded token metadata - pub metadata: TokenMetadata, - /// IPFS URI where the metadata is stored - pub metadata_uri: String, -} - -/// Parameters for creating new token metadata. -#[derive(Debug, Clone)] -pub struct CreateTokenMetadata { - /// Name of the token - pub name: String, - /// Token symbol (e.g. "BTC") - pub symbol: String, - /// Description of the token - pub description: String, - /// Path to the token's image file - pub file: String, - /// Optional Twitter handle - pub twitter: Option, - /// Optional Telegram group - pub telegram: Option, - /// Optional website URL - pub website: Option, - - pub metadata_uri: Option, -} - -pub async fn create_token_metadata(metadata: CreateTokenMetadata, jwt_token: &str) -> Result { - let ipfs_url = if metadata.file.starts_with("http") || metadata.metadata_uri.is_some() { - metadata.file - } else { - let base64_string = file_to_base64(&metadata.file).await?; - upload_base64_file(&base64_string, jwt_token).await? - }; - - let token_metadata = TokenMetadata { - name: metadata.name, - symbol: metadata.symbol, - description: metadata.description, - image: ipfs_url, - show_name: true, - created_on: "https://pump.fun".to_string(), - twitter: metadata.twitter, - telegram: metadata.telegram, - website: metadata.website, - }; - - if metadata.metadata_uri.is_some() { - let token_metadata_ipfs = TokenMetadataIPFS { - metadata: token_metadata, - metadata_uri: metadata.metadata_uri.unwrap(), - }; - Ok(token_metadata_ipfs) - } else { - let client = Client::new(); - let response = client - .post("https://api.pinata.cloud/pinning/pinJSONToIPFS") - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", jwt_token)) - .json(&token_metadata) - .send() - .await?; - - // 确保请求成功 - if response.status().is_success() { - let res_data: serde_json::Value = response.json().await?; - let ipfs_hash = res_data["IpfsHash"].as_str().unwrap(); - let ipfs_url = format!("https://ipfs.io/ipfs/{}", ipfs_hash); - let token_metadata_ipfs = TokenMetadataIPFS { - metadata: token_metadata, - metadata_uri: ipfs_url, - }; - Ok(token_metadata_ipfs) - } else { - eprintln!("Error: {:?}", response.status()); - Err(anyhow::anyhow!("Failed to create token metadata")) - } - } -} - -pub async fn upload_base64_file(base64_string: &str, jwt_token: &str) -> Result { - let decoded_bytes = general_purpose::STANDARD.decode(base64_string)?; - - let client = Client::builder() - .timeout(Duration::from_secs(120)) // 增加超时时间到120秒 - .pool_max_idle_per_host(0) // 禁用连接池 - .pool_idle_timeout(None) // 禁用空闲超时 - .build()?; - - let part = Part::bytes(decoded_bytes) - .file_name("file.png") // 添加文件扩展名 - .mime_str("image/png")?; // 指定正确的MIME类型 - - let form = Form::new().part("file", part); - - let response = client - .post("https://api.pinata.cloud/pinning/pinFileToIPFS") - .header("Authorization", format!("Bearer {}", jwt_token)) - .header("Accept", "application/json") - .multipart(form) - .send() - .await?; - - if response.status().is_success() { - let response_json: Value = response.json().await.map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}", e))?; - println!("{:#?}", response_json); - let ipfs_hash = response_json["IpfsHash"].as_str().unwrap(); - let ipfs_url = format!("https://ipfs.io/ipfs/{}", ipfs_hash); - Ok(ipfs_url) - } else { - let error_text = response.text().await?; - eprintln!("Error: {:?}", error_text); - Err(anyhow::anyhow!("Failed to upload file to IPFS: {}", error_text)) - } -} - -async fn file_to_base64(file_path: &str) -> Result { - let mut file = File::open(file_path).await?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer).await?; - let base64_string = general_purpose::STANDARD.encode(&buffer); - Ok(base64_string) -} diff --git a/src/lib.rs b/src/lib.rs index 9f54e10..db18ab9 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,261 +1,3 @@ -pub mod accounts; -pub mod constants; -pub mod error; -pub mod instruction; -pub mod grpc; pub mod common; -pub mod ipfs; -pub mod swqos; -pub mod pumpfun; - -use std::sync::Arc; - -use swqos::{FeeClient, JitoClient, NextBlockClient, ZeroSlotClient}; -use rustls::crypto::{ring::default_provider, CryptoProvider}; -use solana_sdk::{ - commitment_config::CommitmentConfig, - pubkey::Pubkey, - signature::{Keypair, Signer}, -}; - -use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe, Cluster, PriorityFee, SolanaRpcClient}; -use common::logs_subscribe::SubscriptionHandle; -use ipfs::TokenMetadataIPFS; - -pub struct PumpFun { - pub payer: Arc, - pub rpc: Arc, - pub fee_clients: Vec>, - pub priority_fee: PriorityFee, - pub cluster: Cluster, -} - -impl Clone for PumpFun { - fn clone(&self) -> Self { - Self { - payer: self.payer.clone(), - rpc: self.rpc.clone(), - fee_clients: self.fee_clients.clone(), - priority_fee: self.priority_fee.clone(), - cluster: self.cluster.clone(), - } - } -} - -impl PumpFun { - #[inline] - pub async fn new( - payer: Arc, - cluster: &Cluster, - ) -> Self { - if CryptoProvider::get_default().is_none() { - let _ = default_provider() - .install_default() - .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e)); - } - - let rpc = SolanaRpcClient::new_with_commitment( - cluster.clone().rpc_url, - cluster.clone().commitment - ); - - let mut fee_clients: Vec> = vec![]; - if cluster.clone().use_jito { - let jito_client = JitoClient::new( - cluster.clone().rpc_url, - cluster.clone().block_engine_url - ).await.expect("Failed to create Jito client"); - - fee_clients.push(Arc::new(jito_client)); - } - - if cluster.clone().use_zeroslot { - let zeroslot_client = ZeroSlotClient::new( - cluster.clone().rpc_url, - cluster.clone().zeroslot_url, - cluster.clone().zeroslot_auth_token - ); - - fee_clients.push(Arc::new(zeroslot_client)); - } - - if cluster.clone().use_nextblock { - let nextblock_client = NextBlockClient::new( - cluster.clone().rpc_url, - cluster.clone().nextblock_url, - cluster.clone().nextblock_auth_token - ); - - fee_clients.push(Arc::new(nextblock_client)); - } - - Self { - payer, - rpc: Arc::new(rpc), - fee_clients, - priority_fee: cluster.clone().priority_fee, - cluster: cluster.clone(), - } - } - - /// Buy tokens - pub async fn buy( - &self, - mint: Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - ) -> Result<(), anyhow::Error> { - pumpfun::buy::buy( - self.rpc.clone(), - self.payer.clone(), - mint, - amount_sol, - slippage_basis_points, - self.priority_fee.clone(), - ).await - } - - /// Buy tokens using Jito - pub async fn buy_with_tip( - &self, - mint: Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - ) -> Result<(), anyhow::Error> { - pumpfun::buy::buy_with_tip( - self.rpc.clone(), - self.fee_clients.clone(), - self.payer.clone(), - mint, - amount_sol, - slippage_basis_points, - self.priority_fee.clone(), - ).await - } - - /// Sell tokens - pub async fn sell( - &self, - mint: Pubkey, - amount_token: Option, - ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell( - self.rpc.clone(), - self.payer.clone(), - mint.clone(), - amount_token, - self.priority_fee.clone(), - ).await - } - - /// Sell tokens by percentage - pub async fn sell_by_percent( - &self, - mint: Pubkey, - percent: u64, - ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell_by_percent( - self.rpc.clone(), - self.payer.clone(), - mint.clone(), - percent, - self.priority_fee.clone(), - ).await - } - - pub async fn sell_by_percent_with_tip( - &self, - mint: Pubkey, - percent: u64, - ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell_by_percent_with_tip( - self.rpc.clone(), - self.fee_clients.clone(), - self.payer.clone(), - mint, - percent, - self.priority_fee.clone(), - ).await - } - - /// Sell tokens using Jito - pub async fn sell_with_tip( - &self, - mint: Pubkey, - amount_token: Option, - ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell_with_tip( - self.rpc.clone(), - self.fee_clients.clone(), - self.payer.clone(), - mint, - amount_token, - self.priority_fee.clone(), - ).await - } - - #[inline] - pub async fn tokens_subscription( - &self, - ws_url: &str, - commitment: CommitmentConfig, - callback: F, - bot_wallet: Option, - ) -> Result> - where - F: Fn(PumpfunEvent) + Send + Sync + 'static, - { - logs_subscribe::tokens_subscription(ws_url, commitment, callback, bot_wallet).await - } - - #[inline] - pub async fn stop_subscription(&self, subscription_handle: SubscriptionHandle) { - subscription_handle.shutdown().await; - } - - #[inline] - pub async fn get_sol_balance(&self, payer: &Pubkey) -> Result { - pumpfun::common::get_sol_balance(&self.rpc, payer).await - } - - #[inline] - pub async fn get_payer_sol_balance(&self) -> Result { - pumpfun::common::get_sol_balance(&self.rpc, &self.payer.pubkey()).await - } - - #[inline] - pub async fn get_token_balance(&self, payer: &Pubkey, mint: &Pubkey) -> Result { - println!("get_token_balance payer: {}, mint: {}, cluster: {}", payer, mint, self.cluster.rpc_url); - pumpfun::common::get_token_balance(&self.rpc, payer, mint).await - } - - #[inline] - pub async fn get_payer_token_balance(&self, mint: &Pubkey) -> Result { - pumpfun::common::get_token_balance(&self.rpc, &self.payer.pubkey(), mint).await - } - - #[inline] - pub fn get_payer_pubkey(&self) -> Pubkey { - self.payer.pubkey() - } - - #[inline] - pub fn get_payer(&self) -> &Keypair { - self.payer.as_ref() - } - - #[inline] - pub fn get_token_price(&self,virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { - pumpfun::common::get_token_price(virtual_sol_reserves, virtual_token_reserves) - } - - #[inline] - pub fn get_buy_price(&self, amount: u64, trade_info: &TradeInfo) -> u64 { - pumpfun::common::get_buy_price(amount, trade_info) - } - - #[inline] - pub async fn transfer_sol(&self, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { - pumpfun::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await - } -} +pub mod grpc; +pub mod error; diff --git a/src/main.rs b/src/main.rs index 030c06e..d571856 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,38 @@ -use std::sync::Arc; -use pumpfun_sdk::{common::{logs_events::PumpfunEvent, Cluster, PriorityFee}, grpc::YellowstoneGrpc, ipfs, PumpFun}; -use solana_sdk::{commitment_config::CommitmentConfig, native_token::sol_to_lamports, signature::Keypair, signer::Signer}; +use grpc_parsed::{common::{ + logs_events::PumpfunEvent, +}, grpc::YellowstoneGrpc}; #[tokio::main] async fn main() -> Result<(), Box> { - // create grpc client - let grpc_url = "http://127.0.0.1:10000"; - let client = YellowstoneGrpc::new(grpc_url.to_string()); + let grpc = YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + )?; + + println!("Connected to the network"); - // Define callback function let callback = |event: PumpfunEvent| { + match event { + PumpfunEvent::NewDevTrade(trade_info) => { + println!("Received new dev trade event: {:?}", trade_info); + }, PumpfunEvent::NewToken(token_info) => { println!("Received new token event: {:?}", token_info); }, - PumpfunEvent::NewDevTrade(trade_info) => { - println!("Received dev trade event: {:?}", trade_info); - }, PumpfunEvent::NewUserTrade(trade_info) => { println!("Received new trade event: {:?}", trade_info); }, PumpfunEvent::NewBotTrade(trade_info) => { println!("Received new bot trade event: {:?}", trade_info); - } + }, PumpfunEvent::Error(err) => { println!("Received error: {}", err); } } }; - let payer_keypair = Keypair::from_base58_string("your private key"); - client.subscribe_pumpfun(callback, Some(payer_keypair.pubkey())).await?; + grpc.subscribe_pumpfun(callback, None).await?; Ok(()) } \ No newline at end of file diff --git a/src/pumpfun/buy.rs b/src/pumpfun/buy.rs deleted file mode 100755 index 334db94..0000000 --- a/src/pumpfun/buy.rs +++ /dev/null @@ -1,183 +0,0 @@ -use anyhow::anyhow; -use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} -}; -use solana_hash::Hash; -use spl_associated_token_account::instruction::create_associated_token_account; -use tokio::task::JoinHandle; -use std::{str::FromStr, time::Instant, sync::Arc}; - -use crate::{common::{PriorityFee, SolanaRpcClient}, constants::{self, global_constants::FEE_RECIPIENT}, instruction, swqos::FeeClient}; - -const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 250000; - -use super::common::{calculate_with_slippage_buy, get_bonding_curve_account, get_buy_token_amount_from_sol_amount, get_creator_vault_pda}; - -pub async fn buy( - rpc: Arc, - payer: Arc, - mint: Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result<(), anyhow::Error> { - let transaction = build_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points, priority_fee.clone()).await?; - rpc.send_and_confirm_transaction(&transaction).await?; - Ok(()) -} - -/// Buy tokens using Jito -pub async fn buy_with_tip( - rpc: Arc, - fee_clients: Vec>, - payer: Arc, - mint: Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result<(), anyhow::Error> { - let start_time = Instant::now(); - - let mint = Arc::new(mint.clone()); - let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?; - - let mut transactions = vec![]; - let recent_blockhash = rpc.get_latest_blockhash().await?; - for fee_client in fee_clients.clone() { - let payer = payer.clone(); - let priority_fee = priority_fee.clone(); - let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?; - let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); - - let transaction = build_buy_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?; - transactions.push(transaction); - } - - let mut handles: Vec>> = vec![]; - for i in 0..fee_clients.len() { - let fee_client = fee_clients[i].clone(); - let transactions = transactions.clone(); - let start_time = start_time.clone(); - let transaction = transactions[i].clone(); - let handle = tokio::spawn(async move { - fee_client.send_transaction(&transaction).await?; - println!("index: {}, Total Jito buy operation time: {:?}ms", i, start_time.elapsed().as_millis()); - Ok::<(), anyhow::Error>(()) - }); - - handles.push(handle); - } - - for handle in handles { - match handle.await { - Ok(Ok(_)) => (), - Ok(Err(e)) => println!("Error in task: {}", e), - Err(e) => println!("Task join error: {}", e), - } - } - - Ok(()) -} - -pub async fn build_buy_transaction( - rpc: Arc, - payer: Arc, - mint: Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let mut instructions = vec![ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), - ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), - ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), - ]; - - let build_instructions = build_buy_instructions(rpc.clone(), payer.clone(), Arc::new(mint), amount_sol, slippage_basis_points).await?; - instructions.extend(build_instructions); - - let recent_blockhash = rpc.get_latest_blockhash().await?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - recent_blockhash, - ); - - Ok(transaction) -} - -pub async fn build_buy_transaction_with_tip( - tip_account: Arc, - payer: Arc, - priority_fee: PriorityFee, - build_instructions: Vec, - blockhash: Hash, -) -> Result { - let mut instructions = vec![ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), - ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), - ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), - system_instruction::transfer( - &payer.pubkey(), - &tip_account, - sol_to_lamports(priority_fee.buy_tip_fee), - ), - ]; - - instructions.extend(build_instructions); - - let v0_message: v0::Message = - v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?; - let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; - - Ok(transaction) -} - -pub async fn build_buy_instructions( - rpc: Arc, - payer: Arc, - mint: Arc, - buy_sol_cost: u64, - slippage_basis_points: Option, -) -> Result, anyhow::Error> { - if buy_sol_cost == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let (bonding_curve, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint).await?; - let creator_vault_pda = get_creator_vault_pda(&bonding_curve.creator).unwrap(); - let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(100)); - - let mut buy_token_amount = get_buy_token_amount_from_sol_amount(&bonding_curve, buy_sol_cost); - if buy_token_amount <= 100 * 1_000_000_u64 { - buy_token_amount = if max_sol_cost > sol_to_lamports(0.01) { - 25547619 * 1_000_000_u64 - } else { - 255476 * 1_000_000_u64 - }; - } - - let mut instructions = vec![]; - instructions.push(create_associated_token_account( - &payer.pubkey(), - &payer.pubkey(), - &mint, - &constants::accounts::TOKEN_PROGRAM, - )); - - instructions.push(instruction::buy( - payer.as_ref(), - &mint, - &bonding_curve_pda, - &creator_vault_pda, - &FEE_RECIPIENT, - instruction::Buy { - _amount: buy_token_amount, - _max_sol_cost: max_sol_cost, - }, - )); - - Ok(instructions) -} \ No newline at end of file diff --git a/src/pumpfun/common.rs b/src/pumpfun/common.rs deleted file mode 100755 index fa2995c..0000000 --- a/src/pumpfun/common.rs +++ /dev/null @@ -1,270 +0,0 @@ -use anyhow::anyhow; -use spl_token::state::Account; -use tokio::sync::RwLock; -use std::{collections::HashMap, sync::Arc}; -use solana_sdk::{ - commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction -}; -use spl_associated_token_account::get_associated_token_address; -use crate::{accounts::{self, BondingCurveAccount}, common::{logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}; -use borsh::BorshDeserialize; - -lazy_static::lazy_static! { - static ref ACCOUNT_CACHE: RwLock>> = RwLock::new(HashMap::new()); -} - -pub async fn transfer_sol(rpc: &SolanaRpcClient, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { - if amount == 0 { - return Err(anyhow!("transfer_sol: Amount cannot be zero")); - } - - let balance = get_sol_balance(rpc, &payer.pubkey()).await?; - if balance < amount { - return Err(anyhow!("Insufficient balance")); - } - - let transfer_instruction = system_instruction::transfer( - &payer.pubkey(), - receive_wallet, - amount, - ); - - let recent_blockhash = rpc.get_latest_blockhash().await?; - - let transaction = Transaction::new_signed_with_payer( - &[transfer_instruction], - Some(&payer.pubkey()), - &[payer], - recent_blockhash, - ); - - rpc.send_and_confirm_transaction(&transaction).await?; - - Ok(()) -} - -#[inline] -pub fn create_priority_fee_instructions(priority_fee: PriorityFee) -> Vec { - let mut instructions = Vec::with_capacity(2); - instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit)); - instructions.push(ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price)); - - instructions -} - -// #[inline] -pub async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Pubkey, mint: &Pubkey) -> Result { - let ata = get_associated_token_address(payer, mint); - // let account_data = rpc.get_account_data(&ata).await?; - // let token_account = Account::unpack(&account_data.as_slice())?; - - // Ok(token_account.amount) - - // println!("get_token_balance ata: {}", ata); - let balance = rpc.get_token_account_balance(&ata).await?; - let balance_u64 = balance.amount.parse::() - .map_err(|_| anyhow!("Failed to parse token balance"))?; - Ok(balance_u64) -} - -#[inline] -pub async fn get_token_balance_and_ata(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> { - let ata = get_associated_token_address(&payer.pubkey(), mint); - // let account_data = rpc.get_account_data(&ata).await?; - // let token_account = Account::unpack(&account_data)?; - - // Ok((token_account.amount, ata)) - - let balance = rpc.get_token_account_balance(&ata).await?; - let balance_u64 = balance.amount.parse::() - .map_err(|_| anyhow!("Failed to parse token balance"))?; - - if balance_u64 == 0 { - return Err(anyhow!("Balance is 0")); - } - - Ok((balance_u64, ata)) -} - -#[inline] -pub async fn get_sol_balance(rpc: &SolanaRpcClient, account: &Pubkey) -> Result { - let balance = rpc.get_balance(account).await?; - Ok(balance) -} - -#[inline] -pub fn get_global_pda() -> Pubkey { - static GLOBAL_PDA: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { - Pubkey::find_program_address(&[constants::seeds::GLOBAL_SEED], &constants::accounts::PUMPFUN).0 - }); - *GLOBAL_PDA -} - -#[inline] -pub fn get_mint_authority_pda() -> Pubkey { - static MINT_AUTHORITY_PDA: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { - Pubkey::find_program_address(&[constants::seeds::MINT_AUTHORITY_SEED], &constants::accounts::PUMPFUN).0 - }); - *MINT_AUTHORITY_PDA -} - -#[inline] -pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option { - let seeds: &[&[u8]; 2] = &[constants::seeds::BONDING_CURVE_SEED, mint.as_ref()]; - let program_id: &Pubkey = &constants::accounts::PUMPFUN; - let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); - pda.map(|pubkey| pubkey.0) -} - -#[inline] -pub fn get_creator_vault_pda(creator: &Pubkey) -> Option { - let seeds: &[&[u8]; 2] = &[constants::seeds::CREATOR_VAULT_SEED, creator.as_ref()]; - let program_id: &Pubkey = &constants::accounts::PUMPFUN; - let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); - pda.map(|pubkey| pubkey.0) -} - -#[inline] -pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey { - Pubkey::find_program_address( - &[ - constants::seeds::METADATA_SEED, - constants::accounts::MPL_TOKEN_METADATA.as_ref(), - mint.as_ref(), - ], - &constants::accounts::MPL_TOKEN_METADATA - ).0 -} - -#[inline] -pub async fn get_global_account(rpc: &SolanaRpcClient) -> Result, anyhow::Error> { - let global = get_global_pda(); - if let Some(account) = ACCOUNT_CACHE.read().await.get(&global) { - return Ok(account.clone()); - } - - let account = rpc.get_account(&global).await?; - let global_account = bincode::deserialize::(&account.data)?; - let global_account = Arc::new(global_account); - - ACCOUNT_CACHE.write().await.insert(global, global_account.clone()); - Ok(global_account) -} - -#[inline] -pub async fn get_initial_buy_price(global_account: &Arc, amount_sol: u64) -> Result { - let buy_amount = global_account.get_initial_buy_price(amount_sol); - Ok(buy_amount) -} - -#[inline] -pub async fn get_bonding_curve_account( - rpc: &SolanaRpcClient, - mint: &Pubkey, -) -> Result<(Arc, Pubkey), anyhow::Error> { - let bonding_curve_pda = get_bonding_curve_pda(mint) - .ok_or(anyhow!("Bonding curve not found"))?; - - let account = rpc.get_account(&bonding_curve_pda).await?; - if account.data.is_empty() { - return Err(anyhow!("Bonding curve not found")); - } - - let bonding_curve = Arc::new(bincode::deserialize::(&account.data)?); - - Ok((bonding_curve, bonding_curve_pda)) -} - -#[inline] -pub fn get_buy_token_amount( - bonding_curve_account: &Arc, - buy_sol_cost: u64, - slippage_basis_points: Option, -) -> anyhow::Result<(u64, u64)> { - let buy_token = bonding_curve_account.get_buy_price(buy_sol_cost).map_err(|e| anyhow!(e))?; - - let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - - Ok((buy_token, max_sol_cost)) -} - -pub fn get_buy_token_amount_from_sol_amount( - bonding_curve: &BondingCurveAccount, - amount: u64, -) -> u64 { - if amount == 0 { - return 0; - } - - if bonding_curve.virtual_token_reserves == 0 { - return 0; - } - - let total_fee_basis_points = FEE_BASIS_POINTS - + if bonding_curve.creator != Pubkey::default() { - CREATOR_FEE - } else { - 0 - }; - - // 转为 u128 防止溢出 - let amount_128 = amount as u128; - let total_fee_basis_points_128 = total_fee_basis_points as u128; - let input_amount = amount_128 - .checked_mul(10_000) - .unwrap() - .checked_div(total_fee_basis_points_128 + 10_000) - .unwrap(); - - let virtual_token_reserves = bonding_curve.virtual_token_reserves as u128; - let virtual_sol_reserves = bonding_curve.virtual_sol_reserves as u128; - let real_token_reserves = bonding_curve.real_token_reserves as u128; - - let denominator = virtual_sol_reserves + input_amount; - - let tokens_received = input_amount - .checked_mul(virtual_token_reserves) - .unwrap() - .checked_div(denominator) - .unwrap(); - - tokens_received.min(real_token_reserves) as u64 -} - -#[inline] -pub fn get_buy_amount_with_slippage(amount_sol: u64, slippage_basis_points: Option) -> u64 { - let slippage = slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE); - amount_sol + (amount_sol * slippage / 10000) -} - -#[inline] -pub fn get_token_price(virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { - let v_sol = virtual_sol_reserves as f64 / 100_000_000.0; - let v_tokens = virtual_token_reserves as f64 / 100_000.0; - v_sol / v_tokens -} - -#[inline] -pub fn get_buy_price(amount: u64, trade_info: &TradeInfo) -> u64 { - if amount == 0 { - return 0; - } - - let n: u128 = (trade_info.virtual_sol_reserves as u128) * (trade_info.virtual_token_reserves as u128); - let i: u128 = (trade_info.virtual_sol_reserves as u128) + (amount as u128); - let r: u128 = n / i + 1; - let s: u128 = (trade_info.virtual_token_reserves as u128) - r; - let s_u64 = s as u64; - - s_u64.min(trade_info.real_token_reserves) -} - -#[inline] -pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 { - amount + (amount * basis_points) / 10000 -} - -#[inline] -pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 { - amount - (amount * basis_points) / 10000 -} diff --git a/src/pumpfun/mod.rs b/src/pumpfun/mod.rs deleted file mode 100755 index 663177e..0000000 --- a/src/pumpfun/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod buy; -pub mod sell; -pub mod common; \ No newline at end of file diff --git a/src/pumpfun/sell.rs b/src/pumpfun/sell.rs deleted file mode 100755 index 893c775..0000000 --- a/src/pumpfun/sell.rs +++ /dev/null @@ -1,219 +0,0 @@ -use anyhow::anyhow; -use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} -}; -use solana_hash::Hash; -use spl_associated_token_account::get_associated_token_address; -use spl_token::instruction::close_account; -use tokio::task::JoinHandle; - -use std::{str::FromStr, time::Instant, sync::Arc}; - -use crate::{common::{PriorityFee, SolanaRpcClient}, instruction, swqos::FeeClient}; - -use super::common::{get_bonding_curve_account, get_creator_vault_pda, get_global_account}; - -async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> { - let ata = get_associated_token_address(&payer.pubkey(), mint); - let balance = rpc.get_token_account_balance(&ata).await?; - let balance_u64 = balance.amount.parse::() - .map_err(|_| anyhow!("Failed to parse token balance"))?; - - if balance_u64 == 0 { - return Err(anyhow!("Balance is 0")); - } - - Ok((balance_u64, ata)) -} - -pub async fn sell( - rpc: Arc, - payer: Arc, - mint: Pubkey, - amount_token: Option, - priority_fee: PriorityFee, -) -> Result<(), anyhow::Error> { - let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token).await?; - let transaction = build_sell_transaction(rpc.clone(), payer.clone(), priority_fee, instructions).await?; - rpc.send_and_confirm_transaction(&transaction).await?; - - Ok(()) -} - -/// Sell tokens by percentage -pub async fn sell_by_percent( - rpc: Arc, - payer: Arc, - mint: Pubkey, - percent: u64, - priority_fee: PriorityFee, -) -> Result<(), anyhow::Error> { - if percent == 0 || percent > 100 { - return Err(anyhow!("Percentage must be between 1 and 100")); - } - - let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; - let amount = balance_u64 * percent / 100; - sell(rpc, payer, mint, Some(amount), priority_fee).await -} - -pub async fn sell_by_percent_with_tip( - rpc: Arc, - fee_clients: Vec>, - payer: Arc, - mint: Pubkey, - percent: u64, - priority_fee: PriorityFee, -) -> Result<(), anyhow::Error> { - if percent == 0 || percent > 100 { - return Err(anyhow!("Percentage must be between 1 and 100")); - } - - let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; - let amount = balance_u64 * percent / 100; - sell_with_tip(rpc, fee_clients, payer, mint, Some(amount), priority_fee).await -} - -/// Sell tokens using Jito -pub async fn sell_with_tip( - rpc: Arc, - fee_clients: Vec>, - payer: Arc, - mint: Pubkey, - amount_token: Option, - priority_fee: PriorityFee, -) -> Result<(), anyhow::Error> { - let start_time = Instant::now(); - - let mut transactions = vec![]; - let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token).await?; - - let recent_blockhash = rpc.get_latest_blockhash().await?; - for fee_client in fee_clients.clone() { - let payer = payer.clone(); - let priority_fee = priority_fee.clone(); - let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?; - let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); - - let transaction = build_sell_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?; - transactions.push(transaction); - } - - let mut handles = vec![]; - for i in 0..fee_clients.len() { - let fee_client = fee_clients[i].clone(); - let transaction = transactions[i].clone(); - let handle: JoinHandle> = tokio::spawn(async move { - fee_client.send_transaction(&transaction).await?; - println!("index: {}, Total Jito sell operation time: {:?}ms", i, start_time.elapsed().as_millis()); - Ok(()) - }); - - handles.push(handle); - } - - for handle in handles { - match handle.await { - Ok(Ok(_)) => (), - Ok(Err(e)) => println!("Error in task: {}", e), - Err(e) => println!("Task join error: {}", e), - } - } - - println!("Total Jito sell operation time: {:?}ms", start_time.elapsed().as_millis()); - Ok(()) -} - -pub async fn build_sell_transaction( - rpc: Arc, - payer: Arc, - priority_fee: PriorityFee, - build_instructions: Vec -) -> Result { - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), - ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), - ]; - - instructions.extend(build_instructions); - - let recent_blockhash = rpc.get_latest_blockhash().await?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer.as_ref()], - recent_blockhash, - ); - - Ok(transaction) -} - -pub async fn build_sell_transaction_with_tip( - tip_account: Arc, - payer: Arc, - priority_fee: PriorityFee, - build_instructions: Vec, - blockhash: Hash, -) -> Result { - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), - ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), - system_instruction::transfer( - &payer.pubkey(), - &tip_account, - sol_to_lamports(priority_fee.sell_tip_fee), - ), - ]; - - instructions.extend(build_instructions); - - let v0_message: v0::Message = - v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?; - let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - - let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; - - Ok(transaction) -} - -pub async fn build_sell_instructions( - rpc: Arc, - payer: Arc, - mint: Pubkey, - amount_token: Option, -) -> Result, anyhow::Error> { - let (balance_u64, ata) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; - let amount = amount_token.unwrap_or(balance_u64); - - if amount == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = get_global_account(rpc.as_ref()).await?; - let (bonding_curve_account, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint).await?; - let creator_vault_pda = get_creator_vault_pda(&bonding_curve_account.creator).unwrap(); - - let instructions = vec![ - instruction::sell( - payer.as_ref(), - &mint, - &bonding_curve_pda, - &creator_vault_pda, - &global_account.fee_recipient, - instruction::Sell { - _amount: amount, - _min_sol_output: 0, - }, - ), - - close_account( - &spl_token::ID, - &ata, - &payer.pubkey(), - &payer.pubkey(), - &[&payer.pubkey()], - )? - ]; - - Ok(instructions) -} diff --git a/src/swqos/api.rs b/src/swqos/api.rs deleted file mode 100755 index a69e66d..0000000 --- a/src/swqos/api.rs +++ /dev/null @@ -1,462 +0,0 @@ -// This file is @generated by prost-build. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PostSubmitRequest { - #[prost(message, optional, tag = "1")] - pub transaction: ::core::option::Option, - #[prost(bool, tag = "2")] - pub skip_pre_flight: bool, - #[prost(bool, optional, tag = "3")] - pub front_running_protection: ::core::option::Option, - #[prost(bool, optional, tag = "8")] - pub experimental_front_running_protection: ::core::option::Option, - #[prost(bool, optional, tag = "9")] - pub snipe_transaction: ::core::option::Option, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PostSubmitRequestEntry { - #[prost(message, optional, tag = "1")] - pub transaction: ::core::option::Option, - #[prost(bool, tag = "2")] - pub skip_pre_flight: bool, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PostSubmitBatchRequest { - #[prost(message, repeated, tag = "1")] - pub entries: ::prost::alloc::vec::Vec, - #[prost(enumeration = "SubmitStrategy", tag = "2")] - pub submit_strategy: i32, - #[prost(bool, optional, tag = "3")] - pub use_bundle: ::core::option::Option, - #[prost(bool, optional, tag = "4")] - pub front_running_protection: ::core::option::Option, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PostSubmitBatchResponseEntry { - #[prost(string, tag = "1")] - pub signature: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub error: ::prost::alloc::string::String, - #[prost(bool, tag = "3")] - pub submitted: bool, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PostSubmitBatchResponse { - #[prost(message, repeated, tag = "1")] - pub transactions: ::prost::alloc::vec::Vec, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PostSubmitResponse { - #[prost(string, tag = "1")] - pub signature: ::prost::alloc::string::String, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TransactionMessage { - #[prost(string, tag = "1")] - pub content: ::prost::alloc::string::String, - #[prost(bool, tag = "2")] - pub is_cleanup: bool, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TransactionMessageV2 { - #[prost(string, tag = "1")] - pub content: ::prost::alloc::string::String, -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum SubmitStrategy { - PUknown = 0, - PSubmitAll = 1, - PAbortOnFirstError = 2, - PWaitForConfirmation = 3, -} -impl SubmitStrategy { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::PUknown => "P_UKNOWN", - Self::PSubmitAll => "P_SUBMIT_ALL", - Self::PAbortOnFirstError => "P_ABORT_ON_FIRST_ERROR", - Self::PWaitForConfirmation => "P_WAIT_FOR_CONFIRMATION", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "P_UKNOWN" => Some(Self::PUknown), - "P_SUBMIT_ALL" => Some(Self::PSubmitAll), - "P_ABORT_ON_FIRST_ERROR" => Some(Self::PAbortOnFirstError), - "P_WAIT_FOR_CONFIRMATION" => Some(Self::PWaitForConfirmation), - _ => None, - } - } -} -/// Generated client implementations. -pub mod api_client { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tonic::codegen::*; - use tonic::codegen::http::Uri; - #[derive(Debug, Clone)] - pub struct ApiClient { - inner: tonic::client::Grpc, - } - impl ApiClient { - /// Attempt to create a new client by connecting to a given endpoint. - pub async fn connect(dst: D) -> Result - where - D: std::convert::TryInto, - D::Error: Into, - { - let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; - Ok(Self::new(conn)) - } - } - impl ApiClient - where - T: tonic::client::GrpcService, - T::Error: Into, - T::ResponseBody: Body + std::marker::Send + 'static, - ::Error: Into + std::marker::Send, - { - pub fn new(inner: T) -> Self { - let inner = tonic::client::Grpc::new(inner); - Self { inner } - } - pub fn with_origin(inner: T, origin: Uri) -> Self { - let inner = tonic::client::Grpc::with_origin(inner, origin); - Self { inner } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> ApiClient> - where - F: tonic::service::Interceptor, - T::ResponseBody: Default, - T: tonic::codegen::Service< - http::Request, - Response = http::Response< - >::ResponseBody, - >, - >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, - { - ApiClient::new(InterceptedService::new(inner, interceptor)) - } - /// Compress requests with the given encoding. - /// - /// This requires the server to support it otherwise it might respond with an - /// error. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.send_compressed(encoding); - self - } - /// Enable decompressing responses. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.accept_compressed(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_decoding_message_size(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_encoding_message_size(limit); - self - } - pub async fn post_submit_v2( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/api.Api/PostSubmitV2"); - let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitV2")); - self.inner.unary(req, path, codec).await - } - pub async fn post_submit_batch_v2( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/api.Api/PostSubmitBatchV2", - ); - let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitBatchV2")); - self.inner.unary(req, path, codec).await - } - } -} -/// Generated server implementations. -pub mod api_server { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tonic::codegen::*; - /// Generated trait containing gRPC methods that should be implemented for use with ApiServer. - #[async_trait] - pub trait Api: std::marker::Send + std::marker::Sync + 'static { - async fn post_submit_v2( - &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - async fn post_submit_batch_v2( - &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - } - #[derive(Debug)] - pub struct ApiServer { - inner: Arc, - accept_compression_encodings: EnabledCompressionEncodings, - send_compression_encodings: EnabledCompressionEncodings, - max_decoding_message_size: Option, - max_encoding_message_size: Option, - } - impl ApiServer { - pub fn new(inner: T) -> Self { - Self::from_arc(Arc::new(inner)) - } - pub fn from_arc(inner: Arc) -> Self { - Self { - inner, - accept_compression_encodings: Default::default(), - send_compression_encodings: Default::default(), - max_decoding_message_size: None, - max_encoding_message_size: None, - } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService - where - F: tonic::service::Interceptor, - { - InterceptedService::new(Self::new(inner), interceptor) - } - /// Enable decompressing requests with the given encoding. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.accept_compression_encodings.enable(encoding); - self - } - /// Compress responses with the given encoding, if the client supports it. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.send_compression_encodings.enable(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.max_decoding_message_size = Some(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.max_encoding_message_size = Some(limit); - self - } - } - impl tonic::codegen::Service> for ApiServer - where - T: Api, - B: Body + std::marker::Send + 'static, - B::Error: Into + std::marker::Send + 'static, - { - type Response = http::Response; - type Error = std::convert::Infallible; - type Future = BoxFuture; - fn poll_ready( - &mut self, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Ok(())) - } - fn call(&mut self, req: http::Request) -> Self::Future { - match req.uri().path() { - "/api.Api/PostSubmitV2" => { - #[allow(non_camel_case_types)] - struct PostSubmitV2Svc(pub Arc); - impl tonic::server::UnaryService - for PostSubmitV2Svc { - type Response = super::PostSubmitResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { - ::post_submit_v2(&inner, request).await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let max_decoding_message_size = self.max_decoding_message_size; - let max_encoding_message_size = self.max_encoding_message_size; - let inner = self.inner.clone(); - let fut = async move { - let method = PostSubmitV2Svc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec) - .apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ) - .apply_max_message_size_config( - max_decoding_message_size, - max_encoding_message_size, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/api.Api/PostSubmitBatchV2" => { - #[allow(non_camel_case_types)] - struct PostSubmitBatchV2Svc(pub Arc); - impl< - T: Api, - > tonic::server::UnaryService - for PostSubmitBatchV2Svc { - type Response = super::PostSubmitBatchResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { - ::post_submit_batch_v2(&inner, request).await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let max_decoding_message_size = self.max_decoding_message_size; - let max_encoding_message_size = self.max_encoding_message_size; - let inner = self.inner.clone(); - let fut = async move { - let method = PostSubmitBatchV2Svc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec) - .apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ) - .apply_max_message_size_config( - max_decoding_message_size, - max_encoding_message_size, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - _ => { - Box::pin(async move { - let mut response = http::Response::new(empty_body()); - let headers = response.headers_mut(); - headers - .insert( - tonic::Status::GRPC_STATUS, - (tonic::Code::Unimplemented as i32).into(), - ); - headers - .insert( - http::header::CONTENT_TYPE, - tonic::metadata::GRPC_CONTENT_TYPE, - ); - Ok(response) - }) - } - } - } - } - impl Clone for ApiServer { - fn clone(&self) -> Self { - let inner = self.inner.clone(); - Self { - inner, - accept_compression_encodings: self.accept_compression_encodings, - send_compression_encodings: self.send_compression_encodings, - max_decoding_message_size: self.max_decoding_message_size, - max_encoding_message_size: self.max_encoding_message_size, - } - } - } - /// Generated gRPC service name - pub const SERVICE_NAME: &str = "api.Api"; - impl tonic::server::NamedService for ApiServer { - const NAME: &'static str = SERVICE_NAME; - } -} \ No newline at end of file diff --git a/src/swqos/common.rs b/src/swqos/common.rs deleted file mode 100755 index e726be7..0000000 --- a/src/swqos/common.rs +++ /dev/null @@ -1,127 +0,0 @@ -use bincode::serialize; -use serde_json::json; -use solana_client::rpc_client::SerializableTransaction; -use solana_sdk::signature::Signature; -use solana_sdk::transaction::Transaction; -use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding}; -use std::str::FromStr; -use std::time::{Duration, Instant}; -use tokio::time::sleep; -use crate::common::types::SolanaRpcClient; -use anyhow::Result; -use base64::Engine; -use base64::engine::general_purpose::STANDARD; -use reqwest::Client; - -pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signature) -> Result { - // 15 second timeout - let timeout: Duration = Duration::from_secs(5); - // 5 second retry interval - let interval: Duration = Duration::from_millis(300); - let start: Instant = Instant::now(); - - loop { - if start.elapsed() >= timeout { - return Err(anyhow::anyhow!("Transaction {}'s confirmation timed out", txt_sig)); - } - - let status = rpc.get_signature_statuses(&[txt_sig]).await?; - - match status.value[0].clone() { - Some(status) => { - if status.err.is_none() - && (status.confirmation_status == Some(TransactionConfirmationStatus::Confirmed) - || status.confirmation_status == Some(TransactionConfirmationStatus::Finalized)) - { - return Ok(txt_sig); - } - if status.err.is_some() { - return Err(anyhow::anyhow!(status.err.unwrap())); - } - } - None => { - sleep(interval).await; - } - } - } -} - -pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result { - // 序列化交易 - let serialized = bincode::serialize(transaction) - .map_err(|e| anyhow::anyhow!("序列化交易失败: {}", e))?; - - // Base64编码 - let encoded = STANDARD.encode(serialized); - - let request_data = json!({ - "transaction": { - "content": encoded - }, - "frontRunningProtection": true - }); - - let url = format!("{}/api/v2/submit", endpoint); - let response = client - .post(url) - .header("Authorization", auth_token) - .header("Content-Type", "application/json") - .json(&request_data) - .send() - .await - .map_err(|e| anyhow::anyhow!("请求失败: {}", e))?; - - let resp = response.json::().await - .map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?; - - if let Some(reason) = resp["reason"].as_str() { - return Err(anyhow::anyhow!(reason.to_string())); - } - - let signature = resp["signature"].as_str() - .ok_or_else(|| anyhow::anyhow!("响应中缺少signature字段"))?; - - let signature = Signature::from_str(signature) - .map_err(|e| anyhow::anyhow!("无效的签名: {}", e))?; - - Ok(signature) -} - -pub async fn serialize_and_encode( - transaction: &Vec, - encoding: UiTransactionEncoding, -) -> Result { - let serialized = match encoding { - UiTransactionEncoding::Base58 => bs58::encode(transaction).into_string(), - UiTransactionEncoding::Base64 => STANDARD.encode(transaction), - _ => return Err(anyhow::anyhow!("Unsupported encoding")), - }; - Ok(serialized) -} - -pub async fn serialize_transaction_and_encode( - transaction: &impl SerializableTransaction, - encoding: UiTransactionEncoding, -) -> Result { - let serialized_tx = serialize(transaction)?; - let serialized = match encoding { - UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(), - UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx), - _ => return Err(anyhow::anyhow!("Unsupported encoding")), - }; - Ok(serialized) -} - -pub async fn serialize_smart_transaction_and_encode( - transaction: &impl SerializableTransaction, - encoding: UiTransactionEncoding, -) -> Result<(String, Signature)> { - let signature = transaction.get_signature(); - let serialized_tx = serialize(transaction)?; - let serialized = match encoding { - UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(), - UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx), - _ => return Err(anyhow::anyhow!("Unsupported encoding")), - }; - Ok((serialized, *signature)) -} \ No newline at end of file diff --git a/src/swqos/jito_grpc/bundle.rs b/src/swqos/jito_grpc/bundle.rs deleted file mode 100755 index 5409c92..0000000 --- a/src/swqos/jito_grpc/bundle.rs +++ /dev/null @@ -1,171 +0,0 @@ -// This file is @generated by prost-build. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Bundle { - #[prost(message, optional, tag = "2")] - pub header: ::core::option::Option, - #[prost(message, repeated, tag = "3")] - pub packets: ::prost::alloc::vec::Vec, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct BundleUuid { - #[prost(message, optional, tag = "1")] - pub bundle: ::core::option::Option, - #[prost(string, tag = "2")] - pub uuid: ::prost::alloc::string::String, -} -/// Indicates the bundle was accepted and forwarded to a validator. -/// NOTE: A single bundle may have multiple events emitted if forwarded to many validators. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Accepted { - /// Slot at which bundle was forwarded. - #[prost(uint64, tag = "1")] - pub slot: u64, - /// Validator identity bundle was forwarded to. - #[prost(string, tag = "2")] - pub validator_identity: ::prost::alloc::string::String, -} -/// Indicates the bundle was dropped and therefore not forwarded to any validator. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Rejected { - #[prost(oneof = "rejected::Reason", tags = "1, 2, 3, 4, 5")] - pub reason: ::core::option::Option, -} -/// Nested message and enum types in `Rejected`. -pub mod rejected { - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Reason { - #[prost(message, tag = "1")] - StateAuctionBidRejected(super::StateAuctionBidRejected), - #[prost(message, tag = "2")] - WinningBatchBidRejected(super::WinningBatchBidRejected), - #[prost(message, tag = "3")] - SimulationFailure(super::SimulationFailure), - #[prost(message, tag = "4")] - InternalError(super::InternalError), - #[prost(message, tag = "5")] - DroppedBundle(super::DroppedBundle), - } -} -/// Indicates the bundle's bid was high enough to win its state auction. -/// However, not high enough relative to other state auction winners and therefore excluded from being forwarded. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct WinningBatchBidRejected { - /// Auction's unique identifier. - #[prost(string, tag = "1")] - pub auction_id: ::prost::alloc::string::String, - /// Bundle's simulated bid. - #[prost(uint64, tag = "2")] - pub simulated_bid_lamports: u64, - #[prost(string, optional, tag = "3")] - pub msg: ::core::option::Option<::prost::alloc::string::String>, -} -/// Indicates the bundle's bid was __not__ high enough to be included in its state auction's set of winners. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct StateAuctionBidRejected { - /// Auction's unique identifier. - #[prost(string, tag = "1")] - pub auction_id: ::prost::alloc::string::String, - /// Bundle's simulated bid. - #[prost(uint64, tag = "2")] - pub simulated_bid_lamports: u64, - #[prost(string, optional, tag = "3")] - pub msg: ::core::option::Option<::prost::alloc::string::String>, -} -/// Bundle dropped due to simulation failure. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SimulationFailure { - /// Signature of the offending transaction. - #[prost(string, tag = "1")] - pub tx_signature: ::prost::alloc::string::String, - #[prost(string, optional, tag = "2")] - pub msg: ::core::option::Option<::prost::alloc::string::String>, -} -/// Bundle dropped due to an internal error. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct InternalError { - #[prost(string, tag = "1")] - pub msg: ::prost::alloc::string::String, -} -/// Bundle dropped (e.g. because no leader upcoming) -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DroppedBundle { - #[prost(string, tag = "1")] - pub msg: ::prost::alloc::string::String, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct Finalized {} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Processed { - #[prost(string, tag = "1")] - pub validator_identity: ::prost::alloc::string::String, - #[prost(uint64, tag = "2")] - pub slot: u64, - /// / Index within the block. - #[prost(uint64, tag = "3")] - pub bundle_index: u64, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct Dropped { - #[prost(enumeration = "DroppedReason", tag = "1")] - pub reason: i32, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct BundleResult { - /// Bundle's Uuid. - #[prost(string, tag = "1")] - pub bundle_id: ::prost::alloc::string::String, - #[prost(oneof = "bundle_result::Result", tags = "2, 3, 4, 5, 6")] - pub result: ::core::option::Option, -} -/// Nested message and enum types in `BundleResult`. -pub mod bundle_result { - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Result { - /// Indicated accepted by the block-engine and forwarded to a jito-solana validator. - #[prost(message, tag = "2")] - Accepted(super::Accepted), - /// Rejected by the block-engine. - #[prost(message, tag = "3")] - Rejected(super::Rejected), - /// Reached finalized commitment level. - #[prost(message, tag = "4")] - Finalized(super::Finalized), - /// Reached a processed commitment level. - #[prost(message, tag = "5")] - Processed(super::Processed), - /// Was accepted and forwarded by the block-engine but never landed on-chain. - #[prost(message, tag = "6")] - Dropped(super::Dropped), - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum DroppedReason { - BlockhashExpired = 0, - /// One or more transactions in the bundle landed on-chain, invalidating the bundle. - PartiallyProcessed = 1, - /// This indicates bundle was processed but not finalized. This could occur during forks. - NotFinalized = 2, -} -impl DroppedReason { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::BlockhashExpired => "BlockhashExpired", - Self::PartiallyProcessed => "PartiallyProcessed", - Self::NotFinalized => "NotFinalized", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "BlockhashExpired" => Some(Self::BlockhashExpired), - "PartiallyProcessed" => Some(Self::PartiallyProcessed), - "NotFinalized" => Some(Self::NotFinalized), - _ => None, - } - } -} diff --git a/src/swqos/jito_grpc/convert.rs b/src/swqos/jito_grpc/convert.rs deleted file mode 100755 index 500d989..0000000 --- a/src/swqos/jito_grpc/convert.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::{ - cmp::min, - net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr}, - str::FromStr, -}; - -use bincode::serialize; -use solana_perf::packet::{Packet, PacketBatch, PACKET_DATA_SIZE}; -use solana_sdk::{ - packet::{Meta, PacketFlags}, - transaction::VersionedTransaction, -}; - -use crate::swqos::jito_grpc::packet::{ - Meta as ProtoMeta, Packet as ProtoPacket, PacketBatch as ProtoPacketBatch, - PacketFlags as ProtoPacketFlags, -}; -use crate::swqos::jito_grpc::shared::Socket; - -/// Converts a Solana packet to a protobuf packet -/// NOTE: the packet.data() function will filter packets marked for discard -pub fn packet_to_proto_packet(p: &Packet) -> Option { - Some(ProtoPacket { - data: p.data(..)?.to_vec(), - meta: Some(ProtoMeta { - size: p.meta().size as u64, - addr: p.meta().addr.to_string(), - port: p.meta().port as u32, - flags: Some(ProtoPacketFlags { - discard: p.meta().discard(), - forwarded: p.meta().forwarded(), - repair: p.meta().repair(), - simple_vote_tx: p.meta().is_simple_vote_tx(), - tracer_packet: p.meta().is_perf_track_packet(), - from_staked_node: p.meta().is_from_staked_node(), - }), - sender_stake: 0, - }), - }) -} - -pub fn packet_batches_to_proto_packets( - batches: &[PacketBatch], -) -> impl Iterator + '_ { - batches - .iter() - .flat_map(|b| b.iter().filter_map(packet_to_proto_packet)) -} - -/// converts from a protobuf packet to packet -pub fn proto_packet_to_packet(p: &ProtoPacket) -> Packet { - let mut data = [0u8; PACKET_DATA_SIZE]; - let copy_len = min(data.len(), p.data.len()); - data[..copy_len].copy_from_slice(&p.data[..copy_len]); - let mut packet = Packet::new(data, Meta::default()); - if let Some(meta) = &p.meta { - packet.meta_mut().size = meta.size as usize; - packet.meta_mut().addr = meta - .addr - .parse() - .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); - packet.meta_mut().port = meta.port as u16; - if let Some(flags) = &meta.flags { - if flags.simple_vote_tx { - packet.meta_mut().flags.insert(PacketFlags::SIMPLE_VOTE_TX); - } - if flags.forwarded { - packet.meta_mut().flags.insert(PacketFlags::FORWARDED); - } - if flags.tracer_packet { - packet.meta_mut().flags.insert(PacketFlags::PERF_TRACK_PACKET); - } - if flags.repair { - packet.meta_mut().flags.insert(PacketFlags::REPAIR); - } - if flags.discard { - packet.meta_mut().flags.insert(PacketFlags::DISCARD); - } - } - } - packet -} - -pub fn proto_packet_batch_to_packets( - packet_batch: ProtoPacketBatch, -) -> impl Iterator { - packet_batch - .packets - .into_iter() - .map(|proto_packet| proto_packet_to_packet(&proto_packet)) -} - -/// Converts a protobuf packet to a VersionedTransaction -pub fn versioned_tx_from_packet(p: &ProtoPacket) -> Option { - let mut data = [0; PACKET_DATA_SIZE]; - let copy_len = min(data.len(), p.data.len()); - data[..copy_len].copy_from_slice(&p.data[..copy_len]); - let mut packet = Packet::new(data, Default::default()); - if let Some(meta) = &p.meta { - packet.meta_mut().size = meta.size as usize; - } - packet.deserialize_slice(..).ok() -} - -/// Coverts a VersionedTransaction to packet -pub fn packet_from_versioned_tx(tx: VersionedTransaction) -> Packet { - let tx_data = serialize(&tx).expect("serializes"); - let mut data = [0; PACKET_DATA_SIZE]; - let copy_len = min(tx_data.len(), data.len()); - data[..copy_len].copy_from_slice(&tx_data[..copy_len]); - let mut packet = Packet::new(data, Default::default()); - packet.meta_mut().size = copy_len; - packet -} - -/// Converts a VersionedTransaction to a protobuf packet -pub fn proto_packet_from_versioned_tx(tx: &VersionedTransaction) -> ProtoPacket { - let data = serialize(tx).expect("serializes"); - let size = data.len() as u64; - ProtoPacket { - data, - meta: Some(ProtoMeta { - size, - addr: "".to_string(), - port: 0, - flags: None, - sender_stake: 0, - }), - } -} - -/// Converts a GRPC Socket to stdlib SocketAddr -impl TryFrom<&Socket> for SocketAddr { - type Error = AddrParseError; - - fn try_from(value: &Socket) -> Result { - IpAddr::from_str(&value.ip).map(|ip| SocketAddr::new(ip, value.port as u16)) - } -} - -// #[cfg(test)] -// mod tests { -// use solana_perf::test_tx::test_tx; -// use solana_sdk::transaction::VersionedTransaction; - -// use crate::convert::{proto_packet_from_versioned_tx, versioned_tx_from_packet}; - -// #[test] -// fn test_proto_to_packet() { -// let tx_before = VersionedTransaction::from(test_tx()); -// let tx_after = versioned_tx_from_packet(&proto_packet_from_versioned_tx(&tx_before)) -// .expect("tx_after"); - -// assert_eq!(tx_before, tx_after); -// } -// } diff --git a/src/swqos/jito_grpc/mod.rs b/src/swqos/jito_grpc/mod.rs deleted file mode 100755 index 6bad46f..0000000 --- a/src/swqos/jito_grpc/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod bundle; -pub mod packet; -pub mod searcher; -pub mod shared; -pub mod convert; \ No newline at end of file diff --git a/src/swqos/jito_grpc/packet.rs b/src/swqos/jito_grpc/packet.rs deleted file mode 100755 index fd20158..0000000 --- a/src/swqos/jito_grpc/packet.rs +++ /dev/null @@ -1,41 +0,0 @@ -// This file is @generated by prost-build. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PacketBatch { - #[prost(message, repeated, tag = "1")] - pub packets: ::prost::alloc::vec::Vec, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Packet { - #[prost(bytes = "vec", tag = "1")] - pub data: ::prost::alloc::vec::Vec, - #[prost(message, optional, tag = "2")] - pub meta: ::core::option::Option, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Meta { - #[prost(uint64, tag = "1")] - pub size: u64, - #[prost(string, tag = "2")] - pub addr: ::prost::alloc::string::String, - #[prost(uint32, tag = "3")] - pub port: u32, - #[prost(message, optional, tag = "4")] - pub flags: ::core::option::Option, - #[prost(uint64, tag = "5")] - pub sender_stake: u64, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct PacketFlags { - #[prost(bool, tag = "1")] - pub discard: bool, - #[prost(bool, tag = "2")] - pub forwarded: bool, - #[prost(bool, tag = "3")] - pub repair: bool, - #[prost(bool, tag = "4")] - pub simple_vote_tx: bool, - #[prost(bool, tag = "5")] - pub tracer_packet: bool, - #[prost(bool, tag = "6")] - pub from_staked_node: bool, -} diff --git a/src/swqos/jito_grpc/searcher.rs b/src/swqos/jito_grpc/searcher.rs deleted file mode 100755 index ad922df..0000000 --- a/src/swqos/jito_grpc/searcher.rs +++ /dev/null @@ -1,363 +0,0 @@ -// This file is @generated by prost-build. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SlotList { - #[prost(uint64, repeated, tag = "1")] - pub slots: ::prost::alloc::vec::Vec, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ConnectedLeadersResponse { - /// Mapping of validator pubkey to leader slots for the current epoch. - #[prost(map = "string, message", tag = "1")] - pub connected_validators: ::std::collections::HashMap< - ::prost::alloc::string::String, - SlotList, - >, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SendBundleRequest { - #[prost(message, optional, tag = "1")] - pub bundle: ::core::option::Option, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SendBundleResponse { - /// server uuid for the bundle - #[prost(string, tag = "1")] - pub uuid: ::prost::alloc::string::String, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct NextScheduledLeaderRequest { - /// Defaults to the currently connected region if no region provided. - #[prost(string, repeated, tag = "1")] - pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct NextScheduledLeaderResponse { - /// the current slot the backend is on - #[prost(uint64, tag = "1")] - pub current_slot: u64, - /// the slot of the next leader - #[prost(uint64, tag = "2")] - pub next_leader_slot: u64, - /// the identity pubkey (base58) of the next leader - #[prost(string, tag = "3")] - pub next_leader_identity: ::prost::alloc::string::String, - /// the block engine region of the next leader - #[prost(string, tag = "4")] - pub next_leader_region: ::prost::alloc::string::String, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct ConnectedLeadersRequest {} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ConnectedLeadersRegionedRequest { - /// Defaults to the currently connected region if no region provided. - #[prost(string, repeated, tag = "1")] - pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ConnectedLeadersRegionedResponse { - #[prost(map = "string, message", tag = "1")] - pub connected_validators: ::std::collections::HashMap< - ::prost::alloc::string::String, - ConnectedLeadersResponse, - >, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct GetTipAccountsRequest {} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetTipAccountsResponse { - #[prost(string, repeated, tag = "1")] - pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct SubscribeBundleResultsRequest {} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct GetRegionsRequest {} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetRegionsResponse { - /// The region the client is currently connected to - #[prost(string, tag = "1")] - pub current_region: ::prost::alloc::string::String, - /// Regions that are online and ready for connections - /// All regions: - #[prost(string, repeated, tag = "2")] - pub available_regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, -} -/// Generated client implementations. -pub mod searcher_service_client { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tonic::codegen::*; - use tonic::codegen::http::Uri; - #[derive(Debug, Clone)] - pub struct SearcherServiceClient { - inner: tonic::client::Grpc, - } - impl SearcherServiceClient { - /// Attempt to create a new client by connecting to a given endpoint. - pub async fn connect(dst: D) -> Result - where - D: TryInto, - D::Error: Into, - { - let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; - Ok(Self::new(conn)) - } - } - impl SearcherServiceClient - where - T: tonic::client::GrpcService, - T::Error: Into, - T::ResponseBody: Body + std::marker::Send + 'static, - ::Error: Into + std::marker::Send, - { - pub fn new(inner: T) -> Self { - let inner = tonic::client::Grpc::new(inner); - Self { inner } - } - pub fn with_origin(inner: T, origin: Uri) -> Self { - let inner = tonic::client::Grpc::with_origin(inner, origin); - Self { inner } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> SearcherServiceClient> - where - F: tonic::service::Interceptor, - T::ResponseBody: Default, - T: tonic::codegen::Service< - http::Request, - Response = http::Response< - >::ResponseBody, - >, - >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, - { - SearcherServiceClient::new(InterceptedService::new(inner, interceptor)) - } - /// Compress requests with the given encoding. - /// - /// This requires the server to support it otherwise it might respond with an - /// error. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.send_compressed(encoding); - self - } - /// Enable decompressing responses. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.accept_compressed(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_decoding_message_size(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_encoding_message_size(limit); - self - } - /// Searchers can invoke this endpoint to subscribe to their respective bundle results. - /// A success result would indicate the bundle won its state auction and was submitted to the validator. - pub async fn subscribe_bundle_results( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response>, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/searcher.SearcherService/SubscribeBundleResults", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new("searcher.SearcherService", "SubscribeBundleResults"), - ); - self.inner.server_streaming(req, path, codec).await - } - pub async fn send_bundle( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/searcher.SearcherService/SendBundle", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("searcher.SearcherService", "SendBundle")); - self.inner.unary(req, path, codec).await - } - /// Returns the next scheduled leader connected to the block engine. - pub async fn get_next_scheduled_leader( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/searcher.SearcherService/GetNextScheduledLeader", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new("searcher.SearcherService", "GetNextScheduledLeader"), - ); - self.inner.unary(req, path, codec).await - } - /// Returns leader slots for connected jito validators during the current epoch. Only returns data for this region. - pub async fn get_connected_leaders( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/searcher.SearcherService/GetConnectedLeaders", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new("searcher.SearcherService", "GetConnectedLeaders"), - ); - self.inner.unary(req, path, codec).await - } - /// Returns leader slots for connected jito validators during the current epoch. - pub async fn get_connected_leaders_regioned( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/searcher.SearcherService/GetConnectedLeadersRegioned", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new( - "searcher.SearcherService", - "GetConnectedLeadersRegioned", - ), - ); - self.inner.unary(req, path, codec).await - } - /// Returns the tip accounts searchers shall transfer funds to for the leader to claim. - pub async fn get_tip_accounts( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/searcher.SearcherService/GetTipAccounts", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("searcher.SearcherService", "GetTipAccounts")); - self.inner.unary(req, path, codec).await - } - /// Returns region the client is directly connected to, along with all available regions - pub async fn get_regions( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/searcher.SearcherService/GetRegions", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("searcher.SearcherService", "GetRegions")); - self.inner.unary(req, path, codec).await - } - } -} diff --git a/src/swqos/jito_grpc/shared.rs b/src/swqos/jito_grpc/shared.rs deleted file mode 100755 index b2b0680..0000000 --- a/src/swqos/jito_grpc/shared.rs +++ /dev/null @@ -1,18 +0,0 @@ -// This file is @generated by prost-build. -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct Header { - #[prost(message, optional, tag = "1")] - pub ts: ::core::option::Option<::prost_types::Timestamp>, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct Heartbeat { - #[prost(uint64, tag = "1")] - pub count: u64, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Socket { - #[prost(string, tag = "1")] - pub ip: ::prost::alloc::string::String, - #[prost(int64, tag = "2")] - pub port: i64, -} diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs deleted file mode 100755 index eca4b4c..0000000 --- a/src/swqos/mod.rs +++ /dev/null @@ -1,335 +0,0 @@ -use api::api_client::ApiClient; -use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode}; -use crate::swqos::jito_grpc::searcher::searcher_service_client::SearcherServiceClient; -use reqwest::Client; -use searcher_client::{get_searcher_client_no_auth, send_bundle_with_confirmation}; -use serde_json::json; -use tonic::transport::Channel; -use yellowstone_grpc_client::Interceptor; -use std::{sync::Arc, time::Instant}; -use tokio::sync::{Mutex, RwLock}; - -use solana_sdk::signature::Signature; - -use std::str::FromStr; -use rustls::crypto::{ring::default_provider, CryptoProvider}; - -use tonic::{service::interceptor::InterceptedService, transport::Uri, Status}; -use std::time::Duration; -use solana_transaction_status::UiTransactionEncoding; -use tonic::transport::ClientTlsConfig; - -use anyhow::{anyhow, Result}; -use rand::{rng, seq::{IndexedRandom, IteratorRandom}}; -use solana_sdk::transaction::VersionedTransaction; - -use crate::{common::SolanaRpcClient, constants::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS}}; - -pub mod common; -pub mod searcher_client; -pub mod api; -pub mod jito_grpc; - -lazy_static::lazy_static! { - static ref TIP_ACCOUNT_CACHE: RwLock> = RwLock::new(Vec::new()); -} - -#[derive(Debug, Clone, Copy)] -pub enum ClientType { - Jito, - NextBlock, - ZeroSlot, -} - -pub type FeeClient = dyn FeeClientTrait + Send + Sync + 'static; - -#[async_trait::async_trait] -pub trait FeeClientTrait { - async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result; - async fn send_transactions(&self, transactions: &Vec) -> Result>; - async fn get_tip_account(&self) -> Result; - async fn get_client_type(&self) -> ClientType; -} - -pub struct JitoClient { - pub rpc_client: Arc, - pub searcher_client: Arc>>, -} - -#[async_trait::async_trait] -impl FeeClientTrait for JitoClient { - async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { - self.send_bundle_with_confirmation(&vec![transaction.clone()]).await?.first().cloned().ok_or(anyhow!("Failed to send transaction")) - } - - async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { - self.send_bundle_with_confirmation(transactions).await - } - - async fn get_tip_account(&self) -> Result { - if let Some(acc) = JITO_TIP_ACCOUNTS.iter().choose(&mut rng()) { - Ok(acc.to_string()) - } else { - Err(anyhow!("no valid tip accounts found")) - } - } - - async fn get_client_type(&self) -> ClientType { - ClientType::Jito - } -} - -impl JitoClient { - pub async fn new(rpc_url: String, block_engine_url: String) -> Result { - let rpc_client = SolanaRpcClient::new(rpc_url); - let searcher_client = get_searcher_client_no_auth(block_engine_url.as_str()).await?; - Ok(Self { rpc_client: Arc::new(rpc_client), searcher_client: Arc::new(Mutex::new(searcher_client)) }) - } - - pub async fn send_bundle_with_confirmation( - &self, - transactions: &Vec, - ) -> Result, anyhow::Error> { - send_bundle_with_confirmation(self.rpc_client.clone(), &transactions, self.searcher_client.clone()).await - } - - pub async fn send_bundle_no_wait( - &self, - transactions: &Vec, - ) -> Result, anyhow::Error> { - searcher_client::send_bundle_no_wait(&transactions, self.searcher_client.clone()).await - } -} - -#[derive(Clone)] -pub struct MyInterceptor { - auth_token: String, -} - -impl MyInterceptor { - pub fn new(auth_token: String) -> Self { - Self { auth_token } - } -} - -impl Interceptor for MyInterceptor { - fn call(&mut self, mut request: tonic::Request<()>) -> Result, Status> { - request.metadata_mut().insert( - "authorization", - tonic::metadata::MetadataValue::from_str(&self.auth_token) - .map_err(|_| Status::invalid_argument("Invalid auth token"))? - ); - Ok(request) - } -} - -#[derive(Clone)] -pub struct NextBlockClient { - pub rpc_client: Arc, - pub client: ApiClient>, -} - -#[async_trait::async_trait] -impl FeeClientTrait for NextBlockClient { - async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { - self.send_transaction(transaction).await - } - - async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { - self.send_transactions(transactions).await - } - - async fn get_tip_account(&self) -> Result { - let tip_account = self.get_tip_account().await?; - Ok(tip_account) - } - - async fn get_client_type(&self) -> ClientType { - ClientType::NextBlock - } -} - -impl NextBlockClient { - pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { - if CryptoProvider::get_default().is_none() { - let _ = default_provider() - .install_default() - .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e)); - } - - let endpoint = endpoint.parse::().unwrap(); - let tls = ClientTlsConfig::new().with_native_roots(); - let channel = Channel::builder(endpoint) - .tls_config(tls).expect("Failed to create TLS config") - .tcp_keepalive(Some(Duration::from_secs(60))) - .http2_keep_alive_interval(Duration::from_secs(30)) - .keep_alive_while_idle(true) - .timeout(Duration::from_secs(30)) - .connect_timeout(Duration::from_secs(10)) - .connect_lazy(); - - let client = ApiClient::with_interceptor(channel, MyInterceptor::new(auth_token)); - let rpc_client = SolanaRpcClient::new(rpc_url); - Self { rpc_client: Arc::new(rpc_client), client } - } - - pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { - let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; - - self.client.clone().post_submit_v2(api::PostSubmitRequest { - transaction: Some(api::TransactionMessage { - content, - is_cleanup: false, - }), - skip_pre_flight: true, - front_running_protection: Some(true), - experimental_front_running_protection: Some(true), - snipe_transaction: Some(true), - }).await?; - - let timeout: Duration = Duration::from_secs(10); - let start_time: Instant = Instant::now(); - while Instant::now().duration_since(start_time) < timeout { - match poll_transaction_confirmation(&self.rpc_client, signature).await { - Ok(sig) => return Ok(sig), - Err(_) => continue, - } - } - - Ok(signature) - } - - pub async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { - let mut entries = Vec::new(); - let encoding = UiTransactionEncoding::Base64; - - let mut signatures = Vec::new(); - for transaction in transactions { - let (content, signature) = serialize_smart_transaction_and_encode(transaction, encoding).await?; - entries.push(api::PostSubmitRequestEntry { - transaction: Some(api::TransactionMessage { - content, - is_cleanup: false, - }), - skip_pre_flight: true, - }); - signatures.push(signature); - } - - self.client.clone().post_submit_batch_v2(api::PostSubmitBatchRequest { - entries, - submit_strategy: api::SubmitStrategy::PSubmitAll as i32, - use_bundle: Some(true), - front_running_protection: Some(true), - }).await?; - - let timeout: Duration = Duration::from_secs(10); - let start_time: Instant = Instant::now(); - while Instant::now().duration_since(start_time) < timeout { - for signature in signatures.clone() { - match poll_transaction_confirmation(&self.rpc_client, signature).await { - Ok(sig) => signatures.push(sig), - Err(_) => continue, - } - } - } - - Ok(signatures) - } - - async fn get_tip_account(&self) -> Result { - let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap(); - Ok(tip_account.to_string()) - } -} - -#[derive(Clone)] -pub struct ZeroSlotClient { - pub endpoint: String, - pub auth_token: String, - pub rpc_client: Arc, -} - -#[async_trait::async_trait] -impl FeeClientTrait for ZeroSlotClient { - async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { - self.send_transaction(transaction).await - } - - async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { - self.send_transactions(transactions).await - } - - async fn get_tip_account(&self) -> Result { - let tip_account = self.get_tip_account().await?; - Ok(tip_account) - } - - async fn get_client_type(&self) -> ClientType { - ClientType::ZeroSlot - } -} - -impl ZeroSlotClient { - pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { - let rpc_client = SolanaRpcClient::new(rpc_url); - Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token } - } - - pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { - let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; - - let client = Client::new(); - let request_body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "sendTransaction", - "params": [ - content, - { - "encoding": "base64", - "skipPreflight": true, - } - ] - }); - - // Send the request - let response = client.post(format!("{}/?api-key={}", self.endpoint, self.auth_token)) - .json(&request_body) - .send() - .await?; - - // Parse the response - let response_json: serde_json::Value = response.json().await?; - if let Some(result) = response_json.get("result") { - println!("Transaction sent successfully: {}", result); - } else if let Some(error) = response_json.get("error") { - eprintln!("Failed to send transaction: {}", error); - } - - let timeout: Duration = Duration::from_secs(10); - let start_time: Instant = Instant::now(); - while Instant::now().duration_since(start_time) < timeout { - match poll_transaction_confirmation(&self.rpc_client, signature).await { - Ok(sig) => return Ok(sig), - Err(_) => continue, - } - } - - Ok(signature) - } - - pub async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { - let mut signatures = Vec::new(); - for transaction in transactions { - let signature = self.send_transaction(transaction).await?; - signatures.push(signature); - } - Ok(signatures) - } - - async fn get_tip_account(&self) -> Result { - let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap(); - Ok(tip_account.to_string()) - } -} \ No newline at end of file diff --git a/src/swqos/searcher_client.rs b/src/swqos/searcher_client.rs deleted file mode 100755 index a6eb3c4..0000000 --- a/src/swqos/searcher_client.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; - -use crate::swqos::jito_grpc::{ - bundle::{ - Bundle, BundleResult, - }, - convert::proto_packet_from_versioned_tx, - searcher::{ - searcher_service_client::SearcherServiceClient, SendBundleRequest, SubscribeBundleResultsRequest, - }, -}; -use solana_sdk::{ - signature::Signature, - transaction::VersionedTransaction, -}; -use thiserror::Error; -use tokio::sync::Mutex; -use tonic::{transport::{self, Channel, Endpoint}, Status}; -use yellowstone_grpc_client::ClientTlsConfig; - -use crate::swqos::common::poll_transaction_confirmation; -use crate::common::SolanaRpcClient; - -#[derive(Debug, Error)] -pub enum BlockEngineConnectionError { - #[error("transport error {0}")] - TransportError(#[from] transport::Error), - #[error("client error {0}")] - ClientError(#[from] Status), -} - -#[derive(Debug, Error)] -pub enum BundleRejectionError { - #[error("bundle lost state auction, auction: {0}, tip {1} lamports")] - StateAuctionBidRejected(String, u64), - #[error("bundle won state auction but failed global auction, auction {0}, tip {1} lamports")] - WinningBatchBidRejected(String, u64), - #[error("bundle simulation failure on tx {0}, message: {1:?}")] - SimulationFailure(String, Option), - #[error("internal error {0}")] - InternalError(String), -} - -pub type BlockEngineConnectionResult = Result; - -pub async fn get_searcher_client_no_auth( - block_engine_url: &str, -) -> BlockEngineConnectionResult> { - let searcher_channel = create_grpc_channel(block_engine_url).await?; - let searcher_client = SearcherServiceClient::new(searcher_channel); - Ok(searcher_client) -} - -pub async fn create_grpc_channel(url: &str) -> BlockEngineConnectionResult { - let mut endpoint = Endpoint::from_shared(url.to_string()).expect("invalid url"); - if url.starts_with("https") { - endpoint = endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?; - } - - endpoint = endpoint.tcp_nodelay(true); - endpoint = endpoint.tcp_keepalive(Some(Duration::from_secs(10))); - endpoint = endpoint.connect_timeout(Duration::from_secs(20)); - endpoint = endpoint.http2_keep_alive_interval(Duration::from_secs(10)); - - Ok(endpoint.connect().await?) -} - -pub async fn subscribe_bundle_results( - searcher_client: Arc>>, - request: impl tonic::IntoRequest, -) -> std::result::Result< - tonic::Response>, - tonic::Status, -> { - let mut searcher = searcher_client.lock().await; - searcher.subscribe_bundle_results(request).await -} - -pub async fn send_bundle_with_confirmation( - rpc: Arc, - transactions: &Vec, - searcher_client: Arc>>, -) -> Result, anyhow::Error> { - let mut signatures = send_bundle_no_wait(transactions, searcher_client).await?; - - let timeout: Duration = Duration::from_secs(10); - let start_time: Instant = Instant::now(); - while Instant::now().duration_since(start_time) < timeout { - for signature in signatures.clone() { - match poll_transaction_confirmation(&rpc, signature).await { - Ok(sig) => signatures.push(sig), - Err(_) => continue, - } - } - } - - Ok(signatures) -} - -pub async fn send_bundle_no_wait( - transactions: &Vec, - searcher_client: Arc>>, -) -> Result, anyhow::Error> { - let mut packets = vec![]; - let mut signatures = vec![]; - for transaction in transactions { - let packet = proto_packet_from_versioned_tx(transaction); - packets.push(packet); - signatures.push(transaction.signatures[0]); - } - - let mut searcher = searcher_client.lock().await; - searcher - .send_bundle(SendBundleRequest { - bundle: Some(Bundle { - header: None, - packets, - }), - }) - .await?; - - Ok(signatures) -}