diff --git a/Cargo.toml b/Cargo.toml index 8b0422a..c4f0871 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,8 @@ tokio-tungstenite = { version = "0.26.1", features = ["native-tls"] } indicatif = "0.17.11" toml = "0.8.20" +pumpfun_program = { version = "4.2.0", package = "pumpfun" } + diff --git a/src/constants/pumpfun/mod.rs b/src/constants/pumpfun/mod.rs index 762b3be..8ddadc8 100755 --- a/src/constants/pumpfun/mod.rs +++ b/src/constants/pumpfun/mod.rs @@ -172,3 +172,11 @@ pub struct Symbol; impl Symbol { pub const SOLANA: &'static str = "solana"; } + +pub mod trade_type { + pub const COPY_BUY: &'static str = "copy_buy"; + pub const COPY_SELL: &'static str = "copy_sell"; + pub const SNIPER_BUY: &'static str = "sniper_buy"; + pub const SNIPER_SELL: &'static str = "sniper_sell"; +} + diff --git a/src/lib.rs b/src/lib.rs index 344674f..026dcd4 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod swqos; pub mod pumpfun; use std::sync::Arc; +use std::sync::Mutex; use swqos::{FeeClient, JitoClient, NextBlockClient, NozomiClient, SolRpcClient, ZeroSlotClient}; use rustls::crypto::{ring::default_provider, CryptoProvider}; @@ -23,6 +24,8 @@ use common::{pumpfun::logs_data::TradeInfo, pumpfun::logs_events::PumpfunEvent, use common::pumpfun::logs_subscribe::SubscriptionHandle; use ipfs::TokenMetadataIPFS; +use constants::pumpfun::trade_type::{COPY_BUY, SNIPER_BUY}; + pub struct PumpFun { pub payer: Arc, pub rpc: Arc, @@ -31,6 +34,8 @@ pub struct PumpFun { pub cluster: Cluster, } +static INSTANCE: Mutex>> = Mutex::new(None); + impl Clone for PumpFun { fn clone(&self) -> Self { Self { @@ -105,13 +110,29 @@ impl PumpFun { fee_clients.push(Arc::new(rpc_client)); } - Self { + let instance = Self { payer, rpc, fee_clients, priority_fee: cluster.clone().priority_fee, cluster: cluster.clone(), - } + }; + + let mut current = INSTANCE.lock().unwrap(); + *current = Some(Arc::new(instance.clone())); + + instance + } + + /// Get the RPC client instance + pub fn get_rpc(&self) -> &Arc { + &self.rpc + } + + /// Get the current instance + pub fn get_instance() -> Arc { + let instance = INSTANCE.lock().unwrap(); + instance.as_ref().expect("PumpFun instance not initialized. Please call new() first.").clone() } /// Create a new token @@ -172,7 +193,7 @@ impl PumpFun { } /// Buy tokens - pub async fn buy( + pub async fn sniper_buy( &self, mint: Pubkey, creator: Pubkey, @@ -194,11 +215,38 @@ impl PumpFun { self.priority_fee.clone(), self.cluster.clone().lookup_table_key, recent_blockhash, + SNIPER_BUY.to_string(), + ).await + } + + pub async fn copy_buy( + &self, + mint: Pubkey, + creator: Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, + buy_sol_cost: u64, + slippage_basis_points: Option, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::buy::buy( + self.rpc.clone(), + self.payer.clone(), + mint, + creator, + dev_buy_token, + dev_sol_cost, + buy_sol_cost, + slippage_basis_points, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + COPY_BUY.to_string(), ).await } /// Buy tokens using Jito - pub async fn buy_with_tip( + pub async fn sniper_buy_with_tip( &self, mint: Pubkey, creator: Pubkey, @@ -220,6 +268,33 @@ impl PumpFun { self.priority_fee.clone(), self.cluster.clone().lookup_table_key, recent_blockhash, + SNIPER_BUY.to_string(), + ).await + } + + pub async fn copy_buy_with_tip( + &self, + mint: Pubkey, + creator: Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, + buy_sol_cost: u64, + slippage_basis_points: Option, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::buy::buy_with_tip( + self.fee_clients.clone(), + self.payer.clone(), + mint, + creator, + dev_buy_token, + dev_sol_cost, + buy_sol_cost, + slippage_basis_points, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + COPY_BUY.to_string(), ).await } @@ -265,6 +340,26 @@ impl PumpFun { ).await } + /// Sell tokens by amount + pub async fn sell_by_amount( + &self, + mint: Pubkey, + creator: Pubkey, + amount: u64, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::sell::sell_by_amount( + self.rpc.clone(), + self.payer.clone(), + mint.clone(), + creator, + amount, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + ).await + } + pub async fn sell_by_percent_with_tip( &self, mint: Pubkey, @@ -286,6 +381,25 @@ impl PumpFun { ).await } + pub async fn sell_by_amount_with_tip( + &self, + mint: Pubkey, + creator: Pubkey, + amount: u64, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::sell::sell_by_amount_with_tip( + self.fee_clients.clone(), + self.payer.clone(), + mint, + creator, + amount, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + ).await + } + /// Sell tokens using Jito pub async fn sell_with_tip( &self, @@ -375,4 +489,23 @@ impl PumpFun { pub async fn close_token_account(&self, mint: &Pubkey) -> Result<(), anyhow::Error> { pumpfun::common::close_token_account(&self.rpc, self.payer.as_ref(), mint).await } + + #[inline] + pub async fn get_current_price(&self, mint: &Pubkey) -> Result { + let (bonding_curve, _) = pumpfun::common::get_bonding_curve_account_v2(&self.rpc, mint).await?; + + let virtual_sol_reserves = bonding_curve.virtual_sol_reserves; + let virtual_token_reserves = bonding_curve.virtual_token_reserves; + + Ok(pumpfun::common::get_token_price(virtual_sol_reserves, virtual_token_reserves)) + } + + #[inline] + pub async fn get_real_sol_reserves(&self, mint: &Pubkey) -> Result { + let (bonding_curve, _) = pumpfun::common::get_bonding_curve_account_v2(&self.rpc, mint).await?; + + let actual_sol_reserves = bonding_curve.real_sol_reserves; + + Ok(actual_sol_reserves) + } } diff --git a/src/main.rs b/src/main.rs index 190e8e7..9cc1c50 100755 --- a/src/main.rs +++ b/src/main.rs @@ -5,14 +5,16 @@ use solana_sdk::{commitment_config::CommitmentConfig, transaction::VersionedTran #[tokio::main] async fn main() -> Result<(), Box> { - // test_pumpfun().await?; - test_pumpswap().await?; + // test_pumpfun_with_shreds().await?; + // test_pumpfun_with_grpc().await?; + // test_pumpswap_with_shreds().await?; + test_pumpswap_with_grpc().await?; Ok(()) } -async fn test_pumpfun() -> Result<(), Box> { +async fn test_pumpfun_with_shreds() -> Result<(), Box> { let grpc = ShredStreamGrpc::new( - "http://127.0.0.1:10800".to_string(), + "http://127.0.0.1:10000".to_string(), ).await?; let callback = |event: PumpfunEvent| { @@ -52,12 +54,44 @@ async fn test_pumpfun() -> Result<(), Box> { Ok(()) } -async fn test_pumpswap() -> Result<(), Box> { - // 使用 GRPC 客户端订阅 PumpSwap 事件 - println!("正在订阅 PumpSwap GRPC 事件..."); +async fn test_pumpfun_with_grpc() -> Result<(), Box> { + let grpc = YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + )?; + + let callback = |event: PumpfunEvent| { + + match event { + PumpfunEvent::NewDevTrade(trade_info) => { + println!("Received new dev trade event: {:?}", trade_info); + }, + PumpfunEvent::NewToken(token_info) => { + println!("Received new token event: {:?}", token_info); + }, + PumpfunEvent::NewUserTrade(trade_info) => { + println!("Received new trade event: {:?}", trade_info); + }, + PumpfunEvent::NewBotTrade(trade_info) => { + println!("Received new bot trade event: {:?}", trade_info); + }, + PumpfunEvent::Error(err) => { + println!("Received error: {}", err); + } + } + }; + + grpc.subscribe_pumpfun(callback, None).await?; + + Ok(()) +} + +async fn test_pumpswap_with_shreds() -> Result<(), Box> { + // 使用 ShredStream 客户端订阅 PumpSwap 事件 + println!("正在订阅 PumpSwap ShredStream 事件..."); let grpc_client = ShredStreamGrpc::new( - "http://127.0.0.1:10800".to_string(), + "http://127.0.0.1:10000".to_string(), ).await?; // 定义回调函数处理 PumpSwap 事件 @@ -100,6 +134,55 @@ async fn test_pumpswap() -> Result<(), Box> { Ok(()) } +async fn test_pumpswap_with_grpc() -> Result<(), Box> { + // 使用 GRPC 客户端订阅 PumpSwap 事件 + println!("正在订阅 PumpSwap GRPC 事件..."); + + let grpc = YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None + )?; + + // 定义回调函数处理 PumpSwap 事件 + let callback = |event: PumpSwapEvent| { + match event { + PumpSwapEvent::Buy(buy_event) => { + println!("buy_event: {:?}", buy_event); + }, + PumpSwapEvent::Sell(sell_event) => { + println!("sell_event: {:?}", sell_event); + }, + PumpSwapEvent::CreatePool(create_event) => { + println!("create_event: {:?}", create_event); + }, + PumpSwapEvent::Deposit(deposit_event) => { + println!("deposit_event: {:?}", deposit_event); + }, + PumpSwapEvent::Withdraw(withdraw_event) => { + println!("withdraw_event: {:?}", withdraw_event); + }, + PumpSwapEvent::Disable(disable_event) => { + println!("disable_event: {:?}", disable_event); + }, + PumpSwapEvent::UpdateAdmin(update_admin_event) => { + println!("update_admin_event: {:?}", update_admin_event); + }, + PumpSwapEvent::UpdateFeeConfig(update_fee_event) => { + println!("update_fee_event: {:?}", update_fee_event); + }, + PumpSwapEvent::Error(err) => { + println!("error: {}", err); + } + } + }; + // 订阅 PumpSwap 事件 + println!("开始监听 PumpSwap 事件,按 Ctrl+C 停止..."); + + grpc.subscribe_pumpswap(callback).await?; + + Ok(()) +} + async fn test_wss() -> AnyResult<()> { println!("Starting token subscription\n"); diff --git a/src/pumpfun/buy.rs b/src/pumpfun/buy.rs index 5f73de7..24d7248 100755 --- a/src/pumpfun/buy.rs +++ b/src/pumpfun/buy.rs @@ -22,7 +22,9 @@ use crate::{ const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 250000; -use super::common::{calculate_with_slippage_buy, get_buy_token_amount_from_sol_amount, init_bonding_curve_account}; +use super::common::{calculate_with_slippage_buy, get_buy_token_amount_from_sol_amount, init_bonding_curve_account, get_bonding_curve_account_v2, get_bonding_curve_pda}; +use crate::constants::pumpfun::trade_type::{SNIPER_BUY}; +use crate::PumpFun; /// 添加nonce消费指令到指令集合中 /// @@ -76,10 +78,11 @@ pub async fn buy( priority_fee: PriorityFee, lookup_table_key: Option, recent_blockhash: Hash, + trade_type: String, ) -> Result<(), anyhow::Error> { let start_time = Instant::now(); let mint = Arc::new(mint.clone()); - let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points).await?; + let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points, trade_type).await?; println!(" 买入交易指令: {:?}", start_time.elapsed()); let start_time = Instant::now(); @@ -191,10 +194,11 @@ pub async fn buy_with_tip( priority_fee: PriorityFee, lookup_table_key: Option, recent_blockhash: Hash, + trade_type: String, ) -> Result<(), anyhow::Error> { let start_time = Instant::now(); let mint = Arc::new(mint.clone()); - let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points).await?; + let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points, trade_type).await?; println!(" 买入交易指令: {:?}", start_time.elapsed()); let start_time = Instant::now(); @@ -356,7 +360,6 @@ pub async fn build_buy_transaction_with_tip( } pub async fn build_buy_instructions( - // rpc: Arc, payer: Arc, mint: Arc, creator: Pubkey, @@ -364,12 +367,29 @@ pub async fn build_buy_instructions( dev_sol_cost: u64, buy_sol_cost: u64, slippage_basis_points: Option, + trade_type: String, ) -> Result, anyhow::Error> { if buy_sol_cost == 0 { return Err(anyhow!("Amount cannot be zero")); } - let bonding_curve = init_bonding_curve_account(&mint, dev_buy_token, dev_sol_cost, creator).await?; + let bonding_curve = if trade_type == SNIPER_BUY { + init_bonding_curve_account(&mint, dev_buy_token, dev_sol_cost, creator).await? + } else { + let (bonding_curve, _) = get_bonding_curve_account_v2(&PumpFun::get_instance().get_rpc(), &mint).await?; + Arc::new(crate::accounts::BondingCurveAccount { + discriminator: bonding_curve.discriminator, + account: get_bonding_curve_pda(&mint).unwrap(), + virtual_token_reserves: bonding_curve.virtual_token_reserves, + virtual_sol_reserves: bonding_curve.virtual_sol_reserves, + real_token_reserves: bonding_curve.real_token_reserves, + real_sol_reserves: bonding_curve.real_sol_reserves, + token_total_supply: bonding_curve.token_total_supply, + complete: bonding_curve.complete, + creator: creator, + }) + }; + let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(100)); let creator_vault_pda = bonding_curve.get_creator_vault_pda(); diff --git a/src/pumpfun/common.rs b/src/pumpfun/common.rs index 27f268a..3da8b4e 100755 --- a/src/pumpfun/common.rs +++ b/src/pumpfun/common.rs @@ -7,6 +7,7 @@ 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 pumpfun_program::accounts::BondingCurveAccount as PumpfunBondingCurveAccount; use crate::{accounts::{self, BondingCurveAccount}, common::{pumpfun::logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, pumpfun::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}}; lazy_static::lazy_static! { @@ -224,6 +225,25 @@ pub async fn get_bonding_curve_account( Ok((bonding_curve, bonding_curve_pda)) } +#[inline] +pub async fn get_bonding_curve_account_v2( + rpc: &SolanaRpcClient, + mint: &Pubkey, +) -> Result<(Arc, Pubkey), anyhow::Error> { + let bonding_curve_pda = get_bonding_curve_pda(mint) + .ok_or(anyhow!("Bonding curve not found"))?; + + let account = rpc.get_account(&bonding_curve_pda).await?; + if account.data.is_empty() { + return Err(anyhow!("Bonding curve not found")); + } + + let bonding_curve = solana_sdk::borsh1::try_from_slice_unchecked::(&account.data) + .map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?; + + Ok((Arc::new(bonding_curve), bonding_curve_pda)) +} + // #[inline] // pub fn get_buy_token_amount( // mint: &Pubkey, diff --git a/src/pumpfun/sell.rs b/src/pumpfun/sell.rs index 96130f5..15334e4 100755 --- a/src/pumpfun/sell.rs +++ b/src/pumpfun/sell.rs @@ -56,6 +56,24 @@ pub async fn sell_by_percent( sell(rpc, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await } +/// Sell tokens by amount +pub async fn sell_by_amount( + rpc: Arc, + payer: Arc, + mint: Pubkey, + creator: Pubkey, + amount: u64, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + if amount == 0 { + return Err(anyhow!("Amount must be greater than 0")); + } + + sell(rpc, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await +} + pub async fn sell_by_percent_with_tip( fee_clients: Vec>, payer: Arc, @@ -75,6 +93,23 @@ pub async fn sell_by_percent_with_tip( sell_with_tip(fee_clients, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await } +pub async fn sell_by_amount_with_tip( + fee_clients: Vec>, + payer: Arc, + mint: Pubkey, + creator: Pubkey, + amount: u64, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + if amount == 0 { + return Err(anyhow!("Amount must be greater than 0")); + } + + sell_with_tip(fee_clients, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await +} + /// Sell tokens using Jito pub async fn sell_with_tip( fee_clients: Vec>,