diff --git a/Cargo.toml b/Cargo.toml index 848bc5f..beda717 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "mai3-pumpfun-sdk" -version = "2.4.5" +version = "2.4.3" edition = "2021" -authors = ["William "] +authors = ["William "] repository = "https://github.com/MiracleAI-Labs/pumpfun-sdk" description = "Rust SDK to interact with the Pump.fun Solana program." license = "MIT" -keywords = ["solana", "memecoins", "pumpfun", "pumpfun-sdk"] +keywords = ["solana", "memecoins", "pumpfun", "pumpfun-sdk", "pumpbot"] readme = "README.md" [lib] @@ -33,7 +33,7 @@ bs58 = "0.5.1" rand = "0.9.0" bincode = "1.3.3" anyhow = "1.0.90" -reqwest = { version = "0.12.12", features = ["json"] } +reqwest = { version = "0.12.12", features = ["json", "multipart"] } tonic = { version = "0.12.3", features = ["tls", "tls-webpki-roots", "tls-roots"] } tokio = { version = "1.42.0" , features = ["full", "rt-multi-thread"]} yellowstone-grpc-client = { version = "5.0.0" } diff --git a/src/constants/mod.rs b/src/constants/mod.rs index e13c78b..bf883d5 100755 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -51,3 +51,10 @@ pub mod accounts { /// Rent Sysvar ID pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111"); } + +pub mod trade { + pub const JITO_TIP_AMOUNT: f64 = 0.0001; + pub const DEFAULT_SLIPPAGE: u64 = 3000; // 30% + pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000; + pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000; +} diff --git a/src/instruction/mod.rs b/src/instruction/mod.rs index 9543774..8d205c7 100755 --- a/src/instruction/mod.rs +++ b/src/instruction/mod.rs @@ -10,7 +10,7 @@ //! - `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}; +use crate::{constants, trade::common::{get_bonding_curve_pda, get_global_pda, get_metadata_pda, get_mint_authority_pda}, PumpFun}; use spl_associated_token_account::get_associated_token_address; use solana_sdk::{ @@ -28,11 +28,23 @@ pub struct Create { impl Create { pub fn data(&self) -> Vec { - let mut data = Vec::with_capacity(8 + 8 + 8); + let mut data = Vec::with_capacity(8 + 4 + self._name.len() + 4 + self._symbol.len() + 4 + self._uri.len()); + + // 追加 discriminator data.extend_from_slice(&[24, 30, 200, 40, 5, 28, 7, 119]); // discriminator - data.extend_from_slice(&self._name.as_bytes()); - data.extend_from_slice(&self._symbol.as_bytes()); - data.extend_from_slice(&self._uri.as_bytes()); + + // 添加 name 字符串长度和内容 + data.extend_from_slice(&(self._name.len() as u32).to_le_bytes()); // 添加 name 长度 + data.extend_from_slice(self._name.as_bytes()); // 添加 name 内容 + + // 添加 symbol 字符串长度和内容 + data.extend_from_slice(&(self._symbol.len() as u32).to_le_bytes()); // 添加 symbol 长度 + data.extend_from_slice(self._symbol.as_bytes()); // 添加 symbol 内容 + + // 添加 uri 字符串长度和内容 + data.extend_from_slice(&(self._uri.len() as u32).to_le_bytes()); // 添加 uri 长度 + data.extend_from_slice(self._uri.as_bytes()); // 添加 uri 内容 + data } } @@ -82,21 +94,21 @@ impl Sell { /// /// Returns a Solana instruction that when executed will create the token and its accounts pub fn create(payer: &Keypair, mint: &Keypair, args: Create) -> Instruction { - let bonding_curve: Pubkey = PumpFun::get_bonding_curve_pda(&mint.pubkey()).unwrap(); + let bonding_curve: Pubkey = get_bonding_curve_pda(&mint.pubkey()).unwrap(); Instruction::new_with_bytes( constants::accounts::PUMPFUN, &args.data(), vec![ AccountMeta::new(mint.pubkey(), true), - AccountMeta::new(PumpFun::get_mint_authority_pda(), false), + AccountMeta::new(get_mint_authority_pda(), false), AccountMeta::new(bonding_curve, false), AccountMeta::new( get_associated_token_address(&bonding_curve, &mint.pubkey()), false, ), - AccountMeta::new_readonly(PumpFun::get_global_pda(), false), + AccountMeta::new_readonly(get_global_pda(), false), AccountMeta::new_readonly(constants::accounts::MPL_TOKEN_METADATA, false), - AccountMeta::new(PumpFun::get_metadata_pda(&mint.pubkey()), false), + AccountMeta::new(get_metadata_pda(&mint.pubkey()), false), AccountMeta::new(payer.pubkey(), true), AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false), AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false), @@ -130,12 +142,12 @@ pub fn buy( fee_recipient: &Pubkey, args: Buy, ) -> Instruction { - let bonding_curve: Pubkey = PumpFun::get_bonding_curve_pda(mint).unwrap(); + let bonding_curve: Pubkey = get_bonding_curve_pda(mint).unwrap(); Instruction::new_with_bytes( constants::accounts::PUMPFUN, &args.data(), vec![ - AccountMeta::new_readonly(PumpFun::get_global_pda(), false), + AccountMeta::new_readonly(get_global_pda(), false), AccountMeta::new(*fee_recipient, false), AccountMeta::new_readonly(*mint, false), AccountMeta::new(bonding_curve, false), @@ -173,12 +185,12 @@ pub fn sell( fee_recipient: &Pubkey, args: Sell, ) -> Instruction { - let bonding_curve: Pubkey = PumpFun::get_bonding_curve_pda(mint).unwrap(); + let bonding_curve: Pubkey = get_bonding_curve_pda(mint).unwrap(); Instruction::new_with_bytes( constants::accounts::PUMPFUN, &args.data(), vec![ - AccountMeta::new_readonly(PumpFun::get_global_pda(), false), + AccountMeta::new_readonly(get_global_pda(), false), AccountMeta::new(*fee_recipient, false), AccountMeta::new_readonly(*mint, false), AccountMeta::new(bonding_curve, false), diff --git a/src/ipfs/mod.rs b/src/ipfs/mod.rs new file mode 100755 index 0000000..a365e30 --- /dev/null +++ b/src/ipfs/mod.rs @@ -0,0 +1,161 @@ +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, api_key: &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, api_key).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 {}", api_key)) + .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, api_key: &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 {}", api_key)) + .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/jito/mod.rs b/src/jito/mod.rs index 2ef8d93..cee2175 100755 --- a/src/jito/mod.rs +++ b/src/jito/mod.rs @@ -60,7 +60,7 @@ impl JitoClient { { let accounts = self.tip_accounts.read().await; if !accounts.is_empty() { - if let Some(acc) = accounts.iter().choose(&mut rand::thread_rng()) { + if let Some(acc) = accounts.iter().choose(&mut rand::rng()) { return Pubkey::from_str(acc) .map_err(|err| { error!("jito: failed to parse Pubkey: {:?}", err); diff --git a/src/lib.rs b/src/lib.rs index 9416855..643cfdc 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,75 +2,43 @@ pub mod accounts; pub mod constants; pub mod error; pub mod instruction; -pub mod utils; pub mod jito; pub mod grpc; pub mod common; +pub mod ipfs; +pub mod trade; + +use std::sync::Arc; use anyhow::anyhow; -use solana_client::{ - rpc_client::RpcClient, - rpc_config::RpcSimulateTransactionConfig -}; +use solana_client::rpc_client::RpcClient; use solana_sdk::{ - commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction -}; -use spl_associated_token_account::{ - get_associated_token_address, - instruction::create_associated_token_account, + commitment_config::CommitmentConfig, + pubkey::Pubkey, + signature::{Keypair, Signer, Signature}, }; use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe}; use common::logs_subscribe::SubscriptionHandle; -use spl_token::instruction::close_account; - -use std::sync::Arc; -use std::time::Instant; -use std::collections::HashMap; -use tokio::sync::RwLock; +use ipfs::TokenMetadataIPFS; use crate::jito::JitoClient; - -use borsh::BorshDeserialize; - -// Constants -const DEFAULT_SLIPPAGE: u64 = 1000; // 10% -const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000; -const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000; -const JITO_TIP_AMOUNT: f64 = 0.00006; - -// Cache -lazy_static::lazy_static! { - static ref ACCOUNT_CACHE: RwLock>> = RwLock::new(HashMap::new()); - static ref BONDING_CURVE_CACHE: RwLock>> = RwLock::new(HashMap::new()); -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PriorityFee { - pub limit: Option, - pub price: Option, -} - -impl Default for PriorityFee { - fn default() -> Self { - Self { limit: Some(DEFAULT_COMPUTE_UNIT_LIMIT), price: Some(DEFAULT_COMPUTE_UNIT_PRICE) } - } -} +use crate::trade::common::PriorityFee; pub struct PumpFun { - pub rpc: RpcClient, pub payer: Arc, + pub rpc: RpcClient, pub jito_client: Option, } impl Clone for PumpFun { fn clone(&self) -> Self { Self { + payer: self.payer.clone(), rpc: RpcClient::new_with_commitment( self.rpc.url().to_string(), self.rpc.commitment() ), - payer: self.payer.clone(), jito_client: self.jito_client.clone(), } } @@ -79,9 +47,9 @@ impl Clone for PumpFun { impl PumpFun { #[inline] pub fn new( + payer: Arc, rpc_url: String, commitment: Option, - payer: Arc, jito_url: Option, ) -> Self { let rpc = RpcClient::new_with_commitment( @@ -92,8 +60,8 @@ impl PumpFun { let jito_client = jito_url.map(|url| JitoClient::new(&url, None)); Self { - rpc, payer, + rpc, jito_client, } } @@ -102,105 +70,78 @@ impl PumpFun { pub async fn create( &self, mint: &Keypair, - metadata: utils::CreateTokenMetadata, + ipfs: TokenMetadataIPFS, priority_fee: Option, ) -> Result { - let ipfs = utils::create_token_metadata(metadata) - .await - .map_err(|e| anyhow!("Failed to upload metadata: {}", e))?; - - let mut instructions = self.create_priority_fee_instructions(priority_fee); - - instructions.push(instruction::create( - &self.payer.clone(), + trade::create::create( + &self.rpc, + &self.payer, mint, - instruction::Create { - _name: ipfs.metadata.name, - _symbol: ipfs.metadata.symbol, - _uri: ipfs.metadata.image, - }, - )); - - let recent_blockhash = self.rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&self.payer.pubkey()), - &[&self.payer.clone(), mint], - recent_blockhash, - ); - - let signature = self.rpc.send_and_confirm_transaction(&transaction)?; - - Ok(signature) + ipfs, + priority_fee, + ).await } - /// Create and buy tokens in one transaction pub async fn create_and_buy( &self, mint: &Keypair, - metadata: utils::CreateTokenMetadata, + ipfs: TokenMetadataIPFS, amount_sol: u64, slippage_basis_points: Option, priority_fee: Option, ) -> Result { - if amount_sol == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let ipfs = utils::create_token_metadata(metadata) - .await - .map_err(|e| anyhow!("Failed to upload metadata: {}", e))?; - - let global_account = self.get_global_account().await?; - let buy_amount = global_account.get_initial_buy_price(amount_sol); - let buy_amount_with_slippage = - utils::calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - - let mut instructions = self.create_priority_fee_instructions(priority_fee); - - instructions.push(instruction::create( - &self.payer.clone(), + trade::create::create_and_buy( + &self.rpc, + &self.payer, mint, - instruction::Create { - _name: ipfs.metadata.name, - _symbol: ipfs.metadata.symbol, - _uri: ipfs.metadata.image, - }, - )); - - let ata = get_associated_token_address(&self.payer.pubkey(), &mint.pubkey()); - if self.rpc.get_account(&ata).is_err() { - instructions.push(create_associated_token_account( - &self.payer.pubkey(), - &self.payer.pubkey(), - &mint.pubkey(), - &constants::accounts::TOKEN_PROGRAM, - )); - } - - instructions.push(instruction::buy( - &self.payer.clone(), - &mint.pubkey(), - &global_account.fee_recipient, - instruction::Buy { - _amount: buy_amount, - _max_sol_cost: buy_amount_with_slippage, - }, - )); - - let recent_blockhash = self.rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&self.payer.pubkey()), - &[&self.payer.clone(), mint], - recent_blockhash, - ); - - let signature = self.rpc.send_and_confirm_transaction(&transaction)?; - - Ok(signature) + ipfs, + amount_sol, + slippage_basis_points, + priority_fee, + ).await } + pub async fn create_and_buy_list_with_jito( + &self, + payers: Vec<&Keypair>, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + amount_sols: Vec, + slippage_basis_points: Option, + jito_fee: Option, + ) -> Result { + trade::create::create_and_buy_list_with_jito( + &self.rpc, + &self.jito_client.as_ref().unwrap(), + payers, + mint, + ipfs, + amount_sols, + slippage_basis_points, + jito_fee, + ).await + } + + pub async fn create_and_buy_with_jito( + &self, + payer: &Keypair, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + jito_fee: Option, + ) -> Result { + trade::create::create_and_buy_with_jito( + &self.rpc, + &self.jito_client.as_ref().unwrap(), + payer, + mint, + ipfs, + amount_sol, + slippage_basis_points, + jito_fee, + ).await + } /// Buy tokens pub async fn buy( &self, @@ -209,117 +150,52 @@ impl PumpFun { slippage_basis_points: Option, priority_fee: Option, ) -> Result { - if amount_sol == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = self.get_global_account().await?; - let bonding_curve_account = self.get_bonding_curve_account(mint).await?; - let buy_amount = bonding_curve_account - .get_buy_price(amount_sol) - .map_err(|e| anyhow!(e))?; - let buy_amount_with_slippage = - utils::calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - - let mut instructions = self.create_priority_fee_instructions(priority_fee); - - let ata = get_associated_token_address(&self.payer.pubkey(), mint); - if self.rpc.get_account(&ata).is_err() { - instructions.push(create_associated_token_account( - &self.payer.pubkey(), - &self.payer.pubkey(), - mint, - &constants::accounts::TOKEN_PROGRAM, - )); - } - - instructions.push(instruction::buy( - &self.payer.clone(), + trade::buy::buy( + &self.rpc, + &self.payer, mint, - &global_account.fee_recipient, - instruction::Buy { - _amount: buy_amount, - _max_sol_cost: buy_amount_with_slippage, - }, - )); - - let recent_blockhash = self.rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&self.payer.pubkey()), - &[&self.payer.clone()], - recent_blockhash, - ); - - let signature = self.rpc.send_transaction(&transaction)?; - Ok(signature) + amount_sol, + slippage_basis_points, + priority_fee, + ).await } /// Buy tokens using Jito pub async fn buy_with_jito( &self, mint: &Pubkey, - buy_token_amount: u64, - max_sol_cost: u64, + amount_sol: u64, slippage_basis_points: Option, jito_fee: Option, ) -> Result { - if buy_token_amount == 0 || max_sol_cost == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let start_time = Instant::now(); - - let jito_client = self.jito_client.as_ref() - .ok_or_else(|| anyhow!("Jito client not found"))?; - - let global_account = self.get_global_account().await?; - let buy_amount_with_slippage = - utils::calculate_with_slippage_buy(max_sol_cost, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - - let mut instructions = self.create_priority_fee_instructions(None); - let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?; - let ata = get_associated_token_address(&self.payer.pubkey(), mint); - if self.rpc.get_account(&ata).is_err() { - instructions.push(create_associated_token_account( - &self.payer.pubkey(), - &self.payer.pubkey(), - mint, - &constants::accounts::TOKEN_PROGRAM, - )); - } - - instructions.push(instruction::buy( - &self.payer.clone(), + trade::buy::buy_with_jito( + &self.rpc, + &self.jito_client.as_ref().unwrap(), + &self.payer, mint, - &global_account.fee_recipient, - instruction::Buy { - _amount: buy_token_amount, - _max_sol_cost: buy_amount_with_slippage, - }, - )); + amount_sol, + slippage_basis_points, + jito_fee, + ).await + } - let jito_fee = jito_fee.unwrap_or(JITO_TIP_AMOUNT); - instructions.push( - system_instruction::transfer( - &self.payer.pubkey(), - &tip_account, - sol_to_lamports(jito_fee), - ), - ); - - let recent_blockhash = self.rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&self.payer.pubkey()), - &[&self.payer.clone()], - recent_blockhash, - ); - - let signature = jito_client.send_transaction(&transaction).await?; - println!("Total Jito buy operation time: {:?}ms", start_time.elapsed().as_millis()); - - Ok(signature) + pub async fn buy_list_with_jito( + &self, + payers: Vec<&Keypair>, + mint: &Pubkey, + amount_sols: Vec, + slippage_basis_points: Option, + jito_fee: Option, + ) -> Result { + trade::buy::buy_list_with_jito( + &self.rpc, + &self.jito_client.as_ref().unwrap(), + payers, + mint, + amount_sols, + slippage_basis_points, + jito_fee, + ).await } /// Sell tokens @@ -329,103 +205,15 @@ impl PumpFun { amount_token: Option, slippage_basis_points: Option, priority_fee: Option, - ) -> Result<(), anyhow::Error> { - 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(|_| anyhow!("Failed to parse token balance"))?; - let amount = amount_token.unwrap_or(balance_u64); - - if amount == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = self.get_global_account().await?; - let bonding_curve_account = self.get_bonding_curve_account(mint).await?; - let min_sol_output = bonding_curve_account - .get_sell_price(amount, global_account.fee_basis_points) - .map_err(|e| anyhow!(e))?; - let min_sol_output_with_slippage = utils::calculate_with_slippage_sell( - min_sol_output, - slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), - ); - - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), - ComputeBudgetInstruction::set_compute_unit_price(0), - ]; - - instructions.push(instruction::sell( - &self.payer.clone(), + ) -> Result { + trade::sell::sell( + &self.rpc, + &self.payer, mint, - &global_account.fee_recipient, - instruction::Sell { - _amount: amount, - _min_sol_output: min_sol_output_with_slippage, - }, - )); - - instructions.push(close_account( - &spl_token::ID, - &ata, - &self.payer.pubkey(), - &self.payer.pubkey(), - &[&self.payer.pubkey()], - )?); - - let commitment_config = CommitmentConfig::confirmed(); - let recent_blockhash = self.rpc.get_latest_blockhash_with_commitment(commitment_config)? - .0; - - let simulate_tx = Transaction::new_signed_with_payer( - &instructions, - Some(&self.payer.pubkey()), - &[&self.payer.clone()], - recent_blockhash, - ); - - let config = RpcSimulateTransactionConfig { - sig_verify: true, - commitment: Some(commitment_config), - ..RpcSimulateTransactionConfig::default() - }; - - let result = self.rpc.simulate_transaction_with_config(&simulate_tx, config)? - .value; - - if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) { - return Err(anyhow!("Simulation failed: {:?}", result.err)); - } - - let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?; - let fees = self.rpc.get_recent_prioritization_fees(&[])?; - let average_fees = if fees.is_empty() { - DEFAULT_COMPUTE_UNIT_PRICE - } else { - fees.iter() - .map(|fee| fee.prioritization_fee) - .sum::() / fees.len() as u64 - }; - - let unit_price = match priority_fee { - None => average_fees, - Some(pf) => pf.price.unwrap_or(DEFAULT_COMPUTE_UNIT_PRICE) - }; - - let unit_price = if unit_price == 0 { DEFAULT_COMPUTE_UNIT_PRICE } else { unit_price }; - - instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32); - instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price); - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&self.payer.pubkey()), - &[&self.payer.clone()], - recent_blockhash, - ); - - self.rpc.send_and_confirm_transaction(&transaction)?; - Ok(()) + amount_token, + slippage_basis_points, + priority_fee, + ).await } /// Sell tokens by percentage @@ -435,22 +223,15 @@ impl PumpFun { percent: u64, slippage_basis_points: Option, priority_fee: Option, - ) -> Result<(), anyhow::Error> { - if percent == 0 || percent > 100 { - return Err(anyhow!("Percentage must be between 1 and 100")); - } - - 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(|_| anyhow!("Failed to parse token balance"))?; - - if balance_u64 == 0 { - return Err(anyhow!("Balance is 0")); - } - - let amount = balance_u64 * percent / 100; - self.sell(mint, Some(amount), slippage_basis_points, priority_fee).await + ) -> Result { + trade::sell::sell_by_percent( + &self.rpc, + &self.payer, + mint, + percent, + slippage_basis_points, + priority_fee, + ).await } pub async fn sell_by_percent_with_jito( @@ -460,21 +241,15 @@ impl PumpFun { slippage_basis_points: Option, jito_fee: Option, ) -> Result { - if percent == 0 || percent > 100 { - return Err(anyhow!("Percentage must be between 1 and 100")); - } - - 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(|_| anyhow!("Failed to parse token balance"))?; - - if balance_u64 == 0 { - return Err(anyhow!("Balance is 0")); - } - - let amount = balance_u64 * percent / 100; - self.sell_with_jito(mint, Some(amount), slippage_basis_points, jito_fee).await + trade::sell::sell_by_percent_with_jito( + &self.rpc, + &self.payer, + self.jito_client.as_ref().unwrap(), + mint, + percent, + slippage_basis_points, + jito_fee, + ).await } /// Sell tokens using Jito @@ -485,232 +260,18 @@ impl PumpFun { slippage_basis_points: Option, jito_fee: Option, ) -> Result { - let start_time = Instant::now(); - let jito_client = self.jito_client.as_ref() .ok_or_else(|| anyhow!("Jito client not found"))?; - 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(|_| anyhow!("Failed to parse token balance"))?; - let amount = amount_token.unwrap_or(balance_u64); - - if amount == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = self.get_global_account().await?; - let bonding_curve_account = self.get_bonding_curve_account(mint).await?; - let min_sol_output = bonding_curve_account - .get_sell_price(amount, global_account.fee_basis_points) - .map_err(|e| anyhow!(e))?; - let min_sol_output_with_slippage = utils::calculate_with_slippage_sell( - min_sol_output, - slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), - ); - - let mut instructions = self.create_priority_fee_instructions(None); - let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?; - instructions.push(instruction::sell( - &self.payer.clone(), + trade::sell::sell_with_jito( + &self.rpc, + &self.payer, + jito_client, mint, - &global_account.fee_recipient, - instruction::Sell { - _amount: amount, - _min_sol_output: min_sol_output_with_slippage, - }, - )); - - instructions.push(close_account( - &spl_token::ID, - &ata, - &self.payer.pubkey(), - &self.payer.pubkey(), - &[&self.payer.pubkey()], - )?); - - let jito_fee = jito_fee.unwrap_or(JITO_TIP_AMOUNT); - instructions.push( - system_instruction::transfer( - &self.payer.pubkey(), - &tip_account, - sol_to_lamports(jito_fee), - ), - ); - - let recent_blockhash = self.rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&self.payer.pubkey()), - &[&self.payer.clone()], - recent_blockhash, - ); - - let signature = jito_client.send_transaction(&transaction).await?; - println!("Total Jito sell operation time: {:?}ms", start_time.elapsed().as_millis()); - - Ok(signature) - } - - pub async fn transfer_sol(&self, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { - if amount == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let balance = self.get_payer_sol_balance()?; - if balance < amount { - return Err(anyhow!("Insufficient balance")); - } - - let transfer_instruction = system_instruction::transfer( - &self.payer.pubkey(), - receive_wallet, - amount, - ); - - let recent_blockhash = self.rpc.get_latest_blockhash()?; - - let transaction = Transaction::new_signed_with_payer( - &[transfer_instruction], - Some(&self.payer.pubkey()), - &[&self.payer.clone()], - recent_blockhash, - ); - - self.rpc.send_and_confirm_transaction(&transaction)?; - - Ok(()) - } - - // Helper methods - #[inline] - fn create_priority_fee_instructions(&self, priority_fee: Option) -> Vec { - let mut instructions = Vec::with_capacity(2); - let fee = priority_fee.unwrap_or(PriorityFee::default()); - if let Some(limit) = fee.limit { - instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(limit)); - } - if let Some(price) = fee.price { - instructions.push(ComputeBudgetInstruction::set_compute_unit_price(price)); - } - - instructions - } - - #[inline] - pub fn get_rpc(&self) -> &RpcClient { - &self.rpc - } - - #[inline] - pub fn get_payer_pubkey(&self) -> Pubkey { - self.payer.pubkey() - } - - #[inline] - 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(|_| anyhow!("Failed to parse token balance")) - } - - #[inline] - pub fn get_sol_balance(&self, account: &Pubkey) -> Result { - self.rpc.get_balance(account).map_err(|_| anyhow!("Failed to get SOL balance")) - } - - #[inline] - pub fn get_payer_token_balance(&self, mint: &Pubkey) -> Result { - self.get_token_balance(&self.payer.pubkey(), mint) - } - - #[inline] - pub fn get_payer_sol_balance(&self) -> Result { - self.get_sol_balance(&self.payer.pubkey()) - } - - #[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 { - Pubkey::try_find_program_address( - &[constants::seeds::BONDING_CURVE_SEED, mint.as_ref()], - &constants::accounts::PUMPFUN - ).map(|(pubkey, _)| pubkey) - } - - #[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(&self) -> Result, anyhow::Error> { - let global = Self::get_global_pda(); - - // Try cache first - if let Some(account) = ACCOUNT_CACHE.read().await.get(&global) { - return Ok(account.clone()); - } - - // Cache miss, fetch from RPC - let account = self.rpc.get_account(&global)?; - let global_account = Arc::new(accounts::GlobalAccount::try_from_slice(&account.data)?); - - // Update cache - ACCOUNT_CACHE.write().await.insert(global, global_account.clone()); - - Ok(global_account) - } - - #[inline] - pub async fn get_bonding_curve_account( - &self, - mint: &Pubkey, - ) -> Result, anyhow::Error> { - let bonding_curve_pda = Self::get_bonding_curve_pda(mint) - .ok_or(anyhow!("Bonding curve not found"))?; - - // Try cache first - if let Some(account) = BONDING_CURVE_CACHE.read().await.get(&bonding_curve_pda) { - return Ok(account.clone()); - } - - // Cache miss, fetch from RPC - let account = self.rpc.get_account(&bonding_curve_pda)?; - let bonding_curve = Arc::new(accounts::BondingCurveAccount::try_from_slice(&account.data)?); - - // Update cache - BONDING_CURVE_CACHE.write().await.insert(bonding_curve_pda, bonding_curve.clone()); - - Ok(bonding_curve) + amount_token, + slippage_basis_points, + jito_fee, + ).await } #[inline] @@ -733,29 +294,32 @@ impl PumpFun { } #[inline] - pub fn get_buy_amount_with_slippage(&self, amount_sol: u64, slippage_basis_points: Option) -> u64 { - utils::calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)) + pub async fn get_sol_balance(&self, payer: &Pubkey) -> Result { + trade::common::get_sol_balance(&self.rpc, payer) } #[inline] - pub fn get_token_price(&self, 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 + pub async fn get_token_balance(&self, payer: &Pubkey, mint: &Pubkey) -> Result { + trade::common::get_token_balance(&self.rpc, payer, mint) } #[inline] - pub fn get_buy_price(&self, amount: u64, trade_info: &TradeInfo) -> Result { - if amount == 0 { - return Ok(0); - } + pub async fn get_payer_token_balance(&self, mint: &Pubkey) -> Result { + trade::common::get_token_balance(&self.rpc, &self.payer.pubkey(), mint) + } - 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; - - Ok(s_u64.min(trade_info.real_token_reserves)) + #[inline] + pub fn get_token_price(&self,virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { + trade::common::get_token_price(virtual_sol_reserves, virtual_token_reserves) + } + + #[inline] + pub fn get_buy_price(&self, amount: u64, trade_info: &TradeInfo) -> u64 { + trade::common::get_buy_price(amount, trade_info) + } + + #[inline] + pub async fn transfer_sol(&self, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { + trade::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await } } diff --git a/src/trade/buy.rs b/src/trade/buy.rs new file mode 100755 index 0000000..1d9543c --- /dev/null +++ b/src/trade/buy.rs @@ -0,0 +1,264 @@ +use anyhow::anyhow; +use solana_client::{rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig}; +use solana_sdk::{ + commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction +}; +use spl_associated_token_account::{ + get_associated_token_address, + instruction::create_associated_token_account, +}; +use std::time::Instant; + +use crate::{constants::{self, trade::{DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SLIPPAGE, JITO_TIP_AMOUNT}}, instruction, jito::JitoClient}; + +use super::common::{calculate_with_slippage_buy, get_bonding_curve_account, get_global_account, get_initial_buy_price, PriorityFee}; + +pub async fn buy( + rpc: &RpcClient, + payer: &Keypair, + mint: &Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result { + let transaction = build_buy_transaction(rpc, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?; + let signature = rpc.send_transaction(&transaction)?; + Ok(signature) +} + +/// Buy tokens using Jito +pub async fn buy_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payer: &Keypair, + mint: &Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result { + let start_time = Instant::now(); + + let transaction = build_buy_transaction_with_jito(rpc, jito_client, payer, mint, amount_sol, slippage_basis_points, jito_fee).await?; + let signature = jito_client.send_transaction(&transaction).await?; + + println!("Total Jito buy operation time: {:?}ms", start_time.elapsed().as_millis()); + + Ok(signature) +} + +pub async fn buy_list_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payers: Vec<&Keypair>, + mint: &Pubkey, + amount_sols: Vec, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result { + let start_time = Instant::now(); + + let mut transactions = vec![]; + for (i, payer) in payers.iter().enumerate() { + let transaction = build_buy_transaction_with_jito(rpc, jito_client, payer, mint, amount_sols[i], slippage_basis_points, jito_fee).await?; + transactions.push(transaction); + } + + let signature = jito_client.send_transactions(&transactions).await?; + + println!("Total Jito buy operation time: {:?}ms", start_time.elapsed().as_millis()); + + Ok(signature) +} + +pub async fn build_buy_transaction( + rpc: &RpcClient, + payer: &Keypair, + mint: &Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result { + let instructions = build_buy_instructions(rpc, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?; + + let recent_blockhash = rpc.get_latest_blockhash()?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_buy_transaction_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payer: &Keypair, + mint: &Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result { + let instructions = build_buy_instructions_with_jito(rpc, jito_client, payer, mint, amount_sol, slippage_basis_points, jito_fee).await?; + let recent_blockhash = rpc.get_latest_blockhash()?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_buy_instructions( + rpc: &RpcClient, + payer: &Keypair, + mint: &Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let global_account = get_global_account(rpc).await?; + let bonding_curve_account = get_bonding_curve_account(rpc, mint).await?; + let buy_amount = bonding_curve_account + .get_buy_price(amount_sol) + .map_err(|e| anyhow!(e))?; + let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); + + let mut instructions = vec![ + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + ComputeBudgetInstruction::set_compute_unit_price(0), + ]; + + let ata = get_associated_token_address(&payer.pubkey(), mint); + if rpc.get_account(&ata).is_err() { + instructions.push(create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + mint, + &constants::accounts::TOKEN_PROGRAM, + )); + } + + instructions.push(instruction::buy( + payer, + mint, + &global_account.fee_recipient, + instruction::Buy { + _amount: buy_amount, + _max_sol_cost: buy_amount_with_slippage, + }, + )); + + let commitment_config = CommitmentConfig::confirmed(); + let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config)? + .0; + + let simulate_tx = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + let config = RpcSimulateTransactionConfig { + sig_verify: true, + commitment: Some(commitment_config), + ..RpcSimulateTransactionConfig::default() + }; + + let result = rpc.simulate_transaction_with_config(&simulate_tx, config)? + .value; + + if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) { + return Err(anyhow!("Simulation failed: {:?}", result.err)); + } + + let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?; + let fees = rpc.get_recent_prioritization_fees(&[])?; + let average_fees = if fees.is_empty() { + DEFAULT_COMPUTE_UNIT_PRICE + } else { + fees.iter() + .map(|fee| fee.prioritization_fee) + .sum::() / fees.len() as u64 + }; + + let unit_price = match priority_fee { + None => average_fees, + Some(pf) => pf.price.unwrap_or(DEFAULT_COMPUTE_UNIT_PRICE) + }; + + let unit_price = if unit_price == 0 { DEFAULT_COMPUTE_UNIT_PRICE } else { unit_price }; + + instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32); + instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price); + + Ok(instructions) + +} + +pub async fn build_buy_instructions_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payer: &Keypair, + mint: &Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let global_account = get_global_account(rpc).await?; + let buy_amount = match get_bonding_curve_account(rpc, mint).await { + Ok(account) => account.get_buy_price(amount_sol).map_err(|e| anyhow!(e))?, + Err(_e) => { + let initial_buy_amount = get_initial_buy_price(&global_account, amount_sol).await?; + initial_buy_amount * 80 / 100 + } + }; + + let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); + + let mut instructions = vec![]; + let ata = get_associated_token_address(&payer.pubkey(), mint); + if rpc.get_account(&ata).is_err() { + instructions.push(create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + mint, + &constants::accounts::TOKEN_PROGRAM, + )); + } + + instructions.push(instruction::buy( + payer, + mint, + &global_account.fee_recipient, + instruction::Buy { + _amount: buy_amount, + _max_sol_cost: buy_amount_with_slippage, + }, + )); + + let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?; + + let jito_fee = jito_fee.unwrap_or(JITO_TIP_AMOUNT); + instructions.push( + system_instruction::transfer( + &payer.pubkey(), + &tip_account, + sol_to_lamports(jito_fee), + ), + ); + + Ok(instructions) +} diff --git a/src/trade/common.rs b/src/trade/common.rs new file mode 100755 index 0000000..79a7b64 --- /dev/null +++ b/src/trade/common.rs @@ -0,0 +1,199 @@ +use anyhow::anyhow; +use tokio::sync::RwLock; +use std::{collections::HashMap, sync::Arc}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction +}; +use spl_associated_token_account::get_associated_token_address; +use crate::{accounts, common::logs_data::TradeInfo, constants::{self, trade::{DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SLIPPAGE}}}; +use borsh::BorshDeserialize; + +lazy_static::lazy_static! { + static ref ACCOUNT_CACHE: RwLock>> = RwLock::new(HashMap::new()); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PriorityFee { + pub limit: Option, + pub price: Option, +} + +impl Default for PriorityFee { + fn default() -> Self { + Self { limit: Some(DEFAULT_COMPUTE_UNIT_LIMIT), price: Some(DEFAULT_COMPUTE_UNIT_PRICE) } + } +} + +pub async fn transfer_sol(rpc: &RpcClient, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { + if amount == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let balance = get_sol_balance(rpc, &payer.pubkey())?; + 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()?; + + let transaction = Transaction::new_signed_with_payer( + &[transfer_instruction], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + rpc.send_and_confirm_transaction(&transaction)?; + + Ok(()) +} + +#[inline] +pub fn create_priority_fee_instructions(priority_fee: Option) -> Vec { + let mut instructions = Vec::with_capacity(2); + let fee = priority_fee.unwrap_or(PriorityFee::default()); + if let Some(limit) = fee.limit { + instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(limit)); + } + if let Some(price) = fee.price { + instructions.push(ComputeBudgetInstruction::set_compute_unit_price(price)); + } + + instructions +} + +pub fn get_token_balance(rpc: &RpcClient, account: &Pubkey, mint: &Pubkey) -> Result { + let ata = get_associated_token_address(account, mint); + if rpc.get_account(&ata).is_err() { + return Ok(0); + } + + let balance = rpc.get_token_account_balance(&ata)?; + balance.amount.parse::() + .map_err(|_| anyhow!("Failed to parse token balance")) +} + +pub fn get_sol_balance(rpc: &RpcClient, account: &Pubkey) -> Result { + rpc.get_balance(account).map_err(|_| anyhow!("Failed to get SOL 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_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: &RpcClient) -> Result, anyhow::Error> { + let global = get_global_pda(); + + // Try cache first + if let Some(account) = ACCOUNT_CACHE.read().await.get(&global) { + return Ok(account.clone()); + } + + // Cache miss, fetch from RPC + let account = rpc.get_account(&global)?; + let global_account = Arc::new(accounts::GlobalAccount::try_from_slice(&account.data)?); + + // Update cache + 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: &RpcClient, + mint: &Pubkey, +) -> Result, anyhow::Error> { + let bonding_curve_pda = get_bonding_curve_pda(mint) + .ok_or(anyhow!("Bonding curve not found"))?; + + if rpc.get_account(&bonding_curve_pda).is_err() { + return Err(anyhow!("Bonding curve not found")); + } + + let account = rpc.get_account(&bonding_curve_pda)?; + let bonding_curve = Arc::new(accounts::BondingCurveAccount::try_from_slice(&account.data)?); + Ok(bonding_curve) +} + +#[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) +} + +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 +} + +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/trade/create.rs b/src/trade/create.rs new file mode 100755 index 0000000..1dc2dfc --- /dev/null +++ b/src/trade/create.rs @@ -0,0 +1,326 @@ +use std::time::Instant; + +use anyhow::anyhow; +use solana_client::{rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig}; +use solana_sdk::{ + commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction +}; +use spl_associated_token_account::{ + get_associated_token_address, + instruction::create_associated_token_account, +}; + +use crate::{constants::{self, trade::{DEFAULT_COMPUTE_UNIT_PRICE, JITO_TIP_AMOUNT}}, instruction, ipfs::TokenMetadataIPFS, jito::JitoClient, trade::buy::build_buy_transaction_with_jito}; + +use super::common::{create_priority_fee_instructions, get_buy_amount_with_slippage, get_global_account, PriorityFee}; + +/// Create a new token +pub async fn create( + rpc: &RpcClient, + payer: &Keypair, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + priority_fee: Option, +) -> Result { + let mut instructions = create_priority_fee_instructions(priority_fee); + + instructions.push(instruction::create( + payer, + mint, + instruction::Create { + _name: ipfs.metadata.name, + _symbol: ipfs.metadata.symbol, + _uri: ipfs.metadata_uri, + }, + )); + + let recent_blockhash = rpc.get_latest_blockhash()?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, mint], + recent_blockhash, + ); + + let signature = rpc.send_and_confirm_transaction(&transaction)?; + + Ok(signature) +} + +/// Create and buy tokens in one transaction +pub async fn create_and_buy( + rpc: &RpcClient, + payer: &Keypair, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let transaction = build_create_and_buy_transaction(rpc, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?; + let signature = rpc.send_and_confirm_transaction(&transaction)?; + + Ok(signature) +} + +pub async fn create_and_buy_list_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payers: Vec<&Keypair>, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + amount_sols: Vec, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result { + + let start_time = Instant::now(); + + let mut transactions = Vec::new(); + let transaction = build_create_and_buy_transaction_with_jito(rpc, jito_client, payers[0], mint, ipfs, amount_sols[0], slippage_basis_points, jito_fee).await?; + transactions.push(transaction); + + for (i, payer) in payers.iter().skip(1).enumerate() { + println!("Creating and buying token index: {}", i); + let buy_transaction = build_buy_transaction_with_jito(rpc, jito_client, payer, &mint.pubkey(), amount_sols[i], slippage_basis_points, jito_fee).await?; + transactions.push(buy_transaction); + } + + let signatures = jito_client.send_transactions(&transactions).await?; + + println!("Total Jito create and buy operation time: {:?}ms", start_time.elapsed().as_millis()); + + Ok(signatures) +} + +pub async fn create_and_buy_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payer: &Keypair, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result { + + let start_time = Instant::now(); + + let transaction = build_create_and_buy_transaction_with_jito(rpc, jito_client, payer, mint, ipfs, amount_sol, slippage_basis_points, jito_fee).await?; + + let signature = jito_client.send_transaction(&transaction).await?; + + println!("Total Jito create and buy operation time: {:?}ms, signature: {}", start_time.elapsed().as_millis(), signature); + + Ok(signature) +} + +pub async fn build_create_and_buy_transaction( + rpc: &RpcClient, + payer: &Keypair, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result { + let instructions = build_create_and_buy_instructions(rpc, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?; + let recent_blockhash = rpc.get_latest_blockhash()?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, mint], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_create_and_buy_transaction_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payer: &Keypair, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result { + let instructions = build_create_and_buy_instructions_with_jito(rpc, jito_client, payer, mint, ipfs, amount_sol, slippage_basis_points, jito_fee).await?; + let recent_blockhash = rpc.get_latest_blockhash()?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, mint], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_create_and_buy_instructions( + rpc: &RpcClient, + payer: &Keypair, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let global_account = get_global_account(rpc).await?; + let buy_amount = global_account.get_initial_buy_price(amount_sol); + let buy_amount_with_slippage = + get_buy_amount_with_slippage(amount_sol, slippage_basis_points); + + let mut instructions = vec![ + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + ComputeBudgetInstruction::set_compute_unit_price(0), + ]; + + instructions.push(instruction::create( + payer, + mint, + instruction::Create { + _name: ipfs.metadata.name, + _symbol: ipfs.metadata.symbol, + _uri: ipfs.metadata_uri, + }, + )); + + let ata = get_associated_token_address(&payer.pubkey(), &mint.pubkey()); + if rpc.get_account(&ata).is_err() { + instructions.push(create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + &mint.pubkey(), + &constants::accounts::TOKEN_PROGRAM, + )); + } + + instructions.push(instruction::buy( + payer, + &mint.pubkey(), + &global_account.fee_recipient, + instruction::Buy { + _amount: buy_amount, + _max_sol_cost: buy_amount_with_slippage, + }, + )); + + let commitment_config = CommitmentConfig::confirmed(); + let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config)? + .0; + + let simulate_tx = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, mint], + recent_blockhash, + ); + + let config = RpcSimulateTransactionConfig { + sig_verify: true, + commitment: Some(commitment_config), + ..RpcSimulateTransactionConfig::default() + }; + + let result = rpc.simulate_transaction_with_config(&simulate_tx, config)? + .value; + + if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) { + return Err(anyhow!("Simulation failed: {:?}", result.err)); + } + + let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?; + let fees = rpc.get_recent_prioritization_fees(&[])?; + let average_fees = if fees.is_empty() { + DEFAULT_COMPUTE_UNIT_PRICE + } else { + fees.iter() + .map(|fee| fee.prioritization_fee) + .sum::() / fees.len() as u64 + }; + + let unit_price = match priority_fee { + None => average_fees, + Some(pf) => pf.price.unwrap_or(DEFAULT_COMPUTE_UNIT_PRICE) + }; + + let unit_price = if unit_price == 0 { DEFAULT_COMPUTE_UNIT_PRICE } else { unit_price }; + + instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32); + instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price); + + Ok(instructions) +} + +pub async fn build_create_and_buy_instructions_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payer: &Keypair, + mint: &Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let global_account = get_global_account(rpc).await?; + let buy_amount = global_account.get_initial_buy_price(amount_sol); + let buy_amount_with_slippage = + get_buy_amount_with_slippage(amount_sol, slippage_basis_points); + + let mut instructions = vec![]; + instructions.push(instruction::create( + payer, + mint, + instruction::Create { + _name: ipfs.metadata.name, + _symbol: ipfs.metadata.symbol, + _uri: ipfs.metadata_uri, + }, + )); + + let ata = get_associated_token_address(&payer.pubkey(), &mint.pubkey()); + if rpc.get_account(&ata).is_err() { + instructions.push(create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + &mint.pubkey(), + &constants::accounts::TOKEN_PROGRAM, + )); + } + + instructions.push(instruction::buy( + payer, + &mint.pubkey(), + &global_account.fee_recipient, + instruction::Buy { + _amount: buy_amount, + _max_sol_cost: buy_amount_with_slippage, + }, + )); + + let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?; + let jito_fee = jito_fee.unwrap_or(JITO_TIP_AMOUNT); + instructions.push( + system_instruction::transfer( + &payer.pubkey(), + &tip_account, + sol_to_lamports(jito_fee * 2.0), + ), + ); + + Ok(instructions) +} diff --git a/src/trade/mod.rs b/src/trade/mod.rs new file mode 100755 index 0000000..8126389 --- /dev/null +++ b/src/trade/mod.rs @@ -0,0 +1,4 @@ +pub mod buy; +pub mod create; +pub mod sell; +pub mod common; \ No newline at end of file diff --git a/src/trade/sell.rs b/src/trade/sell.rs new file mode 100755 index 0000000..3a1791b --- /dev/null +++ b/src/trade/sell.rs @@ -0,0 +1,291 @@ +use anyhow::anyhow; +use solana_client::{rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig}; +use solana_sdk::{ + commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction +}; +use spl_associated_token_account::get_associated_token_address; +use spl_token::instruction::close_account; + +use std::time::Instant; + +use crate::{constants::trade::{DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SLIPPAGE, JITO_TIP_AMOUNT}, instruction, jito::JitoClient}; + +use super::common::{calculate_with_slippage_sell, get_bonding_curve_account, get_global_account, PriorityFee}; + +async fn get_token_balance(rpc: &RpcClient, 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)?; + 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: &RpcClient, + payer: &Keypair, + mint: &Pubkey, + amount_token: Option, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result { + + let transaction = build_sell_transaction(rpc, payer, mint, amount_token, slippage_basis_points, priority_fee).await?; + let signature = rpc.send_and_confirm_transaction(&transaction)?; + + Ok(signature) +} + +/// Sell tokens by percentage +pub async fn sell_by_percent( + rpc: &RpcClient, + payer: &Keypair, + mint: &Pubkey, + percent: u64, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result { + if percent == 0 || percent > 100 { + return Err(anyhow!("Percentage must be between 1 and 100")); + } + + let (balance_u64, _) = get_token_balance(rpc, payer, mint).await?; + let amount = balance_u64 * percent / 100; + sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee).await +} + +pub async fn sell_by_percent_with_jito( + rpc: &RpcClient, + payer: &Keypair, + jito_client: &JitoClient, + mint: &Pubkey, + percent: u64, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result { + if percent == 0 || percent > 100 { + return Err(anyhow!("Percentage must be between 1 and 100")); + } + + let (balance_u64, _) = get_token_balance(rpc, payer, mint).await?; + let amount = balance_u64 * percent / 100; + sell_with_jito(rpc, payer, jito_client, mint, Some(amount), slippage_basis_points, jito_fee).await +} + +/// Sell tokens using Jito +pub async fn sell_with_jito( + rpc: &RpcClient, + payer: &Keypair, + jito_client: &JitoClient, + mint: &Pubkey, + amount_token: Option, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result { + let start_time = Instant::now(); + + let transaction = build_sell_transaction_with_jito(rpc, jito_client, payer, mint, amount_token, slippage_basis_points, jito_fee).await?; + let signature = jito_client.send_transaction(&transaction).await?; + + println!("Total Jito sell operation time: {:?}ms, signature: {}", start_time.elapsed().as_millis(), signature); + + Ok(signature) +} + +pub async fn build_sell_transaction( + rpc: &RpcClient, + payer: &Keypair, + mint: &Pubkey, + amount_token: Option, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result { + let instructions = build_sell_instructions(rpc, payer, mint, amount_token, slippage_basis_points, priority_fee).await?; + let recent_blockhash = rpc.get_latest_blockhash()?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_sell_transaction_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payer: &Keypair, + mint: &Pubkey, + amount_token: Option, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result { + let instructions = build_sell_instructions_with_jito(rpc, jito_client, payer, mint, amount_token, slippage_basis_points, jito_fee).await?; + let recent_blockhash = rpc.get_latest_blockhash()?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_sell_instructions( + rpc: &RpcClient, + payer: &Keypair, + mint: &Pubkey, + amount_token: Option, + slippage_basis_points: Option, + priority_fee: Option, +) -> Result, anyhow::Error> { + let (balance_u64, ata) = get_token_balance(rpc, payer, 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).await?; + let bonding_curve_account = get_bonding_curve_account(rpc, mint).await?; + let min_sol_output = bonding_curve_account + .get_sell_price(amount, global_account.fee_basis_points) + .map_err(|e| anyhow!(e))?; + let min_sol_output_with_slippage = calculate_with_slippage_sell( + min_sol_output, + slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + ); + + let mut instructions = vec![ + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + ComputeBudgetInstruction::set_compute_unit_price(0), + ]; + + instructions.push(instruction::sell( + payer, + mint, + &global_account.fee_recipient, + instruction::Sell { + _amount: amount, + _min_sol_output: min_sol_output_with_slippage, + }, + )); + + instructions.push(close_account( + &spl_token::ID, + &ata, + &payer.pubkey(), + &payer.pubkey(), + &[&payer.pubkey()], + )?); + + let commitment_config = CommitmentConfig::confirmed(); + let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config)? + .0; + + let simulate_tx = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + let config = RpcSimulateTransactionConfig { + sig_verify: true, + commitment: Some(commitment_config), + ..RpcSimulateTransactionConfig::default() + }; + + let result = rpc.simulate_transaction_with_config(&simulate_tx, config)? + .value; + + if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) { + return Err(anyhow!("Simulation failed: {:?}", result.err)); + } + + let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?; + let fees = rpc.get_recent_prioritization_fees(&[])?; + let average_fees = if fees.is_empty() { + DEFAULT_COMPUTE_UNIT_PRICE + } else { + fees.iter() + .map(|fee| fee.prioritization_fee) + .sum::() / fees.len() as u64 + }; + + let unit_price = match priority_fee { + None => average_fees, + Some(pf) => pf.price.unwrap_or(DEFAULT_COMPUTE_UNIT_PRICE) + }; + + let unit_price = if unit_price == 0 { DEFAULT_COMPUTE_UNIT_PRICE } else { unit_price }; + + instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32); + instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price); + + Ok(instructions) +} + +pub async fn build_sell_instructions_with_jito( + rpc: &RpcClient, + jito_client: &JitoClient, + payer: &Keypair, + mint: &Pubkey, + amount_token: Option, + slippage_basis_points: Option, + jito_fee: Option, +) -> Result, anyhow::Error> { + let (balance_u64, ata) = get_token_balance(rpc, payer, 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).await?; + let bonding_curve_account = get_bonding_curve_account(rpc, mint).await?; + let min_sol_output = bonding_curve_account + .get_sell_price(amount, global_account.fee_basis_points) + .map_err(|e| anyhow!(e))?; + let min_sol_output_with_slippage = calculate_with_slippage_sell( + min_sol_output, + slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + ); + + let mut instructions = vec![]; + instructions.push(instruction::sell( + payer, + mint, + &global_account.fee_recipient, + instruction::Sell { + _amount: amount, + _min_sol_output: min_sol_output_with_slippage, + }, + )); + + instructions.push(close_account( + &spl_token::ID, + &ata, + &payer.pubkey(), + &payer.pubkey(), + &[&payer.pubkey()], + )?); + + let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?; + let jito_fee = jito_fee.unwrap_or(JITO_TIP_AMOUNT); + instructions.push( + system_instruction::transfer( + &payer.pubkey(), + &tip_account, + sol_to_lamports(jito_fee), + ), + ); + + Ok(instructions) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs deleted file mode 100755 index 01720b5..0000000 --- a/src/utils/mod.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! Utilities for working with token metadata and IPFS uploads. -//! -//! This module provides functionality for creating and managing token metadata, -//! including uploading image and metadata to IPFS via the Pump.fun API. - -use isahc::AsyncReadResponseExt; -use serde::{Deserialize, Serialize}; -use std::{fs::File, io::Read}; - -/// Metadata structure for a token, matching the format expected by Pump.fun. -#[derive(Debug, 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, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TokenMetadataResponse { - /// 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, -} - -/// Creates and uploads token metadata to IPFS via the Pump.fun API. -/// -/// This function takes token metadata and an image file, constructs a multipart form request, -/// and uploads it to the Pump.fun IPFS API endpoint. The metadata and image are stored on IPFS -/// and the function returns the IPFS locations. -/// -/// # Arguments -/// -/// * `metadata` - Token metadata and image file information -/// -/// # Returns -/// -/// Returns a `Result` containing the `TokenMetadataResponse` with IPFS locations on success, -/// or an error if the upload fails. -/// -/// # Examples -/// -/// ```rust,no_run -/// use mai3_pumpfun_sdk::utils::{CreateTokenMetadata, create_token_metadata}; -/// -/// # async fn run() -> Result<(), Box> { -/// let metadata = CreateTokenMetadata { -/// name: "My Token".to_string(), -/// symbol: "MT".to_string(), -/// description: "A test token".to_string(), -/// file: "path/to/image.png".to_string(), -/// twitter: None, -/// telegram: None, -/// website: Some("https://example.com".to_string()), -/// }; -/// -/// let response = create_token_metadata(metadata).await?; -/// println!("Metadata URI: {}", response.metadata_uri); -/// # Ok(()) -/// # } -/// ``` -pub async fn create_token_metadata( - metadata: CreateTokenMetadata, -) -> Result> { - let boundary = "------------------------f4d9c2e8b7a5310f"; - let mut body = Vec::new(); - - // Helper function to append form data - fn append_text_field(body: &mut Vec, boundary: &str, name: &str, value: &str) { - body.extend_from_slice(b"--"); - body.extend_from_slice(boundary.as_bytes()); - body.extend_from_slice(b"\r\n"); - body.extend_from_slice( - format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", name).as_bytes(), - ); - body.extend_from_slice(value.as_bytes()); - body.extend_from_slice(b"\r\n"); - } - - // Append form fields - append_text_field(&mut body, boundary, "name", &metadata.name); - append_text_field(&mut body, boundary, "symbol", &metadata.symbol); - append_text_field(&mut body, boundary, "description", &metadata.description); - if let Some(twitter) = metadata.twitter { - append_text_field(&mut body, boundary, "twitter", &twitter); - } - if let Some(telegram) = metadata.telegram { - append_text_field(&mut body, boundary, "telegram", &telegram); - } - if let Some(website) = metadata.website { - append_text_field(&mut body, boundary, "website", &website); - } - append_text_field(&mut body, boundary, "showName", "true"); - - // Append file part - body.extend_from_slice(b"--"); - body.extend_from_slice(boundary.as_bytes()); - body.extend_from_slice(b"\r\n"); - body.extend_from_slice(b"Content-Disposition: form-data; name=\"file\"; filename=\"file\"\r\n"); - body.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n"); - - // Read the file contents - let mut file = File::open(&metadata.file)?; - let mut file_contents = Vec::new(); - file.read_to_end(&mut file_contents)?; - body.extend_from_slice(&file_contents); - - // Close the boundary - body.extend_from_slice(b"\r\n--"); - body.extend_from_slice(boundary.as_bytes()); - body.extend_from_slice(b"--\r\n"); - - let client = isahc::HttpClient::new()?; - let request = isahc::Request::builder() - .method("POST") - .uri("https://pump.fun/api/ipfs") - .header( - "Content-Type", - format!("multipart/form-data; boundary={}", boundary), - ) - .header("Content-Length", body.len() as u64) - .body(isahc::AsyncBody::from(body))?; - - // Send request and print response - let mut response = client.send_async(request).await?; - let text = response.text().await?; - let json: TokenMetadataResponse = serde_json::from_str(&text)?; - - Ok(json) -} - -/// Calculates the maximum amount to pay when buying tokens, accounting for slippage tolerance -/// -/// # Arguments -/// * `amount` - The base amount in lamports (1 SOL = 1,000,000,000 lamports) -/// * `basis_points` - The slippage tolerance in basis points (1% = 100 basis points) -/// -/// # Returns -/// The maximum amount to pay, including slippage tolerance -/// -/// # Example -/// ```rust -/// use mai3_pumpfun_sdk::utils; -/// -/// let amount = 1_000_000_000; // 1 SOL in lamports -/// let slippage = 100; // 1% slippage tolerance -/// -/// let max_amount = utils::calculate_with_slippage_buy(amount, slippage); -/// assert_eq!(max_amount, 1_010_000_000); // 1.01 SOL -/// ``` -pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 { - amount + (amount * basis_points) / 10000 -} - -/// Calculates the minimum amount to receive when selling tokens, accounting for slippage tolerance -/// -/// # Arguments -/// * `amount` - The base amount in lamports (1 SOL = 1,000,000,000 lamports) -/// * `basis_points` - The slippage tolerance in basis points (1% = 100 basis points) -/// -/// # Returns -/// The minimum amount to receive, accounting for slippage tolerance -/// -/// # Example -/// ```rust -/// use mai3_pumpfun_sdk::utils; -/// -/// let amount = 1_000_000_000; // 1 SOL in lamports -/// let slippage = 100; // 1% slippage tolerance -/// -/// let min_amount = utils::calculate_with_slippage_sell(amount, slippage); -/// assert_eq!(min_amount, 990_000_000); // 0.99 SOL -/// ``` -pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 { - amount - (amount * basis_points) / 10000 -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs::write; - - #[tokio::test] - async fn test_create_token_metadata() { - // Create a temporary file - let temp_dir = std::env::temp_dir(); - let file_path = temp_dir.join("test_image.png"); - write(&file_path, b"fake image data").unwrap(); - - // Create test metadata - let metadata = CreateTokenMetadata { - name: "Test Token".to_string(), - symbol: "TEST".to_string(), - description: "Test Description".to_string(), - file: file_path.to_str().unwrap().to_string(), - twitter: None, - telegram: None, - website: Some("https://example.com".to_string()), - }; - - // Call the function - let result = create_token_metadata(metadata).await; - - // Assert the result - assert!(result.is_ok()); - let response = result.unwrap(); - - // Verify response fields - assert_eq!(response.metadata.name, "Test Token"); - assert_eq!(response.metadata.symbol, "TEST"); - assert_eq!(response.metadata.description, "Test Description"); - assert!(response.metadata.image.starts_with("https://ipfs.io/ipfs/")); - assert!(response.metadata.show_name); - assert_eq!(response.metadata.created_on, "https://pump.fun"); - assert!(response.metadata_uri.starts_with("https://ipfs.io/ipfs/")); - } - - #[test] - fn test_calculate_with_slippage_buy() { - let amount = 1_000_000_000; // 1 SOL in lamports - let slippage = 100; // 1% slippage tolerance - let max_amount = calculate_with_slippage_buy(amount, slippage); - assert_eq!(max_amount, 1_010_000_000); // 1.01 SOL - } - - #[test] - fn test_calculate_with_slippage_sell() { - let amount = 1_000_000_000; // 1 SOL in lamports - let slippage = 100; // 1% slippage tolerance - let min_amount = calculate_with_slippage_sell(amount, slippage); - assert_eq!(min_amount, 990_000_000); // 0.99 SOL - } -}