diff --git a/.gitignore b/.gitignore index d01bd1a..c77fa9e 100755 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ Cargo.lock # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ + +.cargo/ \ No newline at end of file diff --git a/README.md b/README.md index 5d8a5d9..32864ba 100755 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ # Sol Trade SDK -A comprehensive Rust SDK for seamless interaction with Solana DEX trading programs. This SDK provides a robust set of tools and interfaces to integrate PumpFun and PumpSwap functionality into your applications. +A comprehensive Rust SDK for seamless interaction with Solana DEX trading programs. This SDK provides a robust set of tools and interfaces to integrate PumpFun, PumpSwap, and Raydium Launchpad (Bonk.fun) functionality into your applications. -## Features +## Project Features -1. **PumpFun Trading**: Support for `buy`, `sell` operations +1. **PumpFun Trading**: Support for `buy` and `sell` operations 2. **PumpSwap Trading**: Support for PumpSwap pool trading operations -3. **Raydium Trading**: Support for Raydium DEX trading operations -4. **Logs Subscription**: Subscribe to PumpFun, PumpSwap, and Raydium program transaction logs -5. **Yellowstone gRPC**: Subscribe to program logs using Yellowstone gRPC -6. **ShredStream Support**: Subscribe to program logs using ShredStream -7. **Multiple MEV Protection**: Support for Jito, Nextblock, 0slot, Nozomi services -8. **Concurrent Transactions**: Submit transactions using multiple MEV services simultaneously; the fastest succeeds while others fail -9. **Real-time Pricing**: Get real-time token prices and liquidity information +3. **Raydium Trading**: Support for Raydium Launchpad (Bonk.fun) trading operations +4. **Event Subscription**: Subscribe to PumpFun, PumpSwap, and Raydium Launchpad (Bonk.fun) program trading events +5. **Yellowstone gRPC**: Subscribe to program events using Yellowstone gRPC +6. **ShredStream Support**: Subscribe to program events using ShredStream +7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, and other services +8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail +9. **Unified Trading Interface**: Use unified parameter structures for trading operations ## Installation @@ -32,53 +32,425 @@ sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.1.0" } ## Usage Examples -### 1. Logs Subscription - Monitor Token Trading +### 1. Event Subscription - Monitor Token Trading + +#### 1.1 Subscribe to Events Using Yellowstone gRPC ```rust -use sol_trade_sdk::{common::pumpfun::logs_events::PumpfunEvent, grpc::YellowstoneGrpc}; -use solana_sdk::signature::Keypair; - -// Create gRPC client with Yellowstone -let grpc_url = "https://solana-yellowstone-grpc.publicnode.com:443"; -let x_token = None; // Optional auth token -let client = YellowstoneGrpc::new(grpc_url.to_string(), x_token)?; - -// Define callback function -let callback = |event: PumpfunEvent| match event { - PumpfunEvent::NewDevTrade(trade_info) => { - println!("Received new dev trade event: {:?}", trade_info); - } - PumpfunEvent::NewToken(token_info) => { - println!("Received new token event: {:?}", token_info); - } - PumpfunEvent::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); - } +use sol_trade_sdk::{ + event_parser::{ + protocols::{ + pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent}, + pumpswap::{ + PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent, + PumpSwapSellEvent, PumpSwapWithdrawEvent, + }, + raydium_launchpad::{RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent}, + }, + Protocol, UnifiedEvent, + }, + grpc::YellowstoneGrpc, + match_event, }; -client.subscribe_pumpfun(callback, None).await?; +async fn test_grpc() -> Result<(), Box> { + // Subscribe to events using GRPC client + println!("Subscribing to GRPC events..."); + + let grpc = YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + )?; + + // Define callback function to handle events + let callback = |event: Box| { + match_event!(event, { + RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| { + println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol); + }, + RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| { + println!("RaydiumLaunchpadTradeEvent: {:?}", e); + }, + PumpFunTradeEvent => |e: PumpFunTradeEvent| { + println!("PumpFunTradeEvent: {:?}", e); + }, + PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| { + println!("PumpFunCreateTokenEvent: {:?}", e); + }, + PumpSwapBuyEvent => |e: PumpSwapBuyEvent| { + println!("Buy event: {:?}", e); + }, + PumpSwapSellEvent => |e: PumpSwapSellEvent| { + println!("Sell event: {:?}", e); + }, + PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| { + println!("CreatePool event: {:?}", e); + }, + PumpSwapDepositEvent => |e: PumpSwapDepositEvent| { + println!("Deposit event: {:?}", e); + }, + PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| { + println!("Withdraw event: {:?}", e); + }, + }); + }; + + // Subscribe to events from multiple protocols + println!("Starting to listen for events, press Ctrl+C to stop..."); + let protocols = vec![ + Protocol::PumpFun, + Protocol::PumpSwap, + Protocol::RaydiumLaunchpad, + ]; + grpc.subscribe_events(protocols, None, None, None, callback) + .await?; + + Ok(()) +} +``` + +#### 1.2 Subscribe to Events Using ShredStream + +```rust +use sol_trade_sdk::grpc::ShredStreamGrpc; + +async fn test_shreds() -> Result<(), Box> { + // Subscribe to events using ShredStream client + println!("Subscribing to ShredStream events..."); + + let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?; + + // Define callback function to handle events (same as above) + let callback = |event: Box| { + match_event!(event, { + RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| { + println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol); + }, + RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| { + println!("RaydiumLaunchpadTradeEvent: {:?}", e); + }, + PumpFunTradeEvent => |e: PumpFunTradeEvent| { + println!("PumpFunTradeEvent: {:?}", e); + }, + PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| { + println!("PumpFunCreateTokenEvent: {:?}", e); + }, + PumpSwapBuyEvent => |e: PumpSwapBuyEvent| { + println!("Buy event: {:?}", e); + }, + PumpSwapSellEvent => |e: PumpSwapSellEvent| { + println!("Sell event: {:?}", e); + }, + PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| { + println!("CreatePool event: {:?}", e); + }, + PumpSwapDepositEvent => |e: PumpSwapDepositEvent| { + println!("Deposit event: {:?}", e); + }, + PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| { + println!("Withdraw event: {:?}", e); + }, + }); + }; + + // Subscribe to events + println!("Starting to listen for events, press Ctrl+C to stop..."); + let protocols = vec![ + Protocol::PumpFun, + Protocol::PumpSwap, + Protocol::RaydiumLaunchpad, + ]; + shred_stream + .shredstream_subscribe(protocols, None, callback) + .await?; + + Ok(()) +} ``` ### 2. Initialize SolanaTrade Instance ```rust use std::{str::FromStr, sync::Arc}; - use sol_trade_sdk::{ - common::{AnyResult, PriorityFee, TradeConfig}, - swqos::{SwqosConfig, SwqosRegion, SwqosType}, + common::{AnyResult, PriorityFee, TradeConfig}, + swqos::{SwqosConfig, SwqosRegion}, SolanaTrade }; use solana_client::rpc_client::RpcClient; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair}; -// Configure priority fees +// Create trader account +let payer = Keypair::new(); +let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); + +// Configure multiple MEV services to support concurrent trading +let swqos_configs = vec![ + SwqosConfig::Jito(SwqosRegion::Frankfurt), + SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Default(rpc_url.clone()), +]; + +// Define trading configuration +let trade_config = TradeConfig { + rpc_url: rpc_url.clone(), + commitment: CommitmentConfig::confirmed(), + priority_fee: PriorityFee::default(), + swqos_configs, + lookup_table_key: None, +}; + +// Create SolanaTrade instance +let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; +``` + +### 3. PumpFun Trading Operations + +```rust +use sol_trade_sdk::{ + accounts::BondingCurveAccount, + constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type}, + pumpfun::common::get_bonding_curve_account_v2, + trading::{ + core::params::{PumpFunParams, PumpFunSellParams}, + BuyParams, SellParams, + }, +}; + +async fn test_pumpfun() -> AnyResult<()> { + // Basic parameter setup + let creator = Pubkey::from_str("xxxxxx")?; // Developer account + let mint_pubkey = Pubkey::from_str("xxxxxx")?; // Token address + let buy_sol_cost = 100_000; // 0.0001 SOL + let slippage_basis_points = Some(100); + let rpc = RpcClient::new(rpc_url); + let recent_blockhash = rpc.get_latest_blockhash().unwrap(); + + println!("Buying tokens from PumpFun..."); + + // Get bonding curve information + let (bonding_curve, bonding_curve_pda) = + get_bonding_curve_account_v2(&solana_trade_client.rpc, &mint_pubkey).await?; + + let bonding_curve = BondingCurveAccount { + discriminator: bonding_curve.discriminator, + account: bonding_curve_pda, + 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: TOKEN_TOTAL_SUPPLY, + complete: false, + creator: creator, + }; + + // Buy operation + let buy_protocol_params = PumpFunParams { + trade_type: trade_type::COPY_BUY.to_string(), + bonding_curve: Some(Arc::new(bonding_curve)), + }; + + let buy_params = BuyParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_sol: buy_sol_cost, + slippage_basis_points: slippage_basis_points, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + data_size_limit: 0, + protocol_params: Box::new(buy_protocol_params.clone()), + }; + + // Buy with MEV protection + let buy_with_tip_params = buy_params + .clone() + .with_tip(solana_trade_client.swqos_clients.clone()); + + solana_trade_client + .buy_use_buy_params(buy_with_tip_params, None) + .await?; + + // Sell operation + println!("Selling tokens from PumpFun..."); + let sell_protocol_params = PumpFunSellParams {}; + let amount_token = 1000000; // Enter the actual token amount + + let sell_params = SellParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_token: Some(amount_token), + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + protocol_params: Box::new(sell_protocol_params.clone()), + }; + + solana_trade_client + .sell_by_amount_use_sell_params(sell_params) + .await?; + + Ok(()) +} +``` + +### 4. PumpSwap Trading Operations + +```rust +use sol_trade_sdk::trading::core::params::PumpSwapParams; + +async fn test_pumpswap() -> AnyResult<()> { + // Basic parameter setup + let creator = Pubkey::from_str("11111111111111111111111111111111")?; // Developer account + let mint_pubkey = Pubkey::from_str("2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv")?; // Token address + let buy_sol_cost = 100_000; // 0.0001 SOL + let slippage_basis_points = Some(100); + let rpc = RpcClient::new(rpc_url); + let recent_blockhash = rpc.get_latest_blockhash().unwrap(); + + println!("Buying tokens from PumpSwap..."); + + // PumpSwap parameter configuration + let protocol_params = PumpSwapParams { + pool: None, + pool_base_token_account: None, + pool_quote_token_account: None, + user_base_token_account: None, + user_quote_token_account: None, + auto_handle_wsol: true, + }; + + // Buy operation + let buy_params = BuyParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_sol: buy_sol_cost, + slippage_basis_points: slippage_basis_points, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + data_size_limit: 0, + protocol_params: Box::new(protocol_params.clone()), + }; + + let buy_with_tip_params = buy_params + .clone() + .with_tip(solana_trade_client.swqos_clients.clone()); + + solana_trade_client + .buy_use_buy_params(buy_with_tip_params, None) + .await?; + + // Sell operation + println!("Selling tokens from PumpSwap..."); + let amount_token = 1000000; // Enter the actual token amount + + let sell_params = SellParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_token: Some(amount_token), + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + protocol_params: Box::new(protocol_params.clone()), + }; + + solana_trade_client + .sell_by_amount_use_sell_params(sell_params) + .await?; + + Ok(()) +} +``` + +### 5. Raydium Launchpad Trading Operations + +```rust +use sol_trade_sdk::trading::core::params::RaydiumLaunchpadParams; + +async fn test_raydium_launchpad() -> Result<(), Box> { + // Basic parameter setup + let amount = 100_000; // 0.0001 SOL + let mint = Pubkey::from_str("xxxxxxx")?; + let recent_blockhash = solana_trade_client.rpc.get_latest_blockhash().await?; + + // Raydium Launchpad parameter configuration + let raydium_launchpad_params = RaydiumLaunchpadParams { + virtual_base: None, + virtual_quote: None, + real_base_before: None, + real_quote_before: None, + auto_handle_wsol: true, + }; + + println!("Buying tokens from Raydium Launchpad..."); + + // Buy operation + let buy_params = BuyParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint, + creator: Pubkey::default(), + amount_sol: amount, + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + data_size_limit: 0, + protocol_params: Box::new(raydium_launchpad_params.clone()), + }; + + let buy_with_tip_params = buy_params + .clone() + .with_tip(solana_trade_client.swqos_clients.clone()); + + solana_trade_client + .buy_use_buy_params(buy_with_tip_params, None) + .await?; + + // Sell operation + println!("Selling tokens from Raydium Launchpad..."); + + let sell_params = SellParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint, + creator: Pubkey::default(), + amount_token: None, + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + protocol_params: Box::new(raydium_launchpad_params.clone()), + }; + + solana_trade_client + .sell_by_amount_use_sell_params(sell_params) + .await?; + + Ok(()) +} +``` + +### 6. Custom Priority Fee Configuration + +```rust +use sol_trade_sdk::common::PriorityFee; + +// Custom priority fee configuration let priority_fee = PriorityFee { unit_limit: 190000, unit_price: 1000000, @@ -89,327 +461,79 @@ let priority_fee = PriorityFee { sell_tip_fee: 0.0001, }; -// Configure multiple swqos in single region, can send transactions concurrently -let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); -let swqos_configs = vec![ - SwqosConfig::Jito(SwqosRegion::Frankfurt), - SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Default(rpc_url.clone()), -]; - -// Define sdk configuration +// Use custom priority fee in TradeConfig let trade_config = TradeConfig { rpc_url: rpc_url.clone(), commitment: CommitmentConfig::confirmed(), - priority_fee, + priority_fee, // Use custom priority fee swqos_configs, lookup_table_key: None, }; - -// Create SolanaTrade instance -let payer = Keypair::from_base58_string("your_private_key"); -let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; -``` - -### 3. Buy Tokens - -### 3.1 Buy Tokens --- Sniping -```rust -use solana_sdk::{pubkey::Pubkey, hash::Hash}; -use std::sync::Arc; -use sol_trade_sdk::accounts::BondingCurveAccount; - -let mint_pubkey = Pubkey::from_str("token_address")?; -let creator = Pubkey::from_str("creator_address")?; -let recent_blockhash = Hash::default(); // Get latest blockhash -let buy_sol_cost = 50000; // 0.00005 SOL -let slippage_basis_points = Some(100); // 1% - -// Sniping buy (quick purchase when new token launches) -let dev_buy_token = 100_000; // Test value -let dev_cost_sol = 10_000; // Test value -let bonding_curve = BondingCurveAccount::new(&mint_pubkey, dev_buy_token, dev_cost_sol, creator); - -solana_trade_client.sniper_buy( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), -).await?; - -// Buy with MEV protection using tips -solana_trade_client.sniper_buy_with_tip( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), - None, // Custom tip -).await?; -``` - -### 3.2 Buy Tokens --- Copy Trading -```rust -use solana_sdk::{pubkey::Pubkey, hash::Hash}; -use std::sync::Arc; -use sol_trade_sdk::accounts::BondingCurveAccount; -use sol_trade_sdk::{constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type::COPY_BUY}, pumpfun::common::get_bonding_curve_pda}; - -let mint_pubkey = Pubkey::from_str("token_address")?; -let creator = Pubkey::from_str("creator_address")?; -let recent_blockhash = Hash::default(); // Get latest blockhash -let buy_sol_cost = 50000; // 0.00005 SOL -let slippage_basis_points = Some(100); // 1% - -// Copy trading buy -let dev_buy_token = 100_000; // Test value -let dev_cost_sol = 10_000; // Test value -// trade_info comes from pumpfun parsed data, refer to section 1. Logs Subscription above -let bonding_curve = Some(Arc::new(BondingCurveAccount { - discriminator: 0, - account: get_bonding_curve_pda(&trade_info.mint).unwrap(), - virtual_token_reserves: trade_info.virtual_token_reserves, - virtual_sol_reserves: trade_info.virtual_sol_reserves, - real_token_reserves: trade_info.real_token_reserves, - real_sol_reserves: trade_info.real_sol_reserves, - token_total_supply: TOKEN_TOTAL_SUPPLY, - complete: false, - creator: Pubkey::from_str(&creator).unwrap(), -})); - -solana_trade_client.buy( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), - "pumpfun".to_string(), -).await?; - -// Buy with MEV protection using tips -solana_trade_client.buy_with_tip( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), - "pumpfun".to_string(), - None, // Custom tip -).await?; -``` - -### 4. Sell Tokens - -```rust -// Sell by amount -solana_trade_client.sell_by_amount_with_tip( - mint_pubkey, - creator, - 1000000, // token amount - recent_blockhash, - "pumpfun".to_string(), // trading platform -).await?; - -// Sell by percentage -solana_trade_client.sell_by_percent_with_tip( - mint_pubkey, - creator, - 50, // percentage (50%) - 2000000, // total token amount - recent_blockhash, - "pumpfun".to_string(), // trading platform -).await?; -``` - -### 5. Get Price and Balance Information - -```rust -// Get current token price -let price = solana_trade_client.get_current_price(&mint_pubkey).await?; -println!("Current price: {}", price); - -// Get SOL balance -let sol_balance = solana_trade_client.get_payer_sol_balance().await?; -println!("SOL balance: {} lamports", sol_balance); - -// Get token balance -let token_balance = solana_trade_client.get_payer_token_balance(&mint_pubkey).await?; -println!("Token balance: {}", token_balance); - -// Get liquidity information -let sol_reserves = solana_trade_client.get_real_sol_reserves(&mint_pubkey).await?; -println!("SOL reserves: {} lamports", sol_reserves); -``` - -### 6. PumpSwap Subscription - Monitor AMM Events - -```rust -use sol_trade_sdk::{common::pumpswap::logs_events::PumpSwapEvent, grpc::YellowstoneGrpc}; - -// Create gRPC client with Yellowstone -let grpc_url = "https://solana-yellowstone-grpc.publicnode.com:443"; -let x_token = None; -let client = YellowstoneGrpc::new(grpc_url.to_string(), x_token)?; - -// Define callback function for PumpSwap events -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); - } -}; - -// Subscribe to PumpSwap events -println!("Monitoring PumpSwap events, press Ctrl+C to stop..."); -client.subscribe_pumpswap(callback).await?; -``` - -### 7. PumpSwap Trading Operations - -```rust -use std::sync::Arc; -use solana_sdk::{pubkey::Pubkey, hash::Hash, signature::Keypair}; -use solana_client::rpc_client::RpcClient; -use sol_trade_sdk::{common::{Cluster, PriorityFee}, SolanaTrade}; - -// Configure multiple swqos in single region, can send transactions concurrently -let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); -let swqos_configs = vec![ - SwqosConfig::Jito(SwqosRegion::Frankfurt), - SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Default(rpc_url.clone()), -]; - -// Define sdk configuration -let trade_config = TradeConfig { - rpc_url: rpc_url.clone(), - commitment: CommitmentConfig::confirmed(), - priority_fee, - swqos_configs, - lookup_table_key: None, -}; - -// Create SolanaTrade instance -let payer = Keypair::from_base58_string("your_private_key"); -let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; - -let creator = Pubkey::from_str("11111111111111111111111111111111")?; // dev account -let buy_sol_cost = 500_000; // 0.0005 SOL -let slippage_basis_points = Some(100); -let rpc = RpcClient::new(cluster.rpc_url); -let recent_blockhash = rpc.get_latest_blockhash().unwrap(); -let trade_platform = "pumpswap".to_string(); -let mint_pubkey = Pubkey::from_str("YOUR_TOKEN_MINT")?; // token mint - -println!("Buying tokens from PumpSwap..."); -solana_trade_client - .buy( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - None, - trade_platform.clone(), - ) - .await?; - -// Sell 30% * amount_token quantity -solana_trade_client - .sell_by_percent( - mint_pubkey, - creator, - 30, // percentage (30%) - 100, // total token amount - recent_blockhash, - trade_platform.clone(), - ) - .await?; -``` - -### 8. PumpSwap Pool Information - -```rust -use solana_sdk::pubkey::Pubkey; - -let pool_address = Pubkey::from_str("pool_address")?; - -// Get current price from PumpSwap pool -let price = solana_trade_client.get_current_price_with_pumpswap(&pool_address).await?; -println!("PumpSwap pool price: {}", price); - -// Get SOL reserves in PumpSwap pool -let sol_reserves = solana_trade_client.get_real_sol_reserves_with_pumpswap(&pool_address).await?; -println!("PumpSwap SOL reserves: {} lamports", sol_reserves); - -// Get token balance in PumpSwap pool -let token_balance = solana_trade_client.get_payer_token_balance_with_pumpswap(&pool_address).await?; -println!("PumpSwap token balance: {}", token_balance); ``` ## Supported Trading Platforms - **PumpFun**: Primary meme coin trading platform - **PumpSwap**: PumpFun's swap protocol -- **Raydium**: Integrated Raydium DEX functionality +- **Raydium Launchpad**: Raydium's token launch platform (Bonk.fun) ## MEV Protection Services - **Jito**: High-performance block space -- **Nextblock**: Fast transaction execution -- **0slot**: Zero-latency transactions -- **Nozomi**: MEV protection service +- **NextBlock**: Fast transaction execution +- **ZeroSlot**: Zero-latency transactions +- **Temporal**: Time-sensitive transactions +- **Bloxroute**: Blockchain network acceleration + +## New Architecture Features + +### Unified Parameter Structure + +- **BuyParams**: Unified buy parameter structure +- **SellParams**: Unified sell parameter structure +- **Protocol-specific Parameters**: Each protocol has its own parameter structure (PumpFunParams, PumpSwapParams, RaydiumLaunchpadParams) + +### Event Parsing System + +- **Unified Event Interface**: All protocol events implement the UnifiedEvent trait +- **Protocol-specific Events**: Each protocol has its own event types +- **Event Factory**: Automatically identifies and parses events from different protocols + +### Trading Engine + +- **Unified Trading Interface**: All trading operations use the same methods +- **Protocol Abstraction**: Supports trading operations across multiple protocols +- **Concurrent Execution**: Supports sending transactions to multiple MEV services simultaneously ## Project Structure ``` src/ -├── accounts/ # Account-related definitions -├── common/ # Common utilities and tools -├── constants/ # Constant definitions -├── error/ # Error handling -├── grpc/ # gRPC clients -├── instruction/ # Instruction building -├── pumpfun/ # PumpFun trading functionality -├── pumpswap/ # PumpSwap trading functionality -├── swqos/ # MEV service clients -├── trading/ # Unified trading engine -├── lib.rs # Main library file -└── main.rs # Example program +├── accounts/ # Account-related definitions +├── common/ # Common functionality and tools +├── constants/ # Constant definitions +├── error/ # Error handling +├── event_parser/ # Event parsing system +│ ├── common/ # Common event parsing tools +│ ├── core/ # Core parsing traits and interfaces +│ ├── protocols/ # Protocol-specific parsers +│ │ ├── pumpfun/ # PumpFun event parsing +│ │ ├── pumpswap/ # PumpSwap event parsing +│ │ └── raydium_launchpad/ # Raydium Launchpad event parsing +│ └── factory.rs # Parser factory +├── grpc/ # gRPC clients +├── instruction/ # Instruction building +├── protos/ # Protocol buffer definitions +├── pumpfun/ # PumpFun trading functionality +├── pumpswap/ # PumpSwap trading functionality +├── raydium_launchpad/ # Raydium Launchpad trading functionality +├── swqos/ # MEV service clients +├── trading/ # Unified trading engine +│ ├── common/ # Common trading tools +│ ├── core/ # Core trading engine +│ └── protocols/ # Protocol-specific trading implementations +├── lib.rs # Main library file +└── main.rs # Example program ``` ## License @@ -418,7 +542,7 @@ MIT License ## Contact -- Repository: https://github.com/0xfnzero/sol-trade-sdk +- Project Repository: https://github.com/0xfnzero/sol-trade-sdk - Telegram Group: https://t.me/fnzero_group ## Important Notes diff --git a/README_CN.md b/README_CN.md index 712f733..d815144 100755 --- a/README_CN.md +++ b/README_CN.md @@ -1,18 +1,18 @@ # Sol Trade SDK -一个全面的Rust SDK,用于与Solana DEX交易程序进行无缝交互。此SDK提供强大的工具和接口集,将PumpFun和PumpSwap功能集成到您的应用程序中。 +一个全面的 Rust SDK,用于与 Solana DEX 交易程序进行无缝交互。此 SDK 提供强大的工具和接口集,将 PumpFun、PumpSwap 和 Raydium Launchpad(Bonk.fun)功能集成到您的应用程序中。 ## 项目特性 -1. **PumpFun交易**: 支持`购买`、`卖出`功能 -2. **PumpSwap交易**: 支持PumpSwap池的交易操作 -3. **Raydium交易**: 支持Raydium DEX的交易操作 -4. **日志订阅**: 订阅PumpFun、PumpSwap和Raydium程序的交易日志 -5. **Yellowstone gRPC**: 使用Yellowstone gRPC订阅程序日志 -6. **ShredStream支持**: 使用ShredStream订阅程序日志 -7. **多种MEV保护**: 支持Jito、Nextblock、0slot、Nozomi等服务 -8. **并发交易**: 同时使用多个MEV服务发送交易,最快的成功,其他失败 -9. **实时价格**: 获取代币实时价格和流动性信息 +1. **PumpFun 交易**: 支持`购买`、`卖出`功能 +2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作 +3. **Raydium 交易**: 支持 Raydium Launchpad(Bonk.fun)的交易操作 +4. **事件订阅**: 订阅 PumpFun、PumpSwap 和 Raydium Launchpad(Bonk.fun)程序的交易事件 +5. **Yellowstone gRPC**: 使用 Yellowstone gRPC 订阅程序事件 +6. **ShredStream 支持**: 使用 ShredStream 订阅程序事件 +7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute 等服务 +8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败 +9. **统一交易接口**: 使用统一的参数结构进行交易操作 ## 安装 @@ -32,53 +32,425 @@ sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.1.0" } ## 使用示例 -### 1. 日志订阅 - 监听代币交易 +### 1. 事件订阅 - 监听代币交易 + +#### 1.1 使用 Yellowstone gRPC 订阅事件 ```rust -use sol_trade_sdk::{common::pumpfun::logs_events::PumpfunEvent, grpc::YellowstoneGrpc}; -use solana_sdk::signature::Keypair; - -// 创建Yellowstone gRPC客户端 -let grpc_url = "https://solana-yellowstone-grpc.publicnode.com:443"; -let x_token = None; // 可选的认证令牌 -let client = YellowstoneGrpc::new(grpc_url.to_string(), x_token)?; - -// 定义回调函数 -let callback = |event: PumpfunEvent| match event { - PumpfunEvent::NewDevTrade(trade_info) => { - println!("收到开发者交易事件: {:?}", trade_info); - } - PumpfunEvent::NewToken(token_info) => { - println!("收到新代币事件: {:?}", token_info); - } - PumpfunEvent::NewUserTrade(trade_info) => { - println!("收到用户交易事件: {:?}", trade_info); - } - PumpfunEvent::NewBotTrade(trade_info) => { - println!("收到机器人交易事件: {:?}", trade_info); - } - PumpfunEvent::Error(err) => { - println!("收到错误: {}", err); - } +use sol_trade_sdk::{ + event_parser::{ + protocols::{ + pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent}, + pumpswap::{ + PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent, + PumpSwapSellEvent, PumpSwapWithdrawEvent, + }, + raydium_launchpad::{RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent}, + }, + Protocol, UnifiedEvent, + }, + grpc::YellowstoneGrpc, + match_event, }; -client.subscribe_pumpfun(callback, None).await?; +async fn test_grpc() -> Result<(), Box> { + // 使用 GRPC 客户端订阅事件 + println!("正在订阅 GRPC 事件..."); + + let grpc = YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + )?; + + // 定义回调函数处理事件 + let callback = |event: Box| { + match_event!(event, { + RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| { + println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol); + }, + RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| { + println!("RaydiumLaunchpadTradeEvent: {:?}", e); + }, + PumpFunTradeEvent => |e: PumpFunTradeEvent| { + println!("PumpFunTradeEvent: {:?}", e); + }, + PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| { + println!("PumpFunCreateTokenEvent: {:?}", e); + }, + PumpSwapBuyEvent => |e: PumpSwapBuyEvent| { + println!("Buy event: {:?}", e); + }, + PumpSwapSellEvent => |e: PumpSwapSellEvent| { + println!("Sell event: {:?}", e); + }, + PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| { + println!("CreatePool event: {:?}", e); + }, + PumpSwapDepositEvent => |e: PumpSwapDepositEvent| { + println!("Deposit event: {:?}", e); + }, + PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| { + println!("Withdraw event: {:?}", e); + }, + }); + }; + + // 订阅多个协议的事件 + println!("开始监听事件,按 Ctrl+C 停止..."); + let protocols = vec![ + Protocol::PumpFun, + Protocol::PumpSwap, + Protocol::RaydiumLaunchpad, + ]; + grpc.subscribe_events(protocols, None, None, None, callback) + .await?; + + Ok(()) +} ``` -### 2. 初始化SolanaTrade实例 +#### 1.2 使用 ShredStream 订阅事件 + +```rust +use sol_trade_sdk::grpc::ShredStreamGrpc; + +async fn test_shreds() -> Result<(), Box> { + // 使用 ShredStream 客户端订阅事件 + println!("正在订阅 ShredStream 事件..."); + + let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?; + + // 定义回调函数处理事件(与上面相同) + let callback = |event: Box| { + match_event!(event, { + RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| { + println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol); + }, + RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| { + println!("RaydiumLaunchpadTradeEvent: {:?}", e); + }, + PumpFunTradeEvent => |e: PumpFunTradeEvent| { + println!("PumpFunTradeEvent: {:?}", e); + }, + PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| { + println!("PumpFunCreateTokenEvent: {:?}", e); + }, + PumpSwapBuyEvent => |e: PumpSwapBuyEvent| { + println!("Buy event: {:?}", e); + }, + PumpSwapSellEvent => |e: PumpSwapSellEvent| { + println!("Sell event: {:?}", e); + }, + PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| { + println!("CreatePool event: {:?}", e); + }, + PumpSwapDepositEvent => |e: PumpSwapDepositEvent| { + println!("Deposit event: {:?}", e); + }, + PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| { + println!("Withdraw event: {:?}", e); + }, + }); + }; + + // 订阅事件 + println!("开始监听事件,按 Ctrl+C 停止..."); + let protocols = vec![ + Protocol::PumpFun, + Protocol::PumpSwap, + Protocol::RaydiumLaunchpad, + ]; + shred_stream + .shredstream_subscribe(protocols, None, callback) + .await?; + + Ok(()) +} +``` + +### 2. 初始化 SolanaTrade 实例 ```rust use std::{str::FromStr, sync::Arc}; - use sol_trade_sdk::{ - common::{AnyResult, PriorityFee, TradeConfig}, - swqos::{SwqosConfig, SwqosRegion, SwqosType}, + common::{AnyResult, PriorityFee, TradeConfig}, + swqos::{SwqosConfig, SwqosRegion}, SolanaTrade }; use solana_client::rpc_client::RpcClient; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair}; -// 配置优先费用 +// 创建交易者账户 +let payer = Keypair::new(); +let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); + +// 配置多个MEV服务,支持并发交易 +let swqos_configs = vec![ + SwqosConfig::Jito(SwqosRegion::Frankfurt), + SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Default(rpc_url.clone()), +]; + +// 定义交易配置 +let trade_config = TradeConfig { + rpc_url: rpc_url.clone(), + commitment: CommitmentConfig::confirmed(), + priority_fee: PriorityFee::default(), + swqos_configs, + lookup_table_key: None, +}; + +// 创建SolanaTrade实例 +let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; +``` + +### 3. PumpFun 交易操作 + +```rust +use sol_trade_sdk::{ + accounts::BondingCurveAccount, + constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type}, + pumpfun::common::get_bonding_curve_account_v2, + trading::{ + core::params::{PumpFunParams, PumpFunSellParams}, + BuyParams, SellParams, + }, +}; + +async fn test_pumpfun() -> AnyResult<()> { + // 基本参数设置 + let creator = Pubkey::from_str("xxxxxx")?; // 开发者账户 + let mint_pubkey = Pubkey::from_str("xxxxxx")?; // 代币地址 + let buy_sol_cost = 100_000; // 0.0001 SOL + let slippage_basis_points = Some(100); + let rpc = RpcClient::new(rpc_url); + let recent_blockhash = rpc.get_latest_blockhash().unwrap(); + + println!("Buying tokens from PumpFun..."); + + // 获取bonding curve信息 + let (bonding_curve, bonding_curve_pda) = + get_bonding_curve_account_v2(&solana_trade_client.rpc, &mint_pubkey).await?; + + let bonding_curve = BondingCurveAccount { + discriminator: bonding_curve.discriminator, + account: bonding_curve_pda, + 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: TOKEN_TOTAL_SUPPLY, + complete: false, + creator: creator, + }; + + // 购买操作 + let buy_protocol_params = PumpFunParams { + trade_type: trade_type::COPY_BUY.to_string(), + bonding_curve: Some(Arc::new(bonding_curve)), + }; + + let buy_params = BuyParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_sol: buy_sol_cost, + slippage_basis_points: slippage_basis_points, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + data_size_limit: 0, + protocol_params: Box::new(buy_protocol_params.clone()), + }; + + // 使用MEV保护的购买 + let buy_with_tip_params = buy_params + .clone() + .with_tip(solana_trade_client.swqos_clients.clone()); + + solana_trade_client + .buy_use_buy_params(buy_with_tip_params, None) + .await?; + + // 卖出操作 + println!("Selling tokens from PumpFun..."); + let sell_protocol_params = PumpFunSellParams {}; + let amount_token = 1000000; // 写上真实的代币数量 + + let sell_params = SellParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_token: Some(amount_token), + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + protocol_params: Box::new(sell_protocol_params.clone()), + }; + + solana_trade_client + .sell_by_amount_use_sell_params(sell_params) + .await?; + + Ok(()) +} +``` + +### 4. PumpSwap 交易操作 + +```rust +use sol_trade_sdk::trading::core::params::PumpSwapParams; + +async fn test_pumpswap() -> AnyResult<()> { + // 基本参数设置 + let creator = Pubkey::from_str("11111111111111111111111111111111")?; // 开发者账户 + let mint_pubkey = Pubkey::from_str("2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv")?; // 代币地址 + let buy_sol_cost = 100_000; // 0.0001 SOL + let slippage_basis_points = Some(100); + let rpc = RpcClient::new(rpc_url); + let recent_blockhash = rpc.get_latest_blockhash().unwrap(); + + println!("Buying tokens from PumpSwap..."); + + // PumpSwap参数配置 + let protocol_params = PumpSwapParams { + pool: None, + pool_base_token_account: None, + pool_quote_token_account: None, + user_base_token_account: None, + user_quote_token_account: None, + auto_handle_wsol: true, + }; + + // 购买操作 + let buy_params = BuyParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_sol: buy_sol_cost, + slippage_basis_points: slippage_basis_points, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + data_size_limit: 0, + protocol_params: Box::new(protocol_params.clone()), + }; + + let buy_with_tip_params = buy_params + .clone() + .with_tip(solana_trade_client.swqos_clients.clone()); + + solana_trade_client + .buy_use_buy_params(buy_with_tip_params, None) + .await?; + + // 卖出操作 + println!("Selling tokens from PumpSwap..."); + let amount_token = 1000000; // 写上真实的代币数量 + + let sell_params = SellParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_token: Some(amount_token), + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + protocol_params: Box::new(protocol_params.clone()), + }; + + solana_trade_client + .sell_by_amount_use_sell_params(sell_params) + .await?; + + Ok(()) +} +``` + +### 5. Raydium Launchpad 交易操作 + +```rust +use sol_trade_sdk::trading::core::params::RaydiumLaunchpadParams; + +async fn test_raydium_launchpad() -> Result<(), Box> { + // 基本参数设置 + let amount = 100_000; // 0.0001 SOL + let mint = Pubkey::from_str("xxxxxxx")?; + let recent_blockhash = solana_trade_client.rpc.get_latest_blockhash().await?; + + // Raydium Launchpad参数配置 + let raydium_launchpad_params = RaydiumLaunchpadParams { + virtual_base: None, + virtual_quote: None, + real_base_before: None, + real_quote_before: None, + auto_handle_wsol: true, + }; + + println!("Buying tokens from Raydium Launchpad..."); + + // 购买操作 + let buy_params = BuyParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint, + creator: Pubkey::default(), + amount_sol: amount, + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + data_size_limit: 0, + protocol_params: Box::new(raydium_launchpad_params.clone()), + }; + + let buy_with_tip_params = buy_params + .clone() + .with_tip(solana_trade_client.swqos_clients.clone()); + + solana_trade_client + .buy_use_buy_params(buy_with_tip_params, None) + .await?; + + // 卖出操作 + println!("Selling tokens from Raydium Launchpad..."); + + let sell_params = SellParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint, + creator: Pubkey::default(), + amount_token: None, + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + protocol_params: Box::new(raydium_launchpad_params.clone()), + }; + + solana_trade_client + .sell_by_amount_use_sell_params(sell_params) + .await?; + + Ok(()) +} +``` + +### 6. 自定义优先费用配置 + +```rust +use sol_trade_sdk::common::PriorityFee; + +// 自定义优先费用配置 let priority_fee = PriorityFee { unit_limit: 190000, unit_price: 1000000, @@ -89,342 +461,94 @@ let priority_fee = PriorityFee { sell_tip_fee: 0.0001, }; -// 单区域配置多个swqos,可同时发送交易 -let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); -let swqos_configs = vec![ - SwqosConfig::Jito(SwqosRegion::Frankfurt), - SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Default(rpc_url.clone()), -]; - -// 定义sdk配置参数 +// 在TradeConfig中使用自定义优先费用 let trade_config = TradeConfig { rpc_url: rpc_url.clone(), commitment: CommitmentConfig::confirmed(), - priority_fee, + priority_fee, // 使用自定义优先费用 swqos_configs, lookup_table_key: None, }; - -// 创建SolanaTrade实例 -let payer = Keypair::from_base58_string("your_private_key"); -let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; -``` - -### 3. 购买代币 - -### 3.1 购买代币 --- 狙击 -```rust -use solana_sdk::{pubkey::Pubkey, hash::Hash}; -use std::sync::Arc; -use sol_trade_sdk::accounts::BondingCurveAccount; - -let mint_pubkey = Pubkey::from_str("代币地址")?; -let creator = Pubkey::from_str("创建者地址")?; -let recent_blockhash = Hash::default(); // 获取最新区块哈希 -let buy_sol_cost = 50000; // 0.00005 SOL -let slippage_basis_points = Some(100); // 1% - -// 狙击购买(新代币上线时快速购买) -let dev_buy_token = 100_000; // 测试值 -let dev_cost_sol = 10_000; // 测试值 -let bonding_curve = BondingCurveAccount::new(&mint_pubkey, dev_buy_token, dev_cost_sol, creator); - -solana_trade_client.sniper_buy( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), -).await?; - -// 使用小费进行MEV保护的购买 -solana_trade_client.sniper_buy_with_tip( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), - None, // 自定义小费 -).await?; -``` - -### 3.2 购买代币 --- 跟单 -```rust -use solana_sdk::{pubkey::Pubkey, hash::Hash}; -use std::sync::Arc; -use sol_trade_sdk::accounts::BondingCurveAccount; -use sol_trade_sdk::{constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type::COPY_BUY}, pumpfun::common::get_bonding_curve_pda}; - -let mint_pubkey = Pubkey::from_str("代币地址")?; -let creator = Pubkey::from_str("创建者地址")?; -let recent_blockhash = Hash::default(); // 获取最新区块哈希 -let buy_sol_cost = 50000; // 0.00005 SOL -let slippage_basis_points = Some(100); // 1% - -// 跟单购买 -let dev_buy_token = 100_000; // 测试值 -let dev_cost_sol = 10_000; // 测试值 -// trade_info来自pumpfun解析出来的数据,可以参考上面 1. 日志订阅 -let bonding_curve = Some(Arc::new(BondingCurveAccount { - discriminator: 0, - account: get_bonding_curve_pda(&trade_info.mint).unwrap(), - virtual_token_reserves: trade_info.virtual_token_reserves, - virtual_sol_reserves: trade_info.virtual_sol_reserves, - real_token_reserves: trade_info.real_token_reserves, - real_sol_reserves: trade_info.real_sol_reserves, - token_total_supply: TOKEN_TOTAL_SUPPLY, - complete: false, - creator: Pubkey::from_str(&creator).unwrap(), -})); - -solana_trade_client.buy( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), - "pumpfun".to_string(), -).await?; - -// 使用小费进行MEV保护的购买 -solana_trade_client.buy_with_tip( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), - "pumpfun".to_string(), - None, // 自定义小费 -).await?; -``` - -### 4. 卖出代币 - -```rust -// 按数量卖出 -solana_trade_client.sell_by_amount_with_tip( - mint_pubkey, - creator, - 1000000, // 代币数量 - recent_blockhash, - "pumpfun".to_string(), // 交易平台 -).await?; - -// 按百分比卖出 -solana_trade_client.sell_by_percent_with_tip( - mint_pubkey, - creator, - 50, // 百分比 (50%) - 2000000, // 总代币数量 - recent_blockhash, - "pumpfun".to_string(), // 交易平台 -).await?; -``` - -### 5. 获取价格和余额信息 - -```rust -// 获取代币当前价格 -let price = solana_trade_client.get_current_price(&mint_pubkey).await?; -println!("当前价格: {}", price); - -// 获取SOL余额 -let sol_balance = solana_trade_client.get_payer_sol_balance().await?; -println!("SOL余额: {} lamports", sol_balance); - -// 获取代币余额 -let token_balance = solana_trade_client.get_payer_token_balance(&mint_pubkey).await?; -println!("代币余额: {}", token_balance); - -// 获取流动性信息 -let sol_reserves = solana_trade_client.get_real_sol_reserves(&mint_pubkey).await?; -println!("SOL储备: {} lamports", sol_reserves); -``` - -### 6. PumpSwap订阅 - 监听AMM事件 - -```rust -use sol_trade_sdk::{common::pumpswap::logs_events::PumpSwapEvent, grpc::YellowstoneGrpc}; - -// 创建Yellowstone gRPC客户端 -let grpc_url = "https://solana-yellowstone-grpc.publicnode.com:443"; -let x_token = None; -let client = YellowstoneGrpc::new(grpc_url.to_string(), x_token)?; - -// 定义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停止..."); -client.subscribe_pumpswap(callback).await?; -``` - -### 7. PumpSwap交易操作 - -```rust -use std::sync::Arc; -use solana_sdk::{pubkey::Pubkey, hash::Hash, signature::Keypair}; -use solana_client::rpc_client::RpcClient; -use sol_trade_sdk::{common::{Cluster, PriorityFee}, SolanaTrade}; - -// 单区域配置多个swqos,可同时发送交易 -let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); -let swqos_configs = vec![ - SwqosConfig::Jito(SwqosRegion::Frankfurt), - SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Default(rpc_url.clone()), -]; - -// 定义sdk配置参数 -let trade_config = TradeConfig { - rpc_url: rpc_url.clone(), - commitment: CommitmentConfig::confirmed(), - priority_fee, - swqos_configs, - lookup_table_key: None, -}; - -// 创建SolanaTrade实例 -let payer = Keypair::from_base58_string("your_private_key"); -let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; - -let creator = Pubkey::from_str("11111111111111111111111111111111")?; // 开发者账户 -let buy_sol_cost = 500_000; // 0.0005 SOL -let slippage_basis_points = Some(100); -let rpc = RpcClient::new(cluster.rpc_url); -let recent_blockhash = rpc.get_latest_blockhash().unwrap(); -let trade_platform = "pumpswap".to_string(); -let mint_pubkey = Pubkey::from_str("您的代币铸造地址")?; // 代币铸造地址 - -println!("从PumpSwap购买代币..."); -solana_trade_client - .buy( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - None, - trade_platform.clone(), - ) - .await?; - -// 卖出30%的代币数量 -solana_trade_client - .sell_by_percent( - mint_pubkey, - creator, - 30, // 百分比 (30%) - 100, // 总代币数量 - recent_blockhash, - trade_platform.clone(), - ) - .await?; -``` - -### 8. PumpSwap池信息 - -```rust -use solana_sdk::pubkey::Pubkey; - -let pool_address = Pubkey::from_str("池地址")?; - -// 从PumpSwap池获取当前价格 -let price = solana_trade_client.get_current_price_with_pumpswap(&pool_address).await?; -println!("PumpSwap池价格: {}", price); - -// 获取PumpSwap池中的SOL储备 -let sol_reserves = solana_trade_client.get_real_sol_reserves_with_pumpswap(&pool_address).await?; -println!("PumpSwap SOL储备: {} lamports", sol_reserves); - -// 获取PumpSwap池中的代币余额 -let token_balance = solana_trade_client.get_payer_token_balance_with_pumpswap(&pool_address).await?; -println!("PumpSwap代币余额: {}", token_balance); ``` ## 支持的交易平台 -- **PumpFun**: 主要的meme币交易平台 -- **PumpSwap**: PumpFun的交换协议 -- **Raydium**: 集成Raydium DEX功能 +- **PumpFun**: 主要的 meme 币交易平台 +- **PumpSwap**: PumpFun 的交换协议 +- **Raydium Launchpad**: Raydium 的代币发行平台(Bonk.fun) -## MEV保护服务 +## MEV 保护服务 - **Jito**: 高性能区块空间 -- **Nextblock**: 快速交易执行 -- **0slot**: 零延迟交易 -- **Nozomi**: MEV保护服务 +- **NextBlock**: 快速交易执行 +- **ZeroSlot**: 零延迟交易 +- **Temporal**: 时间敏感交易 +- **Bloxroute**: 区块链网络加速 + +## 新架构特性 + +### 统一参数结构 + +- **BuyParams**: 统一的购买参数结构 +- **SellParams**: 统一的卖出参数结构 +- **协议特定参数**: 每个协议都有自己的参数结构(PumpFunParams、PumpSwapParams、RaydiumLaunchpadParams) + +### 事件解析系统 + +- **统一事件接口**: 所有协议事件都实现 UnifiedEvent 特征 +- **协议特定事件**: 每个协议都有自己的事件类型 +- **事件工厂**: 自动识别和解析不同协议的事件 + +### 交易引擎 + +- **统一交易接口**: 所有交易操作都使用相同的方法 +- **协议抽象**: 支持多个协议的交易操作 +- **并发执行**: 支持同时向多个 MEV 服务发送交易 ## 项目结构 ``` src/ -├── accounts/ # 账户相关定义 -├── common/ # 通用功能和工具 -├── constants/ # 常量定义 -├── error/ # 错误处理 -├── grpc/ # gRPC客户端 -├── instruction/ # 指令构建 -├── pumpfun/ # PumpFun交易功能 -├── pumpswap/ # PumpSwap交易功能 -├── swqos/ # MEV服务客户端 -├── trading/ # 统一交易引擎 -├── lib.rs # 主库文件 -└── main.rs # 示例程序 +├── accounts/ # 账户相关定义 +├── common/ # 通用功能和工具 +├── constants/ # 常量定义 +├── error/ # 错误处理 +├── event_parser/ # 事件解析系统 +│ ├── common/ # 通用事件解析工具 +│ ├── core/ # 核心解析特征和接口 +│ ├── protocols/ # 协议特定解析器 +│ │ ├── pumpfun/ # PumpFun事件解析 +│ │ ├── pumpswap/ # PumpSwap事件解析 +│ │ └── raydium_launchpad/ # Raydium Launchpad事件解析 +│ └── factory.rs # 解析器工厂 +├── grpc/ # gRPC客户端 +├── instruction/ # 指令构建 +├── protos/ # 协议缓冲区定义 +├── pumpfun/ # PumpFun交易功能 +├── pumpswap/ # PumpSwap交易功能 +├── raydium_launchpad/ # Raydium Launchpad交易功能 +├── swqos/ # MEV服务客户端 +├── trading/ # 统一交易引擎 +│ ├── common/ # 通用交易工具 +│ ├── core/ # 核心交易引擎 +│ └── protocols/ # 协议特定交易实现 +├── lib.rs # 主库文件 +└── main.rs # 示例程序 ``` ## 许可证 -MIT许可证 +MIT 许可证 ## 联系方式 - 项目仓库: https://github.com/0xfnzero/sol-trade-sdk -- Telegram群组: https://t.me/fnzero_group +- Telegram 群组: https://t.me/fnzero_group ## 重要注意事项 1. 在主网使用前请充分测试 -2. 正确设置私钥和API令牌 +2. 正确设置私钥和 API 令牌 3. 注意滑点设置避免交易失败 4. 监控余额和交易费用 5. 遵循相关法律法规 @@ -432,4 +556,4 @@ MIT许可证 ## 语言版本 - [English](README.md) -- [中文](README_CN.md) \ No newline at end of file +- [中文](README_CN.md) diff --git a/src/common/mod.rs b/src/common/mod.rs index 72f857b..26b8bc0 100755 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,6 +1,3 @@ -pub mod pumpfun; -pub mod pumpswap; -pub mod raydium; pub mod address_lookup; pub mod nonce_cache; pub mod tip_cache; diff --git a/src/common/pumpfun/logs_data.rs b/src/common/pumpfun/logs_data.rs deleted file mode 100755 index 2ac8781..0000000 --- a/src/common/pumpfun/logs_data.rs +++ /dev/null @@ -1,104 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction}; - -use crate::error::{ClientError, ClientResult}; - -#[derive(Debug)] -pub enum DexInstruction { - CreateToken(CreateTokenInfo), - UserTrade(TradeInfo), - BotTrade(TradeInfo), - Tip(TipInfo), - Other, -} - -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] -pub struct CreateTokenInfo { - pub slot: u64, - pub name: String, - pub symbol: String, - pub uri: String, - pub mint: Pubkey, - pub bonding_curve: Pubkey, - pub user: Pubkey, -} - -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] -pub struct TradeInfo { - pub slot: u64, - pub mint: Pubkey, - pub sol_amount: u64, - pub token_amount: u64, - pub is_buy: bool, - pub user: Pubkey, - pub timestamp: i64, - pub virtual_sol_reserves: u64, - pub virtual_token_reserves: u64, - pub real_sol_reserves: u64, - pub real_token_reserves: u64, -} - -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] -pub struct TipInfo { - pub slot: u64, - pub signature: String, -} - -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] -pub struct CompleteInfo { - pub user: Pubkey, - pub mint: Pubkey, - pub bonding_curve: Pubkey, - pub timestamp: u64, -} - -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] -pub struct SwapBaseInLog { - pub log_type: u8, - // input - pub amount_in: u64, - pub minimum_out: u64, - pub direction: u64, - // user info - pub user_source: u64, - // pool info - pub pool_coin: u64, - pub pool_pc: u64, - // calc result - pub out_amount: u64, -} - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct TransferInfo { - pub slot: u64, - pub signature: String, - pub tx: Option, -} - -pub trait EventTrait: Sized + std::fmt::Debug { - fn from_bytes(bytes: &[u8]) -> ClientResult; -} - -impl EventTrait for CreateTokenInfo { - fn from_bytes(bytes: &[u8]) -> ClientResult { - CreateTokenInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) - } -} - -impl EventTrait for TradeInfo { - fn from_bytes(bytes: &[u8]) -> ClientResult { - TradeInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) - } -} - -impl EventTrait for CompleteInfo { - fn from_bytes(bytes: &[u8]) -> ClientResult { - CompleteInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) - } -} - -impl EventTrait for SwapBaseInLog { - fn from_bytes(bytes: &[u8]) -> ClientResult { - SwapBaseInLog::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) - } -} \ No newline at end of file diff --git a/src/common/pumpfun/logs_events.rs b/src/common/pumpfun/logs_events.rs deleted file mode 100755 index a57bb1d..0000000 --- a/src/common/pumpfun/logs_events.rs +++ /dev/null @@ -1,95 +0,0 @@ -use base64::engine::general_purpose; -use base64::Engine; -use regex::Regex; -use crate::common::pumpfun::logs_data::{CreateTokenInfo, TradeInfo, EventTrait, TransferInfo, TipInfo}; - -pub const PROGRAM_DATA: &str = "Program data: "; - -#[derive(Debug)] -pub enum PumpfunEvent { - NewToken(CreateTokenInfo), - NewDevTrade(TradeInfo), - NewUserTrade(TradeInfo), - NewBotTrade(TradeInfo), - // NewTip(TipInfo), - Error(String), -} - - -#[derive(Debug)] -pub enum DexEvent { - NewToken(CreateTokenInfo), - NewUserTrade(TradeInfo), - NewBotTrade(TradeInfo), - Error(String), -} - -#[derive(Debug)] -pub enum SystemEvent { - NewTransfer(TransferInfo), - Error(String), -} - -// #[derive(Debug, Clone, Copy)] -// pub struct PumpEvent {} - -impl PumpfunEvent { - pub fn parse_logs(logs: &Vec) -> (Option, Option) { - let mut create_info: Option = None; - let mut trade_info: Option = None; - - if !logs.is_empty() { - let logs_iter = logs.iter().peekable(); - - for l in logs_iter.rev() { - if let Some(log) = l.strip_prefix(PROGRAM_DATA) { - let borsh_bytes = general_purpose::STANDARD.decode(log).unwrap(); - let slice: &[u8] = &borsh_bytes[8..]; - - if create_info.is_none() { - if let Ok(e) = CreateTokenInfo::from_bytes(slice) { - create_info = Some(e); - continue; - } - } - - if trade_info.is_none() { - if let Ok(e) = TradeInfo::from_bytes(slice) { - trade_info = Some(e); - } - } - } - } - } - (create_info, trade_info) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct RaydiumEvent {} - -impl RaydiumEvent { - pub fn parse_logs(logs: &Vec) -> Option { - let mut event: Option = None; - - if !logs.is_empty() { - let logs_iter = logs.iter().peekable(); - - for l in logs_iter.rev() { - let re = Regex::new(r"ray_log: (?P[A-Za-z0-9+/=]+)").unwrap(); - - if let Some(caps) = re.captures(l) { - if let Some(base64) = caps.name("base64") { - let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap(); - - if let Ok(e) = T::from_bytes(&bytes) { - event = Some(e); - } - } - } - } - } - - event - } -} \ No newline at end of file diff --git a/src/common/pumpfun/logs_filters.rs b/src/common/pumpfun/logs_filters.rs deleted file mode 100755 index 74705e5..0000000 --- a/src/common/pumpfun/logs_filters.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::common::pumpfun::logs_data::DexInstruction; -use crate::common::pumpfun::logs_parser::{parse_create_token_data, parse_trade_data, parse_instruction_create_token_data, parse_instruction_trade_data}; -use crate::error::ClientResult; -pub struct LogFilter; -use solana_sdk::pubkey::Pubkey; -use std::str::FromStr; - -use solana_sdk::transaction::VersionedTransaction; - -impl LogFilter { - const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; - - /// Parse transaction logs and return instruction type and data - pub fn parse_compiled_instruction( - versioned_tx: VersionedTransaction, - bot_wallet: Option) -> ClientResult> { - let compiled_instructions = versioned_tx.message.instructions(); - let accounts = versioned_tx.message.static_account_keys(); - let program_id = Pubkey::from_str(Self::PROGRAM_ID).unwrap_or_default(); - let pump_index = accounts.iter().position(|key| key == &program_id); - let mut instructions: Vec = Vec::new(); - if let Some(index) = pump_index { - for instruction in compiled_instructions { - if instruction.program_id_index as usize == index { - let all_accounts_valid = instruction.accounts.iter() - .all(|&acc_idx| (acc_idx as usize) < accounts.len()); - if !all_accounts_valid { - continue; - } - match instruction.data.first() { - // create - Some(&24) => { - if let Ok(token_info) = parse_instruction_create_token_data(instruction, accounts) { - instructions.push(DexInstruction::CreateToken(token_info)); - }; - } - // buy - Some(&102) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => { - if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, true) { - if let Some(bot_wallet_pubkey) = bot_wallet { - if trade_info.user.to_string() == bot_wallet_pubkey.to_string() { - instructions.push(DexInstruction::BotTrade(trade_info)); - } else { - instructions.push(DexInstruction::UserTrade(trade_info)); - } - } else { - instructions.push(DexInstruction::UserTrade(trade_info)); - } - }; - } - // sell - Some(&51) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => { - if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, false) { - if let Some(bot_wallet_pubkey) = bot_wallet { - if trade_info.user.to_string() == bot_wallet_pubkey.to_string() { - instructions.push(DexInstruction::BotTrade(trade_info)); - } else { - instructions.push(DexInstruction::UserTrade(trade_info)); - } - } else { - instructions.push(DexInstruction::UserTrade(trade_info)); - } - }; - } - _ => {} - } - } - } - } - Ok(instructions) - } - - - /// Parse transaction logs and return instruction type and data - pub fn parse_instruction(logs: &[String], bot_wallet: Option) -> ClientResult> { - let mut current_instruction = None; - let mut program_data = String::new(); - let mut invoke_depth = 0; - let mut last_data_len = 0; - let mut instructions = Vec::new(); - for log in logs { - // Check program invocation - if log.contains(&format!("Program {} invoke", Self::PROGRAM_ID)) { - invoke_depth += 1; - if invoke_depth == 1 { // Only reset state at top level call - current_instruction = None; - program_data.clear(); - last_data_len = 0; - } - continue; - } - - // Skip if not in our program - if invoke_depth == 0 { - continue; - } - - // Identify instruction type (only at top level) - if invoke_depth == 1 && log.contains("Program log: Instruction:") { - if log.contains("Create") { - current_instruction = Some("create"); - } else if log.contains("Buy") || log.contains("Sell") { - current_instruction = Some("trade"); - } - continue; - } - - // Collect Program data - if log.starts_with("Program data: ") { - let data = log.trim_start_matches("Program data: "); - if data.len() > last_data_len { - program_data = data.to_string(); - last_data_len = data.len(); - } - } - - // Check if program ends - if log.contains(&format!("Program {} success", Self::PROGRAM_ID)) { - invoke_depth -= 1; - if invoke_depth == 0 { // Only process data when top level program ends - if let Some(instruction_type) = current_instruction { - if !program_data.is_empty() { - match instruction_type { - "create" => { - if let Ok(token_info) = parse_create_token_data(&program_data) { - instructions.push(DexInstruction::CreateToken(token_info)); - } - }, - "trade" => { - if let Ok(trade_info) = parse_trade_data(&program_data) { - if let Some(bot_wallet_pubkey) = bot_wallet { - if trade_info.user.to_string() == bot_wallet_pubkey.to_string() { - instructions.push(DexInstruction::BotTrade(trade_info)); - } else { - instructions.push(DexInstruction::UserTrade(trade_info)); - } - } else { - instructions.push(DexInstruction::UserTrade(trade_info)); - } - } - }, - _ => {} - } - } - } - } - } - } - - Ok(instructions) - } -} \ No newline at end of file diff --git a/src/common/pumpfun/logs_parser.rs b/src/common/pumpfun/logs_parser.rs deleted file mode 100755 index dd5dd7d..0000000 --- a/src/common/pumpfun/logs_parser.rs +++ /dev/null @@ -1,236 +0,0 @@ -use std::str::FromStr; - -use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; - -use crate::error::{ClientError, ClientResult}; -use crate::common::pumpfun::{ - logs_data::{DexInstruction, CreateTokenInfo, TradeInfo}, - logs_filters::LogFilter -}; - -use solana_sdk::pubkey::Pubkey; -use solana_sdk::instruction::CompiledInstruction; -use std::time::{SystemTime, UNIX_EPOCH}; - -pub async fn process_logs( - signature: &str, - logs: Vec, - callback: F, - payer: Option, -) -> ClientResult<()> -where - F: Fn(&str, DexInstruction) + Send + Sync, -{ - let instructions = LogFilter::parse_instruction(&logs, payer)?; - for instruction in instructions { - callback(signature, instruction); - } - Ok(()) -} - -// Add parsing function -pub fn parse_create_token_data(data: &str) -> ClientResult { - // First do base64 decoding - let decoded = BASE64.decode(data) - .map_err(|e| ClientError::Other(format!("Failed to decode base64: {}", e)))?; - - // Skip prefix bytes (if any) - let mut cursor = if decoded.len() > 8 { 8 } else { 0 }; - - // Read name length and name - if cursor + 4 > decoded.len() { - return Err(ClientError::Other("Data too short for name length".to_string())); - } - let name_len = read_u32(&decoded[cursor..]) as usize; - cursor += 4; - - if cursor + name_len > decoded.len() { - return Err(ClientError::Other(format!("Data too short for name: need {} bytes", name_len))); - } - let name = String::from_utf8(decoded[cursor..cursor + name_len].to_vec()) - .map_err(|e| ClientError::Other(format!("Invalid UTF-8 in name: {}", e)))?; - cursor += name_len; - - // Read symbol length and symbol - if cursor + 4 > decoded.len() { - return Err(ClientError::Other("Data too short for symbol length".to_string())); - } - let symbol_len = read_u32(&decoded[cursor..]) as usize; - cursor += 4; - - if cursor + symbol_len > decoded.len() { - return Err(ClientError::Other(format!("Data too short for symbol: need {} bytes", symbol_len))); - } - let symbol = String::from_utf8(decoded[cursor..cursor + symbol_len].to_vec()) - .map_err(|e| ClientError::Other(format!("Invalid UTF-8 in symbol: {}", e)))?; - cursor += symbol_len; - - // Read URI length and URI - if cursor + 4 > decoded.len() { - return Err(ClientError::Other("Data too short for URI length".to_string())); - } - let uri_len = read_u32(&decoded[cursor..]) as usize; - cursor += 4; - - if cursor + uri_len > decoded.len() { - return Err(ClientError::Other(format!("Data too short for URI: need {} bytes", uri_len))); - } - let uri = String::from_utf8(decoded[cursor..cursor + uri_len].to_vec()) - .map_err(|e| ClientError::Other(format!("Invalid UTF-8 in uri: {}", e)))?; - cursor += uri_len; - - // Make sure there is enough data to read public keys - if cursor + 32 * 3 > decoded.len() { - return Err(ClientError::Other("Data too short for public keys".to_string())); - } - - // Parse Mint Public Key - let mint = bs58::encode(&decoded[cursor..cursor+32]).into_string(); - cursor += 32; - - // Parse Bonding Curve Public Key - let bonding_curve = bs58::encode(&decoded[cursor..cursor+32]).into_string(); - cursor += 32; - - // Parse User Public Key - let user = bs58::encode(&decoded[cursor..cursor+32]).into_string(); - - Ok(CreateTokenInfo { - slot: 0, - name, - symbol, - uri, - mint: Pubkey::from_str(&mint).unwrap(), - bonding_curve: Pubkey::from_str(&bonding_curve).unwrap(), - user: Pubkey::from_str(&user).unwrap(), - }) -} - -fn read_u32(data: &[u8]) -> u32 { - let mut bytes = [0u8; 4]; - bytes.copy_from_slice(&data[..4]); - u32::from_le_bytes(bytes) -} - -pub fn parse_trade_data(data: &str) -> ClientResult { - let engine = base64::engine::general_purpose::STANDARD; - let decoded = engine.decode(data).map_err(|e| - ClientError::Parse( - "Failed to decode base64".to_string(), - e.to_string() - ) - )?; - - let mut cursor = 8; // Skip prefix - - // 1. Mint (32 bytes) - let mint = bs58::encode(&decoded[cursor..cursor + 32]).into_string(); - cursor += 32; - - // 2. Sol Amount (8 bytes) - let sol_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); - cursor += 8; - - // 3. Token Amount (8 bytes) - let token_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); - cursor += 8; - - // 4. Is Buy (1 byte) - let is_buy = decoded[cursor] != 0; - cursor += 1; - - // 5. User (32 bytes) - let user = bs58::encode(&decoded[cursor..cursor + 32]).into_string(); - cursor += 32; - - // 6. Timestamp (8 bytes) - let timestamp = i64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); - cursor += 8; - - // 7. Virtual Sol Reserves (8 bytes) - let virtual_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); - cursor += 8; - - // 8. Virtual Token Reserves (8 bytes) - let virtual_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); - cursor += 8; - - let real_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); - cursor += 8; - - let real_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); - - Ok(TradeInfo { - slot: 0, - mint: Pubkey::from_str(&mint).unwrap(), - sol_amount, - token_amount, - is_buy, - user: Pubkey::from_str(&user).unwrap(), - timestamp, - virtual_sol_reserves, - virtual_token_reserves, - real_sol_reserves, - real_token_reserves, - }) -} - -fn current_timestamp_millis() -> i64 { - let duration = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards"); - - duration.as_millis() as i64 -} - -pub fn parse_instruction_create_token_data(instruction: &CompiledInstruction, accounts: &[Pubkey]) -> ClientResult { - let data = instruction.data.clone(); - let mut offset = 0; - offset += 8; - let len1 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; - offset += 4; - let name = String::from_utf8_lossy(&data[offset..offset + len1]); - offset += len1; - let len2 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; - offset += 4; - let symbol = String::from_utf8_lossy(&data[offset..offset + len2]); - offset += len2; - let _flag = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()); - offset += 4; - let hash_start = data.len() - 32; - let ipfs_bytes = &data[offset..hash_start]; - let uri = String::from_utf8_lossy(ipfs_bytes); - let mint = accounts[instruction.accounts[0] as usize]; - let user = accounts[instruction.accounts[7] as usize]; - let bonding_curve= accounts[instruction.accounts[2] as usize]; - Ok(CreateTokenInfo { - slot: 0, - name: name.to_string(), - symbol: symbol.to_string(), - uri: uri.to_string(), - mint, - bonding_curve, - user, - }) -} - -pub fn parse_instruction_trade_data(instruction: &CompiledInstruction, accounts: &[Pubkey], is_buy: bool) -> ClientResult { - let data = instruction.data.clone(); - let amount = u64::from_le_bytes(data[8..16].try_into().unwrap()); - let max_sol_cost_or_min_sol_output = u64::from_le_bytes(data[16..24].try_into().unwrap()); - let user = accounts[instruction.accounts[6] as usize]; - let mint = accounts[instruction.accounts[2] as usize]; - Ok(TradeInfo { - slot: 0, - mint, - sol_amount: max_sol_cost_or_min_sol_output, - token_amount: amount, - is_buy, - user, - timestamp: current_timestamp_millis(), - virtual_sol_reserves: 0, - virtual_token_reserves: 0, - real_sol_reserves: 0, - real_token_reserves: 0, - }) -} \ No newline at end of file diff --git a/src/common/pumpfun/logs_subscribe.rs b/src/common/pumpfun/logs_subscribe.rs deleted file mode 100755 index 3b4ac5d..0000000 --- a/src/common/pumpfun/logs_subscribe.rs +++ /dev/null @@ -1,105 +0,0 @@ -use solana_client::{ - nonblocking::pubsub_client::PubsubClient, - rpc_config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter} -}; - -use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; -use std::sync::Arc; -use tokio::sync::mpsc; -use tokio::task::JoinHandle; -use futures::StreamExt; -use crate::{constants, common::pumpfun::{ - logs_data::DexInstruction, logs_events::DexEvent, logs_filters::LogFilter -}}; - -use super::logs_events::PumpfunEvent; - -/// Subscription handle containing task and unsubscribe logic -pub struct SubscriptionHandle { - pub task: JoinHandle<()>, - pub unsub_fn: Box, -} - -impl SubscriptionHandle { - pub async fn shutdown(self) { - (self.unsub_fn)(); - self.task.abort(); - } -} - -pub async fn create_pubsub_client(ws_url: &str) -> PubsubClient { - PubsubClient::new(ws_url).await.unwrap() -} - -/// 启动订阅 -pub async fn tokens_subscription( - ws_url: &str, - commitment: CommitmentConfig, - callback: F, - bot_wallet: Option, -) -> Result> -where - F: Fn(PumpfunEvent) + Send + Sync + 'static, -{ - let program_address = constants::pumpfun::accounts::PUMPFUN.to_string(); - let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]); - - let logs_config = RpcTransactionLogsConfig { - commitment: Some(commitment), - }; - - // Create PubsubClient - let sub_client = Arc::new(PubsubClient::new(ws_url).await.unwrap()); - - let sub_client_clone = Arc::clone(&sub_client); - - // Create channel for unsubscribe - let (unsub_tx, _) = mpsc::channel(1); - - // Start subscription task - let task = tokio::spawn(async move { - let (mut stream, _) = sub_client_clone.logs_subscribe(logs_filter, logs_config).await.unwrap(); - - loop { - let msg = stream.next().await; - match msg { - Some(msg) => { - if let Some(_err) = msg.value.err { - continue; - } - - let instructions = LogFilter::parse_instruction(&msg.value.logs, bot_wallet).unwrap(); - for instruction in instructions { - match instruction { - DexInstruction::CreateToken(token_info) => { - callback(PumpfunEvent::NewToken(token_info)); - } - DexInstruction::UserTrade(trade_info) => { - callback(PumpfunEvent::NewUserTrade(trade_info)); - } - DexInstruction::BotTrade(trade_info) => { - callback(PumpfunEvent::NewBotTrade(trade_info)); - } - _ => {} - } - } - } - None => { - println!("Token subscription stream ended"); - } - } - } - }); - - // Return subscription handle and unsubscribe logic - Ok(SubscriptionHandle { - task, - unsub_fn: Box::new(move || { - let _ = unsub_tx.try_send(()); - }), - }) -} - -pub async fn stop_subscription(handle: SubscriptionHandle) { - handle.shutdown().await; -} diff --git a/src/common/pumpfun/mod.rs b/src/common/pumpfun/mod.rs deleted file mode 100755 index 8de9d17..0000000 --- a/src/common/pumpfun/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod logs_data; -pub mod logs_parser; -pub mod logs_filters; -pub mod logs_subscribe; -pub mod logs_events; - -pub use logs_data::*; -pub use logs_parser::*; -pub use logs_filters::*; -pub use logs_subscribe::*; -pub use logs_events::*; \ No newline at end of file diff --git a/src/common/pumpswap/logs_data.rs b/src/common/pumpswap/logs_data.rs deleted file mode 100755 index 58a67d1..0000000 --- a/src/common/pumpswap/logs_data.rs +++ /dev/null @@ -1,353 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use solana_sdk::pubkey::Pubkey; -use serde::{Deserialize, Serialize}; - -use crate::error::{ClientError, ClientResult}; - -/// PumpSwap指令类型 -#[derive(Debug)] -pub enum PumpSwapInstruction { - Buy(BuyEvent), - Sell(SellEvent), - CreatePool(CreatePoolEvent), - Deposit(DepositEvent), - Withdraw(WithdrawEvent), - Disable(DisableEvent), - UpdateAdmin(UpdateAdminEvent), - UpdateFeeConfig(UpdateFeeConfigEvent), - Other, -} - -/// 买入事件 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct BuyEvent { - #[borsh(skip)] - pub slot: u64, - pub timestamp: i64, - pub base_amount_out: u64, - pub max_quote_amount_in: u64, - pub user_base_token_reserves: u64, - pub user_quote_token_reserves: u64, - pub pool_base_token_reserves: u64, - pub pool_quote_token_reserves: u64, - pub quote_amount_in: u64, - pub lp_fee_basis_points: u64, - pub lp_fee: u64, - pub protocol_fee_basis_points: u64, - pub protocol_fee: u64, - pub quote_amount_in_with_lp_fee: u64, - pub user_quote_amount_in: u64, - pub pool: Pubkey, - pub user: Pubkey, - pub user_base_token_account: Pubkey, - pub user_quote_token_account: Pubkey, - pub protocol_fee_recipient: Pubkey, - pub protocol_fee_recipient_token_account: Pubkey, - pub coin_creator: Pubkey, - pub coin_creator_fee_basis_points: u64, - pub coin_creator_fee: u64, - #[borsh(skip)] - pub signature: String, - - #[borsh(skip)] - pub base_mint: Pubkey, - #[borsh(skip)] - pub quote_mint: Pubkey, - #[borsh(skip)] - pub pool_base_token_account: Pubkey, - #[borsh(skip)] - pub pool_quote_token_account: Pubkey, - #[borsh(skip)] - pub coin_creator_vault_ata: Pubkey, - #[borsh(skip)] - pub coin_creator_vault_authority: Pubkey, -} - -/// 卖出事件 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct SellEvent { - #[borsh(skip)] - pub slot: u64, - pub timestamp: i64, - pub base_amount_in: u64, - pub min_quote_amount_out: u64, - pub user_base_token_reserves: u64, - pub user_quote_token_reserves: u64, - pub pool_base_token_reserves: u64, - pub pool_quote_token_reserves: u64, - pub quote_amount_out: u64, - pub lp_fee_basis_points: u64, - pub lp_fee: u64, - pub protocol_fee_basis_points: u64, - pub protocol_fee: u64, - pub quote_amount_out_without_lp_fee: u64, - pub user_quote_amount_out: u64, - pub pool: Pubkey, - pub user: Pubkey, - pub user_base_token_account: Pubkey, - pub user_quote_token_account: Pubkey, - pub protocol_fee_recipient: Pubkey, - pub protocol_fee_recipient_token_account: Pubkey, - pub coin_creator: Pubkey, - pub coin_creator_fee_basis_points: u64, - pub coin_creator_fee: u64, - #[borsh(skip)] - pub signature: String, - - #[borsh(skip)] - pub base_mint: Pubkey, - #[borsh(skip)] - pub quote_mint: Pubkey, - #[borsh(skip)] - pub pool_base_token_account: Pubkey, - #[borsh(skip)] - pub pool_quote_token_account: Pubkey, - #[borsh(skip)] - pub coin_creator_vault_ata: Pubkey, - #[borsh(skip)] - pub coin_creator_vault_authority: Pubkey, -} - -/// 创建池子事件 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct CreatePoolEvent { - #[borsh(skip)] - pub slot: u64, - pub timestamp: i64, - pub index: u16, - pub creator: Pubkey, - pub base_mint: Pubkey, - pub quote_mint: Pubkey, - pub base_mint_decimals: u8, - pub quote_mint_decimals: u8, - pub base_amount_in: u64, - pub quote_amount_in: u64, - pub pool_base_amount: u64, - pub pool_quote_amount: u64, - pub minimum_liquidity: u64, - pub initial_liquidity: u64, - pub lp_token_amount_out: u64, - pub pool_bump: u8, - pub pool: Pubkey, - pub lp_mint: Pubkey, - pub user_base_token_account: Pubkey, - pub user_quote_token_account: Pubkey, - #[borsh(skip)] - pub signature: String, -} - -/// 存款事件 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct DepositEvent { - #[borsh(skip)] - pub slot: u64, - pub timestamp: i64, - pub lp_token_amount_out: u64, - pub max_base_amount_in: u64, - pub max_quote_amount_in: u64, - pub user_base_token_reserves: u64, - pub user_quote_token_reserves: u64, - pub pool_base_token_reserves: u64, - pub pool_quote_token_reserves: u64, - pub base_amount_in: u64, - pub quote_amount_in: u64, - pub lp_mint_supply: u64, - pub pool: Pubkey, - pub user: Pubkey, - pub user_base_token_account: Pubkey, - pub user_quote_token_account: Pubkey, - pub user_pool_token_account: Pubkey, - #[borsh(skip)] - pub signature: String, -} - -/// 提款事件 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct WithdrawEvent { - #[borsh(skip)] - pub slot: u64, - pub timestamp: i64, - pub lp_token_amount_in: u64, - pub min_base_amount_out: u64, - pub min_quote_amount_out: u64, - pub user_base_token_reserves: u64, - pub user_quote_token_reserves: u64, - pub pool_base_token_reserves: u64, - pub pool_quote_token_reserves: u64, - pub base_amount_out: u64, - pub quote_amount_out: u64, - pub lp_mint_supply: u64, - pub pool: Pubkey, - pub user: Pubkey, - pub user_base_token_account: Pubkey, - pub user_quote_token_account: Pubkey, - pub user_pool_token_account: Pubkey, - #[borsh(skip)] - pub signature: String, -} - -/// 禁用事件 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct DisableEvent { - #[borsh(skip)] - pub slot: u64, - pub timestamp: i64, - pub admin: Pubkey, - pub disable_create_pool: bool, - pub disable_deposit: bool, - pub disable_withdraw: bool, - pub disable_buy: bool, - pub disable_sell: bool, - #[borsh(skip)] - pub signature: String, -} - -/// 更新管理员事件 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct UpdateAdminEvent { - #[borsh(skip)] - pub slot: u64, - pub timestamp: i64, - pub old_admin: Pubkey, - pub new_admin: Pubkey, - #[borsh(skip)] - pub signature: String, -} - -/// 更新费用配置事件 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct UpdateFeeConfigEvent { - #[borsh(skip)] - pub slot: u64, - pub timestamp: i64, - pub admin: Pubkey, - pub old_lp_fee_basis_points: u64, - pub new_lp_fee_basis_points: u64, - pub old_protocol_fee_basis_points: u64, - pub new_protocol_fee_basis_points: u64, - pub old_protocol_fee_recipients: [Pubkey; 8], - pub new_protocol_fee_recipients: [Pubkey; 8], - #[borsh(skip)] - pub signature: String, -} - - - -/// 全局配置 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct GlobalConfig { - pub admin: Pubkey, - pub lp_fee_basis_points: u64, - pub protocol_fee_basis_points: u64, - pub disable_flags: u8, - pub protocol_fee_recipients: [Pubkey; 8], -} - -/// 池子信息 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct Pool { - pub index: u16, - pub base_mint: Pubkey, - pub quote_mint: Pubkey, - pub lp_mint: Pubkey, - pub base_mint_decimals: u8, - pub quote_mint_decimals: u8, - pub lp_mint_decimals: u8, - pub base_token_account: Pubkey, - pub quote_token_account: Pubkey, - pub bump: u8, - pub is_disabled: bool, -} - -/// 事件特性 -pub trait EventTrait: Sized + std::fmt::Debug { - fn from_bytes(bytes: &[u8]) -> ClientResult; - fn discriminator() -> &'static [u8]; -} - -/// 从字节中提取鉴别器 -pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> { - if data.len() < length { - return None; - } - Some((&data[..length], &data[length..])) -} - -/// 事件鉴别器常量 -pub mod discriminators { - // 事件鉴别器 - pub const BUY_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x67, 0xf4, 0x52, 0x1f, 0x2c, 0xf5, 0x77, 0x77]; - pub const SELL_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x3e, 0x2f, 0x37, 0x0a, 0xa5, 0x03, 0xdc, 0x2a]; - pub const CREATE_POOL_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xb1, 0x31, 0x0c, 0xd2, 0xa0, 0x76, 0xa7, 0x74]; - pub const DEPOSIT_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x78, 0xf8, 0x3d, 0x53, 0x1f, 0x8e, 0x6b, 0x90]; - pub const WITHDRAW_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x16, 0x09, 0x85, 0x1a, 0xa0, 0x2c, 0x47, 0xc0]; - pub const DISABLE_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x6b, 0xfd, 0xc1, 0x4c, 0xe4, 0xca, 0x1b, 0x68]; - pub const UPDATE_ADMIN_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xe1, 0x98, 0xab, 0x57, 0xf6, 0x3f, 0x42, 0xea]; - pub const UPDATE_FEE_CONFIG_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x5a, 0x17, 0x41, 0x23, 0x3e, 0xf4, 0xbc, 0xd0]; - - // 指令鉴别器 - pub const BUY_IX: &[u8] = &[102, - 6, - 61, - 18, - 1, - 218, - 235, - 234]; - pub const SELL_IX: &[u8] = &[51, - 230, - 133, - 164, - 1, - 127, - 131, - 173]; - pub const CREATE_POOL_IX: &[u8] = &[233, - 146, - 209, - 142, - 207, - 104, - 64, - 188]; - pub const DEPOSIT_IX: &[u8] = &[242, - 35, - 198, - 137, - 82, - 225, - 242, - 182]; - pub const WITHDRAW_IX: &[u8] = &[183, - 18, - 70, - 156, - 148, - 109, - 161, - 34]; - pub const DISABLE_IX: &[u8] = &[107, - 253, - 193, - 76, - 228, - 202, - 27, - 104]; - pub const UPDATE_ADMIN_IX: &[u8] = &[225, - 152, - 171, - 87, - 246, - 63, - 66, - 234]; - pub const UPDATE_FEE_CONFIG_IX: &[u8] = &[90, - 23, - 65, - 35, - 62, - 244, - 188, - 208]; -} \ No newline at end of file diff --git a/src/common/pumpswap/logs_events.rs b/src/common/pumpswap/logs_events.rs deleted file mode 100755 index 3b69229..0000000 --- a/src/common/pumpswap/logs_events.rs +++ /dev/null @@ -1,106 +0,0 @@ -use base64::engine::general_purpose; -use base64::Engine; -use crate::common::pumpswap::logs_data::{ - BuyEvent, SellEvent, CreatePoolEvent, DepositEvent, WithdrawEvent, - DisableEvent, UpdateAdminEvent, UpdateFeeConfigEvent, discriminators -}; -use borsh::BorshDeserialize; - -pub const PROGRAM_DATA: &str = "Program data: "; -pub const PROGRAM_LOG_PREFIX: &str = "Program log: PumpSwap: "; - -/// PumpSwap事件枚举 -#[derive(Debug)] -pub enum PumpSwapEvent { - Buy(BuyEvent), - Sell(SellEvent), - CreatePool(CreatePoolEvent), - Deposit(DepositEvent), - Withdraw(WithdrawEvent), - Disable(DisableEvent), - UpdateAdmin(UpdateAdminEvent), - UpdateFeeConfig(UpdateFeeConfigEvent), - Error(String), -} - -impl PumpSwapEvent { - /// 解析日志并提取PumpSwap事件 - pub fn parse_logs(logs: &[String]) -> Vec { - let mut events = Vec::new(); - - if logs.is_empty() { - return events; - } - - for log in logs { - // 检查是否是事件日志 - if let Some(event_data) = log.strip_prefix(PROGRAM_DATA) { - let borsh_bytes = match general_purpose::STANDARD.decode(event_data) { - Ok(bytes) => bytes, - Err(_) => continue, - }; - - // 检查鉴别器 - if borsh_bytes.len() < 16 { - continue; - } - let prefix = [0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d]; - let discriminator = &[&prefix[..], &borsh_bytes[..8]].concat(); - let data = &borsh_bytes[8..]; - // 根据鉴别器解析不同类型的事件 - if discriminator == discriminators::BUY_EVENT { - if let Ok(mut event) = BuyEvent::deserialize(&mut &data[..]) { - event.signature = String::new(); // 在外部设置 - events.push(PumpSwapEvent::Buy(event)); - } - } else if discriminator == discriminators::SELL_EVENT { - if let Ok(mut event) = SellEvent::deserialize(&mut &data[..]) { - event.signature = String::new(); // 在外部设置 - events.push(PumpSwapEvent::Sell(event)); - } - } else if discriminator == discriminators::CREATE_POOL_EVENT { - if let Ok(mut event) = CreatePoolEvent::deserialize(&mut &data[..]) { - event.signature = String::new(); // 在外部设置 - events.push(PumpSwapEvent::CreatePool(event)); - } - } else if discriminator == discriminators::DEPOSIT_EVENT { - if let Ok(mut event) = DepositEvent::deserialize(&mut &data[..]) { - event.signature = String::new(); // 在外部设置 - events.push(PumpSwapEvent::Deposit(event)); - } - } else if discriminator == discriminators::WITHDRAW_EVENT { - if let Ok(mut event) = WithdrawEvent::deserialize(&mut &data[..]) { - event.signature = String::new(); // 在外部设置 - events.push(PumpSwapEvent::Withdraw(event)); - } - } else if discriminator == discriminators::DISABLE_EVENT { - if let Ok(mut event) = DisableEvent::deserialize(&mut &data[..]) { - event.signature = String::new(); // 在外部设置 - events.push(PumpSwapEvent::Disable(event)); - } - } else if discriminator == discriminators::UPDATE_ADMIN_EVENT { - if let Ok(mut event) = UpdateAdminEvent::deserialize(&mut &data[..]) { - event.signature = String::new(); // 在外部设置 - events.push(PumpSwapEvent::UpdateAdmin(event)); - } - } else if discriminator == discriminators::UPDATE_FEE_CONFIG_EVENT { - if let Ok(mut event) = UpdateFeeConfigEvent::deserialize(&mut &data[..]) { - event.signature = String::new(); // 在外部设置 - events.push(PumpSwapEvent::UpdateFeeConfig(event)); - } - } - } else if let Some(event_log) = log.strip_prefix(PROGRAM_LOG_PREFIX) { - // 处理程序日志中的事件信息 - if event_log.contains("BuyEvent") { - // 这里可以添加从日志文本中解析事件的逻辑 - // 例如使用正则表达式提取关键信息 - } else if event_log.contains("SellEvent") { - // 同上 - } - // 其他事件类型... - } - } - - events - } -} \ No newline at end of file diff --git a/src/common/pumpswap/logs_filters.rs b/src/common/pumpswap/logs_filters.rs deleted file mode 100755 index 33fc8a6..0000000 --- a/src/common/pumpswap/logs_filters.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::common::pumpswap::logs_data::PumpSwapInstruction; -use crate::common::pumpswap::logs_parser::parse_pumpswap_instruction; -use crate::common::pumpswap::logs_events::PumpSwapEvent; -use crate::constants::pumpswap::accounts; -use crate::error::ClientResult; -use solana_sdk::transaction::VersionedTransaction; - -pub struct LogFilter; - -impl LogFilter { - /// 解析PumpSwap编译后的指令并返回指令类型和数据 - pub fn parse_pumpswap_compiled_instruction( - versioned_tx: VersionedTransaction) -> ClientResult> { - let compiled_instructions = versioned_tx.message.instructions(); - let accounts = versioned_tx.message.static_account_keys(); - let program_id = accounts::AMM_PROGRAM; - let pump_index = accounts.iter().position(|key| key == &program_id); - let mut instructions: Vec = Vec::new(); - - if let Some(index) = pump_index { - for instruction in compiled_instructions { - if instruction.program_id_index as usize == index { - let all_accounts_valid = instruction.accounts.iter() - .all(|&acc_idx| (acc_idx as usize) < accounts.len()); - if !all_accounts_valid { - continue; - } - - if let Some(parsed_instruction) = parse_pumpswap_instruction(instruction, accounts) { - instructions.push(parsed_instruction); - } - } - } - } - - Ok(instructions) - } - - /// 解析PumpSwap交易日志并返回事件 - pub fn parse_pumpswap_logs(logs: &[String]) -> Vec { - PumpSwapEvent::parse_logs(logs) - } -} \ No newline at end of file diff --git a/src/common/pumpswap/logs_parser.rs b/src/common/pumpswap/logs_parser.rs deleted file mode 100755 index e62eb84..0000000 --- a/src/common/pumpswap/logs_parser.rs +++ /dev/null @@ -1,310 +0,0 @@ -use crate::error::ClientResult; -use crate::common::pumpswap::{ - logs_data::{ - PumpSwapInstruction, - BuyEvent, SellEvent, CreatePoolEvent, DepositEvent, WithdrawEvent, - DisableEvent, UpdateAdminEvent, UpdateFeeConfigEvent, discriminators - }, - logs_events::PumpSwapEvent -}; - -use solana_sdk::pubkey::Pubkey; -use solana_sdk::instruction::CompiledInstruction; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// 处理PumpSwap日志并调用回调函数 -pub async fn process_pumpswap_logs( - signature: &str, - logs: Vec, - slot: Option, - callback: F, -) -> ClientResult<()> -where - F: Fn(&str, PumpSwapEvent) + Send + Sync, -{ - let events = PumpSwapEvent::parse_logs(&logs); - for mut event in events { - // 设置签名和slot - match &mut event { - PumpSwapEvent::Buy(e) => { - e.signature = signature.to_string(); - if let Some(s) = slot { - e.slot = s; - } - }, - PumpSwapEvent::Sell(e) => { - e.signature = signature.to_string(); - if let Some(s) = slot { - e.slot = s; - } - }, - PumpSwapEvent::CreatePool(e) => { - e.signature = signature.to_string(); - if let Some(s) = slot { - e.slot = s; - } - }, - PumpSwapEvent::Deposit(e) => { - e.signature = signature.to_string(); - if let Some(s) = slot { - e.slot = s; - } - }, - PumpSwapEvent::Withdraw(e) => { - e.signature = signature.to_string(); - if let Some(s) = slot { - e.slot = s; - } - }, - PumpSwapEvent::Disable(e) => { - e.signature = signature.to_string(); - if let Some(s) = slot { - e.slot = s; - } - }, - PumpSwapEvent::UpdateAdmin(e) => { - e.signature = signature.to_string(); - if let Some(s) = slot { - e.slot = s; - } - }, - PumpSwapEvent::UpdateFeeConfig(e) => { - e.signature = signature.to_string(); - if let Some(s) = slot { - e.slot = s; - } - }, - _ => {} - } - callback(signature, event); - } - Ok(()) -} - -/// 获取当前时间戳 -fn current_timestamp() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() as i64 -} - -/// 从指令中解析PumpSwap指令 -pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, _accounts: &[Pubkey]) -> Option { - if instruction.data.len() < 8 { - return None; - } - - let discriminator = &instruction.data[..8]; - let data = &instruction.data[8..]; - - let accounts: Vec = instruction.accounts.iter() - .map(|&idx| _accounts[idx as usize]) - .collect(); - - match discriminator { - d if d == discriminators::BUY_IX => { - // buy指令参数: base_amount_out: u64, max_quote_amount_in: u64 - // 账户顺序:pool, user, global_config, base_mint, quote_mint, user_base_token_account, - // user_quote_token_account, pool_base_token_account, pool_quote_token_account, - // protocol_fee_recipient, protocol_fee_recipient_token_account, ... - if data.len() < 16 || accounts.len() < 11 { - return None; - } - let base_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?); - let max_quote_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?); - - Some(PumpSwapInstruction::Buy(BuyEvent { - base_amount_out, - max_quote_amount_in, - pool: accounts[0], - user: accounts[1], - user_base_token_account: accounts[5], - user_quote_token_account: accounts[6], - protocol_fee_recipient: accounts[9], - protocol_fee_recipient_token_account: accounts[10], - timestamp: current_timestamp(), - - base_mint: accounts[3], - quote_mint: accounts[4], - pool_base_token_account: accounts[7], - pool_quote_token_account: accounts[8], - coin_creator_vault_ata: if accounts.len() > 17 { accounts[17] } else { Pubkey::default() }, - coin_creator_vault_authority: if accounts.len() > 18 { accounts[18] } else { Pubkey::default() }, - ..Default::default() - })) - }, - d if d == discriminators::SELL_IX => { - // sell指令参数: base_amount_in: u64, min_quote_amount_out: u64 - // 账户顺序:pool, user, global_config, base_mint, quote_mint, user_base_token_account, - // user_quote_token_account, pool_base_token_account, pool_quote_token_account, - // protocol_fee_recipient, protocol_fee_recipient_token_account, ... - if data.len() < 16 || accounts.len() < 11 { - return None; - } - let base_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?); - let min_quote_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?); - - Some(PumpSwapInstruction::Sell(SellEvent { - base_amount_in, - min_quote_amount_out, - pool: accounts[0], - user: accounts[1], - user_base_token_account: accounts[5], - user_quote_token_account: accounts[6], - protocol_fee_recipient: accounts[9], - protocol_fee_recipient_token_account: accounts[10], - timestamp: current_timestamp(), - - base_mint: accounts[3], - quote_mint: accounts[4], - pool_base_token_account: accounts[7], - pool_quote_token_account: accounts[8], - coin_creator_vault_ata: if accounts.len() > 17 { accounts[17] } else { Pubkey::default() }, - coin_creator_vault_authority: if accounts.len() > 18 { accounts[18] } else { Pubkey::default() }, - ..Default::default() - })) - }, - d if d == discriminators::CREATE_POOL_IX => { - // create_pool指令参数: index: u16, base_amount_in: u64, quote_amount_in: u64 - // 账户顺序:pool, global_config, creator, base_mint, quote_mint, lp_mint, - // user_base_token_account, user_quote_token_account, user_pool_token_account, - // pool_base_token_account, pool_quote_token_account, ... - if data.len() < 18 || accounts.len() < 11 { - return None; - } - let index = u16::from_le_bytes(data[0..2].try_into().ok()?); - let base_amount_in = u64::from_le_bytes(data[2..10].try_into().ok()?); - let quote_amount_in = u64::from_le_bytes(data[10..18].try_into().ok()?); - - Some(PumpSwapInstruction::CreatePool(CreatePoolEvent { - index, - base_amount_in, - quote_amount_in, - pool: accounts[0], - creator: accounts[2], - base_mint: accounts[3], - quote_mint: accounts[4], - lp_mint: accounts[5], - user_base_token_account: accounts[6], - user_quote_token_account: accounts[7], - timestamp: current_timestamp(), - ..Default::default() - })) - }, - d if d == discriminators::DEPOSIT_IX => { - // deposit指令参数: lp_token_amount_out: u64, max_base_amount_in: u64, max_quote_amount_in: u64 - // 账户顺序:pool, global_config, user, base_mint, quote_mint, lp_mint, - // user_base_token_account, user_quote_token_account, user_pool_token_account, - // pool_base_token_account, pool_quote_token_account, ... - if data.len() < 24 || accounts.len() < 11 { - return None; - } - let lp_token_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?); - let max_base_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?); - let max_quote_amount_in = u64::from_le_bytes(data[16..24].try_into().ok()?); - - Some(PumpSwapInstruction::Deposit(DepositEvent { - lp_token_amount_out, - max_base_amount_in, - max_quote_amount_in, - pool: accounts[0], - user: accounts[2], - user_base_token_account: accounts[6], - user_quote_token_account: accounts[7], - user_pool_token_account: accounts[8], - timestamp: current_timestamp(), - ..Default::default() - })) - }, - d if d == discriminators::WITHDRAW_IX => { - // withdraw指令参数: lp_token_amount_in: u64, min_base_amount_out: u64, min_quote_amount_out: u64 - // 账户顺序:pool, global_config, user, base_mint, quote_mint, lp_mint, - // user_base_token_account, user_quote_token_account, user_pool_token_account, - // pool_base_token_account, pool_quote_token_account, ... - if data.len() < 24 || accounts.len() < 11 { - return None; - } - let lp_token_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?); - let min_base_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?); - let min_quote_amount_out = u64::from_le_bytes(data[16..24].try_into().ok()?); - - Some(PumpSwapInstruction::Withdraw(WithdrawEvent { - lp_token_amount_in, - min_base_amount_out, - min_quote_amount_out, - pool: accounts[0], - user: accounts[2], - user_base_token_account: accounts[6], - user_quote_token_account: accounts[7], - user_pool_token_account: accounts[8], - timestamp: current_timestamp(), - ..Default::default() - })) - }, - d if d == discriminators::DISABLE_IX => { - // disable指令参数: disable_create_pool: bool, disable_deposit: bool, disable_withdraw: bool, disable_buy: bool, disable_sell: bool - // 账户顺序:admin, global_config, event_authority, program - if data.len() < 5 || accounts.len() < 2 { - return None; - } - let disable_create_pool = data[0] != 0; - let disable_deposit = data[1] != 0; - let disable_withdraw = data[2] != 0; - let disable_buy = data[3] != 0; - let disable_sell = data[4] != 0; - - Some(PumpSwapInstruction::Disable(DisableEvent { - disable_create_pool, - disable_deposit, - disable_withdraw, - disable_buy, - disable_sell, - admin: accounts[0], - timestamp: current_timestamp(), - ..Default::default() - })) - }, - d if d == discriminators::UPDATE_ADMIN_IX => { - // update_admin指令参数: 无 - // 账户顺序:admin, global_config, new_admin, event_authority, program - if accounts.len() < 3 { - return None; - } - Some(PumpSwapInstruction::UpdateAdmin(UpdateAdminEvent { - old_admin: accounts[0], - new_admin: accounts[2], - timestamp: current_timestamp(), - ..Default::default() - })) - }, - d if d == discriminators::UPDATE_FEE_CONFIG_IX => { - // update_fee_config指令参数: lp_fee_basis_points: u64, protocol_fee_basis_points: u64, protocol_fee_recipients: [pubkey; 8] - // 账户顺序:admin, global_config, event_authority, program - if data.len() < 272 || accounts.len() < 2 { // 8 + 8 + 32*8 = 272 bytes - return None; - } - let lp_fee_basis_points = u64::from_le_bytes(data[0..8].try_into().ok()?); - let protocol_fee_basis_points = u64::from_le_bytes(data[8..16].try_into().ok()?); - - let mut protocol_fee_recipients = [Pubkey::default(); 8]; - for i in 0..8 { - let start = 16 + i * 32; - let end = start + 32; - if let Ok(pubkey_bytes) = data[start..end].try_into() { - protocol_fee_recipients[i] = Pubkey::new_from_array(pubkey_bytes); - } - } - - Some(PumpSwapInstruction::UpdateFeeConfig(UpdateFeeConfigEvent { - admin: accounts[0], - new_lp_fee_basis_points: lp_fee_basis_points, - new_protocol_fee_basis_points: protocol_fee_basis_points, - new_protocol_fee_recipients: protocol_fee_recipients, - timestamp: current_timestamp(), - ..Default::default() - })) - }, - _ => Some(PumpSwapInstruction::Other), - } -} \ No newline at end of file diff --git a/src/common/pumpswap/mod.rs b/src/common/pumpswap/mod.rs deleted file mode 100755 index 856a6f0..0000000 --- a/src/common/pumpswap/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod logs_data; -pub mod logs_parser; -pub mod logs_filters; -pub mod logs_events; - -pub use logs_data::*; -pub use logs_parser::*; -pub use logs_filters::*; -pub use logs_events::*; \ No newline at end of file diff --git a/src/common/raydium/logs_data.rs b/src/common/raydium/logs_data.rs deleted file mode 100755 index 53d1cb5..0000000 --- a/src/common/raydium/logs_data.rs +++ /dev/null @@ -1,161 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; -use solana_sdk::pubkey::Pubkey; - -use crate::error::{ClientError, ClientResult}; - -/// Raydium指令类型 -#[derive(Debug)] -pub enum RaydiumInstruction { - V4Swap(V4SwapEvent), - SwapBaseInput(SwapBaseInputEvent), - SwapBaseOutput(SwapBaseOutputEvent), - Other, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct SwapBaseOutputEvent { - #[borsh(skip)] - pub timestamp: i64, - #[borsh(skip)] - pub slot: u64, - #[borsh(skip)] - pub signature: String, - pub max_amount_in: u64, - pub amount_out: u64, - - #[borsh(skip)] - pub payer: Pubkey, - #[borsh(skip)] - pub authority: Pubkey, - #[borsh(skip)] - pub amm_config: Pubkey, - #[borsh(skip)] - pub pool_state: Pubkey, - #[borsh(skip)] - pub input_token_account: Pubkey, - #[borsh(skip)] - pub output_token_account: Pubkey, - #[borsh(skip)] - pub input_vault: Pubkey, - #[borsh(skip)] - pub output_vault: Pubkey, - #[borsh(skip)] - pub input_token_program: Pubkey, - #[borsh(skip)] - pub output_token_program: Pubkey, - #[borsh(skip)] - pub input_token_mint: Pubkey, - #[borsh(skip)] - pub output_token_mint: Pubkey, - #[borsh(skip)] - pub observation_state: Pubkey, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct SwapBaseInputEvent { - #[borsh(skip)] - pub timestamp: i64, - #[borsh(skip)] - pub slot: u64, - #[borsh(skip)] - pub signature: String, - pub amount_in: u64, - pub minimum_amount_out: u64, - - #[borsh(skip)] - pub payer: Pubkey, - #[borsh(skip)] - pub authority: Pubkey, - #[borsh(skip)] - pub amm_config: Pubkey, - #[borsh(skip)] - pub pool_state: Pubkey, - #[borsh(skip)] - pub input_token_account: Pubkey, - #[borsh(skip)] - pub output_token_account: Pubkey, - #[borsh(skip)] - pub input_vault: Pubkey, - #[borsh(skip)] - pub output_vault: Pubkey, - #[borsh(skip)] - pub input_token_program: Pubkey, - #[borsh(skip)] - pub output_token_program: Pubkey, - #[borsh(skip)] - pub input_token_mint: Pubkey, - #[borsh(skip)] - pub output_token_mint: Pubkey, - #[borsh(skip)] - pub observation_state: Pubkey, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] -pub struct V4SwapEvent { - #[borsh(skip)] - pub timestamp: i64, - #[borsh(skip)] - pub slot: u64, - #[borsh(skip)] - pub signature: String, - pub amount_in: u64, - pub minimum_amount_out: u64, - - #[borsh(skip)] - pub amm: Pubkey, - #[borsh(skip)] - pub amm_authority: Pubkey, - #[borsh(skip)] - pub amm_open_orders: Pubkey, - #[borsh(skip)] - pub amm_target_orders: Pubkey, - #[borsh(skip)] - pub pool_coin_token_account: Pubkey, - #[borsh(skip)] - pub pool_pc_token_account: Pubkey, - #[borsh(skip)] - pub serum_program: Pubkey, - #[borsh(skip)] - pub serum_market: Pubkey, - #[borsh(skip)] - pub serum_bids: Pubkey, - #[borsh(skip)] - pub serum_asks: Pubkey, - #[borsh(skip)] - pub serum_event_queue: Pubkey, - #[borsh(skip)] - pub serum_coin_vault_account: Pubkey, - #[borsh(skip)] - pub serum_pc_vault_account: Pubkey, - #[borsh(skip)] - pub serum_vault_signer: Pubkey, - #[borsh(skip)] - pub user_source_token_account: Pubkey, - #[borsh(skip)] - pub user_destination_token_account: Pubkey, - #[borsh(skip)] - pub user_source_owner: Pubkey, -} - -/// 事件特性 -pub trait EventTrait: Sized + std::fmt::Debug { - fn from_bytes(bytes: &[u8]) -> ClientResult; - fn discriminator() -> &'static [u8]; -} - -/// 从字节中提取鉴别器 -pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> { - if data.len() < length { - return None; - } - Some((&data[..length], &data[length..])) -} - -/// 事件鉴别器常量 -pub mod discriminators { - pub const V4_SWAP_IX: &u8 = &9; - - pub const SWAP_BASE_INPUT_IX: &[u8] = &[143, 190, 90, 218, 196, 30, 51, 222]; - pub const SWAP_BASE_OUTPUT_IX: &[u8] = &[55, 217, 98, 86, 163, 74, 180, 173]; -} diff --git a/src/common/raydium/logs_events.rs b/src/common/raydium/logs_events.rs deleted file mode 100755 index 7f32854..0000000 --- a/src/common/raydium/logs_events.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::common::raydium::logs_data::{discriminators, SwapBaseInputEvent, V4SwapEvent}; -use crate::common::raydium::SwapBaseOutputEvent; -use base64::engine::general_purpose; -use base64::Engine; -use borsh::BorshDeserialize; - -pub const PROGRAM_DATA: &str = "Program data: "; -pub const PROGRAM_LOG_PREFIX: &str = "Program log: ray_log: "; - -/// Raydium事件枚举 -#[derive(Debug)] -pub enum RaydiumEvent { - V4Swap(V4SwapEvent), - SwapBaseInput(SwapBaseInputEvent), - SwapBaseOutput(SwapBaseOutputEvent), - Error(String), -} diff --git a/src/common/raydium/logs_filters.rs b/src/common/raydium/logs_filters.rs deleted file mode 100755 index 642ce4e..0000000 --- a/src/common/raydium/logs_filters.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::common::raydium::logs_data::RaydiumInstruction; -use crate::common::raydium::logs_parser::parse_raydium_instruction; -use crate::constants::raydium::accounts; -use crate::error::ClientResult; -use solana_sdk::transaction::VersionedTransaction; - -pub struct LogFilter; - -impl LogFilter { - /// 解析Raydium编译后的指令并返回指令类型和数据 - pub fn parse_raydium_compiled_instruction( - versioned_tx: VersionedTransaction, - ) -> ClientResult> { - let compiled_instructions = versioned_tx.message.instructions(); - let accounts = versioned_tx.message.static_account_keys(); - let ammv4_program_id = accounts::AMMV4_PROGRAM; - let cpmm_program_id = accounts::CPMM_PROGRAM; - let raydium_index = accounts.iter().position(|key| key == &ammv4_program_id); - let cpmm_index = accounts.iter().position(|key| key == &cpmm_program_id); - let mut instructions: Vec = Vec::new(); - if let Some(index) = raydium_index { - for instruction in compiled_instructions { - if instruction.program_id_index as usize == index { - let all_accounts_valid = instruction - .accounts - .iter() - .all(|&acc_idx| (acc_idx as usize) < accounts.len()); - if !all_accounts_valid { - continue; - } - - if let Some(parsed_instruction) = - parse_raydium_instruction(instruction, accounts, &ammv4_program_id) - { - instructions.push(parsed_instruction); - } - } - } - } - if let Some(index) = cpmm_index { - for instruction in compiled_instructions { - if instruction.program_id_index as usize == index { - let all_accounts_valid = instruction - .accounts - .iter() - .all(|&acc_idx| (acc_idx as usize) < accounts.len()); - if !all_accounts_valid { - continue; - } - - if let Some(parsed_instruction) = - parse_raydium_instruction(instruction, accounts, &cpmm_program_id) - { - instructions.push(parsed_instruction); - } - } - } - } - - Ok(instructions) - } -} diff --git a/src/common/raydium/logs_parser.rs b/src/common/raydium/logs_parser.rs deleted file mode 100755 index f039273..0000000 --- a/src/common/raydium/logs_parser.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::common::raydium::{ - logs_data::{discriminators, RaydiumInstruction, V4SwapEvent}, - logs_events::RaydiumEvent, -}; -use crate::error::ClientResult; - -use solana_sdk::instruction::CompiledInstruction; -use solana_sdk::pubkey::Pubkey; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// 获取当前时间戳 -fn current_timestamp() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() as i64 -} - -/// 从指令中解析Raydium指令 -pub fn parse_raydium_instruction( - instruction: &CompiledInstruction, - _accounts: &[Pubkey], - program_id: &Pubkey, -) -> Option { - if instruction.data.len() < 8 { - return None; - } - - if program_id == &crate::constants::raydium::accounts::CPMM_PROGRAM { - let discriminator = &instruction.data[..8]; - let data = &instruction.data[8..]; - - let accounts: Vec = instruction - .accounts - .iter() - .map(|&idx| _accounts[idx as usize]) - .collect(); - - match discriminator { - d if d == discriminators::SWAP_BASE_INPUT_IX => { - if data.len() < 16 || accounts.len() < 12 { - return None; - } - let amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?); - let minimum_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?); - - Some(RaydiumInstruction::SwapBaseInput( - crate::common::raydium::SwapBaseInputEvent { - timestamp: current_timestamp(), - amount_in: amount_in, - minimum_amount_out: minimum_amount_out, - - payer: accounts[0], - authority: accounts[1], - amm_config: accounts[2], - pool_state: accounts[3], - input_token_account: accounts[4], - output_token_account: accounts[5], - input_vault: accounts[6], - output_vault: accounts[7], - input_token_program: accounts[8], - output_token_program: accounts[9], - input_token_mint: accounts[10], - output_token_mint: accounts[11], - observation_state: accounts[12], - ..Default::default() - }, - )) - } - d if d == discriminators::SWAP_BASE_OUTPUT_IX => { - println!("data.len(): {:?}", data.len()); - println!("accounts.len(): {:?}", accounts.len()); - if data.len() < 16 || accounts.len() < 12 { - return None; - } - let max_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?); - let amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?); - - Some(RaydiumInstruction::SwapBaseOutput( - crate::common::raydium::SwapBaseOutputEvent { - timestamp: current_timestamp(), - max_amount_in: max_amount_in, - amount_out: amount_out, - - payer: accounts[0], - authority: accounts[1], - amm_config: accounts[2], - pool_state: accounts[3], - input_token_account: accounts[4], - output_token_account: accounts[5], - input_vault: accounts[6], - output_vault: accounts[7], - input_token_program: accounts[8], - output_token_program: accounts[9], - input_token_mint: accounts[10], - output_token_mint: accounts[11], - observation_state: accounts[12], - ..Default::default() - }, - )) - } - _ => Some(RaydiumInstruction::Other), - } - } else if program_id == &crate::constants::raydium::accounts::AMMV4_PROGRAM { - let discriminator = &instruction.data[0]; - let data = &instruction.data[1..]; - - let mut accounts: Vec = instruction - .accounts - .iter() - .map(|&idx| _accounts[idx as usize]) - .collect(); - - match discriminator { - d if d == discriminators::V4_SWAP_IX => { - if data.len() < 16 || accounts.len() < 17 { - return None; - } - let amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?); - let minimum_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?); - - if accounts.len() == 17 { - accounts.insert(4, Pubkey::default()); - } - - Some(RaydiumInstruction::V4Swap(V4SwapEvent { - timestamp: current_timestamp(), - amount_in: amount_in, - minimum_amount_out: minimum_amount_out, - amm: accounts[1], - amm_authority: accounts[2], - amm_open_orders: accounts[3], - amm_target_orders: accounts[4], - pool_coin_token_account: accounts[5], - pool_pc_token_account: accounts[6], - serum_program: accounts[7], - serum_market: accounts[8], - serum_bids: accounts[9], - serum_asks: accounts[10], - serum_event_queue: accounts[11], - serum_coin_vault_account: accounts[12], - serum_pc_vault_account: accounts[13], - serum_vault_signer: accounts[14], - user_source_token_account: accounts[15], - user_destination_token_account: accounts[16], - user_source_owner: accounts[17], - ..Default::default() - })) - } - _ => Some(RaydiumInstruction::Other), - } - } else { - None - } -} diff --git a/src/common/raydium/mod.rs b/src/common/raydium/mod.rs deleted file mode 100755 index 856a6f0..0000000 --- a/src/common/raydium/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod logs_data; -pub mod logs_parser; -pub mod logs_filters; -pub mod logs_events; - -pub use logs_data::*; -pub use logs_parser::*; -pub use logs_filters::*; -pub use logs_events::*; \ No newline at end of file diff --git a/src/constants/mod.rs b/src/constants/mod.rs index d174ac3..010dfe6 100755 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -1,6 +1,6 @@ pub mod pumpfun; pub mod pumpswap; -pub mod raydium; +pub mod raydium_launchpad; pub mod swqos; pub mod trade_type { @@ -13,5 +13,5 @@ pub mod trade_type { pub mod trade_platform { pub const PUMPFUN: &'static str = "pumpfun"; pub const PUMPFUN_SWAP: &'static str = "pumpswap"; - pub const RAYDIUM: &'static str = "raydium"; + pub const RAYDIUM_LAUNCHPAD: &'static str = "raydium_launchpad"; } \ No newline at end of file diff --git a/src/constants/pumpswap/mod.rs b/src/constants/pumpswap/mod.rs index a095bd3..4785bd9 100755 --- a/src/constants/pumpswap/mod.rs +++ b/src/constants/pumpswap/mod.rs @@ -57,58 +57,6 @@ pub mod accounts { /// Rent Sysvar ID pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111"); - pub const JITO_TIP_ACCOUNTS: &[&str] = &[ - "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", - "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", - "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", - "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", - "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", - "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", - "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", - "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", - ]; - - - /// Tip accounts - pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[ - "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE", - "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2", - "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X", - "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb", - "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At", - "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG", - "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid", - "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc" - ]; - - pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[ - "Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3", - "FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe", - "ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13", - "6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK", - "Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr", - ]; - - pub const NOZOMI_TIP_ACCOUNTS: &[&str] = &[ - "TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq", - "noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4", - "noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE", - "noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo", - "noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ", - "nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L", - "nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z", - "nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu", - "noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7", - "nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP", - "nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P", - "nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge", - "nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3", - "nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ", - "nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk", - "nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne", - "nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb" - ]; - pub const AMM_PROGRAM: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"); } diff --git a/src/constants/raydium/mod.rs b/src/constants/raydium/mod.rs deleted file mode 100755 index 376584a..0000000 --- a/src/constants/raydium/mod.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Constants used by the crate. -//! -//! This module contains various constants used throughout the crate, including: -//! -//! - Seeds for deriving Program Derived Addresses (PDAs) -//! - Program account addresses and public keys -//! -//! The constants are organized into submodules for better organization: -//! -//! - `seeds`: Contains seed values used for PDA derivation -//! - `accounts`: Contains important program account addresses - -/// Constants used as seeds for deriving PDAs (Program Derived Addresses) -pub mod seeds { - /// Seed for the global state PDA - pub const GLOBAL_SEED: &[u8] = b"global"; -} - -/// Constants related to program accounts and authorities -pub mod accounts { - use solana_sdk::{pubkey, pubkey::Pubkey}; - - pub const JITO_TIP_ACCOUNTS: &[&str] = &[ - "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", - "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", - "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", - "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", - "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", - "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", - "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", - "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", - ]; - - /// Tip accounts - pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[ - "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE", - "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2", - "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X", - "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb", - "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At", - "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG", - "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid", - "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc", - ]; - - pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[ - "Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3", - "FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe", - "ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13", - "6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK", - "Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr", - ]; - - pub const NOZOMI_TIP_ACCOUNTS: &[&str] = &[ - "TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq", - "noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4", - "noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE", - "noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo", - "noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ", - "nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L", - "nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z", - "nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu", - "noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7", - "nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP", - "nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P", - "nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge", - "nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3", - "nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ", - "nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk", - "nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne", - "nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb", - ]; - - pub const AMMV4_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); - pub const CPMM_PROGRAM: Pubkey = pubkey!("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"); -} - -pub mod trade { - pub const TRADER_TIP_AMOUNT: u64 = 100000; // 0.0001 SOL in lamports - pub const DEFAULT_SLIPPAGE: u64 = 1000; // 10% - pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000; - pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000; - pub const DEFAULT_BUY_TIP_FEE: u64 = 600000; // 0.0006 SOL in lamports - pub const DEFAULT_SELL_TIP_FEE: u64 = 100000; // 0.0001 SOL in lamports -} diff --git a/src/constants/raydium_launchpad/mod.rs b/src/constants/raydium_launchpad/mod.rs new file mode 100755 index 0000000..b6877e7 --- /dev/null +++ b/src/constants/raydium_launchpad/mod.rs @@ -0,0 +1,46 @@ +//! Constants used by the crate. +//! +//! This module contains various constants used throughout the crate, including: +//! +//! - Seeds for deriving Program Derived Addresses (PDAs) +//! - Program account addresses and public keys +//! +//! The constants are organized into submodules for better organization: +//! +//! - `seeds`: Contains seed values used for PDA derivation +//! - `accounts`: Contains important program account addresses + +/// Constants used as seeds for deriving PDAs (Program Derived Addresses) +pub mod seeds { + pub const POOL_SEED: &[u8] = b"pool"; + pub const POOL_VAULT_SEED: &[u8] = b"pool_vault"; +} + +/// Constants related to program accounts and authorities +pub mod accounts { + use solana_sdk::{pubkey, pubkey::Pubkey}; + + pub const AUTHORITY: Pubkey = pubkey!("WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh"); + pub const GLOBAL_CONFIG: Pubkey = pubkey!("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX"); + pub const PLATFORM_CONFIG: Pubkey = pubkey!("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1"); + pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + pub const EVENT_AUTHORITY: Pubkey = pubkey!("2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr"); + pub const WSOL_TOKEN_ACCOUNT: Pubkey = pubkey!("So11111111111111111111111111111111111111112"); + pub const LAUNCHPAD_PROGRAM: Pubkey = pubkey!("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"); + + pub const PLATFORM_FEE_RATE: u128 = 100; // 1% + pub const PROTOCOL_FEE_RATE: u128 = 25; // 0.25% + pub const SHARE_FEE_RATE: u128 = 0; // 0% +} + +pub const BUY_EXECT_IN_DISCRIMINATOR: [u8; 8] = [250, 234, 13, 123, 213, 156, 19, 236]; +pub const SELL_EXECT_IN_DISCRIMINATOR: [u8; 8] = [149, 39, 222, 155, 211, 124, 152, 26]; + +pub mod trade { + pub const TRADER_TIP_AMOUNT: u64 = 100000; // 0.0001 SOL in lamports + pub const DEFAULT_SLIPPAGE: u64 = 100; // 1% + pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000; + pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000; + pub const DEFAULT_BUY_TIP_FEE: u64 = 600000; // 0.0006 SOL in lamports + pub const DEFAULT_SELL_TIP_FEE: u64 = 100000; // 0.0001 SOL in lamports +} diff --git a/src/event_parser/common/mod.rs b/src/event_parser/common/mod.rs new file mode 100644 index 0000000..5eac189 --- /dev/null +++ b/src/event_parser/common/mod.rs @@ -0,0 +1,54 @@ +pub mod types; +pub mod utils; + +/// 自动生成UnifiedEvent trait实现的宏 +#[macro_export] +macro_rules! impl_unified_event { + // 带有自定义ID表达式的版本 + ($struct_name:ident, $($field:ident),*) => { + impl $crate::event_parser::core::traits::UnifiedEvent for $struct_name { + fn id(&self) -> &str { + &self.metadata.id + } + + fn event_type(&self) -> $crate::event_parser::common::types::EventType { + self.metadata.event_type.clone() + } + + fn signature(&self) -> &str { + &self.metadata.signature + } + + fn slot(&self) -> u64 { + self.metadata.slot + } + + fn program_received_time_ms(&self) -> i64 { + self.metadata.program_received_time_ms + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + + fn merge(&mut self, other: Box) { + if let Some(e) = other.as_any().downcast_ref::<$struct_name>() { + $( + self.$field = e.$field.clone(); + )* + } + } + } + }; +} + +pub use types::*; +pub use utils::*; diff --git a/src/event_parser/common/types.rs b/src/event_parser/common/types.rs new file mode 100644 index 0000000..a4b3699 --- /dev/null +++ b/src/event_parser/common/types.rs @@ -0,0 +1,138 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +#[derive( + Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +pub enum ProtocolType { + #[default] + PumpSwap, + PumpFun, + RaydiumLaunchpad, + Raydium, +} + +/// 事件类型枚举 +#[derive( + Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +pub enum EventType { + // PumpSwap 事件 + #[default] + PumpSwapBuy, + PumpSwapSell, + PumpSwapCreatePool, + PumpSwapDeposit, + PumpSwapWithdraw, + + // PumpFun 事件 + PumpFunCreateToken, + PumpFunBuy, + PumpFunSell, + + // RaydiumLaunchpad 事件 + RaydiumLaunchpadBuyExactIn, + RaydiumLaunchpadBuyExactOut, + RaydiumLaunchpadSellExactIn, + RaydiumLaunchpadSellExactOut, + RaydiumLaunchpadInitialize, + + // 通用事件 + Unknown, +} + +/// 解析结果 +#[derive(Debug, Clone)] +pub struct ParseResult { + pub success: bool, + pub data: Option, + pub error: Option, +} + +impl ParseResult { + pub fn success(data: T) -> Self { + Self { + success: true, + data: Some(data), + error: None, + } + } + + pub fn failure(error: String) -> Self { + Self { + success: false, + data: None, + error: Some(error), + } + } + + pub fn is_success(&self) -> bool { + self.success + } + + pub fn is_failure(&self) -> bool { + !self.success + } +} + +/// 协议信息 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProtocolInfo { + pub name: String, + pub program_ids: Vec, +} + +impl ProtocolInfo { + pub fn new(name: String, program_ids: Vec) -> Self { + Self { name, program_ids } + } + + pub fn supports_program(&self, program_id: &Pubkey) -> bool { + self.program_ids.contains(program_id) + } +} + +/// 事件元数据 +#[derive( + Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +pub struct EventMetadata { + pub id: String, + pub signature: String, + pub slot: u64, + pub program_received_time_ms: i64, + pub protocol: ProtocolType, + pub event_type: EventType, + pub program_id: Pubkey, +} + +impl EventMetadata { + pub fn new( + id: String, + signature: String, + slot: u64, + protocol: ProtocolType, + event_type: EventType, + program_id: Pubkey, + ) -> Self { + Self { + id, + signature, + slot, + program_received_time_ms: chrono::Utc::now().timestamp_millis(), + protocol, + event_type, + program_id, + } + } + pub fn set_id(&mut self, id: String) { + // 对传入的 id 进行哈希处理 + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + let hash_value = hasher.finish(); + self.id = format!("{:x}", hash_value); + } +} diff --git a/src/event_parser/common/utils.rs b/src/event_parser/common/utils.rs new file mode 100644 index 0000000..4c5fee9 --- /dev/null +++ b/src/event_parser/common/utils.rs @@ -0,0 +1,95 @@ +use base64::engine::general_purpose; +use base64::Engine; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// 获取当前时间戳 +pub fn current_timestamp() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() as i64 +} + +/// 从base64字符串解码数据 +pub fn decode_base64(data: &str) -> Result, base64::DecodeError> { + general_purpose::STANDARD.decode(data) +} + +/// 将数据编码为base64字符串 +pub fn encode_base64(data: &[u8]) -> String { + general_purpose::STANDARD.encode(data) +} + +/// 从字节数组中提取鉴别器和剩余数据 +pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> { + if data.len() < length { + return None; + } + Some((&data[..length], &data[length..])) +} + +/// 检查鉴别器是否匹配 +pub fn discriminator_matches(data: &str, expected: &str) -> bool { + if data.len() < expected.len() { + return false; + } + &data[..expected.len()] == expected +} + +/// 从日志中提取程序数据 +pub fn extract_program_data(log: &str) -> Option<&str> { + const PROGRAM_DATA_PREFIX: &str = "Program data: "; + log.strip_prefix(PROGRAM_DATA_PREFIX) +} + +/// 从日志中提取程序日志 +pub fn extract_program_log<'a>(log: &'a str, prefix: &str) -> Option<&'a str> { + log.strip_prefix(prefix) +} + +/// 安全地从字节数组中读取u64 +pub fn read_u64_le(data: &[u8], offset: usize) -> Option { + if data.len() < offset + 8 { + return None; + } + let bytes: [u8; 8] = data[offset..offset + 8].try_into().ok()?; + Some(u64::from_le_bytes(bytes)) +} + +/// 安全地从字节数组中读取u32 +pub fn read_u32_le(data: &[u8], offset: usize) -> Option { + if data.len() < offset + 4 { + return None; + } + let bytes: [u8; 4] = data[offset..offset + 4].try_into().ok()?; + Some(u32::from_le_bytes(bytes)) +} + +/// 安全地从字节数组中读取u16 +pub fn read_u16_le(data: &[u8], offset: usize) -> Option { + if data.len() < offset + 2 { + return None; + } + let bytes: [u8; 2] = data[offset..offset + 2].try_into().ok()?; + Some(u16::from_le_bytes(bytes)) +} + +/// 安全地从字节数组中读取u8 +pub fn read_u8(data: &[u8], offset: usize) -> Option { + data.get(offset).copied() +} + +/// 验证账户索引的有效性 +pub fn validate_account_indices(indices: &[u8], account_count: usize) -> bool { + indices.iter().all(|&idx| (idx as usize) < account_count) +} + +/// 格式化公钥为短字符串 +pub fn format_pubkey_short(pubkey: &solana_sdk::pubkey::Pubkey) -> String { + let s = pubkey.to_string(); + if s.len() <= 8 { + s + } else { + format!("{}...{}", &s[..4], &s[s.len() - 4..]) + } +} diff --git a/src/event_parser/core/mod.rs b/src/event_parser/core/mod.rs new file mode 100644 index 0000000..663ef85 --- /dev/null +++ b/src/event_parser/core/mod.rs @@ -0,0 +1,2 @@ +pub mod traits; +pub use traits::{EventParser, UnifiedEvent}; diff --git a/src/event_parser/core/traits.rs b/src/event_parser/core/traits.rs new file mode 100644 index 0000000..a0aa541 --- /dev/null +++ b/src/event_parser/core/traits.rs @@ -0,0 +1,432 @@ +use anyhow::Result; +use solana_sdk::{ + instruction::CompiledInstruction, pubkey::Pubkey, transaction::VersionedTransaction, +}; +use solana_transaction_status::{ + EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiInstruction, +}; +use std::collections::HashMap; +use std::fmt::Debug; + +use crate::{ + error, + event_parser::{common::{utils::*, EventMetadata, EventType, ProtocolType}, protocols::{pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent}, raydium_launchpad::{RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent}}}, +}; + +/// 统一事件接口 - 所有协议的事件都需要实现此trait +pub trait UnifiedEvent: Debug + Send + Sync { + /// 获取事件ID + fn id(&self) -> &str; + + /// 获取事件类型 + fn event_type(&self) -> EventType; + + /// 获取交易签名 + fn signature(&self) -> &str; + + /// 获取槽位号 + fn slot(&self) -> u64; + + /// 获取程序接收的时间戳(毫秒) + fn program_received_time_ms(&self) -> i64; + + /// 将事件转换为Any以便向下转型 + fn as_any(&self) -> &dyn std::any::Any; + + /// 将事件转换为可变Any以便向下转型 + fn as_any_mut(&mut self) -> &mut dyn std::any::Any; + + /// 克隆事件 + fn clone_boxed(&self) -> Box; + + /// 合并事件(可选实现) + fn merge(&mut self, _other: Box) { + // 默认实现:不进行任何合并操作 + } +} + +/// 事件解析器trait - 定义了事件解析的核心方法 +#[async_trait::async_trait] +pub trait EventParser: Send + Sync { + /// 从内联指令中解析事件数据 + fn parse_events_from_inner_instruction( + &self, + instruction: &UiCompiledInstruction, + signature: &str, + slot: u64, + ) -> Vec>; + + /// 从指令中解析事件数据 + fn parse_events_from_instruction( + &self, + instruction: &CompiledInstruction, + accounts: &[Pubkey], + signature: &str, + slot: u64, + ) -> Vec>; + + /// 从VersionedTransaction中解析指令事件的通用方法 + async fn parse_instruction_events_from_versioned_transaction( + &self, + versioned_tx: &VersionedTransaction, + signature: &str, + slot: Option, + ) -> Result>> { + let mut instruction_events = Vec::new(); + // 获取交易的指令和账户 + let compiled_instructions = versioned_tx.message.instructions(); + let accounts = versioned_tx.message.static_account_keys(); + // 检查交易中是否包含程序 + let has_program = accounts.iter().any(|account| self.should_handle(account)); + if has_program { + // 解析每个指令 + for instruction in compiled_instructions { + if let Some(program_id) = accounts.get(instruction.program_id_index as usize) { + if self.should_handle(program_id) { + // 验证账户索引的有效性 + let all_accounts_valid = instruction + .accounts + .iter() + .all(|&acc_idx| (acc_idx as usize) < accounts.len()); + + if all_accounts_valid { + if let Ok(events) = self + .parse_instruction(instruction, accounts, signature, slot) + .await + { + instruction_events.extend(events); + } + } + } + } + } + } + Ok(instruction_events) + } + + async fn parse_versioned_transaction( + &self, + versioned_tx: &VersionedTransaction, + signature: &str, + slot: Option, + bot_wallet: Option, + ) -> Result>> { + let events = self.parse_instruction_events_from_versioned_transaction(versioned_tx, signature, slot) + .await.unwrap_or_else(|_e| vec![]); + Ok(self.process_events(events, bot_wallet)) + } + + async fn parse_transaction( + &self, + tx: EncodedTransactionWithStatusMeta, + signature: &str, + slot: Option, + bot_wallet: Option, + ) -> Result>> { + let transaction = tx.transaction; + let mut instruction_events = Vec::new(); + + // 解析指令事件 + if let Some(versioned_tx) = transaction.decode() { + instruction_events = self + .parse_instruction_events_from_versioned_transaction(&versioned_tx, signature, slot) + .await + .unwrap_or_else(|_e| vec![]); + } + + // 解析内联指令事件 + let mut inner_instruction_events = Vec::new(); + // 检查交易元数据 + let meta = tx + .meta + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?; + // 检查交易是否成功 + if meta.err.is_none() { + let inner_instructions = meta.inner_instructions.as_ref().unwrap(); + for inner_instruction in inner_instructions { + for instruction in &inner_instruction.instructions { + match instruction { + UiInstruction::Compiled(compiled) => { + if let Ok(events) = self + .parse_inner_instruction(compiled, signature, slot) + .await + { + inner_instruction_events.extend(events); + } + } + _ => {} + } + } + } + } + + if instruction_events.len() > 0 && inner_instruction_events.len() > 0 { + for instruction_event in &mut instruction_events { + for inner_instruction_event in &inner_instruction_events { + if instruction_event.id() == inner_instruction_event.id() + && instruction_event.event_type() == inner_instruction_event.event_type() + { + instruction_event.merge(inner_instruction_event.clone_boxed()); + break; + } + } + } + } + Ok(self.process_events(instruction_events, bot_wallet)) + } + + fn process_events(&self, mut events: Vec>, bot_wallet: Option) -> Vec> { + let mut dev_address = None; + let mut raydium_dev_address = None; + for event in &mut events { + if let Some(token_info) = event.as_any().downcast_ref::() { + dev_address = Some(token_info.user); + } else if let Some(trade_info) = + event.as_any_mut().downcast_mut::() + { + if Some(trade_info.user) == dev_address { + trade_info.is_dev_create_token_trade = true; + } else if Some(trade_info.user) == bot_wallet { + trade_info.is_bot = true; + } else { + trade_info.is_dev_create_token_trade = false; + } + } + if let Some(pool_info) = event + .as_any() + .downcast_ref::() + { + raydium_dev_address = Some(pool_info.creator); + } else if let Some(trade_info) = event + .as_any_mut() + .downcast_mut::() + { + if Some(trade_info.payer) == raydium_dev_address { + trade_info.is_dev_create_token_trade = true; + } else if Some(trade_info.payer) == bot_wallet { + trade_info.is_bot = true; + } else { + trade_info.is_dev_create_token_trade = false; + } + } + } + events + } + + async fn parse_inner_instruction( + &self, + instruction: &UiCompiledInstruction, + signature: &str, + slot: Option, + ) -> Result>> { + let slot = slot.unwrap_or(0); + let events = self.parse_events_from_inner_instruction(instruction, signature, slot); + Ok(events) + } + + async fn parse_instruction( + &self, + instruction: &CompiledInstruction, + accounts: &[Pubkey], + signature: &str, + slot: Option, + ) -> Result>> { + let slot = slot.unwrap_or(0); + let events = self.parse_events_from_instruction(instruction, accounts, signature, slot); + Ok(events) + } + + /// 检查是否应该处理此程序ID + fn should_handle(&self, program_id: &Pubkey) -> bool; + + /// 获取支持的程序ID列表 + fn supported_program_ids(&self) -> Vec; +} + +// 为Box实现Clone +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_boxed() + } +} + +/// 通用事件解析器配置 +#[derive(Debug, Clone)] +pub struct GenericEventParseConfig { + pub inner_instruction_discriminator: &'static str, + pub instruction_discriminator: &'static [u8], + pub event_type: EventType, + pub inner_instruction_parser: InnerInstructionEventParser, + pub instruction_parser: InstructionEventParser, +} + +/// 内联指令事件解析器 +pub type InnerInstructionEventParser = + fn(data: &[u8], metadata: EventMetadata) -> Option>; + +/// 指令事件解析器 +pub type InstructionEventParser = + fn(data: &[u8], accounts: &[Pubkey], metadata: EventMetadata) -> Option>; + +/// 通用事件解析器基类 +pub struct GenericEventParser { + program_id: Pubkey, + protocol_type: ProtocolType, + inner_instruction_configs: HashMap<&'static str, Vec>, + instruction_configs: HashMap, Vec>, +} + +impl GenericEventParser { + /// 创建新的通用事件解析器 + pub fn new( + program_id: Pubkey, + protocol_type: ProtocolType, + configs: Vec, + ) -> Self { + let mut inner_instruction_configs = HashMap::new(); + let mut instruction_configs = HashMap::new(); + + for config in configs { + inner_instruction_configs + .entry(config.inner_instruction_discriminator) + .or_insert(vec![]) + .push(config.clone()); + instruction_configs + .entry(config.instruction_discriminator.to_vec()) + .or_insert(vec![]) + .push(config); + } + + Self { + program_id, + protocol_type, + inner_instruction_configs, + instruction_configs, + } + } + + /// 通用的内联指令解析方法 + fn parse_inner_instruction_event( + &self, + config: &GenericEventParseConfig, + data: &[u8], + signature: &str, + slot: u64, + ) -> Option> { + let metadata = EventMetadata::new( + signature.to_string(), + signature.to_string(), + slot, + self.protocol_type.clone(), + config.event_type.clone(), + self.program_id, + ); + (config.inner_instruction_parser)(data, metadata) + } + + /// 通用的指令解析方法 + fn parse_instruction_event( + &self, + config: &GenericEventParseConfig, + data: &[u8], + account_pubkeys: &[Pubkey], + signature: &str, + slot: u64, + ) -> Option> { + let metadata = EventMetadata::new( + signature.to_string(), + signature.to_string(), + slot, + self.protocol_type.clone(), + config.event_type.clone(), + self.program_id, + ); + (config.instruction_parser)(data, account_pubkeys, metadata) + } +} + +#[async_trait::async_trait] +impl EventParser for GenericEventParser { + /// 从内联指令中解析事件数据 + fn parse_events_from_inner_instruction( + &self, + inner_instruction: &UiCompiledInstruction, + signature: &str, + slot: u64, + ) -> Vec> { + let inner_instruction_data = inner_instruction.data.clone(); + let inner_instruction_data_decoded = + bs58::decode(inner_instruction_data).into_vec().unwrap(); + if inner_instruction_data_decoded.len() < 16 { + return Vec::new(); + } + let inner_instruction_data_decoded_str = + format!("0x{}", hex::encode(&inner_instruction_data_decoded)); + let data = &inner_instruction_data_decoded[16..]; + let mut events = Vec::new(); + for (disc, configs) in &self.inner_instruction_configs { + if discriminator_matches(&inner_instruction_data_decoded_str, disc) { + for config in configs { + if let Some(event) = + self.parse_inner_instruction_event(config, data, signature, slot) + { + events.push(event); + } + } + } + } + events + } + + /// 从指令中解析事件 + fn parse_events_from_instruction( + &self, + instruction: &CompiledInstruction, + accounts: &[Pubkey], + signature: &str, + slot: u64, + ) -> Vec> { + let mut events = Vec::new(); + for (disc, configs) in &self.instruction_configs { + if instruction.data.len() < disc.len() { + continue; + } + let discriminator = &instruction.data[..disc.len()]; + let data = &instruction.data[disc.len()..]; + if discriminator == disc { + // 验证账户索引 + if !validate_account_indices(&instruction.accounts, accounts.len()) { + continue; + } + + let account_pubkeys: Vec = instruction + .accounts + .iter() + .map(|&idx| accounts[idx as usize]) + .collect(); + for config in configs { + if let Some(event) = self.parse_instruction_event( + config, + data, + &account_pubkeys, + signature, + slot, + ) { + events.push(event); + } + } + } + } + + events + } + + fn should_handle(&self, program_id: &Pubkey) -> bool { + *program_id == self.program_id + } + + fn supported_program_ids(&self) -> Vec { + vec![self.program_id] + } +} diff --git a/src/event_parser/factory.rs b/src/event_parser/factory.rs new file mode 100644 index 0000000..795a799 --- /dev/null +++ b/src/event_parser/factory.rs @@ -0,0 +1,85 @@ +use anyhow::{anyhow, Result}; +use solana_sdk::pubkey::Pubkey; +use std::sync::Arc; + +use crate::event_parser::protocols::{ + pumpfun::parser::PUMPFUN_PROGRAM_ID, pumpswap::parser::PUMPSWAP_PROGRAM_ID, raydium_launchpad::parser::RAYDIUM_LAUNCHPAD_PROGRAM_ID, RaydiumLaunchpadEventParser, +}; + +use super::{ + core::traits::EventParser, + protocols::{pumpfun::PumpFunEventParser, pumpswap::PumpSwapEventParser}, +}; + +/// 支持的协议 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Protocol { + PumpSwap, + PumpFun, + RaydiumLaunchpad, +} + +impl Protocol { + pub fn get_program_id(&self) -> Vec { + match self { + Protocol::PumpSwap => vec![PUMPSWAP_PROGRAM_ID], + Protocol::PumpFun => vec![PUMPFUN_PROGRAM_ID], + Protocol::RaydiumLaunchpad => vec![RAYDIUM_LAUNCHPAD_PROGRAM_ID], + } + } +} + +impl std::fmt::Display for Protocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Protocol::PumpSwap => write!(f, "PumpSwap"), + Protocol::PumpFun => write!(f, "PumpFun"), + Protocol::RaydiumLaunchpad => write!(f, "RaydiumLaunchpad"), + } + } +} + +impl std::str::FromStr for Protocol { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "pumpswap" => Ok(Protocol::PumpSwap), + "pumpfun" => Ok(Protocol::PumpFun), + "raydiumlaunchpad" => Ok(Protocol::RaydiumLaunchpad), + _ => Err(anyhow!("Unsupported protocol: {}", s)), + } + } +} + +/// 事件解析器工厂 - 用于创建不同协议的事件解析器 +pub struct EventParserFactory; + +impl EventParserFactory { + /// 创建指定协议的事件解析器 + pub fn create_parser(protocol: Protocol) -> Arc { + match protocol { + Protocol::PumpSwap => Arc::new(PumpSwapEventParser::new()), + Protocol::PumpFun => Arc::new(PumpFunEventParser::new()), + Protocol::RaydiumLaunchpad => Arc::new(RaydiumLaunchpadEventParser::new()), + } + } + + /// 创建所有协议的事件解析器 + pub fn create_all_parsers() -> Vec> { + Self::supported_protocols() + .into_iter() + .map(Self::create_parser) + .collect() + } + + /// 获取所有支持的协议 + pub fn supported_protocols() -> Vec { + vec![Protocol::PumpSwap] + } + + /// 检查协议是否支持 + pub fn is_supported(protocol: &Protocol) -> bool { + Self::supported_protocols().contains(protocol) + } +} diff --git a/src/event_parser/mod.rs b/src/event_parser/mod.rs new file mode 100644 index 0000000..99d71eb --- /dev/null +++ b/src/event_parser/mod.rs @@ -0,0 +1,41 @@ +pub mod common; +pub mod core; +pub mod factory; +pub mod protocols; + +pub use core::traits::{EventParser, UnifiedEvent}; +pub use factory::{EventParserFactory, Protocol}; + +/// 宏:简化 downcast_ref 模式匹配 +/// +/// # 使用示例 +/// ``` +/// use sol_trade_sdk::event_parser::match_event; +/// +/// match_event!(event, { +/// PumpSwapCreatePoolEvent => |typed_event| { +/// println!("CreatePool event: {:?}", typed_event); +/// }, +/// PumpSwapDepositEvent => |typed_event| { +/// // 处理存款事件 +/// }, +/// }); +/// ``` +#[macro_export] +macro_rules! match_event { + ($event:expr, { + $($event_type:ty => $handler:expr),* $(,)? + }) => { + $( + if let Some(typed_event) = $event.as_any().downcast_ref::<$event_type>() { + $handler(typed_event.clone()); + } else + )* + { + // 默认情况:什么都不做 + } + }; +} + +// 重新导出宏以便于使用 +pub use match_event; diff --git a/src/event_parser/protocols/mod.rs b/src/event_parser/protocols/mod.rs new file mode 100644 index 0000000..d3e5fae --- /dev/null +++ b/src/event_parser/protocols/mod.rs @@ -0,0 +1,7 @@ +pub mod pumpfun; +pub mod pumpswap; +pub mod raydium_launchpad; + +pub use pumpfun::PumpFunEventParser; +pub use pumpswap::PumpSwapEventParser; +pub use raydium_launchpad::RaydiumLaunchpadEventParser; \ No newline at end of file diff --git a/src/event_parser/protocols/pumpfun/events.rs b/src/event_parser/protocols/pumpfun/events.rs new file mode 100644 index 0000000..5087ddd --- /dev/null +++ b/src/event_parser/protocols/pumpfun/events.rs @@ -0,0 +1,113 @@ +use borsh::BorshDeserialize; +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +use crate::event_parser::{common::EventMetadata, core::traits::UnifiedEvent}; +use crate::impl_unified_event; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct PumpFunCreateTokenEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub name: String, + pub symbol: String, + pub uri: String, + pub mint: Pubkey, + pub bonding_curve: Pubkey, + pub user: Pubkey, + pub creator: Pubkey, + pub timestamp: i64, + pub virtual_token_reserves: u64, + pub virtual_sol_reserves: u64, + pub real_token_reserves: u64, + pub token_total_supply: u64, + #[borsh(skip)] + pub mint_authority: Pubkey, + #[borsh(skip)] + pub associated_bonding_curve: Pubkey, +} + +impl_unified_event!( + PumpFunCreateTokenEvent, + mint, + bonding_curve, + user, + creator, + timestamp, + virtual_token_reserves, + virtual_sol_reserves, + real_token_reserves, + token_total_supply +); + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct PumpFunTradeEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub mint: Pubkey, + pub sol_amount: u64, + pub token_amount: u64, + pub is_buy: bool, + pub user: Pubkey, + pub timestamp: i64, + pub virtual_sol_reserves: u64, + pub virtual_token_reserves: u64, + pub real_sol_reserves: u64, + pub real_token_reserves: u64, + pub fee_recipient: Pubkey, + pub fee_basis_points: u64, + pub fee: u64, + pub creator: Pubkey, + pub creator_fee_basis_points: u64, + pub creator_fee: u64, + #[borsh(skip)] + pub bonding_curve: Pubkey, + #[borsh(skip)] + pub associated_bonding_curve: Pubkey, + #[borsh(skip)] + pub associated_user: Pubkey, + #[borsh(skip)] + pub creator_vault: Pubkey, + #[borsh(skip)] + pub max_sol_cost: u64, + #[borsh(skip)] + pub min_sol_output: u64, + #[borsh(skip)] + pub amount: u64, + #[borsh(skip)] + pub is_bot: bool, + #[borsh(skip)] + pub is_dev_create_token_trade: bool, // 是否是dev创建token的交易 +} + +impl_unified_event!( + PumpFunTradeEvent, + mint, + sol_amount, + token_amount, + is_buy, + user, + timestamp, + virtual_sol_reserves, + virtual_token_reserves, + real_sol_reserves, + real_token_reserves, + fee_recipient, + fee_basis_points, + fee, + creator, + creator_fee_basis_points, + creator_fee +); + +/// 事件鉴别器常量 +pub mod discriminators { + // 事件鉴别器 + pub const CREATE_TOKEN_EVENT: &str = "0xe445a52e51cb9a1d1b72a94ddeeb6376"; + pub const TRADE_EVENT: &str = "0xe445a52e51cb9a1dbddb7fd34ee661ee"; + + // 指令鉴别器 + pub const CREATE_TOKEN_IX: &[u8] = &[24, 30, 200, 40, 5, 28, 7, 119]; + pub const BUY_IX: &[u8] = &[102, 6, 61, 18, 1, 218, 235, 234]; + pub const SELL_IX: &[u8] = &[51, 230, 133, 164, 1, 127, 131, 173]; +} diff --git a/src/event_parser/protocols/pumpfun/mod.rs b/src/event_parser/protocols/pumpfun/mod.rs new file mode 100644 index 0000000..eee7acf --- /dev/null +++ b/src/event_parser/protocols/pumpfun/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; + +pub use events::*; +pub use parser::PumpFunEventParser; \ No newline at end of file diff --git a/src/event_parser/protocols/pumpfun/parser.rs b/src/event_parser/protocols/pumpfun/parser.rs new file mode 100644 index 0000000..a14e91b --- /dev/null +++ b/src/event_parser/protocols/pumpfun/parser.rs @@ -0,0 +1,250 @@ +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use solana_transaction_status::UiCompiledInstruction; + +use crate::event_parser::{ + common::{utils::*, EventMetadata, EventType, ProtocolType}, + core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent}, + protocols::pumpfun::{discriminators, PumpFunCreateTokenEvent, PumpFunTradeEvent}, +}; + +/// PumpFun程序ID +pub const PUMPFUN_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); + +/// PumpFun事件解析器 +pub struct PumpFunEventParser { + inner: GenericEventParser, +} + +impl PumpFunEventParser { + pub fn new() -> Self { + // 配置所有事件类型 + let configs = vec![ + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::CREATE_TOKEN_EVENT, + instruction_discriminator: discriminators::CREATE_TOKEN_IX, + event_type: EventType::PumpFunCreateToken, + inner_instruction_parser: Self::parse_create_token_inner_instruction, + instruction_parser: Self::parse_create_token_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::TRADE_EVENT, + instruction_discriminator: discriminators::BUY_IX, + event_type: EventType::PumpFunBuy, + inner_instruction_parser: Self::parse_trade_inner_instruction, + instruction_parser: Self::parse_buy_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::TRADE_EVENT, + instruction_discriminator: discriminators::SELL_IX, + event_type: EventType::PumpFunSell, + inner_instruction_parser: Self::parse_trade_inner_instruction, + instruction_parser: Self::parse_sell_instruction, + }, + ]; + + let inner = GenericEventParser::new(PUMPFUN_PROGRAM_ID, ProtocolType::PumpFun, configs); + + Self { inner } + } + + /// 解析创建代币日志事件 + fn parse_create_token_inner_instruction( + data: &[u8], + metadata: EventMetadata, + ) -> Option> { + if let Ok(event) = borsh::from_slice::(data) { + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, + event.name, + event.symbol, + event.mint.to_string() + )); + Some(Box::new(PumpFunCreateTokenEvent { + metadata: metadata, + ..event + })) + } else { + None + } + } + + /// 解析交易事件 + fn parse_trade_inner_instruction( + data: &[u8], + metadata: EventMetadata, + ) -> Option> { + if let Ok(event) = borsh::from_slice::(data) { + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, + event.mint.to_string(), + event.user.to_string(), + event.is_buy.to_string() + )); + Some(Box::new(PumpFunTradeEvent { + metadata: metadata, + ..event + })) + } else { + None + } + } + + /// 解析创建代币指令事件 + fn parse_create_token_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 11 { + return None; + } + let mut offset = 0; + let name_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; + offset += 4; + let name = String::from_utf8_lossy(&data[offset..offset + name_len]); + offset += name_len; + let symbol_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; + offset += 4; + let symbol = String::from_utf8_lossy(&data[offset..offset + symbol_len]); + offset += symbol_len; + let uri_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize; + offset += 4; + let uri = String::from_utf8_lossy(&data[offset..offset + uri_len]); + offset += uri_len; + let creator = if offset + 32 <= data.len() { + Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?) + } else { + Pubkey::default() + }; + + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, + name, + symbol, + accounts[0].to_string() + )); + + Some(Box::new(PumpFunCreateTokenEvent { + metadata, + name: name.to_string(), + symbol: symbol.to_string(), + uri: uri.to_string(), + creator, + mint: accounts[0], + mint_authority: accounts[1], + bonding_curve: accounts[2], + associated_bonding_curve: accounts[3], + user: accounts[7], + ..Default::default() + })) + } + + // 解析买入指令事件 + fn parse_buy_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 11 { + return None; + } + let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let max_sol_cost = u64::from_le_bytes(data[8..16].try_into().unwrap()); + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, + accounts[2].to_string(), + accounts[6].to_string(), + true.to_string() + )); + Some(Box::new(PumpFunTradeEvent { + metadata, + fee_recipient: accounts[1], + mint: accounts[2], + bonding_curve: accounts[3], + associated_bonding_curve: accounts[4], + associated_user: accounts[5], + user: accounts[6], + creator_vault: accounts[8], + max_sol_cost, + amount, + is_buy: true, + ..Default::default() + })) + } + + // 解析卖出指令事件 + fn parse_sell_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 11 { + return None; + } + let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let min_sol_output = u64::from_le_bytes(data[8..16].try_into().unwrap()); + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, + accounts[2].to_string(), + accounts[6].to_string(), + false.to_string() + )); + Some(Box::new(PumpFunTradeEvent { + metadata, + fee_recipient: accounts[1], + mint: accounts[2], + bonding_curve: accounts[3], + associated_bonding_curve: accounts[4], + associated_user: accounts[5], + user: accounts[6], + creator_vault: accounts[8], + min_sol_output, + amount, + is_buy: false, + ..Default::default() + })) + } +} + +#[async_trait::async_trait] +impl EventParser for PumpFunEventParser { + fn parse_events_from_inner_instruction( + &self, + inner_instruction: &UiCompiledInstruction, + signature: &str, + slot: u64, + ) -> Vec> { + self.inner + .parse_events_from_inner_instruction(inner_instruction, signature, slot) + } + + fn parse_events_from_instruction( + &self, + instruction: &CompiledInstruction, + accounts: &[Pubkey], + signature: &str, + slot: u64, + ) -> Vec> { + self.inner + .parse_events_from_instruction(instruction, accounts, signature, slot) + } + + fn should_handle(&self, program_id: &Pubkey) -> bool { + self.inner.should_handle(program_id) + } + + fn supported_program_ids(&self) -> Vec { + self.inner.supported_program_ids() + } +} diff --git a/src/event_parser/protocols/pumpswap/events.rs b/src/event_parser/protocols/pumpswap/events.rs new file mode 100644 index 0000000..eec19dc --- /dev/null +++ b/src/event_parser/protocols/pumpswap/events.rs @@ -0,0 +1,322 @@ +use borsh::BorshDeserialize; +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +use crate::event_parser::{common::EventMetadata, core::traits::UnifiedEvent}; +use crate::impl_unified_event; + +/// 买入事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct PumpSwapBuyEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub timestamp: i64, + pub base_amount_out: u64, + pub max_quote_amount_in: u64, + pub user_base_token_reserves: u64, + pub user_quote_token_reserves: u64, + pub pool_base_token_reserves: u64, + pub pool_quote_token_reserves: u64, + pub quote_amount_in: u64, + pub lp_fee_basis_points: u64, + pub lp_fee: u64, + pub protocol_fee_basis_points: u64, + pub protocol_fee: u64, + pub quote_amount_in_with_lp_fee: u64, + pub user_quote_amount_in: u64, + pub pool: Pubkey, + pub user: Pubkey, + pub user_base_token_account: Pubkey, + pub user_quote_token_account: Pubkey, + pub protocol_fee_recipient: Pubkey, + pub protocol_fee_recipient_token_account: Pubkey, + pub coin_creator: Pubkey, + pub coin_creator_fee_basis_points: u64, + pub coin_creator_fee: u64, + #[borsh(skip)] + pub base_mint: Pubkey, + #[borsh(skip)] + pub quote_mint: Pubkey, + #[borsh(skip)] + pub pool_base_token_account: Pubkey, + #[borsh(skip)] + pub pool_quote_token_account: Pubkey, + #[borsh(skip)] + pub coin_creator_vault_ata: Pubkey, + #[borsh(skip)] + pub coin_creator_vault_authority: Pubkey, +} + +// 使用宏生成UnifiedEvent实现,指定需要合并的字段 +impl_unified_event!( + PumpSwapBuyEvent, + timestamp, + base_amount_out, + max_quote_amount_in, + user_base_token_reserves, + user_quote_token_reserves, + pool_base_token_reserves, + pool_quote_token_reserves, + quote_amount_in, + lp_fee_basis_points, + lp_fee, + protocol_fee_basis_points, + protocol_fee, + quote_amount_in_with_lp_fee, + user_quote_amount_in, + pool, + user, + user_base_token_account, + user_quote_token_account, + protocol_fee_recipient, + protocol_fee_recipient_token_account, + coin_creator, + coin_creator_fee_basis_points, + coin_creator_fee +); + +/// 卖出事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct PumpSwapSellEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub timestamp: i64, + pub base_amount_in: u64, + pub min_quote_amount_out: u64, + pub user_base_token_reserves: u64, + pub user_quote_token_reserves: u64, + pub pool_base_token_reserves: u64, + pub pool_quote_token_reserves: u64, + pub quote_amount_out: u64, + pub lp_fee_basis_points: u64, + pub lp_fee: u64, + pub protocol_fee_basis_points: u64, + pub protocol_fee: u64, + pub quote_amount_out_without_lp_fee: u64, + pub user_quote_amount_out: u64, + pub pool: Pubkey, + pub user: Pubkey, + pub user_base_token_account: Pubkey, + pub user_quote_token_account: Pubkey, + pub protocol_fee_recipient: Pubkey, + pub protocol_fee_recipient_token_account: Pubkey, + pub coin_creator: Pubkey, + pub coin_creator_fee_basis_points: u64, + pub coin_creator_fee: u64, + #[borsh(skip)] + pub base_mint: Pubkey, + #[borsh(skip)] + pub quote_mint: Pubkey, + #[borsh(skip)] + pub pool_base_token_account: Pubkey, + #[borsh(skip)] + pub pool_quote_token_account: Pubkey, + #[borsh(skip)] + pub coin_creator_vault_ata: Pubkey, + #[borsh(skip)] + pub coin_creator_vault_authority: Pubkey, +} + +// 使用宏生成UnifiedEvent实现,指定需要合并的字段 +impl_unified_event!( + PumpSwapSellEvent, + timestamp, + base_amount_in, + min_quote_amount_out, + user_base_token_reserves, + user_quote_token_reserves, + pool_base_token_reserves, + pool_quote_token_reserves, + quote_amount_out, + lp_fee_basis_points, + lp_fee, + protocol_fee_basis_points, + protocol_fee, + quote_amount_out_without_lp_fee, + user_quote_amount_out, + pool, + user, + user_base_token_account, + user_quote_token_account, + protocol_fee_recipient, + protocol_fee_recipient_token_account, + coin_creator, + coin_creator_fee_basis_points, + coin_creator_fee +); + +/// 创建池子事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct PumpSwapCreatePoolEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub timestamp: i64, + pub index: u16, + pub creator: Pubkey, + pub base_mint: Pubkey, + pub quote_mint: Pubkey, + pub base_mint_decimals: u8, + pub quote_mint_decimals: u8, + pub base_amount_in: u64, + pub quote_amount_in: u64, + pub pool_base_amount: u64, + pub pool_quote_amount: u64, + pub minimum_liquidity: u64, + pub initial_liquidity: u64, + pub lp_token_amount_out: u64, + pub pool_bump: u8, + pub pool: Pubkey, + pub lp_mint: Pubkey, + pub user_base_token_account: Pubkey, + pub user_quote_token_account: Pubkey, + pub coin_creator: Pubkey, + #[borsh(skip)] + pub user_pool_token_account: Pubkey, + #[borsh(skip)] + pub pool_base_token_account: Pubkey, + #[borsh(skip)] + pub pool_quote_token_account: Pubkey, +} + +impl_unified_event!( + PumpSwapCreatePoolEvent, + timestamp, + index, + creator, + base_mint, + quote_mint, + base_mint_decimals, + quote_mint_decimals, + base_amount_in, + quote_amount_in, + pool_base_amount, + pool_quote_amount, + minimum_liquidity, + initial_liquidity, + lp_token_amount_out, + pool_bump, + pool, + lp_mint, + user_base_token_account, + user_quote_token_account, + coin_creator +); + +/// 存款事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct PumpSwapDepositEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub timestamp: i64, + pub lp_token_amount_out: u64, + pub max_base_amount_in: u64, + pub max_quote_amount_in: u64, + pub user_base_token_reserves: u64, + pub user_quote_token_reserves: u64, + pub pool_base_token_reserves: u64, + pub pool_quote_token_reserves: u64, + pub base_amount_in: u64, + pub quote_amount_in: u64, + pub lp_mint_supply: u64, + pub pool: Pubkey, + pub user: Pubkey, + pub user_base_token_account: Pubkey, + pub user_quote_token_account: Pubkey, + pub user_pool_token_account: Pubkey, + #[borsh(skip)] + pub base_mint: Pubkey, + #[borsh(skip)] + pub quote_mint: Pubkey, + #[borsh(skip)] + pub pool_base_token_account: Pubkey, + #[borsh(skip)] + pub pool_quote_token_account: Pubkey, +} + +impl_unified_event!( + PumpSwapDepositEvent, + timestamp, + lp_token_amount_out, + max_base_amount_in, + max_quote_amount_in, + user_base_token_reserves, + user_quote_token_reserves, + pool_base_token_reserves, + pool_quote_token_reserves, + base_amount_in, + quote_amount_in, + lp_mint_supply, + pool, + user, + user_base_token_account, + user_quote_token_account, + user_pool_token_account +); + +/// 提款事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct PumpSwapWithdrawEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub timestamp: i64, + pub lp_token_amount_in: u64, + pub min_base_amount_out: u64, + pub min_quote_amount_out: u64, + pub user_base_token_reserves: u64, + pub user_quote_token_reserves: u64, + pub pool_base_token_reserves: u64, + pub pool_quote_token_reserves: u64, + pub base_amount_out: u64, + pub quote_amount_out: u64, + pub lp_mint_supply: u64, + pub pool: Pubkey, + pub user: Pubkey, + pub user_base_token_account: Pubkey, + pub user_quote_token_account: Pubkey, + pub user_pool_token_account: Pubkey, + #[borsh(skip)] + pub base_mint: Pubkey, + #[borsh(skip)] + pub quote_mint: Pubkey, + #[borsh(skip)] + pub pool_base_token_account: Pubkey, + #[borsh(skip)] + pub pool_quote_token_account: Pubkey, +} + +impl_unified_event!( + PumpSwapWithdrawEvent, + timestamp, + lp_token_amount_in, + min_base_amount_out, + min_quote_amount_out, + user_base_token_reserves, + user_quote_token_reserves, + pool_base_token_reserves, + pool_quote_token_reserves, + base_amount_out, + quote_amount_out, + lp_mint_supply, + pool, + user, + user_base_token_account, + user_quote_token_account, + user_pool_token_account +); + +/// 事件鉴别器常量 +pub mod discriminators { + // 事件鉴别器 + pub const BUY_EVENT: &str = "0xe445a52e51cb9a1d67f4521f2cf57777"; + pub const SELL_EVENT: &str = "0xe445a52e51cb9a1d3e2f370aa503dc2a"; + pub const CREATE_POOL_EVENT: &str = "0xe445a52e51cb9a1db1310cd2a076a774"; + pub const DEPOSIT_EVENT: &str = "0xe445a52e51cb9a1d78f83d531f8e6b90"; + pub const WITHDRAW_EVENT: &str = "0xe445a52e51cb9a1d1609851aa02c47c0"; + + // 指令鉴别器 + pub const BUY_IX: &[u8] = &[102, 6, 61, 18, 1, 218, 235, 234]; + pub const SELL_IX: &[u8] = &[51, 230, 133, 164, 1, 127, 131, 173]; + pub const CREATE_POOL_IX: &[u8] = &[233, 146, 209, 142, 207, 104, 64, 188]; + pub const DEPOSIT_IX: &[u8] = &[242, 35, 198, 137, 82, 225, 242, 182]; + pub const WITHDRAW_IX: &[u8] = &[183, 18, 70, 156, 148, 109, 161, 34]; +} diff --git a/src/event_parser/protocols/pumpswap/mod.rs b/src/event_parser/protocols/pumpswap/mod.rs new file mode 100644 index 0000000..cfa440f --- /dev/null +++ b/src/event_parser/protocols/pumpswap/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; + +pub use events::*; +pub use parser::PumpSwapEventParser; \ No newline at end of file diff --git a/src/event_parser/protocols/pumpswap/parser.rs b/src/event_parser/protocols/pumpswap/parser.rs new file mode 100644 index 0000000..e81498f --- /dev/null +++ b/src/event_parser/protocols/pumpswap/parser.rs @@ -0,0 +1,386 @@ +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use solana_transaction_status::UiCompiledInstruction; + +use crate::event_parser::{ + common::{utils::*, EventMetadata, EventType, ProtocolType}, + core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent}, + protocols::pumpswap::{ + discriminators, PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent, + PumpSwapSellEvent, PumpSwapWithdrawEvent, + }, +}; + +/// PumpSwap程序ID +pub const PUMPSWAP_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"); + +/// PumpSwap事件解析器 +pub struct PumpSwapEventParser { + inner: GenericEventParser, +} + +impl PumpSwapEventParser { + pub fn new() -> Self { + // 配置所有事件类型 + let configs = vec![ + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::BUY_EVENT, + instruction_discriminator: discriminators::BUY_IX, + event_type: EventType::PumpSwapBuy, + inner_instruction_parser: Self::parse_buy_inner_instruction, + instruction_parser: Self::parse_buy_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::SELL_EVENT, + instruction_discriminator: discriminators::SELL_IX, + event_type: EventType::PumpSwapSell, + inner_instruction_parser: Self::parse_sell_inner_instruction, + instruction_parser: Self::parse_sell_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::CREATE_POOL_EVENT, + instruction_discriminator: discriminators::CREATE_POOL_IX, + event_type: EventType::PumpSwapCreatePool, + inner_instruction_parser: Self::parse_create_pool_inner_instruction, + instruction_parser: Self::parse_create_pool_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::DEPOSIT_EVENT, + instruction_discriminator: discriminators::DEPOSIT_IX, + event_type: EventType::PumpSwapDeposit, + inner_instruction_parser: Self::parse_deposit_inner_instruction, + instruction_parser: Self::parse_deposit_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::WITHDRAW_EVENT, + instruction_discriminator: discriminators::WITHDRAW_IX, + event_type: EventType::PumpSwapWithdraw, + inner_instruction_parser: Self::parse_withdraw_inner_instruction, + instruction_parser: Self::parse_withdraw_instruction, + }, + ]; + + let inner = GenericEventParser::new(PUMPSWAP_PROGRAM_ID, ProtocolType::PumpSwap, configs); + + Self { inner } + } + + /// 解析买入日志事件 + fn parse_buy_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option> { + if let Ok(event) = borsh::from_slice::(data) { + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, event.user, event.pool, event.base_amount_out + )); + Some(Box::new(PumpSwapBuyEvent { + metadata: metadata, + ..event + })) + } else { + None + } + } + + /// 解析卖出日志事件 + fn parse_sell_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option> { + if let Ok(event) = borsh::from_slice::(data) { + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, event.user, event.pool, event.base_amount_in + )); + Some(Box::new(PumpSwapSellEvent { + metadata: metadata, + ..event + })) + } else { + None + } + } + + /// 解析创建池子日志事件 + fn parse_create_pool_inner_instruction( + data: &[u8], + metadata: EventMetadata, + ) -> Option> { + if let Ok(event) = borsh::from_slice::(data) { + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, event.pool, event.creator, event.base_amount_in + )); + Some(Box::new(PumpSwapCreatePoolEvent { + metadata: metadata, + ..event + })) + } else { + None + } + } + + /// 解析存款日志事件 + fn parse_deposit_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option> { + if let Ok(event) = borsh::from_slice::(data) { + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, event.pool, event.user, event.lp_token_amount_out + )); + Some(Box::new(PumpSwapDepositEvent { + metadata: metadata, + ..event + })) + } else { + None + } + } + + /// 解析提款日志事件 + fn parse_withdraw_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option> { + if let Ok(event) = borsh::from_slice::(data) { + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, event.pool, event.user, event.lp_token_amount_in + )); + Some(Box::new(PumpSwapWithdrawEvent { + metadata: metadata, + ..event + })) + } else { + None + } + } + + /// 解析买入指令事件 + fn parse_buy_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 11 { + return None; + } + + let base_amount_out = read_u64_le(data, 0)?; + let max_quote_amount_in = read_u64_le(data, 8)?; + + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, accounts[1], accounts[0], base_amount_out + )); + + Some(Box::new(PumpSwapBuyEvent { + metadata, + base_amount_out, + max_quote_amount_in, + pool: accounts[0], + user: accounts[1], + base_mint: accounts[3], + quote_mint: accounts[4], + user_base_token_account: accounts[5], + user_quote_token_account: accounts[6], + pool_base_token_account: accounts[7], + pool_quote_token_account: accounts[8], + protocol_fee_recipient: accounts[9], + protocol_fee_recipient_token_account: accounts[10], + coin_creator_vault_ata: accounts.get(17).copied().unwrap_or_default(), + coin_creator_vault_authority: accounts.get(18).copied().unwrap_or_default(), + ..Default::default() + })) + } + + /// 解析卖出指令事件 + fn parse_sell_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 11 { + return None; + } + + let base_amount_in = read_u64_le(data, 0)?; + let min_quote_amount_out = read_u64_le(data, 8)?; + + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, accounts[1], accounts[0], base_amount_in + )); + + Some(Box::new(PumpSwapSellEvent { + metadata, + base_amount_in, + min_quote_amount_out, + pool: accounts[0], + user: accounts[1], + base_mint: accounts[3], + quote_mint: accounts[4], + user_base_token_account: accounts[5], + user_quote_token_account: accounts[6], + pool_base_token_account: accounts[7], + pool_quote_token_account: accounts[8], + protocol_fee_recipient: accounts[9], + protocol_fee_recipient_token_account: accounts[10], + coin_creator_vault_ata: accounts.get(17).copied().unwrap_or_default(), + coin_creator_vault_authority: accounts.get(18).copied().unwrap_or_default(), + ..Default::default() + })) + } + + /// 解析创建池子指令事件 + fn parse_create_pool_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 18 || accounts.len() < 11 { + return None; + } + + let index = u16::from_le_bytes(data[0..2].try_into().ok()?); + let base_amount_in = u64::from_le_bytes(data[2..10].try_into().ok()?); + let quote_amount_in = u64::from_le_bytes(data[10..18].try_into().ok()?); + let coin_creator = if data.len() >= 50 { + Pubkey::new_from_array(data[18..50].try_into().ok()?) + } else { + Pubkey::default() + }; + + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, accounts[0], accounts[2], base_amount_in + )); + + Some(Box::new(PumpSwapCreatePoolEvent { + metadata, + index, + base_amount_in, + quote_amount_in, + pool: accounts[0], + creator: accounts[2], + base_mint: accounts[3], + quote_mint: accounts[4], + lp_mint: accounts[5], + user_base_token_account: accounts[6], + user_quote_token_account: accounts[7], + user_pool_token_account: accounts[8], + pool_base_token_account: accounts[9], + pool_quote_token_account: accounts[10], + coin_creator, + ..Default::default() + })) + } + + /// 解析存款指令事件 + fn parse_deposit_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 24 || accounts.len() < 11 { + return None; + } + + let lp_token_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?); + let max_base_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?); + let max_quote_amount_in = u64::from_le_bytes(data[16..24].try_into().ok()?); + + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, accounts[0], accounts[2], lp_token_amount_out + )); + + Some(Box::new(PumpSwapDepositEvent { + metadata, + lp_token_amount_out, + max_base_amount_in, + max_quote_amount_in, + pool: accounts[0], + user: accounts[2], + base_mint: accounts[3], + quote_mint: accounts[4], + user_base_token_account: accounts[6], + user_quote_token_account: accounts[7], + user_pool_token_account: accounts[8], + pool_base_token_account: accounts[9], + pool_quote_token_account: accounts[10], + ..Default::default() + })) + } + + /// 解析提款指令事件 + fn parse_withdraw_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 24 || accounts.len() < 11 { + return None; + } + + let lp_token_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?); + let min_base_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?); + let min_quote_amount_out = u64::from_le_bytes(data[16..24].try_into().ok()?); + + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, accounts[0], accounts[2], lp_token_amount_in + )); + + Some(Box::new(PumpSwapWithdrawEvent { + metadata, + lp_token_amount_in, + min_base_amount_out, + min_quote_amount_out, + pool: accounts[0], + user: accounts[2], + base_mint: accounts[3], + quote_mint: accounts[4], + user_base_token_account: accounts[6], + user_quote_token_account: accounts[7], + user_pool_token_account: accounts[8], + pool_base_token_account: accounts[9], + pool_quote_token_account: accounts[10], + ..Default::default() + })) + } +} + +#[async_trait::async_trait] +impl EventParser for PumpSwapEventParser { + fn parse_events_from_inner_instruction( + &self, + inner_instruction: &UiCompiledInstruction, + signature: &str, + slot: u64, + ) -> Vec> { + self.inner + .parse_events_from_inner_instruction(inner_instruction, signature, slot) + } + + fn parse_events_from_instruction( + &self, + instruction: &CompiledInstruction, + accounts: &[Pubkey], + signature: &str, + slot: u64, + ) -> Vec> { + self.inner + .parse_events_from_instruction(instruction, accounts, signature, slot) + } + + fn should_handle(&self, program_id: &Pubkey) -> bool { + self.inner.should_handle(program_id) + } + + fn supported_program_ids(&self) -> Vec { + self.inner.supported_program_ids() + } +} diff --git a/src/event_parser/protocols/raydium_launchpad/events.rs b/src/event_parser/protocols/raydium_launchpad/events.rs new file mode 100644 index 0000000..ece3b04 --- /dev/null +++ b/src/event_parser/protocols/raydium_launchpad/events.rs @@ -0,0 +1,126 @@ +use crate::event_parser::protocols::raydium_launchpad::types::{ + CurveParams, MintParams, PoolStatus, TradeDirection, VestingParams, +}; +use crate::event_parser::{common::EventMetadata, core::traits::UnifiedEvent}; +use crate::impl_unified_event; +use borsh::BorshDeserialize; +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +/// 买入事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct RaydiumLaunchpadTradeEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub pool_state: Pubkey, + pub total_base_sell: u64, + pub virtual_base: u64, + pub virtual_quote: u64, + pub real_base_before: u64, + pub real_quote_before: u64, + pub real_base_after: u64, + pub real_quote_after: u64, + pub amount_in: u64, + pub amount_out: u64, + pub protocol_fee: u64, + pub platform_fee: u64, + pub share_fee: u64, + pub trade_direction: TradeDirection, + pub pool_status: PoolStatus, + #[borsh(skip)] + pub minimum_amount_out: u64, + #[borsh(skip)] + pub maximum_amount_in: u64, + #[borsh(skip)] + pub share_fee_rate: u64, + #[borsh(skip)] + pub payer: Pubkey, + #[borsh(skip)] + pub user_base_token: Pubkey, + #[borsh(skip)] + pub user_quote_token: Pubkey, + #[borsh(skip)] + pub base_vault: Pubkey, + #[borsh(skip)] + pub quote_vault: Pubkey, + #[borsh(skip)] + pub base_token_mint: Pubkey, + #[borsh(skip)] + pub quote_token_mint: Pubkey, + #[borsh(skip)] + pub is_dev_create_token_trade: bool, + #[borsh(skip)] + pub is_bot: bool, +} + +// 使用宏生成UnifiedEvent实现,指定需要合并的字段 +impl_unified_event!( + RaydiumLaunchpadTradeEvent, + pool_state, + total_base_sell, + virtual_base, + virtual_quote, + real_base_before, + real_quote_before, + real_base_after, + real_quote_after, + amount_in, + amount_out, + protocol_fee, + platform_fee, + share_fee, + trade_direction, + pool_status +); + +/// 创建池事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct RaydiumLaunchpadPoolCreateEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub pool_state: Pubkey, + pub creator: Pubkey, + pub config: Pubkey, + pub base_mint_param: MintParams, + pub curve_param: CurveParams, + pub vesting_param: VestingParams, + #[borsh(skip)] + pub payer: Pubkey, + #[borsh(skip)] + pub base_mint: Pubkey, + #[borsh(skip)] + pub quote_mint: Pubkey, + #[borsh(skip)] + pub base_vault: Pubkey, + #[borsh(skip)] + pub quote_vault: Pubkey, + #[borsh(skip)] + pub global_config: Pubkey, + #[borsh(skip)] + pub platform_config: Pubkey, +} + +// 使用宏生成UnifiedEvent实现,指定需要合并的字段 +impl_unified_event!( + RaydiumLaunchpadPoolCreateEvent, + pool_state, + creator, + config, + base_mint_param, + curve_param, + vesting_param +); + +/// 事件鉴别器常量 +pub mod discriminators { + // 事件鉴别器 + pub const TRADE_EVENT: &str = "0xe445a52e51cb9a1dbddb7fd34ee661ee"; + pub const POOL_CREATE_EVENT: &str = "0xe445a52e51cb9a1d97d7e20976a173ae"; + + // 指令鉴别器 + pub const BUY_EXACT_IN: &[u8] = &[250, 234, 13, 123, 213, 156, 19, 236]; + pub const BUY_EXACT_OUT: &[u8] = &[24, 211, 116, 40, 105, 3, 153, 56]; + pub const SELL_EXACT_IN: &[u8] = &[149, 39, 222, 155, 211, 124, 152, 26]; + pub const SELL_EXACT_OUT: &[u8] = &[95, 200, 71, 34, 8, 9, 11, 166]; + pub const INITIALIZE: &[u8] = &[175, 175, 109, 31, 13, 152, 155, 237]; +} diff --git a/src/event_parser/protocols/raydium_launchpad/mod.rs b/src/event_parser/protocols/raydium_launchpad/mod.rs new file mode 100644 index 0000000..8c412a6 --- /dev/null +++ b/src/event_parser/protocols/raydium_launchpad/mod.rs @@ -0,0 +1,7 @@ +pub mod events; +pub mod parser; +pub mod types; + +pub use events::*; +pub use parser::RaydiumLaunchpadEventParser; +pub use types::*; \ No newline at end of file diff --git a/src/event_parser/protocols/raydium_launchpad/parser.rs b/src/event_parser/protocols/raydium_launchpad/parser.rs new file mode 100644 index 0000000..521c6ab --- /dev/null +++ b/src/event_parser/protocols/raydium_launchpad/parser.rs @@ -0,0 +1,436 @@ +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use solana_transaction_status::UiCompiledInstruction; + +use crate::event_parser::{ + common::{utils::*, EventMetadata, EventType, ProtocolType}, + core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent}, + protocols::raydium_launchpad::{ + discriminators, ConstantCurve, CurveParams, FixedCurve, LinearCurve, MintParams, + RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent, TradeDirection, VestingParams, + }, +}; + +/// RaydiumLaunchpad程序ID +pub const RAYDIUM_LAUNCHPAD_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"); + +/// RaydiumLaunchpad事件解析器 +pub struct RaydiumLaunchpadEventParser { + inner: GenericEventParser, +} + +impl RaydiumLaunchpadEventParser { + pub fn new() -> Self { + // 配置所有事件类型 + let configs = vec![ + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::TRADE_EVENT, + instruction_discriminator: discriminators::BUY_EXACT_IN, + event_type: EventType::RaydiumLaunchpadBuyExactIn, + inner_instruction_parser: Self::parse_trade_inner_instruction, + instruction_parser: Self::parse_buy_exact_in_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::TRADE_EVENT, + instruction_discriminator: discriminators::BUY_EXACT_OUT, + event_type: EventType::RaydiumLaunchpadBuyExactOut, + inner_instruction_parser: Self::parse_trade_inner_instruction, + instruction_parser: Self::parse_buy_exact_out_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::TRADE_EVENT, + instruction_discriminator: discriminators::SELL_EXACT_IN, + event_type: EventType::RaydiumLaunchpadSellExactIn, + inner_instruction_parser: Self::parse_trade_inner_instruction, + instruction_parser: Self::parse_sell_exact_in_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::TRADE_EVENT, + instruction_discriminator: discriminators::SELL_EXACT_OUT, + event_type: EventType::RaydiumLaunchpadSellExactOut, + inner_instruction_parser: Self::parse_trade_inner_instruction, + instruction_parser: Self::parse_sell_exact_out_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: discriminators::POOL_CREATE_EVENT, + instruction_discriminator: discriminators::INITIALIZE, + event_type: EventType::RaydiumLaunchpadInitialize, + inner_instruction_parser: Self::parse_pool_create_inner_instruction, + instruction_parser: Self::parse_initialize_instruction, + }, + ]; + + let inner = GenericEventParser::new( + RAYDIUM_LAUNCHPAD_PROGRAM_ID, + ProtocolType::RaydiumLaunchpad, + configs, + ); + + Self { inner } + } + + /// 解析创建池事件 + fn parse_pool_create_inner_instruction( + data: &[u8], + metadata: EventMetadata, + ) -> Option> { + if let Ok(event) = borsh::from_slice::(data) { + let mut metadata = metadata; + metadata.set_id(format!("{}", metadata.signature,)); + Some(Box::new(RaydiumLaunchpadPoolCreateEvent { + metadata: metadata, + ..event + })) + } else { + None + } + } + + /// 解析交易事件 + fn parse_trade_inner_instruction( + data: &[u8], + metadata: EventMetadata, + ) -> Option> { + if let Ok(event) = borsh::from_slice::(data) { + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}", + metadata.signature, + event.pool_state.to_string() + )); + Some(Box::new(RaydiumLaunchpadTradeEvent { + metadata: metadata, + ..event + })) + } else { + None + } + } + + /// 解析买入指令事件 + fn parse_buy_exact_in_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 11 { + return None; + } + + let amount_in = read_u64_le(data, 0)?; + let minimum_amount_out = read_u64_le(data, 8)?; + let share_fee_rate = read_u64_le(data, 16)?; + + let mut metadata = metadata; + metadata.set_id(format!("{}-{}", metadata.signature, accounts[4])); + + Some(Box::new(RaydiumLaunchpadTradeEvent { + metadata, + amount_in, + minimum_amount_out, + share_fee_rate, + payer: accounts[0], + pool_state: accounts[4], + user_base_token: accounts[5], + user_quote_token: accounts[6], + base_vault: accounts[7], + quote_vault: accounts[8], + base_token_mint: accounts[9], + quote_token_mint: accounts[10], + trade_direction: TradeDirection::Buy, + ..Default::default() + })) + } + + fn parse_buy_exact_out_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 11 { + return None; + } + + let amount_out = read_u64_le(data, 0)?; + let maximum_amount_in = read_u64_le(data, 8)?; + let share_fee_rate = read_u64_le(data, 16)?; + + let mut metadata = metadata; + metadata.set_id(format!("{}-{}", metadata.signature, accounts[4])); + + Some(Box::new(RaydiumLaunchpadTradeEvent { + metadata, + amount_out, + maximum_amount_in, + share_fee_rate, + payer: accounts[0], + pool_state: accounts[4], + user_base_token: accounts[5], + user_quote_token: accounts[6], + base_vault: accounts[7], + quote_vault: accounts[8], + base_token_mint: accounts[9], + quote_token_mint: accounts[10], + trade_direction: TradeDirection::Buy, + ..Default::default() + })) + } + + fn parse_sell_exact_in_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 11 { + return None; + } + + let amount_in = read_u64_le(data, 0)?; + let minimum_amount_out = read_u64_le(data, 8)?; + let share_fee_rate = read_u64_le(data, 16)?; + + let mut metadata = metadata; + metadata.set_id(format!("{}-{}", metadata.signature, accounts[4])); + + Some(Box::new(RaydiumLaunchpadTradeEvent { + metadata, + amount_in, + minimum_amount_out, + share_fee_rate, + payer: accounts[0], + pool_state: accounts[4], + user_base_token: accounts[5], + user_quote_token: accounts[6], + base_vault: accounts[7], + quote_vault: accounts[8], + base_token_mint: accounts[9], + quote_token_mint: accounts[10], + trade_direction: TradeDirection::Sell, + ..Default::default() + })) + } + + fn parse_sell_exact_out_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 11 { + return None; + } + + let amount_out = read_u64_le(data, 0)?; + let maximum_amount_in = read_u64_le(data, 8)?; + let share_fee_rate = read_u64_le(data, 16)?; + + let mut metadata = metadata; + metadata.set_id(format!("{}-{}", metadata.signature, accounts[4])); + + Some(Box::new(RaydiumLaunchpadTradeEvent { + metadata, + amount_out, + maximum_amount_in, + share_fee_rate, + payer: accounts[0], + pool_state: accounts[4], + user_base_token: accounts[5], + user_quote_token: accounts[6], + base_vault: accounts[7], + quote_vault: accounts[8], + base_token_mint: accounts[9], + quote_token_mint: accounts[10], + trade_direction: TradeDirection::Sell, + ..Default::default() + })) + } + + /// 解析初始化事件 + fn parse_initialize_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 24 { + return None; + } + + let mut offset = 0; + let base_mint_param = Self::parse_mint_params(data, &mut offset)?; + let curve_param = Self::parse_curve_params(data, &mut offset)?; + let vesting_param = Self::parse_vesting_params(data, &mut offset)?; + + let mut metadata = metadata; + metadata.set_id(format!("{}", metadata.signature)); + + Some(Box::new(RaydiumLaunchpadPoolCreateEvent { + metadata, + payer: accounts[0], + creator: accounts[1], + global_config: accounts[2], + platform_config: accounts[3], + pool_state: accounts[5], + base_mint: accounts[6], + quote_mint: accounts[7], + base_vault: accounts[8], + quote_vault: accounts[9], + base_mint_param, + curve_param, + vesting_param, + ..Default::default() + })) + } + + /// 解析 MintParams 结构 + fn parse_mint_params(data: &[u8], offset: &mut usize) -> Option { + // 读取decimals (1字节) + let decimals = read_u8(data, *offset)?; + *offset += 1; + + // 读取name字符串长度和内容 + let name_len = read_u32_le(data, *offset)? as usize; + *offset += 4; + if data.len() < *offset + name_len { + return None; + } + let name = String::from_utf8(data[*offset..*offset + name_len].to_vec()).ok()?; + *offset += name_len; + + // 读取symbol字符串长度和内容 + let symbol_len = read_u32_le(data, *offset)? as usize; + *offset += 4; + if data.len() < *offset + symbol_len { + return None; + } + let symbol = String::from_utf8(data[*offset..*offset + symbol_len].to_vec()).ok()?; + *offset += symbol_len; + + // 读取uri字符串长度和内容 + let uri_len = read_u32_le(data, *offset)? as usize; + *offset += 4; + if data.len() < *offset + uri_len { + return None; + } + let uri = String::from_utf8(data[*offset..*offset + uri_len].to_vec()).ok()?; + *offset += uri_len; + + Some(MintParams { + decimals, + name, + symbol, + uri, + }) + } + + /// 解析 CurveParams 结构 + fn parse_curve_params(data: &[u8], offset: &mut usize) -> Option { + // 读取curve类型标识符 (1字节) + let curve_type = read_u8(data, *offset)?; + *offset += 1; + + match curve_type { + 0 => { + // Constant curve + let supply = read_u64_le(data, *offset)?; + *offset += 8; + let total_base_sell = read_u64_le(data, *offset)?; + *offset += 8; + let total_quote_fund_raising = read_u64_le(data, *offset)?; + *offset += 8; + let migrate_type = read_u8(data, *offset)?; + *offset += 1; + + Some(CurveParams::Constant { + data: ConstantCurve { + supply, + total_base_sell, + total_quote_fund_raising, + migrate_type, + }, + }) + } + 1 => { + // Fixed curve + let supply = read_u64_le(data, *offset)?; + *offset += 8; + let total_quote_fund_raising = read_u64_le(data, *offset)?; + *offset += 8; + let migrate_type = read_u8(data, *offset)?; + *offset += 1; + + Some(CurveParams::Fixed { + data: FixedCurve { + supply, + total_quote_fund_raising, + migrate_type, + }, + }) + } + 2 => { + // Linear curve + let supply = read_u64_le(data, *offset)?; + *offset += 8; + let total_quote_fund_raising = read_u64_le(data, *offset)?; + *offset += 8; + let migrate_type = read_u8(data, *offset)?; + *offset += 1; + + Some(CurveParams::Linear { + data: LinearCurve { + supply, + total_quote_fund_raising, + migrate_type, + }, + }) + } + _ => None, + } + } + + /// 解析 VestingParams 结构 + fn parse_vesting_params(data: &[u8], offset: &mut usize) -> Option { + let total_locked_amount = read_u64_le(data, *offset)?; + *offset += 8; + let cliff_period = read_u64_le(data, *offset)?; + *offset += 8; + let unlock_period = read_u64_le(data, *offset)?; + *offset += 8; + + Some(VestingParams { + total_locked_amount, + cliff_period, + unlock_period, + }) + } +} + +#[async_trait::async_trait] +impl EventParser for RaydiumLaunchpadEventParser { + fn parse_events_from_inner_instruction( + &self, + inner_instruction: &UiCompiledInstruction, + signature: &str, + slot: u64, + ) -> Vec> { + self.inner + .parse_events_from_inner_instruction(inner_instruction, signature, slot) + } + + fn parse_events_from_instruction( + &self, + instruction: &CompiledInstruction, + accounts: &[Pubkey], + signature: &str, + slot: u64, + ) -> Vec> { + self.inner + .parse_events_from_instruction(instruction, accounts, signature, slot) + } + + fn should_handle(&self, program_id: &Pubkey) -> bool { + self.inner.should_handle(program_id) + } + + fn supported_program_ids(&self) -> Vec { + self.inner.supported_program_ids() + } +} diff --git a/src/event_parser/protocols/raydium_launchpad/types.rs b/src/event_parser/protocols/raydium_launchpad/types.rs new file mode 100644 index 0000000..5751f8c --- /dev/null +++ b/src/event_parser/protocols/raydium_launchpad/types.rs @@ -0,0 +1,69 @@ +use borsh::BorshDeserialize; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub enum TradeDirection { + #[default] + Buy, + Sell, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub enum PoolStatus { + #[default] + Fund, + Migrate, + Trade, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct MintParams { + pub decimals: u8, + pub name: String, + pub symbol: String, + pub uri: String, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct VestingParams { + pub total_locked_amount: u64, + pub cliff_period: u64, + pub unlock_period: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct ConstantCurve { + pub supply: u64, + pub total_base_sell: u64, + pub total_quote_fund_raising: u64, + pub migrate_type: u8, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct FixedCurve { + pub supply: u64, + pub total_quote_fund_raising: u64, + pub migrate_type: u8, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct LinearCurve { + pub supply: u64, + pub total_quote_fund_raising: u64, + pub migrate_type: u8, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub enum CurveParams { + Constant { data: ConstantCurve }, + Fixed { data: FixedCurve }, + Linear { data: LinearCurve }, +} + +impl Default for CurveParams { + fn default() -> Self { + Self::Constant { + data: ConstantCurve::default(), + } + } +} diff --git a/src/grpc/shred_stream.rs b/src/grpc/shred_stream.rs index 750cdae..69e3321 100755 --- a/src/grpc/shred_stream.rs +++ b/src/grpc/shred_stream.rs @@ -7,19 +7,16 @@ use tonic::transport::Channel; use log::error; use solana_sdk::transaction::VersionedTransaction; -use crate::common::pumpswap::PumpSwapInstruction; -use crate::common::raydium::{RaydiumEvent, RaydiumInstruction}; use crate::common::AnyResult; +use crate::event_parser::protocols::pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent}; +use crate::event_parser::protocols::raydium_launchpad::{ + RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent, +}; +use crate::event_parser::{EventParserFactory, Protocol, UnifiedEvent}; -use solana_sdk::pubkey::Pubkey; -use crate::common::pumpfun::logs_data::DexInstruction; -use crate::common::pumpfun::logs_events::PumpfunEvent; -use crate::common::pumpswap::logs_events::PumpSwapEvent; -use crate::common::pumpfun::logs_filters::LogFilter; -use crate::common::pumpswap::logs_filters::LogFilter as PumpswapLogFilter; -use crate::common::raydium::logs_filters::LogFilter as RaydiumLogFilter; use crate::protos::shredstream::shredstream_proxy_client::ShredstreamProxyClient; use crate::protos::shredstream::SubscribeEntriesRequest; +use solana_sdk::pubkey::Pubkey; const CHANNEL_SIZE: usize = 1000; @@ -32,18 +29,22 @@ struct TransactionWithSlot { slot: u64, } - impl ShredStreamGrpc { pub async fn new(endpoint: String) -> AnyResult { let shredstream_client = ShredstreamProxyClient::connect(endpoint.clone()).await?; - Ok(Self { - shredstream_client: Arc::new(shredstream_client) + Ok(Self { + shredstream_client: Arc::new(shredstream_client), }) } - pub async fn shredstream_subscribe(&self, callback: F, bot_wallet: Option) -> AnyResult<()> + pub async fn shredstream_subscribe( + &self, + protocols: Vec, + bot_wallet: Option, + callback: F, + ) -> AnyResult<()> where - F: Fn(PumpfunEvent) + Send + Sync + 'static, + F: Fn(Box) + Send + Sync + 'static, { let request = tonic::Request::new(SubscribeEntriesRequest {}); let mut client = (*self.shredstream_client).clone(); @@ -74,215 +75,50 @@ impl ShredStreamGrpc { }); while let Some(transaction_with_slot) = rx.next().await { - if let Err(e) = Self::process_pumpfun_transaction(transaction_with_slot, &*callback, bot_wallet).await { + if let Err(e) = Self::process_transaction( + transaction_with_slot, + protocols.clone(), + bot_wallet, + &*callback, + ) + .await + { error!("Error processing transaction: {:?}", e); } } - + Ok(()) } - pub async fn shredstream_subscribe_pumpswap(&self, callback: F) -> AnyResult<()> + async fn process_transaction( + transaction_with_slot: TransactionWithSlot, + protocols: Vec, + bot_wallet: Option, + callback: &F, + ) -> AnyResult<()> where - F: Fn(PumpSwapEvent) + Send + Sync + 'static, - { - let request = tonic::Request::new(SubscribeEntriesRequest {}); - let mut client = (*self.shredstream_client).clone(); - let mut stream = client.subscribe_entries(request).await?.into_inner(); - let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); - let callback = Box::new(callback); - tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Ok(entries) = bincode::deserialize::>(&msg.entries) { - for entry in entries { - for transaction in entry.transactions { - let _ = tx.try_send(TransactionWithSlot { - transaction: transaction.clone(), - slot: msg.slot, - }); - } - } - } - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } - } - } - }); - - while let Some(transaction_with_slot) = rx.next().await { - if let Err(e) = Self::process_pumpswap_transaction(transaction_with_slot, &*callback).await { - error!("Error processing transaction: {:?}", e); - } - } - - Ok(()) - } - - pub async fn shredstream_subscribe_raydium(&self, callback: F) -> AnyResult<()> - where - F: Fn(RaydiumEvent) + Send + Sync + 'static, - { - let request = tonic::Request::new(SubscribeEntriesRequest {}); - let mut client = (*self.shredstream_client).clone(); - let mut stream = client.subscribe_entries(request).await?.into_inner(); - let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); - let callback = Box::new(callback); - tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Ok(entries) = bincode::deserialize::>(&msg.entries) { - for entry in entries { - for transaction in entry.transactions { - let _ = tx.try_send(TransactionWithSlot { - transaction: transaction.clone(), - slot: msg.slot, - }); - } - } - } - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } - } - } - }); - - while let Some(transaction_with_slot) = rx.next().await { - if let Err(e) = Self::process_raydium_transaction(transaction_with_slot, &*callback).await { - error!("Error processing transaction: {:?}", e); - } - } - - Ok(()) - } - - async fn process_pumpfun_transaction(transaction_with_slot: TransactionWithSlot, callback: &F, bot_wallet: Option) -> AnyResult<()> - where - F: Fn(PumpfunEvent) + Send + Sync, + F: Fn(Box) + Send + Sync, { let slot = transaction_with_slot.slot; let versioned_tx = transaction_with_slot.transaction; - let mut dev_address: Option = None; - // let hash = versioned_tx.signatures[0].to_string(); - let instructions = LogFilter::parse_compiled_instruction(versioned_tx, bot_wallet).unwrap(); - for instruction in instructions { - // println!("hash: {}\ninstruction: {:?}\n\n", hash, instruction); - match instruction { - DexInstruction::CreateToken(mut token_info) => { - token_info.slot = slot; - dev_address = Some(token_info.user); - callback(PumpfunEvent::NewToken(token_info)); - } - DexInstruction::UserTrade(mut trade_info) => { - trade_info.slot = slot; - if Some(trade_info.user) == dev_address { - callback(PumpfunEvent::NewDevTrade(trade_info)); - } else { - callback(PumpfunEvent::NewUserTrade(trade_info)); - } - } - DexInstruction::BotTrade(mut trade_info) => { - trade_info.slot = slot; - callback(PumpfunEvent::NewBotTrade(trade_info)); - } - _ => {} - } - } - Ok(()) - } + let signature = versioned_tx.signatures[0]; - async fn process_pumpswap_transaction(transaction_with_slot: TransactionWithSlot, callback: &F) -> AnyResult<()> - where - F: Fn(PumpSwapEvent) + Send + Sync, - { - let slot = transaction_with_slot.slot; - let versioned_tx = transaction_with_slot.transaction; - let signature = versioned_tx.signatures[0].to_string(); - let instructions = PumpswapLogFilter::parse_pumpswap_compiled_instruction(versioned_tx).unwrap(); - for instruction in instructions { - match instruction { - PumpSwapInstruction::CreatePool(mut create_event) => { - create_event.slot = slot; - create_event.signature = signature.clone(); - callback(PumpSwapEvent::CreatePool(create_event)); - } - PumpSwapInstruction::Deposit(mut deposit_event) => { - deposit_event.slot = slot; - deposit_event.signature = signature.clone(); - callback(PumpSwapEvent::Deposit(deposit_event)); - } - PumpSwapInstruction::Withdraw(mut withdraw_event) => { - withdraw_event.slot = slot; - withdraw_event.signature = signature.clone(); - callback(PumpSwapEvent::Withdraw(withdraw_event)); - } - PumpSwapInstruction::Buy(mut buy_event) => { - buy_event.slot = slot; - buy_event.signature = signature.clone(); - callback(PumpSwapEvent::Buy(buy_event)); - } - PumpSwapInstruction::Sell(mut sell_event) => { - sell_event.slot = slot; - sell_event.signature = signature.clone(); - callback(PumpSwapEvent::Sell(sell_event)); - } - PumpSwapInstruction::UpdateFeeConfig(mut update_fee_event) => { - update_fee_event.slot = slot; - update_fee_event.signature = signature.clone(); - callback(PumpSwapEvent::UpdateFeeConfig(update_fee_event)); - } - PumpSwapInstruction::UpdateAdmin(mut update_admin_event) => { - update_admin_event.slot = slot; - update_admin_event.signature = signature.clone(); - callback(PumpSwapEvent::UpdateAdmin(update_admin_event)); - } - PumpSwapInstruction::Disable(mut disable_event) => { - disable_event.slot = slot; - disable_event.signature = signature.clone(); - callback(PumpSwapEvent::Disable(disable_event)); - } - _ => {} + for protocol in protocols { + let parser = EventParserFactory::create_parser(protocol.clone()); + let events = parser + .parse_versioned_transaction( + &versioned_tx, + &signature.to_string(), + Some(slot), + bot_wallet.clone(), + ) + .await + .unwrap_or_else(|_e| vec![]); + for event in events { + callback(event); } } - Ok(()) - } - async fn process_raydium_transaction(transaction_with_slot: TransactionWithSlot, callback: &F) -> AnyResult<()> - where - F: Fn(RaydiumEvent) + Send + Sync, - { - let slot = transaction_with_slot.slot; - let versioned_tx = transaction_with_slot.transaction; - let signature = versioned_tx.signatures[0].to_string(); - let instructions: Vec = RaydiumLogFilter::parse_raydium_compiled_instruction(versioned_tx).unwrap(); - for instruction in instructions { - match instruction { - RaydiumInstruction::V4Swap(mut v4_swap_event) => { - v4_swap_event.slot = slot; - v4_swap_event.signature = signature.clone(); - callback(RaydiumEvent::V4Swap(v4_swap_event)); - } - RaydiumInstruction::SwapBaseInput(mut swap_base_input_event) => { - swap_base_input_event.slot = slot; - swap_base_input_event.signature = signature.clone(); - callback(RaydiumEvent::SwapBaseInput(swap_base_input_event)); - } - RaydiumInstruction::SwapBaseOutput(mut swap_base_output_event) => { - swap_base_output_event.slot = slot; - swap_base_output_event.signature = signature.clone(); - callback(RaydiumEvent::SwapBaseOutput(swap_base_output_event)); - } - _ => {} - } - } Ok(()) } } diff --git a/src/grpc/yellow_stone.rs b/src/grpc/yellow_stone.rs index f565d18..e049b89 100755 --- a/src/grpc/yellow_stone.rs +++ b/src/grpc/yellow_stone.rs @@ -1,24 +1,31 @@ use std::{collections::HashMap, fmt, time::Duration}; -use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt}; -use rustls::crypto::{ring::default_provider, CryptoProvider}; -use tonic::{transport::channel::ClientTlsConfig, Status}; -use yellowstone_grpc_client::{GeyserGrpcClient, Interceptor}; -use yellowstone_grpc_proto::geyser::{ - CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate, - SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing, -}; -use log::{error, info}; use chrono::Local; +use futures::{channel::mpsc, sink::Sink, SinkExt, Stream, StreamExt}; +use log::{error, info}; +use rustls::crypto::{ring::default_provider, CryptoProvider}; use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature}; use solana_transaction_status::{ option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding, }; +use tonic::{transport::channel::ClientTlsConfig, Status}; +use yellowstone_grpc_client::{GeyserGrpcClient, Interceptor}; +use yellowstone_grpc_proto::geyser::{ + subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest, + SubscribeRequestFilterTransactions, SubscribeRequestPing, SubscribeUpdate, + SubscribeUpdateTransaction, +}; -use crate::common::pumpfun::logs_data::{DexInstruction, TransferInfo}; -use crate::common::pumpfun::logs_events::{PumpfunEvent, SystemEvent}; -use crate::common::pumpfun::logs_filters::LogFilter; +// use crate::common::pumpfun::logs_data::{DexInstruction, TransferInfo}; +// use crate::common::pumpfun::logs_events::{PumpfunEvent, SystemEvent}; +// use crate::common::pumpfun::logs_filters::LogFilter; use crate::common::AnyResult; +use crate::constants::pumpfun::trade; +use crate::event_parser::protocols::pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent}; +use crate::event_parser::protocols::raydium_launchpad::{ + RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent, +}; +use crate::event_parser::{EventParserFactory, Protocol, UnifiedEvent}; type TransactionsFilterMap = HashMap; @@ -84,16 +91,10 @@ impl YellowstoneGrpc { .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?; } - Ok(Self { - endpoint, - x_token, - }) + Ok(Self { endpoint, x_token }) } - pub async fn connect( - &self, - ) -> AnyResult> - { + pub async fn connect(&self) -> AnyResult> { let builder = GeyserGrpcClient::build_from_shared(self.endpoint.clone())? .x_token(self.x_token.clone())? .tls_config(ClientTlsConfig::new().with_native_roots())? @@ -118,7 +119,9 @@ impl YellowstoneGrpc { }; let mut client = self.connect().await?; - let (sink, stream) = client.subscribe_with_request(Some(subscribe_request)).await?; + let (sink, stream) = client + .subscribe_with_request(Some(subscribe_request)) + .await?; Ok((sink, stream)) } @@ -170,434 +173,37 @@ impl YellowstoneGrpc { Ok(()) } - pub async fn subscribe_pumpfun(&self, callback: F, bot_wallet: Option) -> AnyResult<()> - where - F: Fn(PumpfunEvent) + Send + Sync + 'static, - { - let addrs = vec![PUMP_PROGRAM_ID.to_string()]; - let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]); - let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?; - let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); - - let callback = Box::new(callback); - - tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { - error!("Error handling message: {:?}", e); - break; - } - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } - } - } - }); - - while let Some(transaction_pretty) = rx.next().await { - if let Err(e) = Self::process_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { - error!("Error processing transaction: {:?}", e); - } - } - Ok(()) - } - - pub async fn subscribe_pumpfun_with_filter(&self, callback: F, bot_wallet: Option, account_include: Option>, account_exclude: Option>) -> AnyResult<()> - where - F: Fn(PumpfunEvent) + Send + Sync + 'static, - { - let addrs = vec![PUMP_PROGRAM_ID.to_string()]; - let account_include = account_include.unwrap_or_default(); - let account_exclude = account_exclude.unwrap_or_default(); - let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs); - let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?; - let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); - - let callback = Box::new(callback); - - tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { - error!("Error handling message: {:?}", e); - break; - } - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } - } - } - }); - - while let Some(transaction_pretty) = rx.next().await { - if let Err(e) = Self::process_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { - error!("Error processing transaction: {:?}", e); - } - } - Ok(()) - } - - async fn process_pumpfun_transaction(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option) -> AnyResult<()> - where - F: Fn(PumpfunEvent) + Send + Sync, - { - let slot = transaction_pretty.slot; - let trade_raw: EncodedTransactionWithStatusMeta = transaction_pretty.tx; - let meta = trade_raw.meta.as_ref() - .ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?; - - if meta.err.is_some() { - return Ok(()); - } - - let logs = if let OptionSerializer::Some(logs) = &meta.log_messages { - logs - } else { - &vec![] - }; - - let mut dev_address: Option = None; - let instructions = LogFilter::parse_instruction(logs, bot_wallet).unwrap(); - for instruction in instructions { - match instruction { - DexInstruction::CreateToken(mut token_info) => { - token_info.slot = slot; - dev_address = Some(token_info.user); - callback(PumpfunEvent::NewToken(token_info)); - } - DexInstruction::UserTrade(mut trade_info) => { - trade_info.slot = slot; - if Some(trade_info.user) == dev_address { - callback(PumpfunEvent::NewDevTrade(trade_info)); - } else { - callback(PumpfunEvent::NewUserTrade(trade_info)); - } - } - DexInstruction::BotTrade(mut trade_info) => { - trade_info.slot = slot; - callback(PumpfunEvent::NewBotTrade(trade_info)); - } - _ => {} - } - } - - Ok(()) - } - - pub async fn subscribe_system(&self, callback: F, account_include: Option>, account_exclude: Option>) -> AnyResult<()> - where - F: Fn(SystemEvent) + Send + Sync + 'static, - { - let addrs = vec![SYSTEM_PROGRAM_ID.to_string()]; - let account_include = account_include.unwrap_or_default(); - let account_exclude = account_exclude.unwrap_or_default(); - let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs); - let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?; - let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); - - let callback = Box::new(callback); - - tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { - error!("Error handling message: {:?}", e); - break; - } - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } - } - } - }); - - while let Some(transaction_pretty) = rx.next().await { - if let Err(e) = Self::process_system_transaction(transaction_pretty, &*callback).await { - error!("Error processing transaction: {:?}", e); - } - } - Ok(()) - } - - async fn process_system_transaction(transaction_pretty: TransactionPretty, callback: &F) -> AnyResult<()> - where - F: Fn(SystemEvent) + Send + Sync, - { - let trade_raw: EncodedTransactionWithStatusMeta = transaction_pretty.tx; - let meta = trade_raw.meta.as_ref() - .ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?; - - if meta.err.is_some() { - return Ok(()); - } - - callback(SystemEvent::NewTransfer(TransferInfo { - slot: transaction_pretty.slot, - signature: transaction_pretty.signature.to_string(), - tx: trade_raw.transaction.decode(), - })); - - Ok(()) - } - - - // ------------------------------------------------------------ - // PumpSwap - // ------------------------------------------------------------ - - /// 订阅PumpSwap事件 - pub async fn subscribe_pumpswap(&self, callback: F) -> AnyResult<()> - where - F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + Send + Sync + 'static, - { - // 使用constants中定义的AMM_PROGRAM - let pump_program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM; - let addrs = vec![pump_program_id.to_string()]; - - // 创建过滤器 - let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]); - - // 订阅事件 - let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?; - - // 创建通道 - let (mut tx, mut rx) = mpsc::channel::(1000); - - // 创建回调函数 - let callback = Box::new(callback); - - // 启动处理流的任务 - tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { - error!("Error handling message: {:?}", e); - break; - } - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } - } - } - }); - - // 处理交易 - while let Some(transaction_pretty) = rx.next().await { - if let Err(e) = Self::process_pumpswap_transaction(transaction_pretty, &*callback).await { - error!("Error processing transaction: {:?}", e); - } - } - - Ok(()) - } - - /// 使用过滤器订阅PumpSwap事件 - pub async fn subscribe_pumpswap_with_filter( + /// 订阅事件 + pub async fn subscribe_events( &self, - callback: F, + protocols: Vec, + bot_wallet: Option, account_include: Option>, - account_exclude: Option> - ) -> AnyResult<()> - where - F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + Send + Sync + 'static, - { - // 使用constants中定义的AMM_PROGRAM - let pump_program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM; - let addrs = vec![pump_program_id.to_string()]; - - // 创建过滤器 - let account_include = account_include.unwrap_or_default(); - let account_exclude = account_exclude.unwrap_or_default(); - let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs); - - // 订阅事件 - let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?; - - // 创建通道 - let (mut tx, mut rx) = mpsc::channel::(1000); - - // 创建回调函数 - let callback = Box::new(callback); - - // 启动处理流的任务 - tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { - error!("Error handling message: {:?}", e); - break; - } - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } - } - } - }); - - // 处理交易 - while let Some(transaction_pretty) = rx.next().await { - if let Err(e) = Self::process_pumpswap_transaction(transaction_pretty, &*callback).await { - error!("Error processing transaction: {:?}", e); - } - } - - Ok(()) - } - - /// 处理PumpSwap交易 - async fn process_pumpswap_transaction( - transaction_pretty: TransactionPretty, - callback: &F + account_exclude: Option>, + callback: F, ) -> AnyResult<()> where - F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + Send + Sync, + F: Fn(Box) + Send + Sync + 'static, { - let slot = transaction_pretty.slot; - let trade_raw: solana_transaction_status::EncodedTransactionWithStatusMeta = transaction_pretty.tx; - - // 检查交易元数据 - let meta = trade_raw.meta.as_ref() - .ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?; - - // 检查交易是否成功 - if meta.err.is_some() { - return Ok(()); - } - - let mut buy_instruction_events = vec![]; - let mut sell_instruction_events = vec![]; - - if let Some(versioned_tx) = trade_raw.transaction.decode() { - let signature = versioned_tx.signatures[0].to_string(); - let instructions: Vec = - crate::common::pumpswap::logs_filters::LogFilter::parse_pumpswap_compiled_instruction(versioned_tx).unwrap(); - for instruction in instructions { - match instruction { - crate::common::pumpswap::logs_data::PumpSwapInstruction::Buy(mut e) => { - e.signature = signature.clone(); - e.slot = slot; - buy_instruction_events.push(e); - } - crate::common::pumpswap::logs_data::PumpSwapInstruction::Sell(mut e) => { - e.signature = signature.clone(); - e.slot = slot; - sell_instruction_events.push(e); - } - _ => {} - } - } - } - - // 获取日志 - let logs = if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) = &meta.log_messages { - logs - } else { - &vec![] - }; - - // 解析PumpSwap事件 - let events = crate::common::pumpswap::logs_filters::LogFilter::parse_pumpswap_logs(logs); - - // 处理事件 - for mut event in events { - // 设置签名和slot - match &mut event { - crate::common::pumpswap::logs_events::PumpSwapEvent::Buy(e) => { - e.signature = transaction_pretty.signature.to_string(); - e.slot = slot; - if let Some(ie) = buy_instruction_events.iter().find(|ie| ie.signature == e.signature && ie.slot == e.slot && ie.pool == e.pool && ie.user == e.user) { - e.base_mint = ie.base_mint; - e.quote_mint = ie.quote_mint; - e.pool_base_token_account = ie.pool_base_token_account; - e.pool_quote_token_account = ie.pool_quote_token_account; - e.coin_creator_vault_ata = ie.coin_creator_vault_ata; - e.coin_creator_vault_authority = ie.coin_creator_vault_authority; - } - }, - crate::common::pumpswap::logs_events::PumpSwapEvent::Sell(e) => { - e.signature = transaction_pretty.signature.to_string(); - e.slot = slot; - if let Some(ie) = sell_instruction_events.iter().find(|ie| ie.signature == e.signature && ie.slot == e.slot && ie.pool == e.pool && ie.user == e.user) { - e.base_mint = ie.base_mint; - e.quote_mint = ie.quote_mint; - e.pool_base_token_account = ie.pool_base_token_account; - e.pool_quote_token_account = ie.pool_quote_token_account; - e.coin_creator_vault_ata = ie.coin_creator_vault_ata; - e.coin_creator_vault_authority = ie.coin_creator_vault_authority; - } - }, - crate::common::pumpswap::logs_events::PumpSwapEvent::CreatePool(e) => { - e.signature = transaction_pretty.signature.to_string(); - e.slot = slot; - }, - crate::common::pumpswap::logs_events::PumpSwapEvent::Deposit(e) => { - e.signature = transaction_pretty.signature.to_string(); - e.slot = slot; - }, - crate::common::pumpswap::logs_events::PumpSwapEvent::Withdraw(e) => { - e.signature = transaction_pretty.signature.to_string(); - e.slot = slot; - }, - crate::common::pumpswap::logs_events::PumpSwapEvent::Disable(e) => { - e.signature = transaction_pretty.signature.to_string(); - e.slot = slot; - }, - crate::common::pumpswap::logs_events::PumpSwapEvent::UpdateAdmin(e) => { - e.signature = transaction_pretty.signature.to_string(); - e.slot = slot; - }, - crate::common::pumpswap::logs_events::PumpSwapEvent::UpdateFeeConfig(e) => { - e.signature = transaction_pretty.signature.to_string(); - e.slot = slot; - }, - _ => {} - } - - // 调用回调函数 - callback(event); - } - - Ok(()) - } - - // ------------------------------------------------------------ - // Raydium - // ------------------------------------------------------------ - - /// 订阅Raydium事件 - pub async fn subscribe_raydium(&self, callback: F) -> AnyResult<()> - where - F: Fn(crate::common::raydium::logs_events::RaydiumEvent) + Send + Sync + 'static, - { - // 使用constants中定义的AMM_PROGRAM - let raydium_v4_program_id = crate::constants::raydium::accounts::AMMV4_PROGRAM; - let raydium_cpmm_program_id = crate::constants::raydium::accounts::CPMM_PROGRAM; - let addrs = vec![raydium_v4_program_id.to_string(), raydium_cpmm_program_id.to_string()]; - // 创建过滤器 - let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]); + let protocol_accounts = protocols + .iter() + .map(|p| p.get_program_id()) + .flatten() + .map(|p| p.to_string()) + .collect::>(); + let mut account_include = account_include.unwrap_or_default(); + let account_exclude = account_exclude.unwrap_or_default(); + account_include.extend(protocol_accounts.clone()); + + let transactions = + self.get_subscribe_request_filter(account_include, account_exclude, vec![]); // 订阅事件 let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?; // 创建通道 - let (mut tx, mut rx) = mpsc::channel::(1000); + let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); // 创建回调函数 let callback = Box::new(callback); @@ -624,7 +230,13 @@ impl YellowstoneGrpc { // 处理交易 while let Some(transaction_pretty) = rx.next().await { - if let Err(e) = Self::process_raydium_transaction(transaction_pretty, &*callback).await + if let Err(e) = Self::process_event_transaction( + transaction_pretty, + &*callback, + bot_wallet, + protocols.clone(), + ) + .await { error!("Error processing transaction: {:?}", e); } @@ -633,52 +245,31 @@ impl YellowstoneGrpc { Ok(()) } - /// 处理Raydium交易 - async fn process_raydium_transaction( + /// 处理事件交易 + async fn process_event_transaction( transaction_pretty: TransactionPretty, callback: &F, + bot_wallet: Option, + protocols: Vec, ) -> AnyResult<()> where - F: Fn(crate::common::raydium::logs_events::RaydiumEvent) + Send + Sync, + F: Fn(Box) + Send + Sync, { let slot = transaction_pretty.slot; - let trade_raw: solana_transaction_status::EncodedTransactionWithStatusMeta = - transaction_pretty.tx; - - // 检查交易元数据 - let meta = trade_raw - .meta - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?; - - // 检查交易是否成功 - if meta.err.is_some() { - return Ok(()); - } - - if let Some(versioned_tx) = trade_raw.transaction.decode() { - let signature = versioned_tx.signatures[0].to_string(); - let instructions: Vec = - crate::common::raydium::logs_filters::LogFilter::parse_raydium_compiled_instruction(versioned_tx).unwrap(); - for instruction in instructions { - match instruction { - crate::common::raydium::logs_data::RaydiumInstruction::V4Swap(mut v4_swap_event) => { - v4_swap_event.slot = slot; - v4_swap_event.signature = signature.clone(); - callback(crate::common::raydium::logs_events::RaydiumEvent::V4Swap(v4_swap_event)); - } - crate::common::raydium::logs_data::RaydiumInstruction::SwapBaseInput(mut swap_base_input_event) => { - swap_base_input_event.slot = slot; - swap_base_input_event.signature = signature.clone(); - callback(crate::common::raydium::logs_events::RaydiumEvent::SwapBaseInput(swap_base_input_event)); - } - crate::common::raydium::logs_data::RaydiumInstruction::SwapBaseOutput(mut swap_base_output_event) => { - swap_base_output_event.slot = slot; - swap_base_output_event.signature = signature.clone(); - callback(crate::common::raydium::logs_events::RaydiumEvent::SwapBaseOutput(swap_base_output_event)); - } - _ => {} - } + let signature = transaction_pretty.signature.to_string(); + for protocol in protocols { + let parser = EventParserFactory::create_parser(protocol); + let events = parser + .parse_transaction( + transaction_pretty.tx.clone(), + &signature, + Some(slot), + bot_wallet.clone(), + ) + .await + .unwrap_or_else(|_e| vec![]); + for event in events { + callback(event); } } diff --git a/src/lib.rs b/src/lib.rs index 056cb49..6a41dc9 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,45 +1,40 @@ pub mod accounts; +pub mod common; pub mod constants; pub mod error; -pub mod instruction; +pub mod event_parser; pub mod grpc; -pub mod common; -pub mod swqos; +pub mod instruction; +pub mod protos; pub mod pumpfun; pub mod pumpswap; +pub mod raydium_launchpad; +pub mod swqos; pub mod trading; -pub mod protos; use std::sync::Arc; use std::sync::Mutex; -use swqos::SwqosClient; use rustls::crypto::{ring::default_provider, CryptoProvider}; use solana_hash::Hash; use solana_sdk::{ - commitment_config::CommitmentConfig, pubkey::Pubkey, signature::{Keypair, Signer}, }; +use swqos::SwqosClient; -use common::{pumpfun::logs_data::TradeInfo, pumpfun::logs_events::PumpfunEvent, pumpfun::logs_subscribe, TradeConfig, PriorityFee, SolanaRpcClient}; -use common::pumpfun::logs_subscribe::SubscriptionHandle; +use common::{PriorityFee, SolanaRpcClient, TradeConfig}; -use constants::trade_type::{COPY_BUY, SNIPER_BUY}; -use constants::trade_platform::{PUMPFUN, PUMPFUN_SWAP}; use accounts::BondingCurveAccount; +use constants::trade_platform::{PUMPFUN, PUMPFUN_SWAP, RAYDIUM_LAUNCHPAD}; +use constants::trade_type::{COPY_BUY, SNIPER_BUY}; -use crate::swqos::bloxroute::BloxrouteClient; +use crate::event_parser::protocols::pumpfun::PumpFunTradeEvent; use crate::swqos::SwqosConfig; -use crate::swqos::SwqosType; -use crate::swqos::jito::JitoClient; -use crate::swqos::nextblock::NextBlockClient; -use crate::swqos::solana_rpc::SolRpcClient; -use crate::swqos::temporal::TemporalClient; -use crate::swqos::zeroslot::ZeroSlotClient; use crate::trading::core::params::PumpFunParams; use crate::trading::core::params::PumpFunSellParams; use crate::trading::core::params::PumpSwapParams; +use crate::trading::core::params::RaydiumLaunchpadParams; use crate::trading::BuyWithTipParams; use crate::trading::SellParams; use crate::trading::SellWithTipParams; @@ -68,10 +63,7 @@ impl Clone for SolanaTrade { impl SolanaTrade { #[inline] - pub async fn new( - payer: Arc, - trade_config: TradeConfig, - ) -> Self { + pub async fn new(payer: Arc, trade_config: TradeConfig) -> Self { if CryptoProvider::get_default().is_none() { let _ = default_provider() .install_default() @@ -86,13 +78,14 @@ impl SolanaTrade { let mut swqos_clients: Vec> = vec![]; for swqos in swqos_configs { - let swqos_client = SwqosConfig::get_swqos_client(rpc_url.clone(), commitment.clone(), swqos.clone()); + let swqos_client = + SwqosConfig::get_swqos_client(rpc_url.clone(), commitment.clone(), swqos.clone()); swqos_clients.push(swqos_client); } let rpc = Arc::new(SolanaRpcClient::new_with_commitment( rpc_url.clone(), - commitment + commitment, )); let instance = Self { @@ -117,79 +110,10 @@ impl SolanaTrade { /// 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() - } - - /// Buy tokens - pub async fn sniper_buy( - &self, - mint: Pubkey, - creator: Pubkey, - buy_sol_cost: u64, - slippage_basis_points: Option, - recent_blockhash: Hash, - bonding_curve: Option>, - ) -> Result<(), anyhow::Error> { - pumpfun::buy::buy( - self.rpc.clone(), - self.payer.clone(), - mint, - creator, - buy_sol_cost, - slippage_basis_points, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - bonding_curve, - SNIPER_BUY.to_string(), - ).await - } - - pub async fn buy( - &self, - mint: Pubkey, - creator: Pubkey, - buy_sol_cost: u64, - slippage_basis_points: Option, - recent_blockhash: Hash, - bonding_curve: Option>, - trade_platform: String, - ) -> Result<(), anyhow::Error> { - if trade_platform == PUMPFUN { - pumpfun::buy::buy( - self.rpc.clone(), - self.payer.clone(), - mint, - creator, - buy_sol_cost, - slippage_basis_points, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - bonding_curve, - COPY_BUY.to_string(), - ).await - } else if trade_platform == PUMPFUN_SWAP { - pumpswap::buy::buy( - self.rpc.clone(), - self.payer.clone(), - mint, - creator, - buy_sol_cost, - slippage_basis_points, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - None, - None, - None, - None, - None, - true, - ).await - } else { - Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) - } + instance + .as_ref() + .expect("PumpFun instance not initialized. Please call new() first.") + .clone() } pub async fn buy_use_buy_params( @@ -200,7 +124,12 @@ impl SolanaTrade { let mut priority_fee = buy_params.priority_fee.clone(); if custom_buy_tip_fee.is_some() { priority_fee.buy_tip_fee = custom_buy_tip_fee.unwrap(); - priority_fee.buy_tip_fees = vec![custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap()]; + priority_fee.buy_tip_fees = vec![ + custom_buy_tip_fee.unwrap(), + custom_buy_tip_fee.unwrap(), + custom_buy_tip_fee.unwrap(), + custom_buy_tip_fee.unwrap(), + ]; } let mint = buy_params.mint; let creator = buy_params.creator; @@ -210,7 +139,8 @@ impl SolanaTrade { if let Some(protocol_params) = buy_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpfun::buy::buy( self.rpc.clone(), self.payer.clone(), @@ -223,11 +153,13 @@ impl SolanaTrade { recent_blockhash, protocol_params.bonding_curve.clone(), COPY_BUY.to_string(), - ).await + ) + .await } else if let Some(protocol_params) = buy_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpswap::buy::buy( self.rpc.clone(), self.payer.clone(), @@ -244,43 +176,34 @@ impl SolanaTrade { protocol_params.user_base_token_account.clone(), protocol_params.user_quote_token_account.clone(), protocol_params.auto_handle_wsol, - ).await + ) + .await + } else if let Some(protocol_params) = buy_params + .protocol_params + .as_any() + .downcast_ref::() + { + raydium_launchpad::buy::buy( + self.rpc.clone(), + self.payer.clone(), + mint, + protocol_params.virtual_base.unwrap_or(0), + protocol_params.virtual_quote.unwrap_or(0), + protocol_params.real_base_before.unwrap_or(0), + protocol_params.real_quote_before.unwrap_or(0), + buy_sol_cost, + slippage_basis_points, + priority_fee.clone(), + self.trade_config.lookup_table_key, + recent_blockhash, + protocol_params.auto_handle_wsol, + ) + .await } else { - return Err(anyhow::anyhow!("Invalid protocol params for PumpFun")); + return Err(anyhow::anyhow!("Invalid protocol params for Trade")); } } - /// Buy tokens using Jito - pub async fn sniper_buy_with_tip( - &self, - mint: Pubkey, - creator: Pubkey, - buy_sol_cost: u64, - slippage_basis_points: Option, - recent_blockhash: Hash, - bonding_curve: Option>, - custom_buy_tip_fee: Option, - ) -> Result<(), anyhow::Error> { - let mut priority_fee = self.priority_fee.clone(); - if custom_buy_tip_fee.is_some() { - priority_fee.buy_tip_fee = custom_buy_tip_fee.unwrap(); - priority_fee.buy_tip_fees = vec![custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap()]; - } - pumpfun::buy::buy_with_tip( - self.swqos_clients.clone(), - self.payer.clone(), - mint, - creator, - buy_sol_cost, - slippage_basis_points, - priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - bonding_curve, - SNIPER_BUY.to_string(), - ).await - } - pub async fn buy_with_tip_use_buy_params( &self, buy_params: BuyWithTipParams, @@ -289,7 +212,12 @@ impl SolanaTrade { let mut priority_fee = buy_params.priority_fee.clone(); if custom_buy_tip_fee.is_some() { priority_fee.buy_tip_fee = custom_buy_tip_fee.unwrap(); - priority_fee.buy_tip_fees = vec![custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap()]; + priority_fee.buy_tip_fees = vec![ + custom_buy_tip_fee.unwrap(), + custom_buy_tip_fee.unwrap(), + custom_buy_tip_fee.unwrap(), + custom_buy_tip_fee.unwrap(), + ]; } let mint = buy_params.mint; let creator = buy_params.creator; @@ -299,7 +227,8 @@ impl SolanaTrade { if let Some(protocol_params) = buy_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpfun::buy::buy_with_tip( self.swqos_clients.clone(), self.payer.clone(), @@ -312,11 +241,13 @@ impl SolanaTrade { recent_blockhash, protocol_params.bonding_curve.clone(), COPY_BUY.to_string(), - ).await + ) + .await } else if let Some(protocol_params) = buy_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpswap::buy::buy_with_tip( self.rpc.clone(), self.swqos_clients.clone(), @@ -334,284 +265,34 @@ impl SolanaTrade { protocol_params.user_base_token_account.clone(), protocol_params.user_quote_token_account.clone(), protocol_params.auto_handle_wsol, - ).await - } else { - return Err(anyhow::anyhow!("Invalid protocol params for PumpFun")); - } - } - - pub async fn buy_with_tip( - &self, - mint: Pubkey, - creator: Pubkey, - buy_sol_cost: u64, - slippage_basis_points: Option, - recent_blockhash: Hash, - bonding_curve: Option>, - trade_platform: String, - custom_buy_tip_fee: Option, - ) -> Result<(), anyhow::Error> { - let mut priority_fee = self.priority_fee.clone(); - if custom_buy_tip_fee.is_some() { - priority_fee.buy_tip_fee = custom_buy_tip_fee.unwrap(); - priority_fee.buy_tip_fees = vec![custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap(),custom_buy_tip_fee.unwrap()]; - } - if trade_platform == PUMPFUN { - pumpfun::buy::buy_with_tip( - self.swqos_clients.clone(), + ) + .await + } else if let Some(protocol_params) = buy_params + .protocol_params + .as_any() + .downcast_ref::() + { + raydium_launchpad::buy::buy( + self.rpc.clone(), self.payer.clone(), mint, - creator, + protocol_params.virtual_base.unwrap_or(0), + protocol_params.virtual_quote.unwrap_or(0), + protocol_params.real_base_before.unwrap_or(0), + protocol_params.real_quote_before.unwrap_or(0), buy_sol_cost, slippage_basis_points, priority_fee.clone(), self.trade_config.lookup_table_key, recent_blockhash, - bonding_curve, - COPY_BUY.to_string(), - ).await - } else if trade_platform == PUMPFUN_SWAP { - pumpswap::buy::buy_with_tip( - self.rpc.clone(), - self.swqos_clients.clone(), - self.payer.clone(), - mint, - creator, - buy_sol_cost, - slippage_basis_points, - priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - None, - None, - None, - None, - None, - true, - ).await + protocol_params.auto_handle_wsol, + ) + .await } else { - Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) + return Err(anyhow::anyhow!("Invalid protocol params for Trade")); } } - // Sell tokens - pub async fn sell( - &self, - mint: Pubkey, - creator: Pubkey, - amount_token: u64, - recent_blockhash: Hash, - ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell( - self.rpc.clone(), - self.payer.clone(), - mint.clone(), - creator, - amount_token, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - ).await - } - - /// Sell tokens by percentage - pub async fn sell_by_percent( - &self, - mint: Pubkey, - creator: Pubkey, - percent: u64, - amount_token: u64, - recent_blockhash: Hash, - trade_platform: String, - ) -> Result<(), anyhow::Error> { - if trade_platform == PUMPFUN { - pumpfun::sell::sell_by_percent( - self.rpc.clone(), - self.payer.clone(), - mint.clone(), - creator, - percent, - amount_token, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - ).await - } else if trade_platform == PUMPFUN_SWAP { - pumpswap::sell::sell_by_percent( - self.rpc.clone(), - self.payer.clone(), - mint.clone(), - creator, - percent, - None, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - None, - None, - None, - None, - None, - ).await - } else { - Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) - } - } - - /// Sell tokens by amount - pub async fn sell_by_amount( - &self, - mint: Pubkey, - creator: Pubkey, - amount: u64, - recent_blockhash: Hash, - trade_platform: String, - ) -> Result<(), anyhow::Error> { - if trade_platform == PUMPFUN { - pumpfun::sell::sell_by_amount( - self.rpc.clone(), - self.payer.clone(), - mint.clone(), - creator, - amount, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - ).await - } else if trade_platform == PUMPFUN_SWAP { - pumpswap::sell::sell_by_amount( - self.rpc.clone(), - self.payer.clone(), - mint.clone(), - creator, - amount, - None, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - None, - None, - None, - None, - None, - ).await - } else { - Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) - } - } - - pub async fn sell_by_percent_with_tip( - &self, - mint: Pubkey, - creator: Pubkey, - percent: u64, - amount_token: u64, - recent_blockhash: Hash, - trade_platform: String, - ) -> Result<(), anyhow::Error> { - if trade_platform == PUMPFUN { - pumpfun::sell::sell_by_percent_with_tip( - self.rpc.clone(), - self.swqos_clients.clone(), - self.payer.clone(), - mint, - creator, - percent, - amount_token, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - ).await - } else if trade_platform == PUMPFUN_SWAP { - pumpswap::sell::sell_by_percent_with_tip( - self.rpc.clone(), - self.swqos_clients.clone(), - self.payer.clone(), - mint, - creator, - percent, - None, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - None, - None, - None, - None, - None, - ).await - } else { - Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) - } - } - - pub async fn sell_by_amount_with_tip( - &self, - mint: Pubkey, - creator: Pubkey, - amount: u64, - recent_blockhash: Hash, - trade_platform: String, - ) -> Result<(), anyhow::Error> { - if trade_platform == PUMPFUN { - pumpfun::sell::sell_by_amount_with_tip( - self.rpc.clone(), - self.swqos_clients.clone(), - self.payer.clone(), - mint, - creator, - amount, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - ).await - } else if trade_platform == PUMPFUN_SWAP { - pumpswap::sell::sell_by_amount_with_tip( - self.rpc.clone(), - self.swqos_clients.clone(), - self.payer.clone(), - mint, - creator, - amount, - None, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - None, - None, - None, - None, - None, - ).await - } else { - Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) - } - } - - /// Sell tokens using Jito - pub async fn sell_with_tip( - &self, - mint: Pubkey, - creator: Pubkey, - amount_token: u64, - recent_blockhash: Hash, - ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell_with_tip( - self.rpc.clone(), - self.swqos_clients.clone(), - self.payer.clone(), - mint, - creator, - amount_token, - self.priority_fee.clone(), - self.trade_config.lookup_table_key, - recent_blockhash, - ).await - } - - - /// -------- use sell params -------- - /// Sell tokens by percentage pub async fn sell_by_percent_use_sell_params( &self, @@ -625,7 +306,8 @@ impl SolanaTrade { if let Some(_) = sell_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpfun::sell::sell_by_percent( self.rpc.clone(), self.payer.clone(), @@ -636,11 +318,13 @@ impl SolanaTrade { self.priority_fee.clone(), self.trade_config.lookup_table_key, recent_blockhash, - ).await + ) + .await } else if let Some(protocol_params) = sell_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpswap::sell::sell_by_percent( self.rpc.clone(), self.payer.clone(), @@ -656,9 +340,30 @@ impl SolanaTrade { protocol_params.pool_quote_token_account.clone(), protocol_params.user_base_token_account.clone(), protocol_params.user_quote_token_account.clone(), - ).await + ) + .await + } else if let Some(protocol_params) = sell_params + .protocol_params + .as_any() + .downcast_ref::() + { + raydium_launchpad::sell::sell_by_percent( + self.rpc.clone(), + self.payer.clone(), + mint.clone(), + protocol_params.virtual_base.unwrap_or(0), + protocol_params.virtual_quote.unwrap_or(0), + protocol_params.real_base_before.unwrap_or(0), + protocol_params.real_quote_before.unwrap_or(0), + percent, + None, + self.priority_fee.clone(), + self.trade_config.lookup_table_key, + recent_blockhash, + ) + .await } else { - return Err(anyhow::anyhow!("Invalid protocol params for PumpFun")); + return Err(anyhow::anyhow!("Invalid protocol params for Trade")); } } @@ -674,7 +379,8 @@ impl SolanaTrade { if let Some(_) = sell_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpfun::sell::sell_by_amount( self.rpc.clone(), self.payer.clone(), @@ -684,11 +390,13 @@ impl SolanaTrade { self.priority_fee.clone(), self.trade_config.lookup_table_key, recent_blockhash, - ).await + ) + .await } else if let Some(protocol_params) = sell_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpswap::sell::sell_by_amount( self.rpc.clone(), self.payer.clone(), @@ -704,9 +412,30 @@ impl SolanaTrade { protocol_params.pool_quote_token_account.clone(), protocol_params.user_base_token_account.clone(), protocol_params.user_quote_token_account.clone(), - ).await + ) + .await + } else if let Some(protocol_params) = sell_params + .protocol_params + .as_any() + .downcast_ref::() + { + raydium_launchpad::sell::sell_by_amount( + self.rpc.clone(), + self.payer.clone(), + mint.clone(), + protocol_params.virtual_base.unwrap_or(0), + protocol_params.virtual_quote.unwrap_or(0), + protocol_params.real_base_before.unwrap_or(0), + protocol_params.real_quote_before.unwrap_or(0), + amount.unwrap_or(0), + None, + self.priority_fee.clone(), + self.trade_config.lookup_table_key, + recent_blockhash, + ) + .await } else { - Err(anyhow::anyhow!("Invalid protocol params for PumpFun")) + Err(anyhow::anyhow!("Invalid protocol params for Trade")) } } @@ -722,7 +451,8 @@ impl SolanaTrade { if let Some(_) = sell_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpfun::sell::sell_by_percent_with_tip( self.rpc.clone(), self.swqos_clients.clone(), @@ -734,11 +464,13 @@ impl SolanaTrade { self.priority_fee.clone(), self.trade_config.lookup_table_key, recent_blockhash, - ).await + ) + .await } else if let Some(protocol_params) = sell_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpswap::sell::sell_by_percent_with_tip( self.rpc.clone(), self.swqos_clients.clone(), @@ -755,9 +487,31 @@ impl SolanaTrade { protocol_params.pool_quote_token_account.clone(), protocol_params.user_base_token_account.clone(), protocol_params.user_quote_token_account.clone(), - ).await + ) + .await + } else if let Some(protocol_params) = sell_params + .protocol_params + .as_any() + .downcast_ref::() + { + raydium_launchpad::sell::sell_by_percent_with_tip( + self.rpc.clone(), + self.swqos_clients.clone(), + self.payer.clone(), + mint, + protocol_params.virtual_base.unwrap_or(0), + protocol_params.virtual_quote.unwrap_or(0), + protocol_params.real_base_before.unwrap_or(0), + protocol_params.real_quote_before.unwrap_or(0), + percent, + sell_params.slippage_basis_points, + self.priority_fee.clone(), + self.trade_config.lookup_table_key, + recent_blockhash, + ) + .await } else { - Err(anyhow::anyhow!("Invalid protocol params for PumpFun")) + Err(anyhow::anyhow!("Invalid protocol params for Trade")) } } @@ -772,7 +526,8 @@ impl SolanaTrade { if let Some(_) = sell_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpfun::sell::sell_by_amount_with_tip( self.rpc.clone(), self.swqos_clients.clone(), @@ -783,11 +538,13 @@ impl SolanaTrade { self.priority_fee.clone(), self.trade_config.lookup_table_key, recent_blockhash, - ).await + ) + .await } else if let Some(protocol_params) = sell_params .protocol_params .as_any() - .downcast_ref::() { + .downcast_ref::() + { pumpswap::sell::sell_by_amount_with_tip( self.rpc.clone(), self.swqos_clients.clone(), @@ -804,31 +561,34 @@ impl SolanaTrade { protocol_params.pool_quote_token_account.clone(), protocol_params.user_base_token_account.clone(), protocol_params.user_quote_token_account.clone(), - ).await + ) + .await + } else if let Some(protocol_params) = sell_params + .protocol_params + .as_any() + .downcast_ref::() + { + raydium_launchpad::sell::sell_by_amount_with_tip( + self.rpc.clone(), + self.swqos_clients.clone(), + self.payer.clone(), + mint, + protocol_params.virtual_base.unwrap_or(0), + protocol_params.virtual_quote.unwrap_or(0), + protocol_params.real_base_before.unwrap_or(0), + protocol_params.real_quote_before.unwrap_or(0), + amount.unwrap_or(0), + sell_params.slippage_basis_points, + self.priority_fee.clone(), + self.trade_config.lookup_table_key, + recent_blockhash, + ) + .await } else { - Err(anyhow::anyhow!("Invalid protocol params for PumpFun")) + Err(anyhow::anyhow!("Invalid protocol params for Trade")) } } - #[inline] - pub async fn tokens_subscription( - &self, - ws_url: &str, - commitment: CommitmentConfig, - callback: F, - bot_wallet: Option, - ) -> Result> - where - F: Fn(PumpfunEvent) + Send + Sync + 'static, - { - logs_subscribe::tokens_subscription(ws_url, commitment, callback, bot_wallet).await - } - - #[inline] - pub async fn stop_subscription(&self, subscription_handle: SubscriptionHandle) { - subscription_handle.shutdown().await; - } - #[inline] pub async fn get_sol_balance(&self, payer: &Pubkey) -> Result { pumpfun::common::get_sol_balance(&self.rpc, payer).await @@ -840,8 +600,15 @@ impl SolanaTrade { } #[inline] - pub async fn get_token_balance(&self, payer: &Pubkey, mint: &Pubkey) -> Result { - println!("get_token_balance payer: {}, mint: {}, rpc_url: {}", payer, mint, self.trade_config.rpc_url); + pub async fn get_token_balance( + &self, + payer: &Pubkey, + mint: &Pubkey, + ) -> Result { + println!( + "get_token_balance payer: {}, mint: {}, rpc_url: {}", + payer, mint, self.trade_config.rpc_url + ); pumpfun::common::get_token_balance(&self.rpc, payer, mint).await } @@ -861,17 +628,22 @@ impl SolanaTrade { } #[inline] - pub fn get_token_price(&self,virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { + pub fn get_token_price(&self, virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { pumpfun::common::get_token_price(virtual_sol_reserves, virtual_token_reserves) } #[inline] - pub fn get_buy_price(&self, amount: u64, trade_info: &TradeInfo) -> u64 { + pub fn get_buy_price(&self, amount: u64, trade_info: &PumpFunTradeEvent) -> u64 { pumpfun::common::get_buy_price(amount, trade_info) } #[inline] - pub async fn transfer_sol(&self, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { + pub async fn transfer_sol( + &self, + payer: &Keypair, + receive_wallet: &Pubkey, + amount: u64, + ) -> Result<(), anyhow::Error> { pumpfun::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await } @@ -882,64 +654,81 @@ impl SolanaTrade { #[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 (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)) + + 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 (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) } #[inline] pub async fn get_creator(&self, mint: &Pubkey) -> Result { - let (bonding_curve, _) = pumpfun::common::get_bonding_curve_account_v2(&self.rpc, mint).await?; - + let (bonding_curve, _) = + pumpfun::common::get_bonding_curve_account_v2(&self.rpc, mint).await?; + let creator = bonding_curve.creator; - + Ok(creator) } #[inline] - pub async fn get_current_price_with_pumpswap(&self, pool_address: &Pubkey) -> Result { + pub async fn get_current_price_with_pumpswap( + &self, + pool_address: &Pubkey, + ) -> Result { let pool = pumpswap::pool::Pool::fetch(&self.rpc, pool_address).await?; - + let (base_amount, quote_amount) = pool.get_token_balances(&self.rpc).await?; - + // Calculate price using constant product formula (x * y = k) // Price = quote_amount / base_amount if base_amount == 0 { - return Err(anyhow::anyhow!("Base amount is zero, cannot calculate price")); + return Err(anyhow::anyhow!( + "Base amount is zero, cannot calculate price" + )); } - + let price = quote_amount as f64 / base_amount as f64; - + Ok(price) } #[inline] - pub async fn get_real_sol_reserves_with_pumpswap(&self, pool_address: &Pubkey) -> Result { + pub async fn get_real_sol_reserves_with_pumpswap( + &self, + pool_address: &Pubkey, + ) -> Result { let pool = pumpswap::pool::Pool::fetch(&self.rpc, pool_address).await?; - + let (_, quote_amount) = pool.get_token_balances(&self.rpc).await?; - + Ok(quote_amount) } #[inline] - pub async fn get_payer_token_balance_with_pumpswap(&self, pool_address: &Pubkey) -> Result { + pub async fn get_payer_token_balance_with_pumpswap( + &self, + pool_address: &Pubkey, + ) -> Result { let pool = pumpswap::pool::Pool::fetch(&self.rpc, pool_address).await?; - + let (base_amount, _) = pool.get_token_balances(&self.rpc).await?; - + Ok(base_amount) } } diff --git a/src/main.rs b/src/main.rs index b1fa65d..642a161 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,299 +1,45 @@ use std::{str::FromStr, sync::Arc}; -use sol_trade_sdk::{accounts::BondingCurveAccount, common::{pumpfun::PumpfunEvent, pumpswap::PumpSwapEvent, raydium::RaydiumEvent, AnyResult, PriorityFee, TradeConfig}, constants::pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, grpc::{ShredStreamGrpc, YellowstoneGrpc}, pumpfun::common::get_bonding_curve_pda, swqos::{SwqosConfig, SwqosRegion, SwqosType}, SolanaTrade}; +use sol_trade_sdk::{ + accounts::BondingCurveAccount, + common::{AnyResult, PriorityFee, TradeConfig}, + constants::{pumpfun::global_constants::TOKEN_TOTAL_SUPPLY, trade_type}, + event_parser::{ + protocols::{ + pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent}, + pumpswap::{ + PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent, PumpSwapSellEvent, + PumpSwapWithdrawEvent, + }, + raydium_launchpad::{RaydiumLaunchpadPoolCreateEvent, RaydiumLaunchpadTradeEvent}, + }, + Protocol, UnifiedEvent, + }, + grpc::{ShredStreamGrpc, YellowstoneGrpc}, + match_event, + pumpfun::common::get_bonding_curve_account_v2, + swqos::{SwqosConfig, SwqosRegion}, + trading::{ + core::params::{PumpFunParams, PumpFunSellParams, PumpSwapParams, RaydiumLaunchpadParams}, + BuyParams, SellParams, + }, + SolanaTrade, +}; use solana_client::rpc_client::RpcClient; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair}; #[tokio::main] async fn main() -> Result<(), Box> { - // test_pumpfun_with_shreds().await?; - // test_pumpfun_with_grpc().await?; - // test_pumpswap_with_shreds().await?; - // test_pumpswap_with_grpc().await?; - // test_raydium_with_shreds().await?; - // test_raydium_with_grpc().await?; - // test_pumpfun_sniper().await?; - // test_pumpfun().await?; - test_pumpswap().await?; - Ok(()) -} - -async fn test_pumpfun_with_shreds() -> Result<(), Box> { - let grpc = ShredStreamGrpc::new("http://127.0.0.1:10000".to_string()).await?; - - let callback = |event: PumpfunEvent| { - // TradeInfo 的 sol_amount 不是真实线上消费/获取的数量 - // 当 is_buy 为 true 时,sol_amount = max_sol_cost,代表用户愿意支付的最大金额 - // 当 is_buy 为 false 时,sol_amount = min_sol_output,代表用户愿意接受的最小金额 - // - // timestamp 不是真实交易发生的时间,取的值为当前系统的时间 - // - // 无法获取下面4个值 - // virtual_sol_reserves: 0, - // virtual_token_reserves: 0, - // real_sol_reserves: 0, - // real_token_reserves: 0, - 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.shredstream_subscribe(callback, None).await?; - - Ok(()) -} - -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()).await?; - - // 定义回调函数处理 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_client.shredstream_subscribe_pumpswap(callback).await?; - - 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_raydium_with_shreds() -> Result<(), Box> { - // 使用 ShredStream 客户端订阅 Raydium 事件 - println!("正在订阅 Raydium ShredStream 事件..."); - - let grpc_client = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?; - - // 定义回调函数处理 Raydium 事件 - let callback = |event: RaydiumEvent| { - match event { - RaydiumEvent::V4Swap(v4_swap_event) => { - println!("v4_swap_event: {:?}", v4_swap_event); - } - RaydiumEvent::SwapBaseInput(swap_base_input_event) => { - println!("swap_base_input_event: {:?}", swap_base_input_event); - } - RaydiumEvent::SwapBaseOutput(swap_base_output_event) => { - println!("swap_base_output_event: {:?}", swap_base_output_event); - } - RaydiumEvent::Error(err) => { - println!("error: {}", err); - } - } - }; - // 订阅 Raydium 事件 - println!("开始监听 Raydium 事件,按 Ctrl+C 停止..."); - - grpc_client.shredstream_subscribe_raydium(callback).await?; - - Ok(()) -} - -async fn test_raydium_with_grpc() -> Result<(), Box> { - // 使用 GRPC 客户端订阅 Raydium 事件 - println!("正在订阅 Raydium GRPC 事件..."); - - let grpc = YellowstoneGrpc::new( - "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), - None, - )?; - - // 定义回调函数处理 PumpSwap 事件 - let callback = |event: RaydiumEvent| match event { - RaydiumEvent::V4Swap(v4_swap_event) => { - println!("v4_swap_event: {:?}", v4_swap_event); - } - RaydiumEvent::SwapBaseInput(swap_base_input_event) => { - println!("swap_base_input_event: {:?}", swap_base_input_event); - } - RaydiumEvent::SwapBaseOutput(swap_base_output_event) => { - println!("swap_base_output_event: {:?}", swap_base_output_event); - } - RaydiumEvent::Error(err) => { - println!("error: {}", err); - } - }; - // 订阅 Raydium 事件 - println!("开始监听 Raydium 事件,按 Ctrl+C 停止..."); - - grpc.subscribe_raydium(callback).await?; - - Ok(()) -} - -async fn test_pumpfun_sniper() -> AnyResult<()> { - // 创建一个随机账户作为交易者 - let payer = Keypair::new(); - - let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); - let swqos_configs = vec![ - SwqosConfig::Jito(SwqosRegion::Frankfurt), - SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Default(rpc_url.clone()), - ]; - - // Define cluster configuration - let trade_config = TradeConfig { - rpc_url: rpc_url.clone(), - commitment: CommitmentConfig::confirmed(), - priority_fee: PriorityFee::default(), - swqos_configs, - lookup_table_key: None, - }; - let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; - - - let creator = Pubkey::from_str("xxx")?; // dev account - let buy_sol_cost = 500_000; // 0.0005 SOL - let slippage_basis_points = Some(100); - let rpc = RpcClient::new(rpc_url); - let recent_blockhash = rpc.get_latest_blockhash().unwrap(); - let mint_pubkey = Pubkey::from_str("xxx")?; // token mint - println!("Sniping buy tokens from PumpFun..."); - // get bonding curve - let dev_buy_token = 100_000; // test value - let dev_cost_sol = 100_000; // test value - let bonding_curve = BondingCurveAccount::new(&mint_pubkey, dev_buy_token, dev_cost_sol, creator); - solana_trade_client - .sniper_buy( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), - ) - .await?; + test_pumpfun().await?; + // test_pumpswap().await?; + // test_raydium_launchpad().await?; + // test_grpc().await?; + // test_shreds().await?; Ok(()) } async fn test_pumpfun() -> AnyResult<()> { let payer = Keypair::new(); - let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); let swqos_configs = vec![ SwqosConfig::Jito(SwqosRegion::Frankfurt), @@ -303,7 +49,6 @@ async fn test_pumpfun() -> AnyResult<()> { SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::Default(rpc_url.clone()), ]; - // Define cluster configuration let trade_config = TradeConfig { rpc_url: rpc_url.clone(), @@ -312,25 +57,24 @@ async fn test_pumpfun() -> AnyResult<()> { swqos_configs, lookup_table_key: None, }; - let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; - let creator = Pubkey::from_str("xxx")?; // dev account - let buy_sol_cost = 500_000; // 0.0005 SOL + let creator = Pubkey::from_str("xxxxxx")?; // dev account + let buy_sol_cost = 100_000; // 0.0001 SOL let slippage_basis_points = Some(100); let rpc = RpcClient::new(rpc_url); let recent_blockhash = rpc.get_latest_blockhash().unwrap(); - let trade_platform = "pumpfun".to_string(); - let mint_pubkey = Pubkey::from_str("xxx")?; // token mint + let mint_pubkey = Pubkey::from_str("xxxxxx")?; // token mint println!("Buying tokens from PumpFun..."); // get bonding curve - // Relevant on-chain information can be obtained from rpc/grpc - let virtual_token_reserves = 0; - let virtual_sol_reserves = 0; - let real_token_reserves = 0; - let real_sol_reserves = 0; + let (bonding_curve, bonding_curve_pda) = + get_bonding_curve_account_v2(&solana_trade_client.rpc, &mint_pubkey).await?; + let virtual_token_reserves = bonding_curve.virtual_token_reserves; + let virtual_sol_reserves = bonding_curve.virtual_sol_reserves; + let real_token_reserves = bonding_curve.real_token_reserves; + let real_sol_reserves = bonding_curve.real_sol_reserves; let bonding_curve = BondingCurveAccount { - discriminator: 0, - account: get_bonding_curve_pda(&mint_pubkey).unwrap(), + discriminator: bonding_curve.discriminator, + account: bonding_curve_pda, virtual_token_reserves: virtual_token_reserves, virtual_sol_reserves: virtual_sol_reserves, real_token_reserves: real_token_reserves, @@ -339,34 +83,57 @@ async fn test_pumpfun() -> AnyResult<()> { complete: false, creator: creator, }; + // 如果是狙击开发者 + // let bonding_curve = + // BondingCurveAccount::new(&mint_pubkey, dev_buy_token, dev_cost_sol, creator); + // buy + let buy_protocol_params = PumpFunParams { + trade_type: trade_type::COPY_BUY.to_string(), + bonding_curve: Some(Arc::new(bonding_curve)), + }; + let buy_params = BuyParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_sol: buy_sol_cost, + slippage_basis_points: slippage_basis_points, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + data_size_limit: 0, + protocol_params: Box::new(buy_protocol_params.clone()), + }; + let buy_with_tip_params = buy_params + .clone() + .with_tip(solana_trade_client.swqos_clients.clone()); solana_trade_client - .buy( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - Some(Arc::new(bonding_curve)), - trade_platform.clone(), - ) + .buy_use_buy_params(buy_with_tip_params, None) + .await?; + // sell + println!("Selling tokens from PumpFun..."); + let sell_protocol_params = PumpFunSellParams {}; + let amount_token = 0; // 写上真实的amount_token + let sell_params = SellParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_token: Some(amount_token), + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + protocol_params: Box::new(sell_protocol_params.clone()), + }; + solana_trade_client + .sell_by_amount_use_sell_params(sell_params) .await?; - // Sell 30% * amount_token quantity - // solana_trade_client - // .sell_by_percent( - // mint_pubkey, - // creator, - // 30, - // 100, - // recent_blockhash, - // trade_platform.clone(), - // ) - // .await?; Ok(()) } async fn test_pumpswap() -> AnyResult<()> { let payer = Keypair::new(); - let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); let swqos_configs = vec![ SwqosConfig::Jito(SwqosRegion::Frankfurt), @@ -376,7 +143,6 @@ async fn test_pumpswap() -> AnyResult<()> { SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::Default(rpc_url.clone()), ]; - // Define cluster configuration let trade_config = TradeConfig { rpc_url: rpc_url.clone(), @@ -387,34 +153,237 @@ async fn test_pumpswap() -> AnyResult<()> { }; let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; let creator = Pubkey::from_str("11111111111111111111111111111111")?; // dev account - let buy_sol_cost = 500_000; // 0.0005 SOL + let buy_sol_cost = 100_000; // 0.0001 SOL let slippage_basis_points = Some(100); let rpc = RpcClient::new(rpc_url); let recent_blockhash = rpc.get_latest_blockhash().unwrap(); - let trade_platform = "pumpswap".to_string(); let mint_pubkey = Pubkey::from_str("2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv")?; // token mint println!("Buying tokens from PumpSwap..."); + // buy + let protocol_params = PumpSwapParams { + pool: None, + pool_base_token_account: None, + pool_quote_token_account: None, + user_base_token_account: None, + user_quote_token_account: None, + auto_handle_wsol: true, + }; + let buy_params = BuyParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_sol: buy_sol_cost, + slippage_basis_points: slippage_basis_points, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + data_size_limit: 0, + protocol_params: Box::new(protocol_params.clone()), + }; + let buy_with_tip_params = buy_params + .clone() + .with_tip(solana_trade_client.swqos_clients.clone()); solana_trade_client - .buy( - mint_pubkey, - creator, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - None, - trade_platform.clone(), - ) + .buy_use_buy_params(buy_with_tip_params, None) + .await?; + // sell + println!("Selling tokens from PumpSwap..."); + let amount_token = 0; // 写上真实的amount_token + let sell_params = SellParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint_pubkey, + creator: creator, + amount_token: Some(amount_token), + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + protocol_params: Box::new(protocol_params.clone()), + }; + solana_trade_client + .sell_by_amount_use_sell_params(sell_params) .await?; - // Sell 30% * amount_token quantity - // solana_trade_client - // .sell_by_percent( - // mint_pubkey, - // creator, - // 30, - // 100, - // recent_blockhash, - // trade_platform.clone(), - // ) - // .await?; + Ok(()) +} + +async fn test_raydium_launchpad() -> Result<(), Box> { + // 创建一个随机账户作为交易者 + let payer = Keypair::new(); + let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); + let swqos_configs = vec![ + SwqosConfig::Jito(SwqosRegion::Frankfurt), + SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Default(rpc_url.clone()), + ]; + // Define cluster configuration + let trade_config = TradeConfig { + rpc_url: rpc_url.clone(), + commitment: CommitmentConfig::confirmed(), + priority_fee: PriorityFee::default(), + swqos_configs, + lookup_table_key: None, + }; + let solana_trade_client = SolanaTrade::new(Arc::new(payer), trade_config).await; + let amount = 100_000; // 0.0001 SOL + let recent_blockhash = solana_trade_client.rpc.get_latest_blockhash().await?; + let mint = Pubkey::from_str("xxxxxxx")?; + let raydium_launchpad_params = RaydiumLaunchpadParams { + virtual_base: None, + virtual_quote: None, + real_base_before: None, + real_quote_before: None, + auto_handle_wsol: true, + }; + println!("Buying tokens from Raydium Launchpad..."); + // buy + let buy_params = BuyParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint, + creator: Pubkey::default(), + amount_sol: amount, + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + data_size_limit: 0, + protocol_params: Box::new(raydium_launchpad_params.clone()), + }; + let buy_with_tip_params = buy_params + .clone() + .with_tip(solana_trade_client.swqos_clients.clone()); + solana_trade_client + .buy_use_buy_params(buy_with_tip_params, None) + .await?; + // sell + println!("Selling tokens from Raydium Launchpad..."); + let sell_params = SellParams { + rpc: Some(solana_trade_client.rpc.clone()), + payer: solana_trade_client.payer.clone(), + mint: mint, + creator: Pubkey::default(), + amount_token: None, + slippage_basis_points: None, + priority_fee: solana_trade_client.trade_config.clone().priority_fee, + lookup_table_key: solana_trade_client.trade_config.clone().lookup_table_key, + recent_blockhash, + protocol_params: Box::new(raydium_launchpad_params.clone()), + }; + solana_trade_client + .sell_by_amount_use_sell_params(sell_params) + .await?; + Ok(()) +} + +async fn test_grpc() -> Result<(), Box> { + // 使用 GRPC 客户端订阅事件 + println!("正在订阅 GRPC 事件..."); + + let grpc = YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + )?; + + // 定义回调函数处理 PumpSwap 事件 + let callback = |event: Box| { + match_event!(event, { + RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| { + println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol); + }, + RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| { + println!("RaydiumLaunchpadTradeEvent: {:?}", e); + }, + PumpFunTradeEvent => |e: PumpFunTradeEvent| { + println!("PumpFunTradeEvent: {:?}", e); + }, + PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| { + println!("PumpFunCreateTokenEvent: {:?}", e); + }, + PumpSwapBuyEvent => |e: PumpSwapBuyEvent| { + println!("Buy event: {:?}", e); + }, + PumpSwapSellEvent => |e: PumpSwapSellEvent| { + println!("Sell event: {:?}", e); + }, + PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| { + println!("CreatePool event: {:?}", e); + }, + PumpSwapDepositEvent => |e: PumpSwapDepositEvent| { + println!("Deposit event: {:?}", e); + }, + PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| { + println!("Withdraw event: {:?}", e); + }, + }); + }; + + // 订阅 PumpSwap 事件 + println!("开始监听事件,按 Ctrl+C 停止..."); + let protocols = vec![ + Protocol::PumpFun, + Protocol::PumpSwap, + Protocol::RaydiumLaunchpad, + ]; + grpc.subscribe_events(protocols, None, None, None, callback) + .await?; + + Ok(()) +} + +async fn test_shreds() -> Result<(), Box> { + // 使用 ShredStream 客户端订阅事件 + println!("正在订阅 ShredStream 事件..."); + + let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?; + + // 定义回调函数处理 PumpSwap 事件 + let callback = |event: Box| { + match_event!(event, { + RaydiumLaunchpadPoolCreateEvent => |e: RaydiumLaunchpadPoolCreateEvent| { + println!("RaydiumLaunchpadPoolCreateEvent: {:?}", e.base_mint_param.symbol); + }, + RaydiumLaunchpadTradeEvent => |e: RaydiumLaunchpadTradeEvent| { + println!("RaydiumLaunchpadTradeEvent: {:?}", e); + }, + PumpFunTradeEvent => |e: PumpFunTradeEvent| { + println!("PumpFunTradeEvent: {:?}", e); + }, + PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| { + println!("PumpFunCreateTokenEvent: {:?}", e); + }, + PumpSwapBuyEvent => |e: PumpSwapBuyEvent| { + println!("Buy event: {:?}", e); + }, + PumpSwapSellEvent => |e: PumpSwapSellEvent| { + println!("Sell event: {:?}", e); + }, + PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| { + println!("CreatePool event: {:?}", e); + }, + PumpSwapDepositEvent => |e: PumpSwapDepositEvent| { + println!("Deposit event: {:?}", e); + }, + PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| { + println!("Withdraw event: {:?}", e); + }, + }); + }; + + // 订阅 PumpSwap 事件 + println!("开始监听事件,按 Ctrl+C 停止..."); + let protocols = vec![ + Protocol::PumpFun, + Protocol::PumpSwap, + Protocol::RaydiumLaunchpad, + ]; + shred_stream + .shredstream_subscribe(protocols, None, callback) + .await?; + Ok(()) } diff --git a/src/pumpfun/common.rs b/src/pumpfun/common.rs index 01c2631..bab2a1b 100755 --- a/src/pumpfun/common.rs +++ b/src/pumpfun/common.rs @@ -8,7 +8,7 @@ use solana_sdk::{ }; 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}}}; +use crate::{accounts::{self, BondingCurveAccount}, common::{PriorityFee, SolanaRpcClient}, constants::{self, pumpfun::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}, event_parser::protocols::pumpfun::PumpFunTradeEvent}; lazy_static::lazy_static! { static ref ACCOUNT_CACHE: RwLock>> = RwLock::new(HashMap::new()); @@ -327,7 +327,7 @@ pub fn get_token_price(virtual_sol_reserves: u64, virtual_token_reserves: u64) - } #[inline] -pub fn get_buy_price(amount: u64, trade_info: &TradeInfo) -> u64 { +pub fn get_buy_price(amount: u64, trade_info: &PumpFunTradeEvent) -> u64 { if amount == 0 { return 0; } diff --git a/src/pumpfun/create.rs b/src/pumpfun/create.rs deleted file mode 100755 index 89a9e17..0000000 --- a/src/pumpfun/create.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::{str::FromStr, time::Instant, sync::Arc}; - -use anyhow::anyhow; -use solana_hash::Hash; -use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, - instruction::Instruction, message::{v0, VersionedMessage}, - pubkey::Pubkey, - native_token::sol_to_lamports, - signature::Keypair, - signer::Signer, - system_instruction, - transaction::{Transaction, VersionedTransaction} -}; -use spl_associated_token_account::instruction::create_associated_token_account; - -use crate::{ - common::{PriorityFee, SolanaRpcClient}, constants, instruction, - ipfs::TokenMetadataIPFS, swqos::{SwqosClient, TradeType}, -}; - -use crate::pumpfun::common::{ - create_priority_fee_instructions, - get_buy_amount_with_slippage, get_global_account -}; - -use crate::common::tip_cache::TipCache; - -use super::common::{get_bonding_curve_account, get_buy_token_amount, get_creator_vault_pda}; - -/// Create a new token -pub async fn create( - rpc: Arc, - payer: Arc, - mint: Keypair, - ipfs: TokenMetadataIPFS, - priority_fee: PriorityFee, -) -> Result<(), anyhow::Error> { - let mut instructions = create_priority_fee_instructions(priority_fee); - - instructions.push(instruction::create( - payer.as_ref(), - &mint, - instruction::Create { - _name: ipfs.metadata.name, - _symbol: ipfs.metadata.symbol, - _uri: ipfs.metadata_uri, - _creator: payer.pubkey(), - }, - )); - - let recent_blockhash = rpc.get_latest_blockhash().await?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer.as_ref(), &mint], - recent_blockhash, - ); - - rpc.send_and_confirm_transaction(&transaction).await?; - - Ok(()) -} - -/// Create and buy tokens in one transaction -pub async fn create_and_buy( - rpc: Arc, - payer: Arc, - mint: Keypair, - ipfs: TokenMetadataIPFS, - buy_sol_cost: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, - recent_blockhash: Hash, -) -> Result<(), anyhow::Error> { - if buy_sol_cost == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let mint = Arc::new(mint); - let transaction = build_create_and_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), ipfs, buy_sol_cost, slippage_basis_points, priority_fee.clone(), recent_blockhash).await?; - rpc.send_and_confirm_transaction(&transaction).await?; - - Ok(()) -} - -pub async fn create_and_buy_with_tip( - rpc: Arc, - fee_clients: Vec>, - payer: Arc, - mint: Keypair, - ipfs: TokenMetadataIPFS, - buy_sol_cost: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, - recent_blockhash: Hash, -) -> Result<(), anyhow::Error> { - let start_time = Instant::now(); - let mint = Arc::new(mint); - let build_instructions = build_create_and_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), ipfs.clone(), buy_sol_cost, slippage_basis_points).await?; - let mut handles = vec![]; - for fee_client in fee_clients { - let tip_account = fee_client.get_tip_account()?; - let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); - let transaction = build_create_and_buy_transaction_with_tip(/*rpc.clone(),*/ tip_account, payer.clone(), priority_fee.clone(), build_instructions.clone(), recent_blockhash).await?; - let handle = tokio::spawn(async move { - fee_client.send_transaction(TradeType::CreateAndBuy, &transaction).await.map_err(|e| anyhow!(e.to_string()))?; - println!("Total Jito create and buy operation time: {:?}ms", start_time.elapsed().as_millis()); - Ok::<(), anyhow::Error>(()) - }); - - handles.push(handle); - } - - for handle in handles { - match handle.await { - Ok(Ok(_)) => (), - Ok(Err(e)) => println!("Error in task: {}", e), - Err(e) => println!("Task join error: {}", e), - } - } - - Ok(()) -} - -pub async fn build_create_and_buy_transaction( - rpc: Arc, - payer: Arc, - mint: Arc, - ipfs: TokenMetadataIPFS, - buy_sol_cost: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, - recent_blockhash: Hash, -) -> Result { - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), - ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), - ]; - - let build_instructions = build_create_and_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), ipfs, buy_sol_cost, slippage_basis_points).await?; - instructions.extend(build_instructions); - - // let recent_blockhash = rpc.get_latest_blockhash().await?; - // let recent_blockhash = Hash::default(); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer.as_ref(), mint.as_ref()], - recent_blockhash, - ); - - Ok(transaction) -} - -pub async fn build_create_and_buy_transaction_with_tip( - // rpc: Arc, - tip_account: Arc, - payer: Arc, - priority_fee: PriorityFee, - build_instructions: Vec, - recent_blockhash: Hash, -) -> Result { - let tip_cache = TipCache::get_instance(); - let tip_amount = tip_cache.get_tip(); - - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), - ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), - system_instruction::transfer( - &payer.pubkey(), - &tip_account, - sol_to_lamports(tip_amount), - ), - ]; - instructions.extend(build_instructions); - - // let recent_blockhash = rpc.get_latest_blockhash().await?; - // let recent_blockhash = Hash::default(); - let v0_message: v0::Message = - v0::Message::try_compile(&payer.pubkey(), &instructions, &[], recent_blockhash)?; - - let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; - - Ok(transaction) -} - -pub async fn build_create_and_buy_instructions( - rpc: Arc, - payer: Arc, - mint: Arc, - ipfs: TokenMetadataIPFS, - buy_sol_cost: u64, - slippage_basis_points: Option, -) -> Result, anyhow::Error> { - if buy_sol_cost == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let (bonding_curve_account, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint.pubkey()).await?; - let creator_vault_pda = get_creator_vault_pda(&bonding_curve_account.creator).unwrap(); - let (buy_token_amount, max_sol_cost) = get_buy_token_amount(&bonding_curve_account, buy_sol_cost, slippage_basis_points)?; - - let mut instructions = vec![]; - - instructions.push(instruction::create( - payer.as_ref(), - mint.as_ref(), - instruction::Create { - _name: ipfs.metadata.name.clone(), - _symbol: ipfs.metadata.symbol.clone(), - _uri: ipfs.metadata_uri.clone(), - _creator: payer.pubkey(), - }, - )); - - instructions.push(create_associated_token_account( - &payer.pubkey(), - &payer.pubkey(), - &mint.pubkey(), - &constants::pumpfun::accounts::TOKEN_PROGRAM, - )); - - instructions.push(instruction::buy( - payer.as_ref(), - &mint.pubkey(), - &bonding_curve_pda, - &creator_vault_pda, - &constants::pumpfun::global_constants::FEE_RECIPIENT, - instruction::Buy { - _amount: buy_token_amount, - _max_sol_cost: max_sol_cost, - }, - )); - - Ok(instructions) -} diff --git a/src/pumpfun/sell.rs b/src/pumpfun/sell.rs index b8875da..2817c35 100755 --- a/src/pumpfun/sell.rs +++ b/src/pumpfun/sell.rs @@ -81,9 +81,6 @@ pub async fn sell_by_amount( 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, @@ -138,9 +135,6 @@ pub async fn sell_by_amount_with_tip( 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( rpc, fee_clients, diff --git a/src/pumpswap/pool.rs b/src/pumpswap/pool.rs index 23d5b38..7d3e643 100755 --- a/src/pumpswap/pool.rs +++ b/src/pumpswap/pool.rs @@ -1,7 +1,7 @@ -use solana_sdk::pubkey::Pubkey; +use crate::{common::SolanaRpcClient, constants::pumpswap::accounts}; use anyhow::anyhow; use solana_account_decoder::UiAccountEncoding; -use crate::{common::SolanaRpcClient, constants::pumpswap::accounts}; +use solana_sdk::pubkey::Pubkey; use std::str::FromStr; #[derive(Debug, Clone)] @@ -29,16 +29,39 @@ impl Pool { let pool_bump = data[0]; let index = u16::from_le_bytes([data[1], data[2]]); - let creator = Pubkey::new_from_array(data[3..35].try_into().map_err(|e| anyhow!("Failed to convert creator: {:?}", e))?); - let base_mint = Pubkey::new_from_array(data[35..67].try_into().map_err(|e| anyhow!("Failed to convert base_mint: {:?}", e))?); - let quote_mint = Pubkey::new_from_array(data[67..99].try_into().map_err(|e| anyhow!("Failed to convert quote_mint: {:?}", e))?); - let lp_mint = Pubkey::new_from_array(data[99..131].try_into().map_err(|e| anyhow!("Failed to convert lp_mint: {:?}", e))?); - let pool_base_token_account = Pubkey::new_from_array(data[131..163].try_into().map_err(|e| anyhow!("Failed to convert pool_base_token_account: {:?}", e))?); - let pool_quote_token_account = Pubkey::new_from_array(data[163..195].try_into().map_err(|e| anyhow!("Failed to convert pool_quote_token_account: {:?}", e))?); + let creator = Pubkey::new_from_array( + data[3..35] + .try_into() + .map_err(|e| anyhow!("Failed to convert creator: {:?}", e))?, + ); + let base_mint = Pubkey::new_from_array( + data[35..67] + .try_into() + .map_err(|e| anyhow!("Failed to convert base_mint: {:?}", e))?, + ); + let quote_mint = Pubkey::new_from_array( + data[67..99] + .try_into() + .map_err(|e| anyhow!("Failed to convert quote_mint: {:?}", e))?, + ); + let lp_mint = Pubkey::new_from_array( + data[99..131] + .try_into() + .map_err(|e| anyhow!("Failed to convert lp_mint: {:?}", e))?, + ); + let pool_base_token_account = Pubkey::new_from_array( + data[131..163] + .try_into() + .map_err(|e| anyhow!("Failed to convert pool_base_token_account: {:?}", e))?, + ); + let pool_quote_token_account = Pubkey::new_from_array( + data[163..195] + .try_into() + .map_err(|e| anyhow!("Failed to convert pool_quote_token_account: {:?}", e))?, + ); let lp_supply = u64::from_le_bytes([ - data[195], data[196], data[197], data[198], - data[199], data[200], data[201], data[202], + data[195], data[196], data[197], data[198], data[199], data[200], data[201], data[202], ]); Ok(Self { @@ -92,25 +115,21 @@ impl Pool { }; let program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM; - println!("program_id: {:?}", program_id); - let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?; + let accounts = rpc + .get_program_accounts_with_config(&program_id, config) + .await?; if accounts.is_empty() { return Err(anyhow!("No pool found for mint {}", mint)); } - let mut pools: Vec<_> = accounts.into_iter() - .filter_map(|(addr, acc)| { - Self::from_bytes(&acc.data) - .map(|pool| (addr, pool)) - .ok() - }) + let mut pools: Vec<_> = accounts + .into_iter() + .filter_map(|(addr, acc)| Self::from_bytes(&acc.data).map(|pool| (addr, pool)).ok()) .collect(); pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply)); let (address, pool) = pools[0].clone(); - println!("pool: {:?}", pool); - println!("address: {:?}", address); Ok((address, pool)) } @@ -118,11 +137,18 @@ impl Pool { &self, rpc: &SolanaRpcClient, ) -> Result<(u64, u64), anyhow::Error> { - let base_balance = rpc.get_token_account_balance(&self.pool_base_token_account).await?; - let quote_balance = rpc.get_token_account_balance(&self.pool_quote_token_account).await?; + let base_balance = rpc + .get_token_account_balance(&self.pool_base_token_account) + .await?; + let quote_balance = rpc + .get_token_account_balance(&self.pool_quote_token_account) + .await?; let base_amount = base_balance.amount.parse::().map_err(|e| anyhow!(e))?; - let quote_amount = quote_balance.amount.parse::().map_err(|e| anyhow!(e))?; + let quote_amount = quote_balance + .amount + .parse::() + .map_err(|e| anyhow!(e))?; Ok((base_amount, quote_amount)) } diff --git a/src/pumpswap/sell.rs b/src/pumpswap/sell.rs index ece9211..4baf49a 100755 --- a/src/pumpswap/sell.rs +++ b/src/pumpswap/sell.rs @@ -114,10 +114,6 @@ pub async fn sell_by_amount( user_base_token_account: Option, user_quote_token_account: Option, ) -> Result<(), anyhow::Error> { - if amount == 0 { - return Err(anyhow!("Amount must be greater than 0")); - } - sell( rpc, payer, @@ -249,10 +245,6 @@ pub async fn sell_by_amount_with_tip( user_base_token_account: Option, user_quote_token_account: Option, ) -> Result<(), anyhow::Error> { - if amount == 0 { - return Err(anyhow!("Amount must be greater than 0")); - } - sell_with_tip( rpc, swqos_clients, diff --git a/src/raydium_launchpad/buy.rs b/src/raydium_launchpad/buy.rs new file mode 100755 index 0000000..4dd24fb --- /dev/null +++ b/src/raydium_launchpad/buy.rs @@ -0,0 +1,107 @@ +use solana_hash::Hash; +use solana_sdk::{pubkey::Pubkey, signature::Keypair}; +use std::sync::Arc; + +use crate::swqos::SwqosClient; +use crate::trading::{ + core::params::{PumpSwapParams, RaydiumLaunchpadParams}, + factory::Protocol, + BuyParams, TradeFactory, +}; +use crate::{common::PriorityFee, SolanaRpcClient}; + +// Constants for compute budget +// Increased from 64KB to 256KB to handle larger transactions +const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 256 * 1024; + +// Buy tokens from a Pumpswap pool +pub async fn buy( + rpc: Arc, + payer: Arc, + mint: Pubkey, + virtual_base: u128, + virtual_quote: u128, + real_base_before: u128, + real_quote_before: u128, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, + auto_handle_wsol: bool, +) -> Result<(), anyhow::Error> { + // 创建执行器 + let executor = TradeFactory::create_executor(Protocol::RaydiumLaunchpad); + // 创建协议特定参数 + let protocol_params = Box::new(RaydiumLaunchpadParams { + auto_handle_wsol: auto_handle_wsol, + virtual_base: Some(virtual_base), + virtual_quote: Some(virtual_quote), + real_base_before: Some(real_base_before), + real_quote_before: Some(real_quote_before), + }); + // 创建买入参数 + let buy_params = BuyParams { + rpc: Some(rpc.clone()), + payer: payer, + mint: mint, + creator: Pubkey::default(), + amount_sol: amount_sol, + slippage_basis_points: slippage_basis_points, + priority_fee: priority_fee, + lookup_table_key: lookup_table_key, + recent_blockhash: recent_blockhash, + data_size_limit: MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT, + protocol_params, + }; + // 执行买入 + executor.buy(buy_params).await?; + Ok(()) +} + +// Buy tokens using a MEV service +pub async fn buy_with_tip( + rpc: Arc, + swqos_clients: Vec>, + payer: Arc, + mint: Pubkey, + virtual_base: u128, + virtual_quote: u128, + real_base_before: u128, + real_quote_before: u128, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, + auto_handle_wsol: bool, +) -> Result<(), anyhow::Error> { + // 创建执行器 + let executor = TradeFactory::create_executor(Protocol::RaydiumLaunchpad); + // 创建协议特定参数 + let protocol_params = Box::new(RaydiumLaunchpadParams { + auto_handle_wsol: auto_handle_wsol, + virtual_base: Some(virtual_base), + virtual_quote: Some(virtual_quote), + real_base_before: Some(real_base_before), + real_quote_before: Some(real_quote_before), + }); + // 创建买入参数 + let buy_params = BuyParams { + rpc: Some(rpc.clone()), + payer: payer, + mint: mint, + creator: Pubkey::default(), + amount_sol: amount_sol, + slippage_basis_points: slippage_basis_points, + priority_fee: priority_fee, + lookup_table_key: lookup_table_key, + recent_blockhash: recent_blockhash, + data_size_limit: MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT, + protocol_params, + }; + let buy_with_tip_params = buy_params.with_tip(swqos_clients); + // 执行买入 + executor.buy_with_tip(buy_with_tip_params).await?; + Ok(()) +} diff --git a/src/raydium_launchpad/common.rs b/src/raydium_launchpad/common.rs new file mode 100755 index 0000000..256dcd6 --- /dev/null +++ b/src/raydium_launchpad/common.rs @@ -0,0 +1,75 @@ +use anyhow::anyhow; +use solana_sdk::pubkey::Pubkey; +use spl_associated_token_account::get_associated_token_address; + +use crate::{common::SolanaRpcClient, constants}; + +pub fn get_amount_out( + amount_in: u64, + protocol_fee_rate: u128, + platform_fee_rate: u128, + share_fee_rate: u128, + virtual_base: u128, + virtual_quote: u128, + real_base_before: u128, + real_quote_before: u128, + slippage_basis_points: u128, +) -> u64 { + let amount_in_u128 = amount_in as u128; + let protocol_fee = (amount_in_u128 * protocol_fee_rate / 10000) as u128; + let platform_fee = (amount_in_u128 * platform_fee_rate / 10000) as u128; + let share_fee = (amount_in_u128 * share_fee_rate / 10000) as u128; + let amount_in_net = amount_in_u128 + .checked_sub(protocol_fee) + .unwrap() + .checked_sub(platform_fee) + .unwrap() + .checked_sub(share_fee) + .unwrap(); + let input_reserve = virtual_quote.checked_add(real_quote_before).unwrap(); + let output_reserve = virtual_base.checked_sub(real_base_before).unwrap(); + let numerator = amount_in_net.checked_mul(output_reserve).unwrap(); + let denominator = input_reserve.checked_add(amount_in_net).unwrap(); + let mut amount_out = numerator.checked_div(denominator).unwrap(); + + amount_out = amount_out - (amount_out * slippage_basis_points) / 10000; + amount_out as u64 +} + +pub fn get_pool_pda(base_mint: &Pubkey, quote_mint: &Pubkey) -> Option { + let seeds: &[&[u8]; 3] = &[ + constants::raydium_launchpad::seeds::POOL_SEED, + base_mint.as_ref(), + quote_mint.as_ref(), + ]; + let program_id: &Pubkey = &constants::raydium_launchpad::accounts::LAUNCHPAD_PROGRAM; + let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); + pda.map(|pubkey| pubkey.0) +} + +pub fn get_vault_pda(pool_state: &Pubkey, mint: &Pubkey) -> Option { + let seeds: &[&[u8]; 3] = &[ + constants::raydium_launchpad::seeds::POOL_VAULT_SEED, + pool_state.as_ref(), + mint.as_ref(), + ]; + let program_id: &Pubkey = &constants::raydium_launchpad::accounts::LAUNCHPAD_PROGRAM; + let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); + pda.map(|pubkey| pubkey.0) +} + +pub async fn get_token_balance( + rpc: &SolanaRpcClient, + payer: &Pubkey, + mint: &Pubkey, +) -> Result { + println!("payer: {:?}", payer); + println!("mint: {:?}", mint); + let ata = get_associated_token_address(payer, mint); + let balance = rpc.get_token_account_balance(&ata).await?; + let balance_u64 = balance + .amount + .parse::() + .map_err(|_| anyhow!("Failed to parse token balance"))?; + Ok(balance_u64) +} diff --git a/src/raydium_launchpad/mod.rs b/src/raydium_launchpad/mod.rs new file mode 100755 index 0000000..9c60348 --- /dev/null +++ b/src/raydium_launchpad/mod.rs @@ -0,0 +1,4 @@ +pub mod buy; +pub mod sell; +pub mod common; +pub mod pool; diff --git a/src/raydium_launchpad/pool.rs b/src/raydium_launchpad/pool.rs new file mode 100755 index 0000000..2cefaa2 --- /dev/null +++ b/src/raydium_launchpad/pool.rs @@ -0,0 +1,62 @@ +use crate::{common::SolanaRpcClient, constants::raydium_launchpad::accounts}; +use anyhow::anyhow; +use borsh::BorshDeserialize; +use solana_sdk::pubkey::Pubkey; + +#[derive(Debug, Clone, BorshDeserialize)] +pub struct VestingSchedule { + pub total_locked_amount: u64, + pub cliff_period: u64, + pub unlock_period: u64, + pub start_time: u64, + pub allocated_share_amount: u64, +} + +#[derive(Debug, Clone, BorshDeserialize)] +pub struct Pool { + pub epoch: u64, + pub auth_bump: u8, + pub status: u8, + pub base_decimals: u8, + pub quote_decimals: u8, + pub migrate_type: u8, + pub supply: u64, + pub total_base_sell: u64, + pub virtual_base: u64, + pub virtual_quote: u64, + pub real_base: u64, + pub real_quote: u64, + pub total_quote_fund_raising: u64, + pub quote_protocol_fee: u64, + pub platform_fee: u64, + pub migrate_fee: u64, + pub vesting_schedule: VestingSchedule, + pub global_config: Pubkey, + pub platform_config: Pubkey, + pub base_mint: Pubkey, + pub quote_mint: Pubkey, + pub base_vault: Pubkey, + pub quote_vault: Pubkey, + pub creator: Pubkey, + pub padding: [u64; 8], +} + +impl Pool { + pub fn from_bytes(data: &[u8]) -> Result { + let pool = Pool::try_from_slice(&data[8..])?; + Ok(pool) + } + + pub async fn fetch( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, + ) -> Result { + let account = rpc.get_account(pool_address).await?; + + if account.owner != accounts::LAUNCHPAD_PROGRAM { + return Err(anyhow!("Account is not owned by RaydiumLaunchpad program")); + } + + Self::from_bytes(&account.data) + } +} diff --git a/src/raydium_launchpad/sell.rs b/src/raydium_launchpad/sell.rs new file mode 100755 index 0000000..7c30d75 --- /dev/null +++ b/src/raydium_launchpad/sell.rs @@ -0,0 +1,240 @@ +use anyhow::anyhow; +use solana_hash::Hash; +use solana_sdk::{pubkey::Pubkey, signature::Keypair}; +use std::sync::Arc; + +use crate::common::{PriorityFee, SolanaRpcClient}; +use crate::pumpswap::common::get_token_balance; +use crate::swqos::SwqosClient; +use crate::trading::{ + core::params::RaydiumLaunchpadParams, factory::Protocol, SellParams, TradeFactory, +}; + +// Sell tokens to a Pumpswap pool +pub async fn sell( + rpc: Arc, + payer: Arc, + mint: Pubkey, + virtual_base: u128, + virtual_quote: u128, + real_base_before: u128, + real_quote_before: u128, + amount_token: Option, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + let executor = TradeFactory::create_executor(Protocol::RaydiumLaunchpad); + // 创建PumpFun协议参数 + let protocol_params = Box::new(RaydiumLaunchpadParams { + virtual_base: Some(virtual_base), + virtual_quote: Some(virtual_quote), + real_base_before: Some(real_base_before), + real_quote_before: Some(real_quote_before), + auto_handle_wsol: true, + }); + // 创建卖出参数 + let sell_params = SellParams { + rpc: Some(rpc.clone()), + payer: payer.clone(), + mint, + creator: Pubkey::default(), + amount_token: amount_token, + slippage_basis_points: slippage_basis_points, + priority_fee: priority_fee.clone(), + lookup_table_key, + recent_blockhash, + protocol_params, + }; + // 执行卖出交易 + executor.sell(sell_params).await?; + Ok(()) +} + +// Sell tokens by percentage +pub async fn sell_by_percent( + rpc: Arc, + payer: Arc, + mint: Pubkey, + virtual_base: u128, + virtual_quote: u128, + real_base_before: u128, + real_quote_before: u128, + percent: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + if percent == 0 || percent > 100 { + return Err(anyhow!("Percentage must be between 1 and 100")); + } + let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; + let amount = balance_u64 * percent / 100; + sell( + rpc, + payer, + mint, + virtual_base, + virtual_quote, + real_base_before, + real_quote_before, + Some(amount), + slippage_basis_points, + priority_fee, + lookup_table_key, + recent_blockhash, + ) + .await +} + +/// Sell tokens by amount +pub async fn sell_by_amount( + rpc: Arc, + payer: Arc, + mint: Pubkey, + virtual_base: u128, + virtual_quote: u128, + real_base_before: u128, + real_quote_before: u128, + amount: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + sell( + rpc, + payer, + mint, + virtual_base, + virtual_quote, + real_base_before, + real_quote_before, + Some(amount), + slippage_basis_points, + priority_fee, + lookup_table_key, + recent_blockhash, + ) + .await +} + +// Sell tokens using a MEV service +pub async fn sell_with_tip( + rpc: Arc, + swqos_clients: Vec>, + payer: Arc, + mint: Pubkey, + virtual_base: u128, + virtual_quote: u128, + real_base_before: u128, + real_quote_before: u128, + amount_token: Option, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + let executor = TradeFactory::create_executor(Protocol::RaydiumLaunchpad); + // 创建PumpFun协议参数 + let protocol_params = Box::new(RaydiumLaunchpadParams { + virtual_base: Some(virtual_base), + virtual_quote: Some(virtual_quote), + real_base_before: Some(real_base_before), + real_quote_before: Some(real_quote_before), + auto_handle_wsol: true, + }); + // 创建卖出参数 + let sell_params = SellParams { + rpc: Some(rpc.clone()), + payer: payer.clone(), + mint, + creator: Pubkey::default(), + amount_token: amount_token, + slippage_basis_points: slippage_basis_points, + priority_fee: priority_fee.clone(), + lookup_table_key, + recent_blockhash, + protocol_params, + }; + let sell_with_tip_params = sell_params.with_tip(swqos_clients); + // 执行卖出交易 + executor.sell_with_tip(sell_with_tip_params).await?; + Ok(()) +} + +// Sell tokens by percentage using a MEV service +pub async fn sell_by_percent_with_tip( + rpc: Arc, + swqos_clients: Vec>, + payer: Arc, + mint: Pubkey, + virtual_base: u128, + virtual_quote: u128, + real_base_before: u128, + real_quote_before: u128, + percent: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + if percent == 0 || percent > 100 { + return Err(anyhow!("Percentage must be between 1 and 100")); + } + + let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; + let amount = balance_u64 * percent / 100; + sell_with_tip( + rpc, + swqos_clients, + payer, + mint, + virtual_base, + virtual_quote, + real_base_before, + real_quote_before, + Some(amount), + slippage_basis_points, + priority_fee, + lookup_table_key, + recent_blockhash, + ) + .await +} + +// Sell tokens by amount using a MEV service +pub async fn sell_by_amount_with_tip( + rpc: Arc, + swqos_clients: Vec>, + payer: Arc, + mint: Pubkey, + virtual_base: u128, + virtual_quote: u128, + real_base_before: u128, + real_quote_before: u128, + amount: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + sell_with_tip( + rpc, + swqos_clients, + payer, + mint, + virtual_base, + virtual_quote, + real_base_before, + real_quote_before, + Some(amount), + slippage_basis_points, + priority_fee, + lookup_table_key, + recent_blockhash, + ) + .await +} diff --git a/src/trading/core/constants.rs b/src/trading/core/constants.rs deleted file mode 100755 index e95f3ac..0000000 --- a/src/trading/core/constants.rs +++ /dev/null @@ -1 +0,0 @@ -pub const DEFAULT_SLIPPAGE_BASIS_POINTS: u64 = 100; \ No newline at end of file diff --git a/src/trading/core/mod.rs b/src/trading/core/mod.rs index 396c7ce..398bf30 100755 --- a/src/trading/core/mod.rs +++ b/src/trading/core/mod.rs @@ -1,4 +1,3 @@ -pub mod constants; pub mod params; pub mod traits; pub mod executor; diff --git a/src/trading/core/params.rs b/src/trading/core/params.rs index 9a67ea3..35c0c75 100755 --- a/src/trading/core/params.rs +++ b/src/trading/core/params.rs @@ -122,6 +122,26 @@ impl ProtocolParams for PumpSwapParams { } } +/// RaydiumLaunchpad协议特定参数 +#[derive(Clone)] +pub struct RaydiumLaunchpadParams { + pub virtual_base: Option, + pub virtual_quote: Option, + pub real_base_before: Option, + pub real_quote_before: Option, + pub auto_handle_wsol: bool, +} + +impl ProtocolParams for RaydiumLaunchpadParams { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + impl BuyParams { /// 转换为BuyWithTipParams pub fn with_tip(self, swqos_clients: Vec>) -> BuyWithTipParams { diff --git a/src/trading/factory.rs b/src/trading/factory.rs index b6986f0..ec799be 100755 --- a/src/trading/factory.rs +++ b/src/trading/factory.rs @@ -1,6 +1,8 @@ use anyhow::{anyhow, Result}; use std::sync::Arc; +use crate::trading::protocols::raydium_launchpad::RaydiumLaunchpadInstructionBuilder; + use super::{ core::{executor::GenericTradeExecutor, traits::TradeExecutor}, protocols::{pumpfun::PumpFunInstructionBuilder, pumpswap::PumpSwapInstructionBuilder}, @@ -11,6 +13,7 @@ use super::{ pub enum Protocol { PumpFun, PumpSwap, + RaydiumLaunchpad, } impl std::fmt::Display for Protocol { @@ -18,6 +21,7 @@ impl std::fmt::Display for Protocol { match self { Protocol::PumpFun => write!(f, "PumpFun"), Protocol::PumpSwap => write!(f, "PumpSwap"), + Protocol::RaydiumLaunchpad => write!(f, "RaydiumLaunchpad"), } } } @@ -29,6 +33,7 @@ impl std::str::FromStr for Protocol { match s.to_lowercase().as_str() { "pumpfun" => Ok(Protocol::PumpFun), "pumpswap" => Ok(Protocol::PumpSwap), + "raydiumlaunchpad" => Ok(Protocol::RaydiumLaunchpad), _ => Err(anyhow!("Unsupported protocol: {}", s)), } } @@ -49,12 +54,23 @@ impl TradeFactory { let instruction_builder = Arc::new(PumpSwapInstructionBuilder); Arc::new(GenericTradeExecutor::new(instruction_builder, "PumpSwap")) } + Protocol::RaydiumLaunchpad => { + let instruction_builder = Arc::new(RaydiumLaunchpadInstructionBuilder); + Arc::new(GenericTradeExecutor::new( + instruction_builder, + "RaydiumLaunchpad", + )) + } } } /// 获取所有支持的协议 pub fn supported_protocols() -> Vec { - vec![Protocol::PumpFun, Protocol::PumpSwap] + vec![ + Protocol::PumpFun, + Protocol::PumpSwap, + Protocol::RaydiumLaunchpad, + ] } /// 检查协议是否支持 @@ -62,32 +78,3 @@ impl TradeFactory { Self::supported_protocols().contains(protocol) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_protocol_from_str() { - assert_eq!("pumpfun".parse::().unwrap(), Protocol::PumpFun); - assert_eq!("pumpswap".parse::().unwrap(), Protocol::PumpSwap); - assert_eq!("PUMPFUN".parse::().unwrap(), Protocol::PumpFun); - assert!("unknown".parse::().is_err()); - } - - #[test] - fn test_create_executor() { - let pumpfun_executor = TradeFactory::create_executor(Protocol::PumpFun); - assert_eq!(pumpfun_executor.protocol_name(), "PumpFun"); - - let pumpswap_executor = TradeFactory::create_executor(Protocol::PumpSwap); - assert_eq!(pumpswap_executor.protocol_name(), "PumpSwap"); - } - - #[test] - fn test_supported_protocols() { - let protocols = TradeFactory::supported_protocols(); - assert!(protocols.contains(&Protocol::PumpFun)); - assert!(protocols.contains(&Protocol::PumpSwap)); - } -} \ No newline at end of file diff --git a/src/trading/protocols/mod.rs b/src/trading/protocols/mod.rs index 052b725..c3194f2 100755 --- a/src/trading/protocols/mod.rs +++ b/src/trading/protocols/mod.rs @@ -1,2 +1,3 @@ pub mod pumpfun; -pub mod pumpswap; \ No newline at end of file +pub mod pumpswap; +pub mod raydium_launchpad; \ No newline at end of file diff --git a/src/trading/protocols/pumpfun.rs b/src/trading/protocols/pumpfun.rs index 27555bb..fb007eb 100755 --- a/src/trading/protocols/pumpfun.rs +++ b/src/trading/protocols/pumpfun.rs @@ -10,14 +10,13 @@ use std::sync::Arc; use crate::{ accounts::BondingCurveAccount, - constants::{self, pumpfun::global_constants::FEE_RECIPIENT, trade_type::SNIPER_BUY}, + constants::{self, pumpfun::{global_constants::FEE_RECIPIENT, trade::DEFAULT_SLIPPAGE}, trade_type::SNIPER_BUY}, instruction, pumpfun::common::{ calculate_with_slippage_buy, get_bonding_curve_account_v2, get_bonding_curve_pda, get_buy_token_amount_from_sol_amount, get_creator_vault_pda, init_bonding_curve_account, }, trading::core::{ - constants::DEFAULT_SLIPPAGE_BASIS_POINTS, params::{BuyParams, PumpFunParams, SellParams}, traits::InstructionBuilder, }, @@ -50,7 +49,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder { params.amount_sol, params .slippage_basis_points - .unwrap_or(DEFAULT_SLIPPAGE_BASIS_POINTS), + .unwrap_or(DEFAULT_SLIPPAGE), ); let creator_vault_pda = bonding_curve.get_creator_vault_pda(); diff --git a/src/trading/protocols/pumpswap.rs b/src/trading/protocols/pumpswap.rs index e4125b0..199d46a 100755 --- a/src/trading/protocols/pumpswap.rs +++ b/src/trading/protocols/pumpswap.rs @@ -13,7 +13,6 @@ use crate::{ get_token_balance, }, trading::core::{ - constants::DEFAULT_SLIPPAGE_BASIS_POINTS, params::{BuyParams, PumpSwapParams, SellParams}, traits::InstructionBuilder, }, @@ -367,6 +366,17 @@ impl PumpSwapInstructionBuilder { let mut instructions = vec![]; + // 插入wsol + instructions.push( + // 创建wSOL ATA账户,如果不存在 + create_associated_token_account_idempotent( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + &accounts::TOKEN_PROGRAM, + ), + ); + // 创建用户的代币账户 instructions.push(create_associated_token_account_idempotent( ¶ms.payer.pubkey(), diff --git a/src/trading/protocols/raydium_launchpad.rs b/src/trading/protocols/raydium_launchpad.rs new file mode 100755 index 0000000..e84f632 --- /dev/null +++ b/src/trading/protocols/raydium_launchpad.rs @@ -0,0 +1,284 @@ +use anyhow::{anyhow, Result}; +use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer}; +use spl_associated_token_account::instruction::create_associated_token_account_idempotent; + +use crate::{ + constants::raydium_launchpad::{ + accounts, trade::DEFAULT_SLIPPAGE, BUY_EXECT_IN_DISCRIMINATOR, SELL_EXECT_IN_DISCRIMINATOR, + }, + raydium_launchpad::{ + common::{get_amount_out, get_pool_pda, get_token_balance, get_vault_pda}, + pool::Pool, + }, + trading::core::{ + params::{BuyParams, RaydiumLaunchpadParams, SellParams}, + traits::InstructionBuilder, + }, +}; + +/// RaydiumLaunchpad协议的指令构建器 +pub struct RaydiumLaunchpadInstructionBuilder; + +#[async_trait::async_trait] +impl InstructionBuilder for RaydiumLaunchpadInstructionBuilder { + async fn build_buy_instructions(&self, params: &BuyParams) -> Result> { + if params.amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + self.build_buy_instructions_with_accounts(params).await + } + + async fn build_sell_instructions(&self, params: &SellParams) -> Result> { + self.build_sell_instructions_with_accounts(params).await + } +} + +impl RaydiumLaunchpadInstructionBuilder { + /// 使用提供的账户信息构建买入指令 + async fn build_buy_instructions_with_accounts( + &self, + params: &BuyParams, + ) -> Result> { + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for RaydiumLaunchpad"))?; + + let pool_state = get_pool_pda(¶ms.mint, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); + + // 创建用户代币账户 + let user_base_token_account = spl_associated_token_account::get_associated_token_address( + ¶ms.payer.pubkey(), + ¶ms.mint, + ); + let user_quote_token_account = spl_associated_token_account::get_associated_token_address( + ¶ms.payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + ); + + // 获取池的代币账户 + let base_vault_account = get_vault_pda(&pool_state, ¶ms.mint).unwrap(); + let quote_vault_account = + get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); + + let mut virtual_base = protocol_params.virtual_base.unwrap_or(0); + let mut virtual_quote = protocol_params.virtual_quote.unwrap_or(0); + let mut real_base_before = protocol_params.real_base_before.unwrap_or(0); + let mut real_quote_before = protocol_params.real_quote_before.unwrap_or(0); + + if virtual_base == 0 + || virtual_quote == 0 + || real_base_before == 0 + || real_quote_before == 0 + { + let pool = Pool::fetch(params.rpc.as_ref().unwrap(), &pool_state).await?; + virtual_base = pool.virtual_base as u128; + virtual_quote = pool.virtual_quote as u128; + real_base_before = pool.real_base as u128; + real_quote_before = pool.real_quote as u128; + } + + let amount_in: u64 = params.amount_sol; + let share_fee_rate: u64 = 0; + let minimum_amount_out: u64 = get_amount_out( + amount_in, + accounts::PROTOCOL_FEE_RATE, + accounts::PLATFORM_FEE_RATE, + accounts::SHARE_FEE_RATE, + virtual_base, + virtual_quote, + real_base_before, + real_quote_before, + params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE) as u128, + ); + + let mut instructions = vec![]; + + if protocol_params.auto_handle_wsol { + // 插入wsol + instructions.push( + // 创建wSOL ATA账户,如果不存在 + create_associated_token_account_idempotent( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + &accounts::TOKEN_PROGRAM, + ), + ); + instructions.push( + // 将SOL转入wSOL ATA账户 + solana_sdk::system_instruction::transfer( + ¶ms.payer.pubkey(), + &user_quote_token_account, + amount_in, + ), + ); + + // 同步wSOL余额 + instructions.push( + spl_token::instruction::sync_native( + &accounts::TOKEN_PROGRAM, + &user_quote_token_account, + ) + .unwrap(), + ); + } + + // 创建用户的基础代币账户 + instructions.push(create_associated_token_account_idempotent( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + ¶ms.mint, + &accounts::TOKEN_PROGRAM, + )); + + // 创建买入指令 + let accounts = vec![ + solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_CONFIG, false), // Global Config (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::PLATFORM_CONFIG, false), // Platform Config (readonly) + solana_sdk::instruction::AccountMeta::new(pool_state, false), // Pool State + solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // User Base Token + solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // User Quote Token + solana_sdk::instruction::AccountMeta::new(base_vault_account, false), // Base Vault + solana_sdk::instruction::AccountMeta::new(quote_vault_account, false), // Quote Vault + solana_sdk::instruction::AccountMeta::new(params.mint, false), // Base Token Mint (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Quote Token Mint (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Base Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Quote Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // Event Authority (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::LAUNCHPAD_PROGRAM, false), // Program (readonly) + ]; + // 创建指令数据 + let mut data = vec![]; + data.extend_from_slice(&BUY_EXECT_IN_DISCRIMINATOR); + data.extend_from_slice(&amount_in.to_le_bytes()); + data.extend_from_slice(&minimum_amount_out.to_le_bytes()); + data.extend_from_slice(&share_fee_rate.to_le_bytes()); + + instructions.push(Instruction { + program_id: accounts::LAUNCHPAD_PROGRAM, + accounts, + data, + }); + + if protocol_params.auto_handle_wsol { + // 关闭wSOL ATA账户,回收租金 + instructions.push( + spl_token::instruction::close_account( + &accounts::TOKEN_PROGRAM, + &user_quote_token_account, + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &[], + ) + .unwrap(), + ); + } + + Ok(instructions) + } + + /// 使用提供的账户信息构建卖出指令 + async fn build_sell_instructions_with_accounts( + &self, + params: &SellParams, + ) -> Result> { + if params.rpc.is_none() { + return Err(anyhow!("RPC is not set")); + } + let rpc = params.rpc.as_ref().unwrap().clone(); + + // 获取代币余额 + let mut amount = params.amount_token; + if params.amount_token.is_none() || params.amount_token.unwrap_or(0) == 0 { + let balance_u64 = + get_token_balance(rpc.as_ref(), ¶ms.payer.pubkey(), ¶ms.mint).await?; + amount = Some(balance_u64); + } + let amount = amount.unwrap_or(0); + + if amount == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + // 计算预期的SOL数量 + let minimum_amount_out: u64 = 1; + + let pool_state = get_pool_pda(¶ms.mint, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); + + // 创建用户代币账户 + let user_base_token_account = spl_associated_token_account::get_associated_token_address( + ¶ms.payer.pubkey(), + ¶ms.mint, + ); + let user_quote_token_account = spl_associated_token_account::get_associated_token_address( + ¶ms.payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + ); + + // 获取池的代币账户 + let base_vault_account = get_vault_pda(&pool_state, ¶ms.mint).unwrap(); + let quote_vault_account = + get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); + + let share_fee_rate: u64 = 0; + + let mut instructions = vec![]; + + // 插入wsol + instructions.push( + // 创建wSOL ATA账户,如果不存在 + create_associated_token_account_idempotent( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + &accounts::TOKEN_PROGRAM, + ), + ); + + // 创建用户的代币账户 + instructions.push(create_associated_token_account_idempotent( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + ¶ms.mint, + &accounts::TOKEN_PROGRAM, + )); + + // 创建卖出指令 + let accounts = vec![ + solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_CONFIG, false), // Global Config (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::PLATFORM_CONFIG, false), // Platform Config (readonly) + solana_sdk::instruction::AccountMeta::new(pool_state, false), // Pool State + solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // User Base Token + solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // User Quote Token + solana_sdk::instruction::AccountMeta::new(base_vault_account, false), // Base Vault + solana_sdk::instruction::AccountMeta::new(quote_vault_account, false), // Quote Vault + solana_sdk::instruction::AccountMeta::new(params.mint, false), // Base Token Mint (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Quote Token Mint (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Base Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Quote Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // Event Authority (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::LAUNCHPAD_PROGRAM, false), // Program (readonly) + ]; + + // 创建指令数据 + let mut data = vec![]; + data.extend_from_slice(&SELL_EXECT_IN_DISCRIMINATOR); + data.extend_from_slice(&amount.to_le_bytes()); + data.extend_from_slice(&minimum_amount_out.to_le_bytes()); + data.extend_from_slice(&share_fee_rate.to_le_bytes()); + + instructions.push(Instruction { + program_id: accounts::LAUNCHPAD_PROGRAM, + accounts, + data, + }); + + Ok(instructions) + } +}