From 429fc78ecf65632518808d9bb46af2e6de44f37e Mon Sep 17 00:00:00 2001 From: William Date: Tue, 14 Jan 2025 19:18:48 +0800 Subject: [PATCH] add jito transaction --- README.md | 19 +- crates/pumpfun/Cargo.toml | 1 + crates/pumpfun/src/jito/mod.rs | 341 +++++++++++++++++---------------- crates/pumpfun/src/lib.rs | 72 +++---- 4 files changed, 224 insertions(+), 209 deletions(-) diff --git a/README.md b/README.md index fe5c255..49e1e55 100755 --- a/README.md +++ b/README.md @@ -4,15 +4,12 @@ A comprehensive Rust SDK for seamless interaction with the PumpFun Solana progra # Explanation -This repository is forked from [https://github.com/nhuxhr/pumpfun-rs](https://github.com/nhuxhr/pumpfun-rs). - -1. Change `PumpFun<'a>` to `PumpFun`, and `payer: &'a Keypair` to `payer: Arc`. -2. Add `logs_filters` and `logs_paser` to parse the logs of the PumpFun program. -3. Add `logs_data` to define the data structure of the logs. -4. Add `logs_subscribe` to subscribe the logs of the PumpFun program. -5. Add `logs_events` to define the event of the logs. -6. Add `logs_parser` to parse the logs. -7. Add `jito` to send transaction with Jito. +1. Add `logs_filters` to parse the logs. +1. Add `logs_parser` to process the logs. +2. Add `logs_data` to define the data structure of the logs. +4. Add `logs_events` to define the event of the logs. +3. Add `logs_subscribe` to subscribe the logs of the PumpFun program. +6. Add `jito` to send transaction with Jito. ## Installation @@ -20,7 +17,7 @@ Add the following to your `Cargo.toml`: ```toml [dependencies] -mai3-pumpfun-sdk = "2.4.4" +mai3-pumpfun-sdk = "2.4.5" ``` ## Usage @@ -47,7 +44,7 @@ let callback = |event: DexEvent| { DexEvent::NewToken(token_info) => { println!("Received new token event: {:?}", token_info); }, - DexEvent::NewTrade(trade_info) => { + DexEvent::NewUserTrade(trade_info) => { println!("Received new trade event: {:?}", trade_info); }, DexEvent::NewBotTrade(trade_info) => { diff --git a/crates/pumpfun/Cargo.toml b/crates/pumpfun/Cargo.toml index e4598b4..e6d08e7 100755 --- a/crates/pumpfun/Cargo.toml +++ b/crates/pumpfun/Cargo.toml @@ -29,5 +29,6 @@ base64 = "0.22.1" bs58 = "0.5.1" rand = "0.8.5" bincode = "1.3.3" +anyhow = "1.0.90" reqwest = { version = "0.11.27", features = ["json"] } diff --git a/crates/pumpfun/src/jito/mod.rs b/crates/pumpfun/src/jito/mod.rs index 0a8213a..e4d8e27 100755 --- a/crates/pumpfun/src/jito/mod.rs +++ b/crates/pumpfun/src/jito/mod.rs @@ -1,115 +1,130 @@ -use rand::Rng; -use bincode; -use bs58; -use reqwest; -use serde::Deserialize; -use serde_json::{json, Value}; +use std::fmt; use std::str::FromStr; -use std::time::Duration; -use tokio::sync::Mutex; +use rand::seq::SliceRandom; +use reqwest::Client; +use serde_json::{json, Value}; + use anchor_client::solana_sdk::{ - commitment_config::CommitmentConfig, pubkey::Pubkey, - signature::Signature, transaction::Transaction, }; -use crate::error::ClientError; -pub const MAX_RETRIES: u8 = 3; -pub const RETRY_DELAY: Duration = Duration::from_millis(200); - -#[derive(Debug, Clone)] -pub struct TransactionConfig { - pub skip_preflight: bool, - pub preflight_commitment: CommitmentConfig, - pub encoding: String, - pub last_n_blocks: u64, -} - -impl Default for TransactionConfig { - fn default() -> Self { - Self { - skip_preflight: true, - preflight_commitment: CommitmentConfig::confirmed(), - encoding: "base58".to_string(), - last_n_blocks: 100, - } - } -} +use crate::error::{ClientError, ClientResult}; #[derive(Clone, Debug)] pub struct JitoClient { - endpoint: String, - client: reqwest::Client, - config: TransactionConfig, + base_url: String, + uuid: Option, + client: Client, +} + +#[derive(Debug)] +pub struct PrettyJsonValue(pub Value); + +impl fmt::Display for PrettyJsonValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", serde_json::to_string_pretty(&self.0).unwrap()) + } +} + +impl From for PrettyJsonValue { + fn from(value: Value) -> Self { + PrettyJsonValue(value) + } } impl JitoClient { - pub fn new(endpoint: &str) -> Self { + pub fn new(base_url: &str, uuid: Option) -> Self { Self { - endpoint: endpoint.to_string(), - client: reqwest::Client::new(), - config: TransactionConfig::default(), + base_url: base_url.to_string(), + uuid, + client: Client::new(), } } - pub async fn get_tip_account(&self) -> Result { - let response = self.send_request("getTipAccounts", json!([])).await?; - - if let Some(accounts) = response["result"].as_array() { - if accounts.is_empty() { - return Err(ClientError::Other("No JITO tip accounts found".to_string())); - } - - let random_index = rand::rngs::OsRng.gen_range(0..accounts.len()); - if let Some(account) = accounts.get(random_index) { - if let Some(address) = account.as_str() { - return Pubkey::from_str(address).map_err(|e| { - ClientError::Parse( - "Invalid tip account address".to_string(), - e.to_string(), - ) - }); - } - } - } - - Err(ClientError::Other("Failed to get Tip Account".to_string())) - } - - pub async fn estimate_priority_fees( - &self, - account: &Pubkey, - ) -> Result { - let params = json!({ - "last_n_blocks": self.config.last_n_blocks, - "account": account.to_string(), - "api_version": 2 + async fn send_request(&self, endpoint: &str, method: &str, params: Option) -> ClientResult { + let url = format!("{}{}", self.base_url, endpoint); + + let data = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params.unwrap_or(json!([])) }); - let response = self.send_request("qn_estimatePriorityFees", params).await?; + // println!("Sending request to: {}", url); + // println!("Request body: {}", serde_json::to_string_pretty(&data).unwrap()); - if let Some(result) = response.get("result") { - let estimate: PriorityFeeEstimate = serde_json::from_value(result.clone()).map_err(|e| { - ClientError::Parse( - "Failed to parse priority fee estimate".to_string(), - e.to_string(), - ) - })?; + let response = self.client + .post(&url) + .header("Content-Type", "application/json") + .json(&data) + .send() + .await + .map_err(|e| ClientError::Other(format!("Request failed: {}", e)))?; - Ok(estimate) + let status = response.status(); + // println!("Response status: {}", status); + + let body = response.json::().await + .map_err(|e| ClientError::Other(format!("Failed to parse response: {}", e)))?; + // println!("Response body: {}", serde_json::to_string_pretty(&body).unwrap()); + + Ok(body) + } + + pub async fn get_tip_accounts(&self) -> ClientResult { + let endpoint = if let Some(uuid) = &self.uuid { + format!("/bundles?uuid={}", uuid) } else { - Err(ClientError::Parse( - "Invalid response format".to_string(), - "Missing result field".to_string(), - )) + "/bundles".to_string() + }; + + self.send_request(&endpoint, "getTipAccounts", None).await + } + + // Get a random tip account + pub async fn get_tip_account(&self) -> ClientResult { + let tip_accounts_response = self.get_tip_accounts().await?; + + let tip_accounts = tip_accounts_response["result"] + .as_array() + .ok_or_else(|| ClientError::Other("Failed to parse tip accounts as array".to_string()))?; + + if tip_accounts.is_empty() { + return Err(ClientError::Other("No tip accounts available".to_string())); } + + let random_account = tip_accounts + .choose(&mut rand::thread_rng()) + .ok_or_else(|| ClientError::Other("Failed to choose random tip account".to_string()))?; + + let address = random_account + .as_str() + .ok_or_else(|| ClientError::Other("Failed to parse tip account as string".to_string()))?; + + Pubkey::from_str(address) + .map_err(|e| ClientError::Other(format!("Failed to parse pubkey: {}", e))) + } + + pub async fn get_bundle_statuses(&self, bundle_uuids: Vec) -> ClientResult { + let endpoint = if let Some(uuid) = &self.uuid { + format!("/bundles?uuid={}", uuid) + } else { + "/bundles".to_string() + }; + + // Construct the params as a list within a list + let params = json!([bundle_uuids]); + + self.send_request(&endpoint, "getBundleStatuses", Some(params)) + .await } pub async fn send_transaction( &self, transaction: &Transaction, - ) -> Result { + ) -> ClientResult { let wire_transaction = bincode::serialize(transaction).map_err(|e| { ClientError::Parse( "Transaction serialization failed".to_string(), @@ -117,44 +132,17 @@ impl JitoClient { ) })?; - let encoded_tx = bs58::encode(&wire_transaction).into_string(); + let serialized_tx = bs58::encode(&wire_transaction).into_string(); - for retry in 0..MAX_RETRIES { - match self.try_send_transaction(&encoded_tx).await { - Ok(signature) => { - return Ok(Signature::from_str(&signature).map_err(|e| { - ClientError::Parse( - "Invalid signature".to_string(), - e.to_string(), - ) - })?); - } - Err(e) => { - println!("Retry {} failed: {:?}", retry, e); - if retry == MAX_RETRIES - 1 { - return Err(e); - } - tokio::time::sleep(RETRY_DELAY).await; - } - } - } + // Prepare bundle for submission (array of transactions) + let bundle = json!([serialized_tx]); - Err(ClientError::Other("Max retries exceeded".to_string())) - } + // UUID for the bundle + let uuid = None; - async fn try_send_transaction(&self, encoded_tx: &str) -> Result { - let params = json!([ - encoded_tx, - { - "skipPreflight": self.config.skip_preflight, - "preflightCommitment": self.config.preflight_commitment.commitment, - "encoding": self.config.encoding, - "maxRetries": MAX_RETRIES, - "minContextSlot": null - } - ]); - - let response = self.send_request("sendTransaction", params).await?; + // Send bundle using Jito SDK + // println!("Sending bundle with 1 transaction..."); + let response = self.send_bundle(Some(bundle), uuid).await?; response["result"] .as_str() @@ -165,54 +153,83 @@ impl JitoClient { )) } - async fn send_request(&self, method: &str, params: Value) -> Result { - let request_body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params - }); - - let response = self.client - .post(&self.endpoint) - .header("Content-Type", "application/json") - .json(&request_body) - .send() + pub async fn send_bundle(&self, params: Option, uuid: Option<&str>) -> ClientResult { + let mut endpoint = "/bundles".to_string(); + + if let Some(uuid) = uuid { + endpoint = format!("{}?uuid={}", endpoint, uuid); + } + + // Ensure params is an array of transactions + let transactions = match params { + Some(Value::Array(transactions)) => { + if transactions.is_empty() { + return Err(ClientError::Other("Bundle must contain at least one transaction".to_string())); + } + if transactions.len() > 5 { + return Err(ClientError::Other("Bundle can contain at most 5 transactions".to_string())); + } + transactions + }, + _ => return Err(ClientError::Other("Invalid bundle format: expected an array of transactions".to_string())), + }; + + // Wrap the transactions array in another array + let params = json!([transactions]); + + // Send the wrapped transactions array + self.send_request(&endpoint, "sendBundle", Some(params)) .await - .map_err(|e| ClientError::Solana( - "Request failed".to_string(), - e.to_string(), - ))?; + } - let response_data: Value = response.json().await.map_err(|e| { - ClientError::Parse( - "Invalid JSON response".to_string(), - e.to_string(), - ) - })?; + pub async fn send_txn(&self, params: Option, bundle_only: bool) -> ClientResult { + let mut query_params = Vec::new(); - if let Some(error) = response_data.get("error") { - return Err(ClientError::Solana( - "RPC error".to_string(), - error.to_string(), - )); + if bundle_only { + query_params.push("bundleOnly=true".to_string()); } - Ok(response_data) + let endpoint = if query_params.is_empty() { + "/transactions".to_string() + } else { + format!("/transactions?{}", query_params.join("&")) + }; + + // Construct params as an array instead of an object + let params = match params { + Some(Value::Object(map)) => { + let tx = map.get("tx").and_then(Value::as_str).unwrap_or_default(); + let skip_preflight = map.get("skipPreflight").and_then(Value::as_bool).unwrap_or(false); + json!([ + tx, + { + "encoding": "base64", + "skipPreflight": skip_preflight + } + ]) + }, + _ => json!([]), + }; + + self.send_request(&endpoint, "sendTransaction", Some(params)).await } -} -#[derive(Debug, Deserialize)] -pub struct PriorityFeeEstimate { - pub recommended: u64, - pub per_compute_unit: PriorityFeeLevel, - pub per_transaction: PriorityFeeLevel, -} + pub async fn get_in_flight_bundle_statuses(&self, bundle_uuids: Vec) -> ClientResult { + let endpoint = if let Some(uuid) = &self.uuid { + format!("/bundles?uuid={}", uuid) + } else { + "/bundles".to_string() + }; -#[derive(Debug, Deserialize)] -pub struct PriorityFeeLevel { - pub extreme: u64, // 95th percentile - pub high: u64, // 80th percentile - pub medium: u64, // 60th percentile - pub low: u64, // 40th percentile + // Construct the params as a list within a list + let params = json!([bundle_uuids]); + + self.send_request(&endpoint, "getInflightBundleStatuses", Some(params)) + .await + } + + // Helper method to convert Value to PrettyJsonValue + pub fn prettify(value: Value) -> PrettyJsonValue { + PrettyJsonValue(value) + } } \ No newline at end of file diff --git a/crates/pumpfun/src/lib.rs b/crates/pumpfun/src/lib.rs index bbaf79f..f13b043 100755 --- a/crates/pumpfun/src/lib.rs +++ b/crates/pumpfun/src/lib.rs @@ -25,6 +25,7 @@ use anchor_spl::associated_token::{ get_associated_token_address, spl_associated_token_account::instruction::create_associated_token_account, }; + use instruction::logs_subscribe; use instruction::logs_subscribe::SubscriptionHandle; use instruction::logs_events::DexEvent; @@ -41,6 +42,7 @@ use crate::error::ClientError; const DEFAULT_SLIPPAGE: u64 = 1000; // 10% const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 10_000_000; const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500_000; +const JITO_TIP_AMOUNT: u64 = 1_000; // 0.000001 SOL /// Priority fee configuration #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -87,7 +89,7 @@ impl PumpFun { commitment.unwrap_or(CommitmentConfig::confirmed()) ); - let jito_client = jito_url.map(|url| JitoClient::new(&url)); + let jito_client = jito_url.map(|url| JitoClient::new(&url, None)); Self { rpc, @@ -251,16 +253,13 @@ impl PumpFun { mint: &Pubkey, amount_sol: u64, slippage_basis_points: Option, - priority_fee: Option, - ) -> Result { + ) -> Result { let start_time = Instant::now(); let jito_client = self.jito_client.as_ref() .ok_or_else(|| ClientError::Other("Jito client not found".to_string()))?; let global_account = self.get_global_account()?; - let bonding_curve_pda = Self::get_bonding_curve_pda(mint) - .ok_or(ClientError::BondingCurveNotFound)?; let bonding_curve_account = self.get_bonding_curve_account(mint)?; let buy_amount = bonding_curve_account .get_buy_price(amount_sol) @@ -268,12 +267,9 @@ impl PumpFun { let buy_amount_with_slippage = utils::calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - let (unit_limit, _unit_price) = self.get_compute_units(priority_fee); - let mut instructions = self.create_priority_fee_instructions(priority_fee); + let mut instructions = vec![]; - let priority_fees = jito_client.estimate_priority_fees(&bonding_curve_pda).await?; let tip_account = jito_client.get_tip_account().await?; - let ata = get_associated_token_address(&self.payer.pubkey(), mint); if self.rpc.get_account(&ata).is_err() { instructions.push(create_associated_token_account( @@ -293,14 +289,12 @@ impl PumpFun { _max_sol_cost: buy_amount_with_slippage, }, )); - - let total_priority_fee = self.calculate_priority_fee(priority_fees.per_compute_unit.extreme, unit_limit); instructions.push( system_instruction::transfer( &self.payer.pubkey(), &tip_account, - total_priority_fee, + JITO_TIP_AMOUNT, ), ); @@ -396,14 +390,36 @@ impl PumpFun { self.sell(mint, Some(amount), slippage_basis_points, priority_fee).await } + pub async fn sell_by_percent_with_jito( + &self, + mint: &Pubkey, + percent: u64, + slippage_basis_points: Option, + ) -> Result { + if percent > 100 { + return Err(ClientError::Other("Percentage must be between 0 and 100".to_string())); + } + + let ata = get_associated_token_address(&self.payer.pubkey(), mint); + let balance = self.rpc.get_token_account_balance(&ata)?; + let balance_u64 = balance.amount.parse::() + .map_err(|_| ClientError::Other("Failed to parse token balance".to_string()))?; + + if balance_u64 == 0 { + return Err(ClientError::Other("Balance is 0".to_string())); + } + + let amount = balance_u64 * percent / 100; + self.sell_with_jito(mint, Some(amount), slippage_basis_points).await + } + /// Sell tokens using Jito pub async fn sell_with_jito( &self, mint: &Pubkey, amount_token: Option, slippage_basis_points: Option, - priority_fee: Option, - ) -> Result { + ) -> Result { let start_time = Instant::now(); let jito_client = self.jito_client.as_ref() @@ -420,8 +436,6 @@ impl PumpFun { } let global_account = self.get_global_account()?; - let bonding_curve_pda = Self::get_bonding_curve_pda(mint) - .ok_or(ClientError::BondingCurveNotFound)?; let bonding_curve_account = self.get_bonding_curve_account(mint)?; let min_sol_output = bonding_curve_account .get_sell_price(amount, global_account.fee_basis_points) @@ -431,12 +445,8 @@ impl PumpFun { slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), ); - let (unit_limit, _unit_price) = self.get_compute_units(priority_fee); - let mut instructions = self.create_priority_fee_instructions(priority_fee); - - let priority_fees = jito_client.estimate_priority_fees(&bonding_curve_pda).await?; + let mut instructions = vec![]; let tip_account = jito_client.get_tip_account().await?; - instructions.push(instruction::sell( &self.payer.clone(), mint, @@ -447,13 +457,11 @@ impl PumpFun { }, )); - let total_priority_fee = self.calculate_priority_fee(priority_fees.per_compute_unit.extreme, unit_limit); - instructions.push( system_instruction::transfer( &self.payer.pubkey(), &tip_account, - total_priority_fee, + JITO_TIP_AMOUNT, ), ); @@ -485,18 +493,6 @@ impl PumpFun { instructions } - fn get_compute_units(&self, priority_fee: Option) -> (u32, u64) { - let fee = priority_fee.unwrap_or(PriorityFee::default()); - let unit_limit = fee.limit.unwrap_or(DEFAULT_COMPUTE_UNIT_LIMIT); - let unit_price = fee.price.unwrap_or(DEFAULT_COMPUTE_UNIT_PRICE); - (unit_limit, unit_price) - } - - fn calculate_priority_fee(&self, priority_fee_per_cu: u64, unit_limit: u32) -> u64 { - let total_priority_fee_microlamports = priority_fee_per_cu as u128 * unit_limit as u128; - (total_priority_fee_microlamports / 1_000_000) as u64 - } - // Public interface methods pub fn get_payer_pubkey(&self) -> Pubkey { self.payer.pubkey() @@ -504,6 +500,10 @@ impl PumpFun { pub fn get_token_balance(&self, account: &Pubkey, mint: &Pubkey) -> Result { let ata = get_associated_token_address(account, mint); + if self.rpc.get_account(&ata).is_err() { + return Ok(0); + } + let balance = self.rpc.get_token_account_balance(&ata)?; balance.amount.parse::() .map_err(|_| ClientError::Other("Failed to parse token balance".to_string()))