From 63109713f4b471fe259307c78d09e119b79df85b Mon Sep 17 00:00:00 2001 From: sgxiang Date: Fri, 6 Jun 2025 22:13:19 +0800 Subject: [PATCH 1/8] refactor: organize code into multi-protocol architecture with PumpFun submodules --- src/accounts/bonding_curve.rs | 2 +- src/accounts/global.rs | 2 +- src/common/address_lookup.rs | 48 +++--- src/common/mod.rs | 6 +- src/common/{ => pumpfun}/logs_data.rs | 0 src/common/{ => pumpfun}/logs_events.rs | 2 +- src/common/{ => pumpfun}/logs_filters.rs | 4 +- src/common/{ => pumpfun}/logs_parser.rs | 2 +- src/common/{ => pumpfun}/logs_subscribe.rs | 4 +- src/common/pumpfun/mod.rs | 11 ++ src/common/types.rs | 2 +- src/constants/mod.rs | 176 +-------------------- src/constants/pumpfun/mod.rs | 174 ++++++++++++++++++++ src/grpc/shred_stream.rs | 6 +- src/grpc/yellow_stone.rs | 6 +- src/instruction/mod.rs | 12 +- src/lib.rs | 4 +- src/main.rs | 4 +- src/pumpfun/buy.rs | 4 +- src/pumpfun/common.rs | 20 +-- src/pumpfun/sell.rs | 2 +- src/swqos/mod.rs | 2 +- 22 files changed, 251 insertions(+), 242 deletions(-) rename src/common/{ => pumpfun}/logs_data.rs (100%) rename src/common/{ => pumpfun}/logs_events.rs (96%) rename src/common/{ => pumpfun}/logs_filters.rs (97%) rename src/common/{ => pumpfun}/logs_parser.rs (99%) rename src/common/{ => pumpfun}/logs_subscribe.rs (96%) create mode 100644 src/common/pumpfun/mod.rs mode change 100755 => 100644 src/constants/mod.rs create mode 100755 src/constants/pumpfun/mod.rs diff --git a/src/accounts/bonding_curve.rs b/src/accounts/bonding_curve.rs index 4e3d59a..2c7ef40 100755 --- a/src/accounts/bonding_curve.rs +++ b/src/accounts/bonding_curve.rs @@ -28,7 +28,7 @@ use serde::{Serialize, Deserialize}; use solana_sdk::pubkey::Pubkey; -use crate::{constants::global_constants::{INITIAL_REAL_TOKEN_RESERVES, INITIAL_VIRTUAL_SOL_RESERVES, INITIAL_VIRTUAL_TOKEN_RESERVES, TOKEN_TOTAL_SUPPLY}, pumpfun::common::{get_bonding_curve_pda, get_creator_vault_pda}}; +use crate::{constants::pumpfun::global_constants::{INITIAL_REAL_TOKEN_RESERVES, INITIAL_VIRTUAL_SOL_RESERVES, INITIAL_VIRTUAL_TOKEN_RESERVES, TOKEN_TOTAL_SUPPLY}, pumpfun::common::{get_bonding_curve_pda, get_creator_vault_pda}}; /// Represents the global configuration account for token pricing and fees #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/accounts/global.rs b/src/accounts/global.rs index b424a63..3a5c414 100755 --- a/src/accounts/global.rs +++ b/src/accounts/global.rs @@ -26,7 +26,7 @@ use solana_sdk::pubkey::Pubkey; use serde::{Serialize, Deserialize}; -use crate::constants::global_constants::*; +use crate::constants::pumpfun::global_constants::*; /// Represents the global configuration account for token pricing and fees #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/common/address_lookup.rs b/src/common/address_lookup.rs index ffbc5be..10af7e4 100755 --- a/src/common/address_lookup.rs +++ b/src/common/address_lookup.rs @@ -351,14 +351,14 @@ pub async fn extend_pumpfun_address_to_lookup_table( pub fn get_pumpfun_addresses(payer: Pubkey, include_addresses: Vec) -> Vec { let mut addresses = vec![ payer, - constants::accounts::PUMPFUN, - constants::accounts::SYSTEM_PROGRAM, - constants::accounts::TOKEN_PROGRAM, - constants::accounts::RENT, - constants::accounts::EVENT_AUTHORITY, - constants::accounts::ASSOCIATED_TOKEN_PROGRAM, - constants::global_constants::GLOBAL_ACCOUNT, - constants::global_constants::FEE_RECIPIENT, + constants::pumpfun::accounts::PUMPFUN, + constants::pumpfun::accounts::SYSTEM_PROGRAM, + constants::pumpfun::accounts::TOKEN_PROGRAM, + constants::pumpfun::accounts::RENT, + constants::pumpfun::accounts::EVENT_AUTHORITY, + constants::pumpfun::accounts::ASSOCIATED_TOKEN_PROGRAM, + constants::pumpfun::global_constants::GLOBAL_ACCOUNT, + constants::pumpfun::global_constants::FEE_RECIPIENT, ]; addresses.extend(include_addresses); @@ -369,22 +369,22 @@ pub fn get_pumpfun_addresses(payer: Pubkey, include_addresses: Vec) -> V pub fn get_pumpfun_filtered_addresses(payer: Pubkey, include_addresses: Vec) -> Vec { let mut addresses = vec![ payer, - constants::accounts::PUMPFUN, - constants::accounts::SYSTEM_PROGRAM, - constants::accounts::TOKEN_PROGRAM, - constants::accounts::RENT, - constants::accounts::EVENT_AUTHORITY, - constants::accounts::ASSOCIATED_TOKEN_PROGRAM, - constants::global_constants::GLOBAL_ACCOUNT, - constants::global_constants::FEE_RECIPIENT, - constants::global_constants::PUMPFUN_AMM_FEE_1, - constants::global_constants::PUMPFUN_AMM_FEE_2, - constants::global_constants::PUMPFUN_AMM_FEE_3, - constants::global_constants::PUMPFUN_AMM_FEE_4, - constants::global_constants::PUMPFUN_AMM_FEE_5, - constants::global_constants::PUMPFUN_AMM_FEE_6, - constants::global_constants::PUMPFUN_AMM_FEE_7, - // constants::global_constants::PUMPFUN_AMM_FEE_8, + constants::pumpfun::accounts::PUMPFUN, + constants::pumpfun::accounts::SYSTEM_PROGRAM, + constants::pumpfun::accounts::TOKEN_PROGRAM, + constants::pumpfun::accounts::RENT, + constants::pumpfun::accounts::EVENT_AUTHORITY, + constants::pumpfun::accounts::ASSOCIATED_TOKEN_PROGRAM, + constants::pumpfun::global_constants::GLOBAL_ACCOUNT, + constants::pumpfun::global_constants::FEE_RECIPIENT, + constants::pumpfun::global_constants::PUMPFUN_AMM_FEE_1, + constants::pumpfun::global_constants::PUMPFUN_AMM_FEE_2, + constants::pumpfun::global_constants::PUMPFUN_AMM_FEE_3, + constants::pumpfun::global_constants::PUMPFUN_AMM_FEE_4, + constants::pumpfun::global_constants::PUMPFUN_AMM_FEE_5, + constants::pumpfun::global_constants::PUMPFUN_AMM_FEE_6, + constants::pumpfun::global_constants::PUMPFUN_AMM_FEE_7, + // constants::pumpfun::global_constants::PUMPFUN_AMM_FEE_8, ]; addresses.extend(include_addresses); diff --git a/src/common/mod.rs b/src/common/mod.rs index d4c5dfe..89da297 100755 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,8 +1,4 @@ -pub mod logs_data; -pub mod logs_parser; -pub mod logs_filters; -pub mod logs_subscribe; -pub mod logs_events; +pub mod pumpfun; pub mod address_lookup; pub mod nonce_cache; pub mod tip_cache; diff --git a/src/common/logs_data.rs b/src/common/pumpfun/logs_data.rs similarity index 100% rename from src/common/logs_data.rs rename to src/common/pumpfun/logs_data.rs diff --git a/src/common/logs_events.rs b/src/common/pumpfun/logs_events.rs similarity index 96% rename from src/common/logs_events.rs rename to src/common/pumpfun/logs_events.rs index e4c3d63..a57bb1d 100755 --- a/src/common/logs_events.rs +++ b/src/common/pumpfun/logs_events.rs @@ -1,7 +1,7 @@ use base64::engine::general_purpose; use base64::Engine; use regex::Regex; -use crate::common::logs_data::{CreateTokenInfo, TradeInfo, EventTrait, TransferInfo, TipInfo}; +use crate::common::pumpfun::logs_data::{CreateTokenInfo, TradeInfo, EventTrait, TransferInfo, TipInfo}; pub const PROGRAM_DATA: &str = "Program data: "; diff --git a/src/common/logs_filters.rs b/src/common/pumpfun/logs_filters.rs similarity index 97% rename from src/common/logs_filters.rs rename to src/common/pumpfun/logs_filters.rs index d0b6c96..74705e5 100755 --- a/src/common/logs_filters.rs +++ b/src/common/pumpfun/logs_filters.rs @@ -1,5 +1,5 @@ -use crate::common::logs_data::DexInstruction; -use crate::common::logs_parser::{parse_create_token_data, parse_trade_data, parse_instruction_create_token_data, parse_instruction_trade_data}; +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; diff --git a/src/common/logs_parser.rs b/src/common/pumpfun/logs_parser.rs similarity index 99% rename from src/common/logs_parser.rs rename to src/common/pumpfun/logs_parser.rs index 7832f74..dd5dd7d 100755 --- a/src/common/logs_parser.rs +++ b/src/common/pumpfun/logs_parser.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use crate::error::{ClientError, ClientResult}; -use crate::common::{ +use crate::common::pumpfun::{ logs_data::{DexInstruction, CreateTokenInfo, TradeInfo}, logs_filters::LogFilter }; diff --git a/src/common/logs_subscribe.rs b/src/common/pumpfun/logs_subscribe.rs similarity index 96% rename from src/common/logs_subscribe.rs rename to src/common/pumpfun/logs_subscribe.rs index 8578e7b..3b4ac5d 100755 --- a/src/common/logs_subscribe.rs +++ b/src/common/pumpfun/logs_subscribe.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use tokio::sync::mpsc; use tokio::task::JoinHandle; use futures::StreamExt; -use crate::{constants, common::{ +use crate::{constants, common::pumpfun::{ logs_data::DexInstruction, logs_events::DexEvent, logs_filters::LogFilter }}; @@ -41,7 +41,7 @@ pub async fn tokens_subscription( where F: Fn(PumpfunEvent) + Send + Sync + 'static, { - let program_address = constants::accounts::PUMPFUN.to_string(); + let program_address = constants::pumpfun::accounts::PUMPFUN.to_string(); let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]); let logs_config = RpcTransactionLogsConfig { diff --git a/src/common/pumpfun/mod.rs b/src/common/pumpfun/mod.rs new file mode 100644 index 0000000..8de9d17 --- /dev/null +++ b/src/common/pumpfun/mod.rs @@ -0,0 +1,11 @@ +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/types.rs b/src/common/types.rs index 4dfd3d9..47095a4 100755 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use solana_client::rpc_client::RpcClient; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair}; use serde::Deserialize; -use crate::{constants::trade::{DEFAULT_BUY_TIP_FEE, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SELL_TIP_FEE}, swqos::FeeClient}; +use crate::{constants::pumpfun::trade::{DEFAULT_BUY_TIP_FEE, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SELL_TIP_FEE}, swqos::FeeClient}; #[derive(Debug, Clone, PartialEq)] pub enum FeeType { diff --git a/src/constants/mod.rs b/src/constants/mod.rs old mode 100755 new mode 100644 index 762b3be..5dc92e6 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -1,174 +1,2 @@ -//! Constants used by the crate. -//! -//! This module contains various constants used throughout the crate, including: -//! -//! - Seeds for deriving Program Derived Addresses (PDAs) -//! - Program account addresses and public keys -//! -//! The constants are organized into submodules for better organization: -//! -//! - `seeds`: Contains seed values used for PDA derivation -//! - `accounts`: Contains important program account addresses - -/// Constants used as seeds for deriving PDAs (Program Derived Addresses) -pub mod seeds { - /// Seed for the global state PDA - pub const GLOBAL_SEED: &[u8] = b"global"; - - /// Seed for the mint authority PDA - pub const MINT_AUTHORITY_SEED: &[u8] = b"mint-authority"; - - /// Seed for bonding curve PDAs - pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve"; - - /// Seed for creator vault PDAs - pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault"; - - /// Seed for metadata PDAs - pub const METADATA_SEED: &[u8] = b"metadata"; -} - -pub mod global_constants { - use solana_sdk::{pubkey, pubkey::Pubkey}; - - pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000; - - pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000; - - pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000; - - pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000; - - pub const FEE_BASIS_POINTS: u64 = 95; - - pub const ENABLE_MIGRATE: bool = false; - - pub const POOL_MIGRATION_FEE: u64 = 15_000_001; - - pub const CREATOR_FEE: u64 = 5; - - pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals - - pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports - - pub const TOTAL_SUPPLY: u64 = 1_000_000_000 * SCALE; // 1 billion tokens - - pub const BONDING_CURVE_SUPPLY: u64 = 793_100_000 * SCALE; // total supply of bonding curve tokens - - pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL - - /// Public key for the fee recipient - pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); - - /// Public key for the global PDA - pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); - - /// Public key for the authority - pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF"); - - /// Public key for the withdraw authority - pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"); - - pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1 - pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2 - pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3 - pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4 - pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5 - pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6 - pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"); // Pump.fun AMM: Protocol Fee 7 - -} - -/// Constants related to program accounts and authorities -pub mod accounts { - use solana_sdk::{pubkey, pubkey::Pubkey}; - - /// Public key for the Pump.fun program - pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); - - /// Public key for the MPL Token Metadata program - pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); - - /// Authority for program events - pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"); - - /// System Program ID - pub const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111"); - - /// Token Program ID - pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); - - /// Associated Token Program ID - pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); - - /// Rent Sysvar ID - pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111"); - - pub const JITO_TIP_ACCOUNTS: [&str; 8] = [ - "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", - "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", - "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", - "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", - "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", - "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", - "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", - "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", - ]; - - /// Tip accounts - pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[ - "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE", - "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2", - "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X", - "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb", - "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At", - "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG", - "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid", - "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc" - ]; - - pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[ - "Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3", - "FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe", - "ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13", - "6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK", - "Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr", - ]; - - pub const NOZOMI_TIP_ACCOUNTS: &[&str] = &[ - "TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq", - "noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4", - "noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE", - "noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo", - "noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ", - "nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L", - "nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z", - "nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu", - "noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7", - "nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP", - "nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P", - "nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge", - "nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3", - "nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ", - "nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk", - "nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne", - "nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb" - ]; - - pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); -} - -pub mod trade { - pub const TRADER_TIP_AMOUNT: f64 = 0.0001; - pub const DEFAULT_SLIPPAGE: u64 = 1000; // 10% - pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000; - pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000; - pub const DEFAULT_BUY_TIP_FEE: f64 = 0.0006; - pub const DEFAULT_SELL_TIP_FEE: f64 = 0.0001; -} - -pub struct Symbol; - -impl Symbol { - pub const SOLANA: &'static str = "solana"; -} +pub mod pumpfun; +pub use pumpfun::*; \ No newline at end of file diff --git a/src/constants/pumpfun/mod.rs b/src/constants/pumpfun/mod.rs new file mode 100755 index 0000000..762b3be --- /dev/null +++ b/src/constants/pumpfun/mod.rs @@ -0,0 +1,174 @@ +//! Constants used by the crate. +//! +//! This module contains various constants used throughout the crate, including: +//! +//! - Seeds for deriving Program Derived Addresses (PDAs) +//! - Program account addresses and public keys +//! +//! The constants are organized into submodules for better organization: +//! +//! - `seeds`: Contains seed values used for PDA derivation +//! - `accounts`: Contains important program account addresses + +/// Constants used as seeds for deriving PDAs (Program Derived Addresses) +pub mod seeds { + /// Seed for the global state PDA + pub const GLOBAL_SEED: &[u8] = b"global"; + + /// Seed for the mint authority PDA + pub const MINT_AUTHORITY_SEED: &[u8] = b"mint-authority"; + + /// Seed for bonding curve PDAs + pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve"; + + /// Seed for creator vault PDAs + pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault"; + + /// Seed for metadata PDAs + pub const METADATA_SEED: &[u8] = b"metadata"; +} + +pub mod global_constants { + use solana_sdk::{pubkey, pubkey::Pubkey}; + + pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000; + + pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000; + + pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000; + + pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000; + + pub const FEE_BASIS_POINTS: u64 = 95; + + pub const ENABLE_MIGRATE: bool = false; + + pub const POOL_MIGRATION_FEE: u64 = 15_000_001; + + pub const CREATOR_FEE: u64 = 5; + + pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals + + pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports + + pub const TOTAL_SUPPLY: u64 = 1_000_000_000 * SCALE; // 1 billion tokens + + pub const BONDING_CURVE_SUPPLY: u64 = 793_100_000 * SCALE; // total supply of bonding curve tokens + + pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL + + /// Public key for the fee recipient + pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); + + /// Public key for the global PDA + pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); + + /// Public key for the authority + pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF"); + + /// Public key for the withdraw authority + pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"); + + pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1 + pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2 + pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3 + pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4 + pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5 + pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6 + pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"); // Pump.fun AMM: Protocol Fee 7 + +} + +/// Constants related to program accounts and authorities +pub mod accounts { + use solana_sdk::{pubkey, pubkey::Pubkey}; + + /// Public key for the Pump.fun program + pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); + + /// Public key for the MPL Token Metadata program + pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); + + /// Authority for program events + pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"); + + /// System Program ID + pub const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111"); + + /// Token Program ID + pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + + /// Associated Token Program ID + pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); + + /// Rent Sysvar ID + pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111"); + + pub const JITO_TIP_ACCOUNTS: [&str; 8] = [ + "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", + "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", + "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", + "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", + "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", + "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", + "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", + "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", + ]; + + /// Tip accounts + pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[ + "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE", + "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2", + "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X", + "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb", + "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At", + "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG", + "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid", + "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc" + ]; + + pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[ + "Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3", + "FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe", + "ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13", + "6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK", + "Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr", + ]; + + pub const NOZOMI_TIP_ACCOUNTS: &[&str] = &[ + "TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq", + "noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4", + "noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE", + "noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo", + "noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ", + "nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L", + "nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z", + "nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu", + "noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7", + "nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP", + "nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P", + "nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge", + "nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3", + "nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ", + "nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk", + "nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne", + "nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb" + ]; + + pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); +} + +pub mod trade { + pub const TRADER_TIP_AMOUNT: f64 = 0.0001; + pub const DEFAULT_SLIPPAGE: u64 = 1000; // 10% + pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000; + pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000; + pub const DEFAULT_BUY_TIP_FEE: f64 = 0.0006; + pub const DEFAULT_SELL_TIP_FEE: f64 = 0.0001; +} + +pub struct Symbol; + +impl Symbol { + pub const SOLANA: &'static str = "solana"; +} diff --git a/src/grpc/shred_stream.rs b/src/grpc/shred_stream.rs index 1e1a52b..657a352 100755 --- a/src/grpc/shred_stream.rs +++ b/src/grpc/shred_stream.rs @@ -10,9 +10,9 @@ use solana_sdk::transaction::VersionedTransaction; use crate::common::AnyResult; use solana_sdk::pubkey::Pubkey; -use crate::common::logs_data::DexInstruction; -use crate::common::logs_events::PumpfunEvent; -use crate::common::logs_filters::LogFilter; +use crate::common::pumpfun::logs_data::DexInstruction; +use crate::common::pumpfun::logs_events::PumpfunEvent; +use crate::common::pumpfun::logs_filters::LogFilter; use crate::swqos::jito_grpc::shredstream::shredstream_proxy_client::ShredstreamProxyClient; use crate::swqos::jito_grpc::shredstream::SubscribeEntriesRequest; diff --git a/src/grpc/yellow_stone.rs b/src/grpc/yellow_stone.rs index b9fb663..85ebaba 100755 --- a/src/grpc/yellow_stone.rs +++ b/src/grpc/yellow_stone.rs @@ -15,9 +15,9 @@ use solana_transaction_status::{ option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding, }; -use crate::common::logs_data::{DexInstruction, TransferInfo}; -use crate::common::logs_events::{PumpfunEvent, SystemEvent}; -use crate::common::logs_filters::LogFilter; +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; type TransactionsFilterMap = HashMap; diff --git a/src/instruction/mod.rs b/src/instruction/mod.rs index 135c246..d730878 100755 --- a/src/instruction/mod.rs +++ b/src/instruction/mod.rs @@ -196,21 +196,21 @@ pub fn sell( ) -> Instruction { let bonding_curve: Pubkey = get_bonding_curve_pda(mint).unwrap(); Instruction::new_with_bytes( - constants::accounts::PUMPFUN, + constants::pumpfun::accounts::PUMPFUN, &args.data(), vec![ - AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false), + AccountMeta::new_readonly(constants::pumpfun::global_constants::GLOBAL_ACCOUNT, false), AccountMeta::new(*fee_recipient, false), AccountMeta::new_readonly(*mint, false), AccountMeta::new(bonding_curve, false), AccountMeta::new(get_associated_token_address(&bonding_curve, mint), false), AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), AccountMeta::new(payer.pubkey(), true), - AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::SYSTEM_PROGRAM, false), AccountMeta::new(*creator_vault_pda, false), - AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false), - AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false), - AccountMeta::new_readonly(constants::accounts::PUMPFUN, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::TOKEN_PROGRAM, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::EVENT_AUTHORITY, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::PUMPFUN, false), ], ) } diff --git a/src/lib.rs b/src/lib.rs index 1163c08..344674f 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,8 +19,8 @@ use solana_sdk::{ signature::{Keypair, Signer}, }; -use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe, Cluster, PriorityFee, SolanaRpcClient}; -use common::logs_subscribe::SubscriptionHandle; +use common::{pumpfun::logs_data::TradeInfo, pumpfun::logs_events::PumpfunEvent, pumpfun::logs_subscribe, Cluster, PriorityFee, SolanaRpcClient}; +use common::pumpfun::logs_subscribe::SubscriptionHandle; use ipfs::TokenMetadataIPFS; pub struct PumpFun { diff --git a/src/main.rs b/src/main.rs index 1c12a22..2c19723 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use pumpfun_sdk::{common::{ - logs_events::PumpfunEvent, - logs_subscribe::{stop_subscription, tokens_subscription}, AnyResult + pumpfun::logs_events::PumpfunEvent, + pumpfun::logs_subscribe::{stop_subscription, tokens_subscription}, AnyResult }, grpc::ShredStreamGrpc}; use solana_sdk::{commitment_config::CommitmentConfig, transaction::VersionedTransaction}; diff --git a/src/pumpfun/buy.rs b/src/pumpfun/buy.rs index 26335e2..5f73de7 100755 --- a/src/pumpfun/buy.rs +++ b/src/pumpfun/buy.rs @@ -15,7 +15,7 @@ use crate::{ PriorityFee, SolanaRpcClient }, - constants::{self, global_constants::FEE_RECIPIENT}, + constants::{self, pumpfun::global_constants::FEE_RECIPIENT}, instruction, swqos::{ClientType, FeeClient, TradeType} }; @@ -387,7 +387,7 @@ pub async fn build_buy_instructions( &payer.pubkey(), &payer.pubkey(), &mint, - &constants::accounts::TOKEN_PROGRAM, + &constants::pumpfun::accounts::TOKEN_PROGRAM, )); instructions.push(instruction::buy( diff --git a/src/pumpfun/common.rs b/src/pumpfun/common.rs index dd72456..27f268a 100755 --- a/src/pumpfun/common.rs +++ b/src/pumpfun/common.rs @@ -7,7 +7,7 @@ use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction }; use spl_associated_token_account::get_associated_token_address; -use crate::{accounts::{self, BondingCurveAccount}, common::{logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}; +use crate::{accounts::{self, BondingCurveAccount}, common::{pumpfun::logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, pumpfun::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}}; lazy_static::lazy_static! { static ref ACCOUNT_CACHE: RwLock>> = RwLock::new(HashMap::new()); @@ -142,7 +142,7 @@ pub async fn get_sol_balance(rpc: &SolanaRpcClient, account: &Pubkey) -> Result< #[inline] pub fn get_global_pda() -> Pubkey { static GLOBAL_PDA: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { - Pubkey::find_program_address(&[constants::seeds::GLOBAL_SEED], &constants::accounts::PUMPFUN).0 + Pubkey::find_program_address(&[constants::pumpfun::seeds::GLOBAL_SEED], &constants::pumpfun::accounts::PUMPFUN).0 }); *GLOBAL_PDA } @@ -150,23 +150,23 @@ pub fn get_global_pda() -> Pubkey { #[inline] pub fn get_mint_authority_pda() -> Pubkey { static MINT_AUTHORITY_PDA: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { - Pubkey::find_program_address(&[constants::seeds::MINT_AUTHORITY_SEED], &constants::accounts::PUMPFUN).0 + Pubkey::find_program_address(&[constants::pumpfun::seeds::MINT_AUTHORITY_SEED], &constants::pumpfun::accounts::PUMPFUN).0 }); *MINT_AUTHORITY_PDA } #[inline] pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option { - let seeds: &[&[u8]; 2] = &[constants::seeds::BONDING_CURVE_SEED, mint.as_ref()]; - let program_id: &Pubkey = &constants::accounts::PUMPFUN; + let seeds: &[&[u8]; 2] = &[constants::pumpfun::seeds::BONDING_CURVE_SEED, mint.as_ref()]; + let program_id: &Pubkey = &constants::pumpfun::accounts::PUMPFUN; let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); pda.map(|pubkey| pubkey.0) } #[inline] pub fn get_creator_vault_pda(creator: &Pubkey) -> Option { - let seeds: &[&[u8]; 2] = &[constants::seeds::CREATOR_VAULT_SEED, creator.as_ref()]; - let program_id: &Pubkey = &constants::accounts::PUMPFUN; + let seeds: &[&[u8]; 2] = &[constants::pumpfun::seeds::CREATOR_VAULT_SEED, creator.as_ref()]; + let program_id: &Pubkey = &constants::pumpfun::accounts::PUMPFUN; let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); pda.map(|pubkey| pubkey.0) } @@ -175,11 +175,11 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option { pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey { Pubkey::find_program_address( &[ - constants::seeds::METADATA_SEED, - constants::accounts::MPL_TOKEN_METADATA.as_ref(), + constants::pumpfun::seeds::METADATA_SEED, + constants::pumpfun::accounts::MPL_TOKEN_METADATA.as_ref(), mint.as_ref(), ], - &constants::accounts::MPL_TOKEN_METADATA + &constants::pumpfun::accounts::MPL_TOKEN_METADATA ).0 } diff --git a/src/pumpfun/sell.rs b/src/pumpfun/sell.rs index 5d17972..96130f5 100755 --- a/src/pumpfun/sell.rs +++ b/src/pumpfun/sell.rs @@ -8,7 +8,7 @@ use spl_token::instruction::close_account; use tokio::task::JoinHandle; use std::{str::FromStr, sync::Arc, time::Instant}; -use crate::{common::{address_lookup_cache::get_address_lookup_table_account, PriorityFee, SolanaRpcClient}, constants::{global_constants::FEE_RECIPIENT}, instruction, swqos::{FeeClient, TradeType, ClientType}}; +use crate::{common::{address_lookup_cache::get_address_lookup_table_account, PriorityFee, SolanaRpcClient}, constants::pumpfun::{global_constants::FEE_RECIPIENT}, instruction, swqos::{FeeClient, TradeType, ClientType}}; use super::common::get_creator_vault_pda; diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index ff40e7d..8a92dab 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -24,7 +24,7 @@ use anyhow::{anyhow, Result}; use rand::{rng, seq::{IndexedRandom, IteratorRandom}}; use solana_sdk::transaction::VersionedTransaction; -use crate::{common::SolanaRpcClient, constants::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS, NOZOMI_TIP_ACCOUNTS}}; +use crate::{common::SolanaRpcClient, constants::pumpfun::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS, NOZOMI_TIP_ACCOUNTS}}; pub mod api; pub mod common; From 94c2a22ab78b913d34bcb9bac86953f4009d518a Mon Sep 17 00:00:00 2001 From: sgxiang Date: Fri, 6 Jun 2025 22:29:30 +0800 Subject: [PATCH 2/8] feat: Add PumpSwap trading functionality module --- src/common/mod.rs | 1 + src/common/pumpswap/logs_data.rs | 321 ++++++++++++++++++++++ src/common/pumpswap/logs_events.rs | 106 +++++++ src/common/pumpswap/logs_filters.rs | 43 +++ src/common/pumpswap/logs_parser.rs | 292 ++++++++++++++++++++ src/common/pumpswap/logs_subscribe.rs | 134 +++++++++ src/common/pumpswap/mod.rs | 11 + src/constants/mod.rs | 2 +- src/constants/pumpswap/mod.rs | 126 +++++++++ src/grpc/shred_stream.rs | 91 ++++++ src/grpc/yellow_stone.rs | 186 +++++++++++++ src/instruction/mod.rs | 28 +- src/main.rs | 62 ++++- src/pumpfun/create.rs | 4 +- src/pumpswap/buy.rs | 382 ++++++++++++++++++++++++++ src/pumpswap/common.rs | 66 +++++ src/pumpswap/mod.rs | 4 + src/pumpswap/pool.rs | 163 +++++++++++ src/pumpswap/sell.rs | 333 ++++++++++++++++++++++ 19 files changed, 2334 insertions(+), 21 deletions(-) create mode 100644 src/common/pumpswap/logs_data.rs create mode 100755 src/common/pumpswap/logs_events.rs create mode 100755 src/common/pumpswap/logs_filters.rs create mode 100755 src/common/pumpswap/logs_parser.rs create mode 100755 src/common/pumpswap/logs_subscribe.rs create mode 100644 src/common/pumpswap/mod.rs create mode 100755 src/constants/pumpswap/mod.rs create mode 100644 src/pumpswap/buy.rs create mode 100644 src/pumpswap/common.rs create mode 100644 src/pumpswap/mod.rs create mode 100644 src/pumpswap/pool.rs create mode 100644 src/pumpswap/sell.rs diff --git a/src/common/mod.rs b/src/common/mod.rs index 89da297..fbbd475 100755 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,4 +1,5 @@ pub mod pumpfun; +pub mod pumpswap; pub mod address_lookup; pub mod nonce_cache; pub mod tip_cache; diff --git a/src/common/pumpswap/logs_data.rs b/src/common/pumpswap/logs_data.rs new file mode 100644 index 0000000..681e0f5 --- /dev/null +++ b/src/common/pumpswap/logs_data.rs @@ -0,0 +1,321 @@ +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, + #[borsh(skip)] + pub signature: String, +} + +/// 卖出事件 +#[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, + #[borsh(skip)] + pub signature: String, +} + +/// 创建池子事件 +#[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 new file mode 100755 index 0000000..3b69229 --- /dev/null +++ b/src/common/pumpswap/logs_events.rs @@ -0,0 +1,106 @@ +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 new file mode 100755 index 0000000..33fc8a6 --- /dev/null +++ b/src/common/pumpswap/logs_filters.rs @@ -0,0 +1,43 @@ +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 new file mode 100755 index 0000000..8715279 --- /dev/null +++ b/src/common/pumpswap/logs_parser.rs @@ -0,0 +1,292 @@ +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..]; + + 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(), + ..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(), + ..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/logs_subscribe.rs b/src/common/pumpswap/logs_subscribe.rs new file mode 100755 index 0000000..f92d68a --- /dev/null +++ b/src/common/pumpswap/logs_subscribe.rs @@ -0,0 +1,134 @@ +use solana_client::{ + nonblocking::pubsub_client::PubsubClient, + rpc_config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter} +}; + +use solana_sdk::commitment_config::CommitmentConfig; +use std::sync::Arc; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; +use futures::StreamExt; +use crate::common::pumpswap::{ + logs_events::PumpSwapEvent, + logs_filters::LogFilter +}; +use crate::constants::pumpswap::accounts; + +/// 订阅句柄,包含任务和取消订阅逻辑 +pub struct SubscriptionHandle { + pub task: JoinHandle<()>, + pub unsub_fn: Box, +} + +impl SubscriptionHandle { + pub async fn shutdown(self) { + (self.unsub_fn)(); + self.task.abort(); + } +} + +/// 创建PubSub客户端 +pub async fn create_pubsub_client(ws_url: &str) -> PubsubClient { + PubsubClient::new(ws_url).await.unwrap() +} + +/// 启动PumpSwap代币订阅 +pub async fn tokens_subscription( + ws_url: &str, + commitment: CommitmentConfig, + callback: F, +) -> Result> +where + F: Fn(PumpSwapEvent) + Send + Sync + 'static, +{ + // 使用constants中定义的AMM_PROGRAM + let program_address = accounts::AMM_PROGRAM.to_string(); + let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]); + + let logs_config = RpcTransactionLogsConfig { + commitment: Some(commitment), + }; + + // 创建PubsubClient + let sub_client = Arc::new(PubsubClient::new(ws_url).await.unwrap()); + + let sub_client_clone = Arc::clone(&sub_client); + + // 创建用于取消订阅的通道 + let (unsub_tx, _) = mpsc::channel(1); + + // 启动订阅任务 + 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 events = LogFilter::parse_pumpswap_logs(&msg.value.logs); + for mut event in events { + // 设置签名和slot + match &mut event { + PumpSwapEvent::Buy(e) => { + e.signature = msg.value.signature.clone(); + e.slot = msg.context.slot; + }, + PumpSwapEvent::Sell(e) => { + e.signature = msg.value.signature.clone(); + e.slot = msg.context.slot; + }, + PumpSwapEvent::CreatePool(e) => { + e.signature = msg.value.signature.clone(); + e.slot = msg.context.slot; + }, + PumpSwapEvent::Deposit(e) => { + e.signature = msg.value.signature.clone(); + e.slot = msg.context.slot; + }, + PumpSwapEvent::Withdraw(e) => { + e.signature = msg.value.signature.clone(); + e.slot = msg.context.slot; + }, + PumpSwapEvent::Disable(e) => { + e.signature = msg.value.signature.clone(); + e.slot = msg.context.slot; + }, + PumpSwapEvent::UpdateAdmin(e) => { + e.signature = msg.value.signature.clone(); + e.slot = msg.context.slot; + }, + PumpSwapEvent::UpdateFeeConfig(e) => { + e.signature = msg.value.signature.clone(); + e.slot = msg.context.slot; + }, + _ => {} + } + callback(event); + } + } + None => { + println!("PumpSwap subscription stream ended"); + } + } + } + }); + + // 返回订阅句柄和取消订阅逻辑 + 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/pumpswap/mod.rs b/src/common/pumpswap/mod.rs new file mode 100644 index 0000000..8de9d17 --- /dev/null +++ b/src/common/pumpswap/mod.rs @@ -0,0 +1,11 @@ +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/constants/mod.rs b/src/constants/mod.rs index 5dc92e6..648fc87 100644 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -1,2 +1,2 @@ pub mod pumpfun; -pub use pumpfun::*; \ No newline at end of file +pub mod pumpswap; \ No newline at end of file diff --git a/src/constants/pumpswap/mod.rs b/src/constants/pumpswap/mod.rs new file mode 100755 index 0000000..a095bd3 --- /dev/null +++ b/src/constants/pumpswap/mod.rs @@ -0,0 +1,126 @@ +//! Constants used by the crate. +//! +//! This module contains various constants used throughout the crate, including: +//! +//! - Seeds for deriving Program Derived Addresses (PDAs) +//! - Program account addresses and public keys +//! +//! The constants are organized into submodules for better organization: +//! +//! - `seeds`: Contains seed values used for PDA derivation +//! - `accounts`: Contains important program account addresses + +/// Constants used as seeds for deriving PDAs (Program Derived Addresses) +pub mod seeds { + /// Seed for the global state PDA + pub const GLOBAL_SEED: &[u8] = b"global"; + + /// Seed for the mint authority PDA + pub const MINT_AUTHORITY_SEED: &[u8] = b"mint-authority"; + + /// Seed for bonding curve PDAs + pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve"; + + /// Seed for metadata PDAs + pub const METADATA_SEED: &[u8] = b"metadata"; +} + +/// Constants related to program accounts and authorities +pub mod accounts { + use solana_sdk::{pubkey, pubkey::Pubkey}; + + /// Public key for the fee recipient + pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); + + pub const FEE_RECIPIENT_ATA:Pubkey = pubkey!("94qWNrtmfn42h3ZjUZwWvK1MEo9uVmmrBPd2hpNjYDjb"); + + /// Public key for the global PDA + pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"); + + /// Authority for program events + pub const EVENT_AUTHORITY: Pubkey = pubkey!("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"); + + pub const WSOL_TOKEN_ACCOUNT: Pubkey = pubkey!("So11111111111111111111111111111111111111112"); + + /// System Program ID + pub const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111"); + + /// Token Program ID + pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + + /// Associated Token Program ID + pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); + + // PumpSwap 协议费用接收者 + pub const PROTOCOL_FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"); + + /// 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"); + +} + +pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234]; +pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173]; + +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/grpc/shred_stream.rs b/src/grpc/shred_stream.rs index 657a352..807bb4a 100755 --- a/src/grpc/shred_stream.rs +++ b/src/grpc/shred_stream.rs @@ -7,12 +7,15 @@ use tonic::transport::Channel; use log::error; use solana_sdk::transaction::VersionedTransaction; +use crate::common::pumpswap::PumpSwapInstruction; use crate::common::AnyResult; 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::swqos::jito_grpc::shredstream::shredstream_proxy_client::ShredstreamProxyClient; use crate::swqos::jito_grpc::shredstream::SubscribeEntriesRequest; @@ -77,6 +80,47 @@ impl ShredStreamGrpc { Ok(()) } + pub async fn shredstream_subscribe_pumpswap(&self, 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(()) + } + async fn process_pumpfun_transaction(transaction_with_slot: TransactionWithSlot, callback: &F, bot_wallet: Option) -> AnyResult<()> where F: Fn(PumpfunEvent) + Send + Sync, @@ -111,4 +155,51 @@ impl ShredStreamGrpc { } Ok(()) } + + 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 instructions = PumpswapLogFilter::parse_pumpswap_compiled_instruction(versioned_tx).unwrap(); + for instruction in instructions { + match instruction { + PumpSwapInstruction::CreatePool(mut create_event) => { + create_event.slot = slot; + callback(PumpSwapEvent::CreatePool(create_event)); + } + PumpSwapInstruction::Deposit(mut deposit_event) => { + deposit_event.slot = slot; + callback(PumpSwapEvent::Deposit(deposit_event)); + } + PumpSwapInstruction::Withdraw(mut withdraw_event) => { + withdraw_event.slot = slot; + callback(PumpSwapEvent::Withdraw(withdraw_event)); + } + PumpSwapInstruction::Buy(mut buy_event) => { + buy_event.slot = slot; + callback(PumpSwapEvent::Buy(buy_event)); + } + PumpSwapInstruction::Sell(mut sell_event) => { + sell_event.slot = slot; + callback(PumpSwapEvent::Sell(sell_event)); + } + PumpSwapInstruction::UpdateFeeConfig(mut update_fee_event) => { + update_fee_event.slot = slot; + callback(PumpSwapEvent::UpdateFeeConfig(update_fee_event)); + } + PumpSwapInstruction::UpdateAdmin(mut update_admin_event) => { + update_admin_event.slot = slot; + callback(PumpSwapEvent::UpdateAdmin(update_admin_event)); + } + PumpSwapInstruction::Disable(mut disable_event) => { + disable_event.slot = slot; + callback(PumpSwapEvent::Disable(disable_event)); + } + _ => {} + } + } + Ok(()) + } } diff --git a/src/grpc/yellow_stone.rs b/src/grpc/yellow_stone.rs index 85ebaba..40f809a 100755 --- a/src/grpc/yellow_stone.rs +++ b/src/grpc/yellow_stone.rs @@ -349,4 +349,190 @@ impl YellowstoneGrpc { 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( + &self, + callback: F, + 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 + ) -> AnyResult<()> + where + F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + 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(()); + } + + // 获取日志 + 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; + }, + crate::common::pumpswap::logs_events::PumpSwapEvent::Sell(e) => { + e.signature = transaction_pretty.signature.to_string(); + e.slot = slot; + }, + 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(()) + } } diff --git a/src/instruction/mod.rs b/src/instruction/mod.rs index d730878..239a101 100755 --- a/src/instruction/mod.rs +++ b/src/instruction/mod.rs @@ -103,7 +103,7 @@ impl Sell { pub fn create(payer: &Keypair, mint: &Keypair, args: Create) -> Instruction { let bonding_curve: Pubkey = get_bonding_curve_pda(&mint.pubkey()).unwrap(); Instruction::new_with_bytes( - constants::accounts::PUMPFUN, + constants::pumpfun::accounts::PUMPFUN, &args.data(), vec![ AccountMeta::new(mint.pubkey(), true), @@ -114,15 +114,15 @@ pub fn create(payer: &Keypair, mint: &Keypair, args: Create) -> Instruction { false, ), AccountMeta::new_readonly(get_global_pda(), false), - AccountMeta::new_readonly(constants::accounts::MPL_TOKEN_METADATA, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::MPL_TOKEN_METADATA, false), AccountMeta::new(get_metadata_pda(&mint.pubkey()), false), AccountMeta::new(payer.pubkey(), true), - AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false), - AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false), - AccountMeta::new_readonly(constants::accounts::ASSOCIATED_TOKEN_PROGRAM, false), - AccountMeta::new_readonly(constants::accounts::RENT, false), - AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false), - AccountMeta::new_readonly(constants::accounts::PUMPFUN, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::SYSTEM_PROGRAM, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::TOKEN_PROGRAM, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::ASSOCIATED_TOKEN_PROGRAM, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::RENT, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::EVENT_AUTHORITY, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::PUMPFUN, false), ], ) } @@ -152,21 +152,21 @@ pub fn buy( args: Buy, ) -> Instruction { Instruction::new_with_bytes( - constants::accounts::PUMPFUN, + constants::pumpfun::accounts::PUMPFUN, &args.data(), vec![ - AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false), + AccountMeta::new_readonly(constants::pumpfun::global_constants::GLOBAL_ACCOUNT, false), AccountMeta::new(*fee_recipient, false), AccountMeta::new_readonly(*mint, false), AccountMeta::new(*bonding_curve_pda, false), AccountMeta::new(get_associated_token_address(bonding_curve_pda, mint), false), AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false), AccountMeta::new(payer.pubkey(), true), - AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false), - AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::SYSTEM_PROGRAM, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::TOKEN_PROGRAM, false), AccountMeta::new(*creator_vault_pda, false), - AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false), - AccountMeta::new_readonly(constants::accounts::PUMPFUN, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::EVENT_AUTHORITY, false), + AccountMeta::new_readonly(constants::pumpfun::accounts::PUMPFUN, false), ], ) } diff --git a/src/main.rs b/src/main.rs index 2c19723..190e8e7 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,16 @@ use pumpfun_sdk::{common::{ - pumpfun::logs_events::PumpfunEvent, - pumpfun::logs_subscribe::{stop_subscription, tokens_subscription}, AnyResult -}, grpc::ShredStreamGrpc}; + pumpfun::{logs_events::PumpfunEvent, logs_subscribe::{stop_subscription, tokens_subscription}}, pumpswap::{self, PumpSwapEvent}, AnyResult +}, grpc::{ShredStreamGrpc, YellowstoneGrpc}}; use solana_sdk::{commitment_config::CommitmentConfig, transaction::VersionedTransaction}; #[tokio::main] async fn main() -> Result<(), Box> { + // test_pumpfun().await?; + test_pumpswap().await?; + Ok(()) +} + +async fn test_pumpfun() -> Result<(), Box> { let grpc = ShredStreamGrpc::new( "http://127.0.0.1:10800".to_string(), ).await?; @@ -44,9 +49,58 @@ async fn main() -> Result<(), Box> { grpc.shredstream_subscribe(callback, None).await?; - Ok(()) + Ok(()) } +async fn test_pumpswap() -> Result<(), Box> { + // 使用 GRPC 客户端订阅 PumpSwap 事件 + println!("正在订阅 PumpSwap GRPC 事件..."); + + 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_wss() -> AnyResult<()> { println!("Starting token subscription\n"); diff --git a/src/pumpfun/create.rs b/src/pumpfun/create.rs index 4f97b5a..0f3b529 100755 --- a/src/pumpfun/create.rs +++ b/src/pumpfun/create.rs @@ -219,7 +219,7 @@ pub async fn build_create_and_buy_instructions( &payer.pubkey(), &payer.pubkey(), &mint.pubkey(), - &constants::accounts::TOKEN_PROGRAM, + &constants::pumpfun::accounts::TOKEN_PROGRAM, )); instructions.push(instruction::buy( @@ -227,7 +227,7 @@ pub async fn build_create_and_buy_instructions( &mint.pubkey(), &bonding_curve_pda, &creator_vault_pda, - &constants::global_constants::FEE_RECIPIENT, + &constants::pumpfun::global_constants::FEE_RECIPIENT, instruction::Buy { _amount: buy_token_amount, _max_sol_cost: max_sol_cost, diff --git a/src/pumpswap/buy.rs b/src/pumpswap/buy.rs new file mode 100644 index 0000000..d258fe5 --- /dev/null +++ b/src/pumpswap/buy.rs @@ -0,0 +1,382 @@ +use std::sync::Arc; +use std::time::Instant; +use std::str::FromStr; +use anyhow::anyhow; +use chrono; +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, + instruction::{AccountMeta, Instruction}, + message::{v0, AddressLookupTableAccount, VersionedMessage}, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::VersionedTransaction, +}; +use spl_associated_token_account::instruction::create_associated_token_account_idempotent; + +use crate::common::{address_lookup_cache::get_address_lookup_table_account, nonce_cache::{self, NonceCache}, PriorityFee, SolanaRpcClient}; +use crate::pumpswap::common::{calculate_with_slippage_buy, find_pool, get_buy_token_amount}; +use crate::constants::{accounts, trade::DEFAULT_SLIPPAGE, BUY_DISCRIMINATOR}; +use crate::swqos::FeeClient; + +// Constants for compute budget +// Increased from 64KB to 256KB to handle larger transactions +const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 256 * 1024; + +/// 添加nonce消费指令到指令集合中 +/// +/// 只有当同时提供了nonce_pubkey和nonce_program_id时才使用nonce功能 +/// 如果nonce被锁定、已使用或未准备好,将返回错误 +/// 成功时会锁定并标记nonce为已使用 +fn add_nonce_instruction(instructions: &mut Vec, payer: &Keypair) -> Result<(), anyhow::Error> { + let nonce_cache = NonceCache::get_instance(); + let nonce_info = nonce_cache.get_nonce_info(); + if let (Some(nonce_pubkey), Some(program_id)) = (nonce_info.nonce_account, nonce_info.program_id) { + let nonce_value = nonce_info.current_nonce; + // 暂不加锁 + // if nonce_info.lock { + // return Err(anyhow!("Nonce is locked")); + // } + if nonce_info.used { + return Err(anyhow!("Nonce is used")); + } + if nonce_info.next_buy_time == 0 || chrono::Utc::now().timestamp() < nonce_info.next_buy_time { + return Err(anyhow!("Nonce is not ready")); + } + // 加锁 - 暂不加锁 + // nonce_cache.lock(); + // 创建自定义nonce消费指令 + let nonce_consume_ix = Instruction { + program_id, + accounts: vec![ + AccountMeta::new(nonce_pubkey, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + // INSTR_CONSUME = 1, 使用传入的nonce值 + data: { + let mut data = vec![1]; // INSTR_CONSUME = 1 + data.extend_from_slice(&nonce_value.to_le_bytes()); // 添加nonce值 + data + }, + }; + instructions.push(nonce_consume_ix); + } + + Ok(()) +} + +/// 验证地址表是否被成功用于编译后的消息中 +fn verify_lookup_table_usage( + v0_message: &v0::Message, + address_lookup_table_accounts: &[AddressLookupTableAccount], +) { + if !address_lookup_table_accounts.is_empty() { + println!("消息已编译,使用了地址表引用"); + // 如果地址表有地址,但没有被使用,给出警告 + if v0_message.address_table_lookups.is_empty() { + println!("警告:编译后的消息没有使用地址表引用!"); + } else { + for (i, lookup) in v0_message.address_table_lookups.iter().enumerate() { + println!( + "使用地址表 {}: 可写索引 {} 个, 只读索引 {} 个", + i, + lookup.writable_indexes.len(), + lookup.readonly_indexes.len() + ); + } + } + } +} + +// Buy tokens from a Pumpswap pool +pub async fn buy( + rpc: Arc, + payer: Arc, + mint: Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, +) -> Result<(), anyhow::Error> { + let start_time = Instant::now(); + let mint = Arc::new(mint.clone()); + let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?; + println!(" Buy transaction instructions: {:?}", start_time.elapsed()); + + let start_time = Instant::now(); + let transaction = build_buy_transaction( + rpc.clone(), + payer.clone(), + priority_fee.clone(), + instructions, + lookup_table_key, + ).await?; + println!(" Buy transaction signature: {:?}", start_time.elapsed()); + + let start_time = Instant::now(); + rpc.send_and_confirm_transaction(&transaction).await?; + println!(" Buy transaction confirmation: {:?}", start_time.elapsed()); + + Ok(()) +} + +// Buy tokens using a MEV service +pub async fn buy_with_tip( + rpc: Arc, + fee_clients: Vec>, + payer: Arc, + mint: Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option, +) -> Result<(), anyhow::Error> { + let start_time = Instant::now(); + let mint = Arc::new(mint.clone()); + let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?; + println!(" Buy transaction instructions: {:?}", start_time.elapsed()); + + let start_time = Instant::now(); + let mut transactions = vec![]; + + for fee_client in fee_clients.clone() { + 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_buy_transaction_with_tip( + rpc.clone(), + tip_account, + payer.clone(), + priority_fee.clone(), + instructions.clone(), + lookup_table_key, + ).await?; + + transactions.push(transaction); + } + + println!(" Buy transaction signature: {:?}", start_time.elapsed()); + + let mut handles = vec![]; + for (i, fee_client) in fee_clients.iter().enumerate() { + let transaction = transactions[i].clone(); + let fee_client = fee_client.clone(); + + let handle = tokio::spawn(async move { + fee_client.send_transaction(crate::swqos::TradeType::Buy, &transaction).await + }); + + handles.push(handle); + } + + for handle in handles { + let _ = handle.await?; + } + + println!(" Buy transaction confirmation: {:?}", start_time.elapsed()); + + Ok(()) +} + +// Build a transaction for buying tokens +pub async fn build_buy_transaction( + rpc: Arc, + payer: Arc, + priority_fee: PriorityFee, + build_instructions: Vec, + lookup_table_key: Option, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + ]; + + // 添加nonce消费指令 + if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref()) { + return Err(e); + } + + instructions.extend(build_instructions); + + let blockhash = rpc.get_latest_blockhash().await?; + + // 确保所有需要签名的账户都被正确标记 + for instruction in &instructions { + for account_meta in &instruction.accounts { + if account_meta.is_signer && account_meta.pubkey != payer.pubkey() { + return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey)); + } + } + } + + let mut address_lookup_table_accounts = vec![]; + if let Some(lookup_table_key) = lookup_table_key { + let account = get_address_lookup_table_account(&lookup_table_key).await; + address_lookup_table_accounts.push(account); + } + + let v0_message = v0::Message::try_compile( + &payer.pubkey(), + &instructions, + &address_lookup_table_accounts, + blockhash, + ).map_err(|e| anyhow!(e))?; + + let versioned_message = VersionedMessage::V0(v0_message.clone()); + let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + + // 验证地址表使用情况 + verify_lookup_table_usage(&v0_message, &address_lookup_table_accounts); + + Ok(transaction) +} + +// Build a transaction with tip for buying tokens +pub async fn build_buy_transaction_with_tip( + rpc: Arc, + tip_account: Arc, + payer: Arc, + priority_fee: PriorityFee, + build_instructions: Vec, + lookup_table_key: Option, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + system_instruction::transfer( + &payer.pubkey(), + &tip_account, + priority_fee.buy_tip_fee, + ), + ]; + + // 添加nonce消费指令 + if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref()) { + return Err(e); + } + + instructions.extend(build_instructions); + + let blockhash = rpc.get_latest_blockhash().await?; + + // 确保所有需要签名的账户都被正确标记 + for instruction in &instructions { + for account_meta in &instruction.accounts { + if account_meta.is_signer && account_meta.pubkey != payer.pubkey() { + return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey)); + } + } + } + + let mut address_lookup_table_accounts = vec![]; + if let Some(lookup_table_key) = lookup_table_key { + let account = get_address_lookup_table_account(&lookup_table_key).await; + address_lookup_table_accounts.push(account); + } + + let v0_message = v0::Message::try_compile( + &payer.pubkey(), + &instructions, + &address_lookup_table_accounts, + blockhash, + ).map_err(|e| anyhow!(e))?; + + let versioned_message = VersionedMessage::V0(v0_message.clone()); + let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + + // 验证地址表使用情况 + verify_lookup_table_usage(&v0_message, &address_lookup_table_accounts); + + Ok(transaction) +} + +// Build instructions for buying tokens +pub async fn build_buy_instructions( + rpc: Arc, + payer: Arc, + mint: Arc, + amount_sol: u64, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + // Find the pool for this mint + let pool = find_pool(rpc.as_ref(), mint.as_ref()).await?; + + // Calculate the expected token amount + let token_amount = get_buy_token_amount(rpc.as_ref(), &pool, amount_sol).await?; + + // Calculate the maximum SOL amount with slippage + let max_sol_amount = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); + + // Create the user's token account if it doesn't exist + let user_base_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), mint.as_ref()); + let user_quote_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &accounts::WSOL_TOKEN_ACCOUNT); + + // Get pool token accounts + let pool_base_token_account = spl_associated_token_account::get_associated_token_address_with_program_id( + &pool, + mint.as_ref(), + &accounts::TOKEN_PROGRAM, + ); + + let pool_quote_token_account = spl_associated_token_account::get_associated_token_address_with_program_id( + &pool, + &accounts::WSOL_TOKEN_ACCOUNT, + &accounts::TOKEN_PROGRAM, + ); + + let mut instructions = vec![]; + + // Create the user's base token account if it doesn't exist + instructions.push( + create_associated_token_account_idempotent( + &payer.pubkey(), + &payer.pubkey(), + mint.as_ref(), + &accounts::TOKEN_PROGRAM, + ) + ); + + // Create the buy instruction + // 注意:账户顺序必须与JavaScript SDK匹配 + let accounts = vec![ + solana_sdk::instruction::AccountMeta::new_readonly(pool, false), // pool_id (readonly) + solana_sdk::instruction::AccountMeta::new(payer.pubkey(), true), // user (signer) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_ACCOUNT, false), // global (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(*mint, false), // mint (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // WSOL_TOKEN_ACCOUNT (readonly) + solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // user_base_token_account + solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // user_quote_token_account + solana_sdk::instruction::AccountMeta::new(pool_base_token_account, false), // pool_base_token_account + solana_sdk::instruction::AccountMeta::new(pool_quote_token_account, false), // pool_quote_token_account + solana_sdk::instruction::AccountMeta::new_readonly(accounts::FEE_RECIPIENT, false), // fee_recipient (readonly) + solana_sdk::instruction::AccountMeta::new(accounts::FEE_RECIPIENT_ATA, false), // fee_recipient_ata + solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly, duplicated as in JS) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::SYSTEM_PROGRAM, false), // System Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false), // ASSOCIATED_TOKEN_PROGRAM_ID (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // event_authority (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::AMM_PROGRAM, false), // PUMP_AMM_PROGRAM_ID (readonly) + ]; + + // Create the instruction data + let mut data = vec![]; + data.extend_from_slice(&BUY_DISCRIMINATOR); + data.extend_from_slice(&token_amount.to_le_bytes()); + data.extend_from_slice(&max_sol_amount.to_le_bytes()); + + instructions.push( + Instruction { + program_id: accounts::AMM_PROGRAM, + accounts, + data, + } + ); + + Ok(instructions) +} diff --git a/src/pumpswap/common.rs b/src/pumpswap/common.rs new file mode 100644 index 0000000..38c663b --- /dev/null +++ b/src/pumpswap/common.rs @@ -0,0 +1,66 @@ +use anyhow::anyhow; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; +use crate::common::SolanaRpcClient; + +// Calculate slippage for buy operations +pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 { + amount + (amount * basis_points / 10000) +} + +// Calculate slippage for sell operations +pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 { + if amount <= basis_points / 10000 { + 1 + } else { + amount - (amount * basis_points / 10000) + } +} + +// Get token balance for a specific mint and owner +pub async fn get_token_balance( + rpc: &SolanaRpcClient, + owner: &Keypair, + mint: &Pubkey, +) -> Result<(u64, Pubkey), anyhow::Error> { + let ata = spl_associated_token_account::get_associated_token_address(&owner.pubkey(), mint); + + match rpc.get_token_account_balance(&ata).await { + Ok(balance) => { + let amount = balance.amount.parse::().map_err(|e| anyhow!(e))?; + Ok((amount, ata)) + } + Err(_) => Ok((0, ata)), + } +} + +// Find a pool for a specific mint +pub async fn find_pool( + rpc: &SolanaRpcClient, + mint: &Pubkey, +) -> Result { + let (pool_address, _) = crate::pumpswap::pool::Pool::find_by_mint(rpc, mint).await?; + Ok(pool_address) +} + +// Calculate the amount of tokens to receive for a given SOL amount +pub async fn get_buy_token_amount( + rpc: &SolanaRpcClient, + pool: &Pubkey, + sol_amount: u64, +) -> Result { + let pool_data = crate::pumpswap::pool::Pool::fetch(rpc, pool).await?; + pool_data.calculate_buy_amount(rpc, sol_amount).await +} + +// Calculate the amount of SOL to receive for a given token amount +pub async fn get_sell_sol_amount( + rpc: &SolanaRpcClient, + pool: &Pubkey, + token_amount: u64, +) -> Result { + let pool_data = crate::pumpswap::pool::Pool::fetch(rpc, pool).await?; + pool_data.calculate_sell_amount(rpc, token_amount).await +} diff --git a/src/pumpswap/mod.rs b/src/pumpswap/mod.rs new file mode 100644 index 0000000..9c60348 --- /dev/null +++ b/src/pumpswap/mod.rs @@ -0,0 +1,4 @@ +pub mod buy; +pub mod sell; +pub mod common; +pub mod pool; diff --git a/src/pumpswap/pool.rs b/src/pumpswap/pool.rs new file mode 100644 index 0000000..0f60514 --- /dev/null +++ b/src/pumpswap/pool.rs @@ -0,0 +1,163 @@ +use solana_sdk::pubkey::Pubkey; +use anyhow::anyhow; +use solana_account_decoder::UiAccountEncoding; +use crate::{common::SolanaRpcClient, constants::accounts}; +use std::str::FromStr; + +#[derive(Debug, Clone)] +pub struct Pool { + pub pool_bump: u8, + pub index: u16, + pub creator: Pubkey, + pub base_mint: Pubkey, + pub quote_mint: Pubkey, + pub lp_mint: Pubkey, + pub pool_base_token_account: Pubkey, + pub pool_quote_token_account: Pubkey, + pub lp_supply: u64, +} + +impl Pool { + pub fn from_bytes(data: &[u8]) -> Result { + if data.len() < 211 { + return Err(anyhow!("Data too short for Pool account")); + } + + // 跳过discriminator (8字节) + let data = &data[8..]; + + 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 lp_supply = u64::from_le_bytes([ + data[195], data[196], data[197], data[198], + data[199], data[200], data[201], data[202], + ]); + + Ok(Self { + pool_bump, + index, + creator, + base_mint, + quote_mint, + lp_mint, + pool_base_token_account, + pool_quote_token_account, + lp_supply, + }) + } + + pub async fn fetch( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, + ) -> Result { + let account = rpc.get_account(pool_address).await?; + + if account.owner != accounts::AMM_PROGRAM { + return Err(anyhow!("Account is not owned by PumpSwap program")); + } + + Self::from_bytes(&account.data) + } + + pub async fn find_by_mint( + rpc: &SolanaRpcClient, + mint: &Pubkey, + ) -> Result<(Pubkey, Self), anyhow::Error> { + // 使用getProgramAccounts查找给定mint的池子 + let filters = vec![ + solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool账户的大小 + solana_rpc_client_api::filter::RpcFilterType::Memcmp( + solana_client::rpc_filter::Memcmp::new_base58_encoded(43, &mint.to_bytes()), + ), + ]; + + let config = solana_rpc_client_api::config::RpcProgramAccountsConfig { + filters: Some(filters), + account_config: solana_rpc_client_api::config::RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: None, + commitment: None, + min_context_slot: None, + }, + with_context: None, + sort_results: None, + }; + + let program_id = crate::constants::accounts::AMM_PROGRAM; + println!("program_id: {:?}", program_id); + 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() + }) + .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)) + } + + pub async fn get_token_balances( + &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_amount = base_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)) + } + + pub async fn calculate_buy_amount( + &self, + rpc: &SolanaRpcClient, + sol_amount: u64, + ) -> Result { + let (base_amount, quote_amount) = self.get_token_balances(rpc).await?; + + // 使用常数乘积公式 (x * y = k) 计算 + let product = base_amount as u128 * quote_amount as u128; + let new_quote_amount = quote_amount as u128 + sol_amount as u128; + let new_base_amount = product / new_quote_amount; + + let token_amount = base_amount as u128 - new_base_amount; + + Ok(token_amount as u64) + } + + pub async fn calculate_sell_amount( + &self, + rpc: &SolanaRpcClient, + token_amount: u64, + ) -> Result { + let (base_amount, quote_amount) = self.get_token_balances(rpc).await?; + + // 使用常数乘积公式 (x * y = k) 计算 + let product = base_amount as u128 * quote_amount as u128; + let new_base_amount = base_amount as u128 + token_amount as u128; + let new_quote_amount = product / new_base_amount; + + let sol_amount = quote_amount as u128 - new_quote_amount; + + Ok(sol_amount as u64) + } +} diff --git a/src/pumpswap/sell.rs b/src/pumpswap/sell.rs new file mode 100644 index 0000000..a06ab58 --- /dev/null +++ b/src/pumpswap/sell.rs @@ -0,0 +1,333 @@ +use std::sync::Arc; +use std::time::Instant; +use std::str::FromStr; +use anyhow::anyhow; +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, + instruction::Instruction, + + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::VersionedTransaction, +}; +use spl_associated_token_account::instruction::create_associated_token_account_idempotent; + +use crate::common::{address_lookup_cache::get_address_lookup_table_account, PriorityFee, SolanaRpcClient}; +use crate::pumpswap::common::{calculate_with_slippage_sell, find_pool, get_sell_sol_amount, get_token_balance}; +use crate::constants::{accounts, trade::DEFAULT_SLIPPAGE, SELL_DISCRIMINATOR}; +use crate::swqos::FeeClient; + +// Constants for compute budget +// Increased from 64KB to 256KB to handle larger transactions +const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 256 * 1024; + +// Sell tokens to a Pumpswap pool +pub async fn sell( + rpc: Arc, + payer: Arc, + mint: Pubkey, + amount_token: Option, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option +) -> Result<(), anyhow::Error> { + let start_time = Instant::now(); + let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?; + println!(" Sell transaction instructions: {:?}", start_time.elapsed()); + + let start_time = Instant::now(); + let recent_blockhash = rpc.get_latest_blockhash().await?; + let transaction = build_sell_transaction( + rpc.clone(), + payer.clone(), + priority_fee, + instructions, + lookup_table_key, + recent_blockhash + ).await?; + println!(" Sell transaction signature: {:?}", start_time.elapsed()); + + let start_time = Instant::now(); + rpc.send_and_confirm_transaction(&transaction).await?; + println!(" Sell transaction confirmation: {:?}", start_time.elapsed()); + Ok(()) +} + +// Sell tokens by percentage +pub async fn sell_by_percent( + rpc: Arc, + payer: Arc, + mint: Pubkey, + percent: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option +) -> Result<(), anyhow::Error> { + if percent == 0 || percent > 100 { + return Err(anyhow!("Percentage must be between 1 and 100")); + } + + let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; + let amount = balance_u64 * percent / 100; + sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await +} + +// Sell tokens using a MEV service +pub async fn sell_with_tip( + rpc: Arc, + fee_clients: Vec>, + payer: Arc, + mint: Pubkey, + amount_token: Option, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option +) -> Result<(), anyhow::Error> { + let mut transactions = vec![]; + let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?; + let recent_blockhash = rpc.get_latest_blockhash().await?; + + for fee_client in fee_clients.clone() { + 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_sell_transaction_with_tip( + rpc.clone(), + tip_account, + payer.clone(), + priority_fee.clone(), + instructions.clone(), + lookup_table_key, + recent_blockhash, + ).await?; + + transactions.push(transaction); + } + + let mut handles = vec![]; + for (i, fee_client) in fee_clients.iter().enumerate() { + let transaction = transactions[i].clone(); + let fee_client = fee_client.clone(); + + let handle = tokio::spawn(async move { + fee_client.send_transaction(crate::swqos::TradeType::Sell, &transaction).await + }); + + handles.push(handle); + } + + for handle in handles { + let _ = handle.await?; + } + + Ok(()) +} + +// Sell tokens by percentage using a MEV service +pub async fn sell_by_percent_with_tip( + rpc: Arc, + fee_clients: Vec>, + payer: Arc, + mint: Pubkey, + percent: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option +) -> Result<(), anyhow::Error> { + if percent == 0 || percent > 100 { + return Err(anyhow!("Percentage must be between 1 and 100")); + } + + let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; + let amount = balance_u64 * percent / 100; + sell_with_tip(rpc, fee_clients, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await +} + +// Build a transaction for selling tokens +pub async fn build_sell_transaction( + _rpc: Arc, + payer: Arc, + priority_fee: PriorityFee, + build_instructions: Vec, + lookup_table_key: Option, + recent_blockhash: solana_sdk::hash::Hash, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + ]; + + instructions.extend(build_instructions); + + // 确保所有需要签名的账户都被正确标记 + for instruction in &instructions { + for account_meta in &instruction.accounts { + if account_meta.is_signer && account_meta.pubkey != payer.pubkey() { + return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey)); + } + } + } + + let mut address_lookup_table_accounts = vec![]; + if let Some(lookup_table_key) = lookup_table_key { + let account = get_address_lookup_table_account(&lookup_table_key).await; + address_lookup_table_accounts.push(account); + } + + let v0_message = solana_sdk::message::v0::Message::try_compile( + &payer.pubkey(), + &instructions, + &address_lookup_table_accounts, + recent_blockhash, + ).map_err(|e| anyhow!(e))?; + + let versioned_message = solana_sdk::message::VersionedMessage::V0(v0_message); + let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + + Ok(transaction) +} + +// Build a transaction with tip for selling tokens +pub async fn build_sell_transaction_with_tip( + _rpc: Arc, + tip_account: Arc, + payer: Arc, + priority_fee: PriorityFee, + build_instructions: Vec, + lookup_table_key: Option, + recent_blockhash: solana_sdk::hash::Hash, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + system_instruction::transfer( + &payer.pubkey(), + &tip_account, + priority_fee.sell_tip_fee, + ), + ]; + + instructions.extend(build_instructions); + + // 确保所有需要签名的账户都被正确标记 + for instruction in &instructions { + for account_meta in &instruction.accounts { + if account_meta.is_signer && account_meta.pubkey != payer.pubkey() { + return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey)); + } + } + } + + let mut address_lookup_table_accounts = vec![]; + if let Some(lookup_table_key) = lookup_table_key { + let account = get_address_lookup_table_account(&lookup_table_key).await; + address_lookup_table_accounts.push(account); + } + + let v0_message = solana_sdk::message::v0::Message::try_compile( + &payer.pubkey(), + &instructions, + &address_lookup_table_accounts, + recent_blockhash, + ).map_err(|e| anyhow!(e))?; + + let versioned_message = solana_sdk::message::VersionedMessage::V0(v0_message); + let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + + Ok(transaction) +} + +// Build instructions for selling tokens +pub async fn build_sell_instructions( + rpc: Arc, + payer: Arc, + mint: Pubkey, + amount_token: Option, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; + let amount = amount_token.unwrap_or(balance_u64); + + if amount == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + // Find the pool for this mint + let pool = find_pool(rpc.as_ref(), &mint).await?; + + // Calculate the expected SOL amount + let sol_amount = get_sell_sol_amount(rpc.as_ref(), &pool, amount).await?; + + // Calculate the minimum SOL amount with slippage + let min_sol_amount = calculate_with_slippage_sell(sol_amount, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); + + // Get token accounts + let user_base_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &mint); + let user_quote_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &accounts::WSOL_TOKEN_ACCOUNT); + + // Get pool token accounts + let pool_base_token_account = spl_associated_token_account::get_associated_token_address_with_program_id( + &pool, + &mint, + &accounts::TOKEN_PROGRAM, + ); + + let pool_quote_token_account = spl_associated_token_account::get_associated_token_address_with_program_id( + &pool, + &accounts::WSOL_TOKEN_ACCOUNT, + &accounts::TOKEN_PROGRAM, + ); + + let mut instructions = vec![]; + + // Create the user's token account if it doesn't exist + instructions.push( + create_associated_token_account_idempotent( + &payer.pubkey(), + &payer.pubkey(), + &mint, + &accounts::TOKEN_PROGRAM, + ) + ); + + // Create the sell instruction + // 注意:账户顺序必须与JavaScript SDK匹配 + let accounts = vec![ + solana_sdk::instruction::AccountMeta::new_readonly(pool, false), // pool_id (readonly) + solana_sdk::instruction::AccountMeta::new(payer.pubkey(), true), // user (signer) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_ACCOUNT, false), // global (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(mint, false), // mint (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // WSOL_TOKEN_ACCOUNT (readonly) + solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // user_base_token_account + solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // user_quote_token_account + solana_sdk::instruction::AccountMeta::new(pool_base_token_account, false), // pool_base_token_account + solana_sdk::instruction::AccountMeta::new(pool_quote_token_account, false), // pool_quote_token_account + solana_sdk::instruction::AccountMeta::new_readonly(accounts::FEE_RECIPIENT, false), // fee_recipient (readonly) + solana_sdk::instruction::AccountMeta::new(accounts::FEE_RECIPIENT_ATA, false), // fee_recipient_ata + solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly, duplicated as in JS) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::SYSTEM_PROGRAM, false), // System Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false), // ASSOCIATED_TOKEN_PROGRAM_ID (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // event_authority (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(accounts::AMM_PROGRAM, false), // PUMP_AMM_PROGRAM_ID (readonly) + ]; + + // Create the instruction data + let mut data = vec![]; + data.extend_from_slice(&SELL_DISCRIMINATOR); + data.extend_from_slice(&amount.to_le_bytes()); + data.extend_from_slice(&min_sol_amount.to_le_bytes()); + + instructions.push( + Instruction { + program_id: accounts::AMM_PROGRAM, + accounts, + data, + } + ); + + Ok(instructions) +} From b3338114837482c5d17b53d347b283959c0f5e45 Mon Sep 17 00:00:00 2001 From: wei <1415121722@qq.com> Date: Sat, 7 Jun 2025 23:16:46 +0800 Subject: [PATCH 3/8] Add a selling method based on quantity --- Cargo.toml | 2 + src/constants/pumpfun/mod.rs | 8 ++ src/lib.rs | 141 ++++++++++++++++++++++++++++++++++- src/main.rs | 99 ++++++++++++++++++++++-- src/pumpfun/buy.rs | 30 ++++++-- src/pumpfun/common.rs | 20 +++++ src/pumpfun/sell.rs | 35 +++++++++ 7 files changed, 318 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8b0422a..c4f0871 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,8 @@ tokio-tungstenite = { version = "0.26.1", features = ["native-tls"] } indicatif = "0.17.11" toml = "0.8.20" +pumpfun_program = { version = "4.2.0", package = "pumpfun" } + diff --git a/src/constants/pumpfun/mod.rs b/src/constants/pumpfun/mod.rs index 762b3be..8ddadc8 100755 --- a/src/constants/pumpfun/mod.rs +++ b/src/constants/pumpfun/mod.rs @@ -172,3 +172,11 @@ pub struct Symbol; impl Symbol { pub const SOLANA: &'static str = "solana"; } + +pub mod trade_type { + pub const COPY_BUY: &'static str = "copy_buy"; + pub const COPY_SELL: &'static str = "copy_sell"; + pub const SNIPER_BUY: &'static str = "sniper_buy"; + pub const SNIPER_SELL: &'static str = "sniper_sell"; +} + diff --git a/src/lib.rs b/src/lib.rs index 344674f..026dcd4 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod swqos; pub mod pumpfun; use std::sync::Arc; +use std::sync::Mutex; use swqos::{FeeClient, JitoClient, NextBlockClient, NozomiClient, SolRpcClient, ZeroSlotClient}; use rustls::crypto::{ring::default_provider, CryptoProvider}; @@ -23,6 +24,8 @@ use common::{pumpfun::logs_data::TradeInfo, pumpfun::logs_events::PumpfunEvent, use common::pumpfun::logs_subscribe::SubscriptionHandle; use ipfs::TokenMetadataIPFS; +use constants::pumpfun::trade_type::{COPY_BUY, SNIPER_BUY}; + pub struct PumpFun { pub payer: Arc, pub rpc: Arc, @@ -31,6 +34,8 @@ pub struct PumpFun { pub cluster: Cluster, } +static INSTANCE: Mutex>> = Mutex::new(None); + impl Clone for PumpFun { fn clone(&self) -> Self { Self { @@ -105,13 +110,29 @@ impl PumpFun { fee_clients.push(Arc::new(rpc_client)); } - Self { + let instance = Self { payer, rpc, fee_clients, priority_fee: cluster.clone().priority_fee, cluster: cluster.clone(), - } + }; + + let mut current = INSTANCE.lock().unwrap(); + *current = Some(Arc::new(instance.clone())); + + instance + } + + /// Get the RPC client instance + pub fn get_rpc(&self) -> &Arc { + &self.rpc + } + + /// Get the current instance + pub fn get_instance() -> Arc { + let instance = INSTANCE.lock().unwrap(); + instance.as_ref().expect("PumpFun instance not initialized. Please call new() first.").clone() } /// Create a new token @@ -172,7 +193,7 @@ impl PumpFun { } /// Buy tokens - pub async fn buy( + pub async fn sniper_buy( &self, mint: Pubkey, creator: Pubkey, @@ -194,11 +215,38 @@ impl PumpFun { self.priority_fee.clone(), self.cluster.clone().lookup_table_key, recent_blockhash, + SNIPER_BUY.to_string(), + ).await + } + + pub async fn copy_buy( + &self, + mint: Pubkey, + creator: Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, + buy_sol_cost: u64, + slippage_basis_points: Option, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::buy::buy( + self.rpc.clone(), + self.payer.clone(), + mint, + creator, + dev_buy_token, + dev_sol_cost, + buy_sol_cost, + slippage_basis_points, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + COPY_BUY.to_string(), ).await } /// Buy tokens using Jito - pub async fn buy_with_tip( + pub async fn sniper_buy_with_tip( &self, mint: Pubkey, creator: Pubkey, @@ -220,6 +268,33 @@ impl PumpFun { self.priority_fee.clone(), self.cluster.clone().lookup_table_key, recent_blockhash, + SNIPER_BUY.to_string(), + ).await + } + + pub async fn copy_buy_with_tip( + &self, + mint: Pubkey, + creator: Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, + buy_sol_cost: u64, + slippage_basis_points: Option, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::buy::buy_with_tip( + self.fee_clients.clone(), + self.payer.clone(), + mint, + creator, + dev_buy_token, + dev_sol_cost, + buy_sol_cost, + slippage_basis_points, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + COPY_BUY.to_string(), ).await } @@ -265,6 +340,26 @@ impl PumpFun { ).await } + /// Sell tokens by amount + pub async fn sell_by_amount( + &self, + mint: Pubkey, + creator: Pubkey, + amount: u64, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::sell::sell_by_amount( + self.rpc.clone(), + self.payer.clone(), + mint.clone(), + creator, + amount, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + ).await + } + pub async fn sell_by_percent_with_tip( &self, mint: Pubkey, @@ -286,6 +381,25 @@ impl PumpFun { ).await } + pub async fn sell_by_amount_with_tip( + &self, + mint: Pubkey, + creator: Pubkey, + amount: u64, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::sell::sell_by_amount_with_tip( + self.fee_clients.clone(), + self.payer.clone(), + mint, + creator, + amount, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + ).await + } + /// Sell tokens using Jito pub async fn sell_with_tip( &self, @@ -375,4 +489,23 @@ impl PumpFun { pub async fn close_token_account(&self, mint: &Pubkey) -> Result<(), anyhow::Error> { pumpfun::common::close_token_account(&self.rpc, self.payer.as_ref(), mint).await } + + #[inline] + pub async fn get_current_price(&self, mint: &Pubkey) -> Result { + let (bonding_curve, _) = pumpfun::common::get_bonding_curve_account_v2(&self.rpc, mint).await?; + + let virtual_sol_reserves = bonding_curve.virtual_sol_reserves; + let virtual_token_reserves = bonding_curve.virtual_token_reserves; + + Ok(pumpfun::common::get_token_price(virtual_sol_reserves, virtual_token_reserves)) + } + + #[inline] + pub async fn get_real_sol_reserves(&self, mint: &Pubkey) -> Result { + let (bonding_curve, _) = pumpfun::common::get_bonding_curve_account_v2(&self.rpc, mint).await?; + + let actual_sol_reserves = bonding_curve.real_sol_reserves; + + Ok(actual_sol_reserves) + } } diff --git a/src/main.rs b/src/main.rs index 190e8e7..9cc1c50 100755 --- a/src/main.rs +++ b/src/main.rs @@ -5,14 +5,16 @@ use solana_sdk::{commitment_config::CommitmentConfig, transaction::VersionedTran #[tokio::main] async fn main() -> Result<(), Box> { - // test_pumpfun().await?; - test_pumpswap().await?; + // test_pumpfun_with_shreds().await?; + // test_pumpfun_with_grpc().await?; + // test_pumpswap_with_shreds().await?; + test_pumpswap_with_grpc().await?; Ok(()) } -async fn test_pumpfun() -> Result<(), Box> { +async fn test_pumpfun_with_shreds() -> Result<(), Box> { let grpc = ShredStreamGrpc::new( - "http://127.0.0.1:10800".to_string(), + "http://127.0.0.1:10000".to_string(), ).await?; let callback = |event: PumpfunEvent| { @@ -52,12 +54,44 @@ async fn test_pumpfun() -> Result<(), Box> { Ok(()) } -async fn test_pumpswap() -> Result<(), Box> { - // 使用 GRPC 客户端订阅 PumpSwap 事件 - println!("正在订阅 PumpSwap GRPC 事件..."); +async fn test_pumpfun_with_grpc() -> Result<(), Box> { + let grpc = YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + )?; + + let callback = |event: PumpfunEvent| { + + match event { + PumpfunEvent::NewDevTrade(trade_info) => { + println!("Received new dev trade event: {:?}", trade_info); + }, + PumpfunEvent::NewToken(token_info) => { + println!("Received new token event: {:?}", token_info); + }, + PumpfunEvent::NewUserTrade(trade_info) => { + println!("Received new trade event: {:?}", trade_info); + }, + PumpfunEvent::NewBotTrade(trade_info) => { + println!("Received new bot trade event: {:?}", trade_info); + }, + PumpfunEvent::Error(err) => { + println!("Received error: {}", err); + } + } + }; + + grpc.subscribe_pumpfun(callback, None).await?; + + Ok(()) +} + +async fn test_pumpswap_with_shreds() -> Result<(), Box> { + // 使用 ShredStream 客户端订阅 PumpSwap 事件 + println!("正在订阅 PumpSwap ShredStream 事件..."); let grpc_client = ShredStreamGrpc::new( - "http://127.0.0.1:10800".to_string(), + "http://127.0.0.1:10000".to_string(), ).await?; // 定义回调函数处理 PumpSwap 事件 @@ -100,6 +134,55 @@ async fn test_pumpswap() -> Result<(), Box> { Ok(()) } +async fn test_pumpswap_with_grpc() -> Result<(), Box> { + // 使用 GRPC 客户端订阅 PumpSwap 事件 + println!("正在订阅 PumpSwap GRPC 事件..."); + + let grpc = YellowstoneGrpc::new( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None + )?; + + // 定义回调函数处理 PumpSwap 事件 + let callback = |event: PumpSwapEvent| { + match event { + PumpSwapEvent::Buy(buy_event) => { + println!("buy_event: {:?}", buy_event); + }, + PumpSwapEvent::Sell(sell_event) => { + println!("sell_event: {:?}", sell_event); + }, + PumpSwapEvent::CreatePool(create_event) => { + println!("create_event: {:?}", create_event); + }, + PumpSwapEvent::Deposit(deposit_event) => { + println!("deposit_event: {:?}", deposit_event); + }, + PumpSwapEvent::Withdraw(withdraw_event) => { + println!("withdraw_event: {:?}", withdraw_event); + }, + PumpSwapEvent::Disable(disable_event) => { + println!("disable_event: {:?}", disable_event); + }, + PumpSwapEvent::UpdateAdmin(update_admin_event) => { + println!("update_admin_event: {:?}", update_admin_event); + }, + PumpSwapEvent::UpdateFeeConfig(update_fee_event) => { + println!("update_fee_event: {:?}", update_fee_event); + }, + PumpSwapEvent::Error(err) => { + println!("error: {}", err); + } + } + }; + // 订阅 PumpSwap 事件 + println!("开始监听 PumpSwap 事件,按 Ctrl+C 停止..."); + + grpc.subscribe_pumpswap(callback).await?; + + Ok(()) +} + async fn test_wss() -> AnyResult<()> { println!("Starting token subscription\n"); diff --git a/src/pumpfun/buy.rs b/src/pumpfun/buy.rs index 5f73de7..24d7248 100755 --- a/src/pumpfun/buy.rs +++ b/src/pumpfun/buy.rs @@ -22,7 +22,9 @@ use crate::{ const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 250000; -use super::common::{calculate_with_slippage_buy, get_buy_token_amount_from_sol_amount, init_bonding_curve_account}; +use super::common::{calculate_with_slippage_buy, get_buy_token_amount_from_sol_amount, init_bonding_curve_account, get_bonding_curve_account_v2, get_bonding_curve_pda}; +use crate::constants::pumpfun::trade_type::{SNIPER_BUY}; +use crate::PumpFun; /// 添加nonce消费指令到指令集合中 /// @@ -76,10 +78,11 @@ pub async fn buy( priority_fee: PriorityFee, lookup_table_key: Option, recent_blockhash: Hash, + trade_type: String, ) -> Result<(), anyhow::Error> { let start_time = Instant::now(); let mint = Arc::new(mint.clone()); - let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points).await?; + let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points, trade_type).await?; println!(" 买入交易指令: {:?}", start_time.elapsed()); let start_time = Instant::now(); @@ -191,10 +194,11 @@ pub async fn buy_with_tip( priority_fee: PriorityFee, lookup_table_key: Option, recent_blockhash: Hash, + trade_type: String, ) -> Result<(), anyhow::Error> { let start_time = Instant::now(); let mint = Arc::new(mint.clone()); - let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points).await?; + let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points, trade_type).await?; println!(" 买入交易指令: {:?}", start_time.elapsed()); let start_time = Instant::now(); @@ -356,7 +360,6 @@ pub async fn build_buy_transaction_with_tip( } pub async fn build_buy_instructions( - // rpc: Arc, payer: Arc, mint: Arc, creator: Pubkey, @@ -364,12 +367,29 @@ pub async fn build_buy_instructions( dev_sol_cost: u64, buy_sol_cost: u64, slippage_basis_points: Option, + trade_type: String, ) -> Result, anyhow::Error> { if buy_sol_cost == 0 { return Err(anyhow!("Amount cannot be zero")); } - let bonding_curve = init_bonding_curve_account(&mint, dev_buy_token, dev_sol_cost, creator).await?; + let bonding_curve = if trade_type == SNIPER_BUY { + init_bonding_curve_account(&mint, dev_buy_token, dev_sol_cost, creator).await? + } else { + let (bonding_curve, _) = get_bonding_curve_account_v2(&PumpFun::get_instance().get_rpc(), &mint).await?; + Arc::new(crate::accounts::BondingCurveAccount { + discriminator: bonding_curve.discriminator, + account: get_bonding_curve_pda(&mint).unwrap(), + virtual_token_reserves: bonding_curve.virtual_token_reserves, + virtual_sol_reserves: bonding_curve.virtual_sol_reserves, + real_token_reserves: bonding_curve.real_token_reserves, + real_sol_reserves: bonding_curve.real_sol_reserves, + token_total_supply: bonding_curve.token_total_supply, + complete: bonding_curve.complete, + creator: creator, + }) + }; + let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(100)); let creator_vault_pda = bonding_curve.get_creator_vault_pda(); diff --git a/src/pumpfun/common.rs b/src/pumpfun/common.rs index 27f268a..3da8b4e 100755 --- a/src/pumpfun/common.rs +++ b/src/pumpfun/common.rs @@ -7,6 +7,7 @@ use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction }; use spl_associated_token_account::get_associated_token_address; +use pumpfun_program::accounts::BondingCurveAccount as PumpfunBondingCurveAccount; use crate::{accounts::{self, BondingCurveAccount}, common::{pumpfun::logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, pumpfun::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}}; lazy_static::lazy_static! { @@ -224,6 +225,25 @@ pub async fn get_bonding_curve_account( Ok((bonding_curve, bonding_curve_pda)) } +#[inline] +pub async fn get_bonding_curve_account_v2( + rpc: &SolanaRpcClient, + mint: &Pubkey, +) -> Result<(Arc, Pubkey), anyhow::Error> { + let bonding_curve_pda = get_bonding_curve_pda(mint) + .ok_or(anyhow!("Bonding curve not found"))?; + + let account = rpc.get_account(&bonding_curve_pda).await?; + if account.data.is_empty() { + return Err(anyhow!("Bonding curve not found")); + } + + let bonding_curve = solana_sdk::borsh1::try_from_slice_unchecked::(&account.data) + .map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?; + + Ok((Arc::new(bonding_curve), bonding_curve_pda)) +} + // #[inline] // pub fn get_buy_token_amount( // mint: &Pubkey, diff --git a/src/pumpfun/sell.rs b/src/pumpfun/sell.rs index 96130f5..15334e4 100755 --- a/src/pumpfun/sell.rs +++ b/src/pumpfun/sell.rs @@ -56,6 +56,24 @@ pub async fn sell_by_percent( sell(rpc, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await } +/// Sell tokens by amount +pub async fn sell_by_amount( + rpc: Arc, + payer: Arc, + mint: Pubkey, + creator: Pubkey, + amount: u64, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + if amount == 0 { + return Err(anyhow!("Amount must be greater than 0")); + } + + sell(rpc, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await +} + pub async fn sell_by_percent_with_tip( fee_clients: Vec>, payer: Arc, @@ -75,6 +93,23 @@ pub async fn sell_by_percent_with_tip( sell_with_tip(fee_clients, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await } +pub async fn sell_by_amount_with_tip( + fee_clients: Vec>, + payer: Arc, + mint: Pubkey, + creator: Pubkey, + amount: u64, + priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + if amount == 0 { + return Err(anyhow!("Amount must be greater than 0")); + } + + sell_with_tip(fee_clients, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await +} + /// Sell tokens using Jito pub async fn sell_with_tip( fee_clients: Vec>, From 651789b553c0410fe7b827b5fc557a8f7cbc370e Mon Sep 17 00:00:00 2001 From: sgxiang Date: Tue, 10 Jun 2025 00:54:42 +0800 Subject: [PATCH 4/8] refactor(pumpswap): refactor account handling logic in logs_parser and optimize account data extraction --- src/common/pumpswap/logs_parser.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/common/pumpswap/logs_parser.rs b/src/common/pumpswap/logs_parser.rs index 8715279..961945c 100755 --- a/src/common/pumpswap/logs_parser.rs +++ b/src/common/pumpswap/logs_parser.rs @@ -90,7 +90,7 @@ fn current_timestamp() -> i64 { } /// 从指令中解析PumpSwap指令 -pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, accounts: &[Pubkey]) -> Option { +pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, _accounts: &[Pubkey]) -> Option { if instruction.data.len() < 8 { return None; } @@ -98,6 +98,10 @@ pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, accounts: & 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 From a3859edf863cc8c008456b58e28b31e6040145c3 Mon Sep 17 00:00:00 2001 From: wei <1415121722@qq.com> Date: Tue, 10 Jun 2025 16:13:26 +0800 Subject: [PATCH 5/8] add signature --- src/common/pumpswap/logs_data.rs | 6 ++++++ src/grpc/shred_stream.rs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/common/pumpswap/logs_data.rs b/src/common/pumpswap/logs_data.rs index 681e0f5..4c3cd26 100644 --- a/src/common/pumpswap/logs_data.rs +++ b/src/common/pumpswap/logs_data.rs @@ -43,6 +43,9 @@ pub struct BuyEvent { 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, } @@ -72,6 +75,9 @@ pub struct SellEvent { 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, } diff --git a/src/grpc/shred_stream.rs b/src/grpc/shred_stream.rs index 807bb4a..a3c1646 100755 --- a/src/grpc/shred_stream.rs +++ b/src/grpc/shred_stream.rs @@ -162,39 +162,48 @@ impl ShredStreamGrpc { { 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)); } _ => {} From 6f819be4e0ea29786a516c45674f381f7f00d657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=A8=81=E5=A8=81?= <1415121722@qq.com> Date: Tue, 10 Jun 2025 19:39:06 +0800 Subject: [PATCH 6/8] Buy and sell according to the trading platform --- src/constants/mod.rs | 15 ++- src/constants/pumpfun/mod.rs | 7 -- src/lib.rs | 237 ++++++++++++++++++++++++----------- src/pumpfun/buy.rs | 2 +- src/pumpswap/buy.rs | 11 +- src/pumpswap/pool.rs | 4 +- src/pumpswap/sell.rs | 41 +++++- 7 files changed, 227 insertions(+), 90 deletions(-) diff --git a/src/constants/mod.rs b/src/constants/mod.rs index 648fc87..945a789 100644 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -1,2 +1,15 @@ pub mod pumpfun; -pub mod pumpswap; \ No newline at end of file +pub mod pumpswap; + +pub mod trade_type { + pub const COPY_BUY: &'static str = "copy_buy"; + pub const COPY_SELL: &'static str = "copy_sell"; + pub const SNIPER_BUY: &'static str = "sniper_buy"; + pub const SNIPER_SELL: &'static str = "sniper_sell"; +} + +pub mod trade_platform { + pub const PUMPFUN: &'static str = "pumpfun"; + pub const PUMPFUN_SWAP: &'static str = "pumpswap"; + pub const RAYDIUM: &'static str = "raydium"; +} \ No newline at end of file diff --git a/src/constants/pumpfun/mod.rs b/src/constants/pumpfun/mod.rs index 8ddadc8..c59a2c9 100755 --- a/src/constants/pumpfun/mod.rs +++ b/src/constants/pumpfun/mod.rs @@ -173,10 +173,3 @@ impl Symbol { pub const SOLANA: &'static str = "solana"; } -pub mod trade_type { - pub const COPY_BUY: &'static str = "copy_buy"; - pub const COPY_SELL: &'static str = "copy_sell"; - pub const SNIPER_BUY: &'static str = "sniper_buy"; - pub const SNIPER_SELL: &'static str = "sniper_sell"; -} - diff --git a/src/lib.rs b/src/lib.rs index 026dcd4..2f19e10 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod common; pub mod ipfs; pub mod swqos; pub mod pumpfun; +pub mod pumpswap; use std::sync::Arc; use std::sync::Mutex; @@ -24,7 +25,8 @@ use common::{pumpfun::logs_data::TradeInfo, pumpfun::logs_events::PumpfunEvent, use common::pumpfun::logs_subscribe::SubscriptionHandle; use ipfs::TokenMetadataIPFS; -use constants::pumpfun::trade_type::{COPY_BUY, SNIPER_BUY}; +use constants::trade_type::{COPY_BUY, SNIPER_BUY}; +use constants::trade_platform::{PUMPFUN, PUMPFUN_SWAP, RAYDIUM}; pub struct PumpFun { pub payer: Arc, @@ -228,21 +230,36 @@ impl PumpFun { buy_sol_cost: u64, slippage_basis_points: Option, recent_blockhash: Hash, + trade_platform: String, ) -> Result<(), anyhow::Error> { - pumpfun::buy::buy( - self.rpc.clone(), - self.payer.clone(), - mint, - creator, - dev_buy_token, - dev_sol_cost, - buy_sol_cost, - slippage_basis_points, - self.priority_fee.clone(), - self.cluster.clone().lookup_table_key, - recent_blockhash, - COPY_BUY.to_string(), - ).await + if trade_platform == PUMPFUN { + pumpfun::buy::buy( + self.rpc.clone(), + self.payer.clone(), + mint, + creator, + dev_buy_token, + dev_sol_cost, + buy_sol_cost, + slippage_basis_points, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + COPY_BUY.to_string(), + ).await + } else if trade_platform == PUMPFUN_SWAP { + pumpswap::buy::buy( + self.rpc.clone(), + self.payer.clone(), + mint, + buy_sol_cost, + slippage_basis_points, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + ).await + } else { + Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) + } } /// Buy tokens using Jito @@ -281,21 +298,37 @@ impl PumpFun { buy_sol_cost: u64, slippage_basis_points: Option, recent_blockhash: Hash, + trade_platform: String, ) -> Result<(), anyhow::Error> { - pumpfun::buy::buy_with_tip( - self.fee_clients.clone(), - self.payer.clone(), - mint, - creator, - dev_buy_token, - dev_sol_cost, - buy_sol_cost, - slippage_basis_points, - self.priority_fee.clone(), - self.cluster.clone().lookup_table_key, - recent_blockhash, - COPY_BUY.to_string(), - ).await + if trade_platform == PUMPFUN { + pumpfun::buy::buy_with_tip( + self.fee_clients.clone(), + self.payer.clone(), + mint, + creator, + dev_buy_token, + dev_sol_cost, + buy_sol_cost, + slippage_basis_points, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + COPY_BUY.to_string(), + ).await + } else if trade_platform == PUMPFUN_SWAP { + pumpswap::buy::buy_with_tip( + self.rpc.clone(), + self.fee_clients.clone(), + self.payer.clone(), + mint, + buy_sol_cost, + slippage_basis_points, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + ).await + } else { + Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) + } } /// Sell tokens @@ -326,18 +359,33 @@ impl PumpFun { percent: u64, amount_token: u64, recent_blockhash: Hash, + trade_platform: String, ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell_by_percent( - self.rpc.clone(), - self.payer.clone(), - mint.clone(), - creator, - percent, - amount_token, - self.priority_fee.clone(), - self.cluster.clone().lookup_table_key, - recent_blockhash, - ).await + 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.cluster.clone().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(), + percent, + None, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + ).await + } else { + Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) + } } /// Sell tokens by amount @@ -347,17 +395,32 @@ impl PumpFun { creator: Pubkey, amount: u64, recent_blockhash: Hash, + trade_platform: String, ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell_by_amount( - self.rpc.clone(), - self.payer.clone(), - mint.clone(), - creator, - amount, - self.priority_fee.clone(), - self.cluster.clone().lookup_table_key, - recent_blockhash, - ).await + if trade_platform == PUMPFUN { + pumpfun::sell::sell_by_amount( + self.rpc.clone(), + self.payer.clone(), + mint.clone(), + creator, + amount, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + ).await + } else if trade_platform == PUMPFUN_SWAP { + pumpswap::sell::sell_by_amount( + self.rpc.clone(), + self.payer.clone(), + mint.clone(), + amount, + None, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + ).await + } else { + Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) + } } pub async fn sell_by_percent_with_tip( @@ -367,18 +430,34 @@ impl PumpFun { percent: u64, amount_token: u64, recent_blockhash: Hash, + trade_platform: String, ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell_by_percent_with_tip( - self.fee_clients.clone(), - self.payer.clone(), - mint, - creator, - percent, - amount_token, - self.priority_fee.clone(), - self.cluster.clone().lookup_table_key, - recent_blockhash, - ).await + if trade_platform == PUMPFUN { + pumpfun::sell::sell_by_percent_with_tip( + self.fee_clients.clone(), + self.payer.clone(), + mint, + creator, + percent, + amount_token, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + ).await + } else if trade_platform == PUMPFUN_SWAP { + pumpswap::sell::sell_by_percent_with_tip( + self.rpc.clone(), + self.fee_clients.clone(), + self.payer.clone(), + mint, + percent, + None, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + ).await + } else { + Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) + } } pub async fn sell_by_amount_with_tip( @@ -387,17 +466,33 @@ impl PumpFun { creator: Pubkey, amount: u64, recent_blockhash: Hash, + trade_platform: String, ) -> Result<(), anyhow::Error> { - pumpfun::sell::sell_by_amount_with_tip( - self.fee_clients.clone(), - self.payer.clone(), - mint, - creator, - amount, - self.priority_fee.clone(), - self.cluster.clone().lookup_table_key, - recent_blockhash, - ).await + if trade_platform == PUMPFUN { + pumpfun::sell::sell_by_amount_with_tip( + self.fee_clients.clone(), + self.payer.clone(), + mint, + creator, + amount, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, + ).await + } else if trade_platform == PUMPFUN_SWAP { + pumpswap::sell::sell_by_amount_with_tip( + self.rpc.clone(), + self.fee_clients.clone(), + self.payer.clone(), + mint, + amount, + None, + self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + ).await + } else { + Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) + } } /// Sell tokens using Jito diff --git a/src/pumpfun/buy.rs b/src/pumpfun/buy.rs index 24d7248..9ce2ceb 100755 --- a/src/pumpfun/buy.rs +++ b/src/pumpfun/buy.rs @@ -23,7 +23,7 @@ use crate::{ const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 250000; use super::common::{calculate_with_slippage_buy, get_buy_token_amount_from_sol_amount, init_bonding_curve_account, get_bonding_curve_account_v2, get_bonding_curve_pda}; -use crate::constants::pumpfun::trade_type::{SNIPER_BUY}; +use crate::constants::trade_type::{SNIPER_BUY}; use crate::PumpFun; /// 添加nonce消费指令到指令集合中 diff --git a/src/pumpswap/buy.rs b/src/pumpswap/buy.rs index d258fe5..f06f5ad 100644 --- a/src/pumpswap/buy.rs +++ b/src/pumpswap/buy.rs @@ -11,12 +11,13 @@ use solana_sdk::{ signature::{Keypair, Signer}, system_instruction, transaction::VersionedTransaction, + native_token::sol_to_lamports, }; use spl_associated_token_account::instruction::create_associated_token_account_idempotent; use crate::common::{address_lookup_cache::get_address_lookup_table_account, nonce_cache::{self, NonceCache}, PriorityFee, SolanaRpcClient}; use crate::pumpswap::common::{calculate_with_slippage_buy, find_pool, get_buy_token_amount}; -use crate::constants::{accounts, trade::DEFAULT_SLIPPAGE, BUY_DISCRIMINATOR}; +use crate::constants::pumpswap::{accounts, trade::DEFAULT_SLIPPAGE, BUY_DISCRIMINATOR}; use crate::swqos::FeeClient; // Constants for compute budget @@ -31,7 +32,7 @@ const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 256 * 1024; fn add_nonce_instruction(instructions: &mut Vec, payer: &Keypair) -> Result<(), anyhow::Error> { let nonce_cache = NonceCache::get_instance(); let nonce_info = nonce_cache.get_nonce_info(); - if let (Some(nonce_pubkey), Some(program_id)) = (nonce_info.nonce_account, nonce_info.program_id) { + if let Some(nonce_pubkey) = nonce_info.nonce_account { let nonce_value = nonce_info.current_nonce; // 暂不加锁 // if nonce_info.lock { @@ -47,7 +48,7 @@ fn add_nonce_instruction(instructions: &mut Vec, payer: &Keypair) - // nonce_cache.lock(); // 创建自定义nonce消费指令 let nonce_consume_ix = Instruction { - program_id, + program_id: crate::constants::pumpswap::accounts::AMM_PROGRAM, accounts: vec![ AccountMeta::new(nonce_pubkey, false), AccountMeta::new_readonly(payer.pubkey(), true), @@ -55,7 +56,7 @@ fn add_nonce_instruction(instructions: &mut Vec, payer: &Keypair) - // INSTR_CONSUME = 1, 使用传入的nonce值 data: { let mut data = vec![1]; // INSTR_CONSUME = 1 - data.extend_from_slice(&nonce_value.to_le_bytes()); // 添加nonce值 + data.extend_from_slice(&nonce_value.to_bytes()); // 添加nonce值 data }, }; @@ -248,7 +249,7 @@ pub async fn build_buy_transaction_with_tip( system_instruction::transfer( &payer.pubkey(), &tip_account, - priority_fee.buy_tip_fee, + sol_to_lamports(priority_fee.buy_tip_fee), ), ]; diff --git a/src/pumpswap/pool.rs b/src/pumpswap/pool.rs index 0f60514..c91952e 100644 --- a/src/pumpswap/pool.rs +++ b/src/pumpswap/pool.rs @@ -1,7 +1,7 @@ use solana_sdk::pubkey::Pubkey; use anyhow::anyhow; use solana_account_decoder::UiAccountEncoding; -use crate::{common::SolanaRpcClient, constants::accounts}; +use crate::{common::SolanaRpcClient, constants::pumpswap::accounts}; use std::str::FromStr; #[derive(Debug, Clone)] @@ -91,7 +91,7 @@ impl Pool { sort_results: None, }; - let program_id = crate::constants::accounts::AMM_PROGRAM; + 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?; diff --git a/src/pumpswap/sell.rs b/src/pumpswap/sell.rs index a06ab58..a9755b2 100644 --- a/src/pumpswap/sell.rs +++ b/src/pumpswap/sell.rs @@ -5,17 +5,17 @@ use anyhow::anyhow; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, - pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction, transaction::VersionedTransaction, + native_token::sol_to_lamports, }; use spl_associated_token_account::instruction::create_associated_token_account_idempotent; use crate::common::{address_lookup_cache::get_address_lookup_table_account, PriorityFee, SolanaRpcClient}; use crate::pumpswap::common::{calculate_with_slippage_sell, find_pool, get_sell_sol_amount, get_token_balance}; -use crate::constants::{accounts, trade::DEFAULT_SLIPPAGE, SELL_DISCRIMINATOR}; +use crate::constants::pumpswap::{accounts, trade::DEFAULT_SLIPPAGE, SELL_DISCRIMINATOR}; use crate::swqos::FeeClient; // Constants for compute budget @@ -73,6 +73,23 @@ pub async fn sell_by_percent( sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await } +/// Sell tokens by amount +pub async fn sell_by_amount( + rpc: Arc, + payer: Arc, + mint: Pubkey, + amount: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option +) -> Result<(), anyhow::Error> { + if amount == 0 { + return Err(anyhow!("Amount must be greater than 0")); + } + + sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await +} + // Sell tokens using a MEV service pub async fn sell_with_tip( rpc: Arc, @@ -144,6 +161,24 @@ pub async fn sell_by_percent_with_tip( sell_with_tip(rpc, fee_clients, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await } +// Sell tokens by amount using a MEV service +pub async fn sell_by_amount_with_tip( + rpc: Arc, + fee_clients: Vec>, + payer: Arc, + mint: Pubkey, + amount: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + lookup_table_key: Option +) -> Result<(), anyhow::Error> { + if amount == 0 { + return Err(anyhow!("Amount must be greater than 0")); + } + + sell_with_tip(rpc, fee_clients, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await +} + // Build a transaction for selling tokens pub async fn build_sell_transaction( _rpc: Arc, @@ -206,7 +241,7 @@ pub async fn build_sell_transaction_with_tip( system_instruction::transfer( &payer.pubkey(), &tip_account, - priority_fee.sell_tip_fee, + sol_to_lamports(priority_fee.sell_tip_fee), ), ]; From 1743fecf1abc184d3e346b7c5126eb06869a447c Mon Sep 17 00:00:00 2001 From: wei <1415121722@qq.com> Date: Wed, 11 Jun 2025 21:08:02 +0800 Subject: [PATCH 7/8] Obtain liquidity of the pool --- src/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 2f19e10..c1cafc9 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -603,4 +603,13 @@ impl PumpFun { Ok(actual_sol_reserves) } + + #[inline] + 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) + } } From 165335af11ac0cd964b5043d1183f2129bd80995 Mon Sep 17 00:00:00 2001 From: sgxiang Date: Thu, 12 Jun 2025 00:00:30 +0800 Subject: [PATCH 8/8] feat(pumpswap): enhance PumpSwap trading functionality and logs parsing system - Expand buy/sell trading logic with optimized execution flow - Improve logs data structures and parser performance - Enhance common utilities and pool management features - Refactor main program architecture for better maintainability - Update library export interfaces with improved API design --- src/common/pumpswap/logs_data.rs | 26 ++ src/common/pumpswap/logs_parser.rs | 16 +- src/lib.rs | 36 +++ src/main.rs | 282 +++++++++++++-------- src/pumpswap/buy.rs | 289 ++++++++++++++++----- src/pumpswap/common.rs | 19 ++ src/pumpswap/pool.rs | 2 +- src/pumpswap/sell.rs | 393 ++++++++++++++++++++++++----- 8 files changed, 828 insertions(+), 235 deletions(-) diff --git a/src/common/pumpswap/logs_data.rs b/src/common/pumpswap/logs_data.rs index 4c3cd26..58a67d1 100644 --- a/src/common/pumpswap/logs_data.rs +++ b/src/common/pumpswap/logs_data.rs @@ -48,6 +48,19 @@ pub struct BuyEvent { 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, } /// 卖出事件 @@ -80,6 +93,19 @@ pub struct SellEvent { 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, } /// 创建池子事件 diff --git a/src/common/pumpswap/logs_parser.rs b/src/common/pumpswap/logs_parser.rs index 961945c..e62eb84 100755 --- a/src/common/pumpswap/logs_parser.rs +++ b/src/common/pumpswap/logs_parser.rs @@ -113,7 +113,7 @@ pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, _accounts: } 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, @@ -124,6 +124,13 @@ pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, _accounts: 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() })) }, @@ -148,6 +155,13 @@ pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, _accounts: 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() })) }, diff --git a/src/lib.rs b/src/lib.rs index 2f19e10..17cb4ca 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -252,10 +252,16 @@ impl PumpFun { self.rpc.clone(), self.payer.clone(), mint, + creator, buy_sol_cost, slippage_basis_points, self.priority_fee.clone(), self.cluster.clone().lookup_table_key, + None, + None, + None, + None, + None, ).await } else { Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) @@ -321,10 +327,16 @@ impl PumpFun { self.fee_clients.clone(), self.payer.clone(), mint, + creator, buy_sol_cost, slippage_basis_points, self.priority_fee.clone(), self.cluster.clone().lookup_table_key, + None, + None, + None, + None, + None, ).await } else { Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) @@ -378,10 +390,16 @@ impl PumpFun { self.rpc.clone(), self.payer.clone(), mint.clone(), + creator, percent, None, self.priority_fee.clone(), self.cluster.clone().lookup_table_key, + None, + None, + None, + None, + None, ).await } else { Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) @@ -413,10 +431,16 @@ impl PumpFun { self.rpc.clone(), self.payer.clone(), mint.clone(), + creator, amount, None, self.priority_fee.clone(), self.cluster.clone().lookup_table_key, + None, + None, + None, + None, + None, ).await } else { Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) @@ -450,10 +474,16 @@ impl PumpFun { self.fee_clients.clone(), self.payer.clone(), mint, + creator, percent, None, self.priority_fee.clone(), self.cluster.clone().lookup_table_key, + None, + None, + None, + None, + None, ).await } else { Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) @@ -485,10 +515,16 @@ impl PumpFun { self.fee_clients.clone(), self.payer.clone(), mint, + creator, amount, None, self.priority_fee.clone(), self.cluster.clone().lookup_table_key, + None, + None, + None, + None, + None, ).await } else { Err(anyhow::anyhow!("Unsupported trade platform: {}", trade_platform)) diff --git a/src/main.rs b/src/main.rs index 9cc1c50..1badebf 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,38 @@ -use pumpfun_sdk::{common::{ - pumpfun::{logs_events::PumpfunEvent, logs_subscribe::{stop_subscription, tokens_subscription}}, pumpswap::{self, PumpSwapEvent}, AnyResult -}, grpc::{ShredStreamGrpc, YellowstoneGrpc}}; -use solana_sdk::{commitment_config::CommitmentConfig, transaction::VersionedTransaction}; +use std::{str::FromStr, sync::Arc}; + +use pumpfun_sdk::{ + common::{ + pumpfun::{ + self, + logs_events::PumpfunEvent, + logs_subscribe::{stop_subscription, tokens_subscription}, + }, + pumpswap::{self, PumpSwapEvent}, + AnyResult, Cluster, PriorityFee, + }, + grpc::{ShredStreamGrpc, YellowstoneGrpc}, + PumpFun, +}; +use solana_hash::Hash; +use solana_sdk::{ + commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair, + transaction::VersionedTransaction, +}; #[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?; - Ok(()) + // test_pumpswap_with_grpc().await?; + test_sell().await?; + Ok(()) } async fn test_pumpfun_with_shreds() -> Result<(), Box> { - let grpc = ShredStreamGrpc::new( - "http://127.0.0.1:10000".to_string(), - ).await?; + 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,代表用户愿意接受的最小金额 @@ -33,16 +47,16 @@ async fn test_pumpfun_with_shreds() -> Result<(), Box> { 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); } @@ -56,71 +70,66 @@ async fn test_pumpfun_with_shreds() -> Result<(), Box> { async fn test_pumpfun_with_grpc() -> Result<(), Box> { let grpc = YellowstoneGrpc::new( - "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + "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); - } + 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(()) + Ok(()) } async fn test_pumpswap_with_shreds() -> Result<(), Box> { // 使用 ShredStream 客户端订阅 PumpSwap 事件 println!("正在订阅 PumpSwap ShredStream 事件..."); - let grpc_client = ShredStreamGrpc::new( - "http://127.0.0.1:10000".to_string(), - ).await?; + let grpc_client = ShredStreamGrpc::new("http://140.82.2.197:10800".to_string()).await?; // 定义回调函数处理 PumpSwap 事件 let callback = |event: PumpSwapEvent| { match event { PumpSwapEvent::Buy(buy_event) => { - println!("buy_event: {:?}", 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); - }, + // println!("create_event: {:?}", create_event); + } PumpSwapEvent::Deposit(deposit_event) => { - println!("deposit_event: {:?}", deposit_event); - }, + // println!("deposit_event: {:?}", deposit_event); + } PumpSwapEvent::Withdraw(withdraw_event) => { - println!("withdraw_event: {:?}", withdraw_event); - }, + // println!("withdraw_event: {:?}", withdraw_event); + } PumpSwapEvent::Disable(disable_event) => { - println!("disable_event: {:?}", disable_event); - }, + // println!("disable_event: {:?}", disable_event); + } PumpSwapEvent::UpdateAdmin(update_admin_event) => { - println!("update_admin_event: {:?}", update_admin_event); - }, + // println!("update_admin_event: {:?}", update_admin_event); + } PumpSwapEvent::UpdateFeeConfig(update_fee_event) => { - println!("update_fee_event: {:?}", update_fee_event); - }, + // println!("update_fee_event: {:?}", update_fee_event); + } PumpSwapEvent::Error(err) => { println!("error: {}", err); } @@ -140,39 +149,37 @@ async fn test_pumpswap_with_grpc() -> Result<(), Box> { let grpc = YellowstoneGrpc::new( "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), - None + 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); - } + 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 事件 @@ -183,43 +190,102 @@ async fn test_pumpswap_with_grpc() -> Result<(), Box> { Ok(()) } +async fn test_sell() -> AnyResult<()> { + let payer = Keypair::new(); + // Define cluster configuration + let cluster = Cluster { + rpc_url: "https://mainnet.helius-rpc.com/?api-key=f2f194bb-6bd6-4f20-9a94-7fe0799ade0b" + .to_string(), + commitment: CommitmentConfig::confirmed(), + priority_fee: PriorityFee::default(), + use_jito: false, + use_zeroslot: false, + use_nozomi: false, + use_nextblock: false, + block_engine_url: "".to_string(), + zeroslot_url: "".to_string(), + zeroslot_auth_token: "".to_string(), + nozomi_url: "".to_string(), + nozomi_auth_token: "".to_string(), + nextblock_url: "".to_string(), + nextblock_auth_token: "".to_string(), + lookup_table_key: None, + use_rpc: true, + }; + + let pumpswap = PumpFun::new(Arc::new(payer), &cluster).await; + let creator = Pubkey::from_str("8BtoThi2ZoXnF7QQK1Wjmh2JuBw9FjVvhnGMVZ2vpump")?; + let dev_buy_token = 0; + let dev_sol_cost = 0; + let buy_sol_cost = 100_000_000; + let slippage_basis_points = Some(100); + let recent_blockhash = Hash::default(); + let trade_platform = "pumpswap".to_string(); + let mint_pubkey = Pubkey::from_str("8BtoThi2ZoXnF7QQK1Wjmh2JuBw9FjVvhnGMVZ2vpump")?; + println!("Buying tokens from PumpSwap..."); + pumpswap + .copy_buy( + mint_pubkey, + creator, + dev_buy_token, + dev_sol_cost, + buy_sol_cost, + slippage_basis_points, + recent_blockhash, + trade_platform, + ) + .await?; + // 需要先转sol到wsol才能buy + // pumpswap + // .buy( + // mint_pubkey, + // 10_000_000, // 0.01 SOL + // Some(100), // 1% slippage + // ) + // .await?; + // println!("Selling tokens to PumpSwap..."); + // pumpswap + // .sell_by_percent( + // mint_pubkey, + // 100, // Sell 100% of tokens + // Some(500), // 5% slippage + // ) + // .await?; + + Ok(()) +} async fn test_wss() -> AnyResult<()> { println!("Starting token subscription\n"); let ws_url = "wss://api.mainnet-beta.solana.com"; - + // Set commitment let commitment = CommitmentConfig::confirmed(); - + // 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); - } + 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); } }; // Start subscription - let subscription = tokens_subscription( - ws_url, - commitment, - callback, - None - ).await.unwrap(); + let subscription = tokens_subscription(ws_url, commitment, callback, None) + .await + .unwrap(); // Wait for a while to receive events tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; @@ -228,4 +294,4 @@ async fn test_wss() -> AnyResult<()> { stop_subscription(subscription).await; Ok(()) -} \ No newline at end of file +} diff --git a/src/pumpswap/buy.rs b/src/pumpswap/buy.rs index f06f5ad..94516c4 100644 --- a/src/pumpswap/buy.rs +++ b/src/pumpswap/buy.rs @@ -1,24 +1,31 @@ -use std::sync::Arc; -use std::time::Instant; -use std::str::FromStr; use anyhow::anyhow; use chrono; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::{AccountMeta, Instruction}, message::{v0, AddressLookupTableAccount, VersionedMessage}, + native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction, transaction::VersionedTransaction, - native_token::sol_to_lamports, }; use spl_associated_token_account::instruction::create_associated_token_account_idempotent; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Instant; -use crate::common::{address_lookup_cache::get_address_lookup_table_account, nonce_cache::{self, NonceCache}, PriorityFee, SolanaRpcClient}; -use crate::pumpswap::common::{calculate_with_slippage_buy, find_pool, get_buy_token_amount}; use crate::constants::pumpswap::{accounts, trade::DEFAULT_SLIPPAGE, BUY_DISCRIMINATOR}; +use crate::pumpswap::common::{calculate_with_slippage_buy, find_pool, get_buy_token_amount}; use crate::swqos::FeeClient; +use crate::{ + common::{ + address_lookup_cache::get_address_lookup_table_account, + nonce_cache::{self, NonceCache}, + PriorityFee, SolanaRpcClient, + }, + pumpswap::common::{coin_creator_vault_ata, coin_creator_vault_authority}, +}; // Constants for compute budget // Increased from 64KB to 256KB to handle larger transactions @@ -29,7 +36,10 @@ const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 256 * 1024; /// 只有当同时提供了nonce_pubkey和nonce_program_id时才使用nonce功能 /// 如果nonce被锁定、已使用或未准备好,将返回错误 /// 成功时会锁定并标记nonce为已使用 -fn add_nonce_instruction(instructions: &mut Vec, payer: &Keypair) -> Result<(), anyhow::Error> { +fn add_nonce_instruction( + instructions: &mut Vec, + payer: &Keypair, +) -> Result<(), anyhow::Error> { let nonce_cache = NonceCache::get_instance(); let nonce_info = nonce_cache.get_nonce_info(); if let Some(nonce_pubkey) = nonce_info.nonce_account { @@ -41,7 +51,9 @@ fn add_nonce_instruction(instructions: &mut Vec, payer: &Keypair) - if nonce_info.used { return Err(anyhow!("Nonce is used")); } - if nonce_info.next_buy_time == 0 || chrono::Utc::now().timestamp() < nonce_info.next_buy_time { + if nonce_info.next_buy_time == 0 + || chrono::Utc::now().timestamp() < nonce_info.next_buy_time + { return Err(anyhow!("Nonce is not ready")); } // 加锁 - 暂不加锁 @@ -94,14 +106,62 @@ pub async fn buy( rpc: Arc, payer: Arc, mint: Pubkey, + creator: Pubkey, amount_sol: u64, slippage_basis_points: Option, priority_fee: PriorityFee, lookup_table_key: Option, + // 可选(必须全部传) + pool: Option, + pool_base_token_account: Option, + pool_quote_token_account: Option, + user_base_token_account: Option, + user_quote_token_account: Option, ) -> Result<(), anyhow::Error> { let start_time = Instant::now(); let mint = Arc::new(mint.clone()); - let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?; + let creator = Arc::new(creator.clone()); + let instructions = match ( + pool, + pool_base_token_account, + pool_quote_token_account, + user_base_token_account, + user_quote_token_account, + ) { + ( + Some(pool), + Some(pool_base_token_account), + Some(pool_quote_token_account), + Some(user_base_token_account), + Some(user_quote_token_account), + ) => { + build_buy_instructions_with_accounts( + rpc.clone(), + payer.clone(), + Arc::new(pool), + Arc::new(pool_base_token_account), + Arc::new(pool_quote_token_account), + Arc::new(user_base_token_account), + Arc::new(user_quote_token_account), + mint.clone(), + creator.clone(), + amount_sol, + slippage_basis_points, + ) + .await? + } + _ => { + build_buy_instructions( + rpc.clone(), + payer.clone(), + mint.clone(), + creator.clone(), + amount_sol, + slippage_basis_points, + ) + .await? + } + }; println!(" Buy transaction instructions: {:?}", start_time.elapsed()); let start_time = Instant::now(); @@ -111,7 +171,8 @@ pub async fn buy( priority_fee.clone(), instructions, lookup_table_key, - ).await?; + ) + .await?; println!(" Buy transaction signature: {:?}", start_time.elapsed()); let start_time = Instant::now(); @@ -127,14 +188,62 @@ pub async fn buy_with_tip( fee_clients: Vec>, payer: Arc, mint: Pubkey, + creator: Pubkey, amount_sol: u64, slippage_basis_points: Option, priority_fee: PriorityFee, lookup_table_key: Option, + // 可选(必须全部传) + pool: Option, + pool_base_token_account: Option, + pool_quote_token_account: Option, + user_base_token_account: Option, + user_quote_token_account: Option, ) -> Result<(), anyhow::Error> { let start_time = Instant::now(); let mint = Arc::new(mint.clone()); - let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?; + let creator = Arc::new(creator.clone()); + let instructions = match ( + pool, + pool_base_token_account, + pool_quote_token_account, + user_base_token_account, + user_quote_token_account, + ) { + ( + Some(pool), + Some(pool_base_token_account), + Some(pool_quote_token_account), + Some(user_base_token_account), + Some(user_quote_token_account), + ) => { + build_buy_instructions_with_accounts( + rpc.clone(), + payer.clone(), + Arc::new(pool), + Arc::new(pool_base_token_account), + Arc::new(pool_quote_token_account), + Arc::new(user_base_token_account), + Arc::new(user_quote_token_account), + mint.clone(), + creator.clone(), + amount_sol, + slippage_basis_points, + ) + .await? + } + _ => { + build_buy_instructions( + rpc.clone(), + payer.clone(), + mint.clone(), + creator.clone(), + amount_sol, + slippage_basis_points, + ) + .await? + } + }; println!(" Buy transaction instructions: {:?}", start_time.elapsed()); let start_time = Instant::now(); @@ -151,7 +260,8 @@ pub async fn buy_with_tip( priority_fee.clone(), instructions.clone(), lookup_table_key, - ).await?; + ) + .await?; transactions.push(transaction); } @@ -164,7 +274,9 @@ pub async fn buy_with_tip( let fee_client = fee_client.clone(); let handle = tokio::spawn(async move { - fee_client.send_transaction(crate::swqos::TradeType::Buy, &transaction).await + fee_client + .send_transaction(crate::swqos::TradeType::Buy, &transaction) + .await }); handles.push(handle); @@ -188,7 +300,9 @@ pub async fn build_buy_transaction( lookup_table_key: Option, ) -> Result { let mut instructions = vec![ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( + MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT, + ), ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), ]; @@ -206,7 +320,10 @@ pub async fn build_buy_transaction( for instruction in &instructions { for account_meta in &instruction.accounts { if account_meta.is_signer && account_meta.pubkey != payer.pubkey() { - return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey)); + return Err(anyhow!( + "Transaction requires a signature from an account other than the payer: {}", + account_meta.pubkey + )); } } } @@ -222,7 +339,8 @@ pub async fn build_buy_transaction( &instructions, &address_lookup_table_accounts, blockhash, - ).map_err(|e| anyhow!(e))?; + ) + .map_err(|e| anyhow!(e))?; let versioned_message = VersionedMessage::V0(v0_message.clone()); let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; @@ -243,7 +361,9 @@ pub async fn build_buy_transaction_with_tip( lookup_table_key: Option, ) -> Result { let mut instructions = vec![ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( + MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT, + ), ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), system_instruction::transfer( @@ -266,7 +386,10 @@ pub async fn build_buy_transaction_with_tip( for instruction in &instructions { for account_meta in &instruction.accounts { if account_meta.is_signer && account_meta.pubkey != payer.pubkey() { - return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey)); + return Err(anyhow!( + "Transaction requires a signature from an account other than the payer: {}", + account_meta.pubkey + )); } } } @@ -282,7 +405,8 @@ pub async fn build_buy_transaction_with_tip( &instructions, &address_lookup_table_accounts, blockhash, - ).map_err(|e| anyhow!(e))?; + ) + .map_err(|e| anyhow!(e))?; let versioned_message = VersionedMessage::V0(v0_message.clone()); let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; @@ -298,6 +422,7 @@ pub async fn build_buy_instructions( rpc: Arc, payer: Arc, mint: Arc, + creator: Arc, amount_sol: u64, slippage_basis_points: Option, ) -> Result, anyhow::Error> { @@ -308,61 +433,111 @@ pub async fn build_buy_instructions( // Find the pool for this mint let pool = find_pool(rpc.as_ref(), mint.as_ref()).await?; + // Create the user's token account if it doesn't exist + let user_base_token_account = + spl_associated_token_account::get_associated_token_address(&payer.pubkey(), mint.as_ref()); + let user_quote_token_account = spl_associated_token_account::get_associated_token_address( + &payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + ); + + // Get pool token accounts + let pool_base_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &pool, + mint.as_ref(), + &accounts::TOKEN_PROGRAM, + ); + + let pool_quote_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &pool, + &accounts::WSOL_TOKEN_ACCOUNT, + &accounts::TOKEN_PROGRAM, + ); + + let instructions = build_buy_instructions_with_accounts( + rpc, + payer, + Arc::new(pool), + Arc::new(pool_base_token_account), + Arc::new(pool_quote_token_account), + Arc::new(user_base_token_account), + Arc::new(user_quote_token_account), + mint, + creator, + amount_sol, + slippage_basis_points, + ) + .await?; + + Ok(instructions) +} + +pub async fn build_buy_instructions_with_accounts( + rpc: Arc, + payer: Arc, + pool: Arc, + pool_base_token_account: Arc, + pool_quote_token_account: Arc, + user_base_token_account: Arc, + user_quote_token_account: Arc, + mint: Arc, + creator: Arc, + amount_sol: u64, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + // Calculate the expected token amount let token_amount = get_buy_token_amount(rpc.as_ref(), &pool, amount_sol).await?; // Calculate the maximum SOL amount with slippage - let max_sol_amount = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - - // Create the user's token account if it doesn't exist - let user_base_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), mint.as_ref()); - let user_quote_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &accounts::WSOL_TOKEN_ACCOUNT); - - // Get pool token accounts - let pool_base_token_account = spl_associated_token_account::get_associated_token_address_with_program_id( - &pool, - mint.as_ref(), - &accounts::TOKEN_PROGRAM, - ); - - let pool_quote_token_account = spl_associated_token_account::get_associated_token_address_with_program_id( - &pool, - &accounts::WSOL_TOKEN_ACCOUNT, - &accounts::TOKEN_PROGRAM, + let max_sol_amount = calculate_with_slippage_buy( + amount_sol, + slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), ); let mut instructions = vec![]; // Create the user's base token account if it doesn't exist - instructions.push( - create_associated_token_account_idempotent( - &payer.pubkey(), - &payer.pubkey(), - mint.as_ref(), - &accounts::TOKEN_PROGRAM, - ) - ); + instructions.push(create_associated_token_account_idempotent( + &payer.pubkey(), + &payer.pubkey(), + mint.as_ref(), + &accounts::TOKEN_PROGRAM, + )); + + let coin_creator_vault_ata = coin_creator_vault_ata(*creator.as_ref()); + let coin_creator_vault_authority = coin_creator_vault_authority(*creator.as_ref()); // Create the buy instruction // 注意:账户顺序必须与JavaScript SDK匹配 let accounts = vec![ - solana_sdk::instruction::AccountMeta::new_readonly(pool, false), // pool_id (readonly) - solana_sdk::instruction::AccountMeta::new(payer.pubkey(), true), // user (signer) + solana_sdk::instruction::AccountMeta::new_readonly(*pool, false), // pool_id (readonly) + solana_sdk::instruction::AccountMeta::new(payer.pubkey(), true), // user (signer) solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_ACCOUNT, false), // global (readonly) solana_sdk::instruction::AccountMeta::new_readonly(*mint, false), // mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // WSOL_TOKEN_ACCOUNT (readonly) - solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // user_base_token_account - solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // user_quote_token_account - solana_sdk::instruction::AccountMeta::new(pool_base_token_account, false), // pool_base_token_account - solana_sdk::instruction::AccountMeta::new(pool_quote_token_account, false), // pool_quote_token_account + solana_sdk::instruction::AccountMeta::new(*user_base_token_account, false), // user_base_token_account + solana_sdk::instruction::AccountMeta::new(*user_quote_token_account, false), // user_quote_token_account + solana_sdk::instruction::AccountMeta::new(*pool_base_token_account, false), // pool_base_token_account + solana_sdk::instruction::AccountMeta::new(*pool_quote_token_account, false), // pool_quote_token_account solana_sdk::instruction::AccountMeta::new_readonly(accounts::FEE_RECIPIENT, false), // fee_recipient (readonly) solana_sdk::instruction::AccountMeta::new(accounts::FEE_RECIPIENT_ATA, false), // fee_recipient_ata solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly, duplicated as in JS) solana_sdk::instruction::AccountMeta::new_readonly(accounts::SYSTEM_PROGRAM, false), // System Program (readonly) - solana_sdk::instruction::AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false), // ASSOCIATED_TOKEN_PROGRAM_ID (readonly) + solana_sdk::instruction::AccountMeta::new_readonly( + accounts::ASSOCIATED_TOKEN_PROGRAM, + false, + ), // ASSOCIATED_TOKEN_PROGRAM_ID (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // event_authority (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::AMM_PROGRAM, false), // PUMP_AMM_PROGRAM_ID (readonly) + solana_sdk::instruction::AccountMeta::new(coin_creator_vault_ata, false), // coin_creator_vault_ata + solana_sdk::instruction::AccountMeta::new_readonly(coin_creator_vault_authority, false), // coin_creator_vault_authority (readonly) ]; // Create the instruction data @@ -371,13 +546,11 @@ pub async fn build_buy_instructions( data.extend_from_slice(&token_amount.to_le_bytes()); data.extend_from_slice(&max_sol_amount.to_le_bytes()); - instructions.push( - Instruction { - program_id: accounts::AMM_PROGRAM, - accounts, - data, - } - ); + instructions.push(Instruction { + program_id: accounts::AMM_PROGRAM, + accounts, + data, + }); Ok(instructions) } diff --git a/src/pumpswap/common.rs b/src/pumpswap/common.rs index 38c663b..8f3f3a6 100644 --- a/src/pumpswap/common.rs +++ b/src/pumpswap/common.rs @@ -64,3 +64,22 @@ pub async fn get_sell_sol_amount( let pool_data = crate::pumpswap::pool::Pool::fetch(rpc, pool).await?; pool_data.calculate_sell_amount(rpc, token_amount).await } + +pub(crate) fn coin_creator_vault_authority(coin_creator: Pubkey) -> Pubkey { + let (pump_pool_authority, _) = Pubkey::find_program_address( + &[b"creator_vault", &coin_creator.to_bytes()], + &crate::constants::pumpswap::accounts::AMM_PROGRAM, + ); + pump_pool_authority +} + +pub(crate) fn coin_creator_vault_ata(coin_creator: Pubkey) -> Pubkey { + let creator_vault_authority = coin_creator_vault_authority(coin_creator); + let associated_token_creator_vault_authority = + spl_associated_token_account::get_associated_token_address_with_program_id( + &creator_vault_authority, + &crate::constants::pumpswap::accounts::WSOL_TOKEN_ACCOUNT, + &crate::constants::pumpswap::accounts::TOKEN_PROGRAM, + ); + associated_token_creator_vault_authority +} \ No newline at end of file diff --git a/src/pumpswap/pool.rs b/src/pumpswap/pool.rs index c91952e..23d5b38 100644 --- a/src/pumpswap/pool.rs +++ b/src/pumpswap/pool.rs @@ -73,7 +73,7 @@ impl Pool { ) -> Result<(Pubkey, Self), anyhow::Error> { // 使用getProgramAccounts查找给定mint的池子 let filters = vec![ - solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool账户的大小 + // solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool账户的大小 solana_rpc_client_api::filter::RpcFilterType::Memcmp( solana_client::rpc_filter::Memcmp::new_base58_encoded(43, &mint.to_bytes()), ), diff --git a/src/pumpswap/sell.rs b/src/pumpswap/sell.rs index a9755b2..60391c5 100644 --- a/src/pumpswap/sell.rs +++ b/src/pumpswap/sell.rs @@ -1,22 +1,29 @@ -use std::sync::Arc; -use std::time::Instant; -use std::str::FromStr; use anyhow::anyhow; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, + native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction, transaction::VersionedTransaction, - native_token::sol_to_lamports, }; use spl_associated_token_account::instruction::create_associated_token_account_idempotent; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Instant; -use crate::common::{address_lookup_cache::get_address_lookup_table_account, PriorityFee, SolanaRpcClient}; -use crate::pumpswap::common::{calculate_with_slippage_sell, find_pool, get_sell_sol_amount, get_token_balance}; use crate::constants::pumpswap::{accounts, trade::DEFAULT_SLIPPAGE, SELL_DISCRIMINATOR}; +use crate::pumpswap::common::{ + calculate_with_slippage_sell, find_pool, get_sell_sol_amount, get_token_balance, +}; use crate::swqos::FeeClient; +use crate::{ + common::{ + address_lookup_cache::get_address_lookup_table_account, PriorityFee, SolanaRpcClient, + }, + pumpswap::common::{coin_creator_vault_ata, coin_creator_vault_authority}, +}; // Constants for compute budget // Increased from 64KB to 256KB to handle larger transactions @@ -27,13 +34,60 @@ pub async fn sell( rpc: Arc, payer: Arc, mint: Pubkey, + creator: Pubkey, amount_token: Option, slippage_basis_points: Option, priority_fee: PriorityFee, - lookup_table_key: Option + lookup_table_key: Option, + // 可选(必须全部传) + pool: Option, + pool_base_token_account: Option, + pool_quote_token_account: Option, + user_base_token_account: Option, + user_quote_token_account: Option, ) -> Result<(), anyhow::Error> { let start_time = Instant::now(); - let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?; + let instructions = match ( + pool, + pool_base_token_account, + pool_quote_token_account, + user_base_token_account, + user_quote_token_account, + ) { + ( + Some(pool), + Some(pool_base_token_account), + Some(pool_quote_token_account), + Some(user_base_token_account), + Some(user_quote_token_account), + ) => { + build_sell_instructions_with_accounts( + rpc.clone(), + payer.clone(), + Arc::new(pool), + Arc::new(pool_base_token_account), + Arc::new(pool_quote_token_account), + Arc::new(user_base_token_account), + Arc::new(user_quote_token_account), + Arc::new(mint), + Arc::new(creator), + amount_token, + slippage_basis_points, + ) + .await? + } + _ => { + build_sell_instructions( + rpc.clone(), + payer.clone(), + mint.clone(), + creator.clone(), + amount_token, + slippage_basis_points, + ) + .await? + } + }; println!(" Sell transaction instructions: {:?}", start_time.elapsed()); let start_time = Instant::now(); @@ -44,8 +98,9 @@ pub async fn sell( priority_fee, instructions, lookup_table_key, - recent_blockhash - ).await?; + recent_blockhash, + ) + .await?; println!(" Sell transaction signature: {:?}", start_time.elapsed()); let start_time = Instant::now(); @@ -59,10 +114,17 @@ pub async fn sell_by_percent( rpc: Arc, payer: Arc, mint: Pubkey, + creator: Pubkey, percent: u64, slippage_basis_points: Option, priority_fee: PriorityFee, - lookup_table_key: Option + lookup_table_key: Option, + // 可选(必须全部传) + pool: Option, + pool_base_token_account: Option, + pool_quote_token_account: Option, + user_base_token_account: Option, + user_quote_token_account: Option, ) -> Result<(), anyhow::Error> { if percent == 0 || percent > 100 { return Err(anyhow!("Percentage must be between 1 and 100")); @@ -70,7 +132,22 @@ pub async fn sell_by_percent( let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; let amount = balance_u64 * percent / 100; - sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await + sell( + rpc, + payer, + mint, + creator, + Some(amount), + slippage_basis_points, + priority_fee, + lookup_table_key, + pool, + pool_base_token_account, + pool_quote_token_account, + user_base_token_account, + user_quote_token_account, + ) + .await } /// Sell tokens by amount @@ -78,16 +155,38 @@ pub async fn sell_by_amount( rpc: Arc, payer: Arc, mint: Pubkey, + creator: Pubkey, amount: u64, slippage_basis_points: Option, priority_fee: PriorityFee, - lookup_table_key: Option + lookup_table_key: Option, + // 可选(必须全部传) + pool: Option, + pool_base_token_account: Option, + pool_quote_token_account: Option, + 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, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await + sell( + rpc, + payer, + mint, + creator, + Some(amount), + slippage_basis_points, + priority_fee, + lookup_table_key, + pool, + pool_base_token_account, + pool_quote_token_account, + user_base_token_account, + user_quote_token_account, + ) + .await } // Sell tokens using a MEV service @@ -96,13 +195,60 @@ pub async fn sell_with_tip( fee_clients: Vec>, payer: Arc, mint: Pubkey, + creator: Pubkey, amount_token: Option, slippage_basis_points: Option, priority_fee: PriorityFee, - lookup_table_key: Option + lookup_table_key: Option, + // 可选(必须全部传) + pool: Option, + pool_base_token_account: Option, + pool_quote_token_account: Option, + user_base_token_account: Option, + user_quote_token_account: Option, ) -> Result<(), anyhow::Error> { let mut transactions = vec![]; - let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?; + let instructions = match ( + pool, + pool_base_token_account, + pool_quote_token_account, + user_base_token_account, + user_quote_token_account, + ) { + ( + Some(pool), + Some(pool_base_token_account), + Some(pool_quote_token_account), + Some(user_base_token_account), + Some(user_quote_token_account), + ) => { + build_sell_instructions_with_accounts( + rpc.clone(), + payer.clone(), + Arc::new(pool), + Arc::new(pool_base_token_account), + Arc::new(pool_quote_token_account), + Arc::new(user_base_token_account), + Arc::new(user_quote_token_account), + Arc::new(mint), + Arc::new(creator), + amount_token, + slippage_basis_points, + ) + .await? + } + _ => { + build_sell_instructions( + rpc.clone(), + payer.clone(), + mint.clone(), + creator.clone(), + amount_token, + slippage_basis_points, + ) + .await? + } + }; let recent_blockhash = rpc.get_latest_blockhash().await?; for fee_client in fee_clients.clone() { @@ -117,7 +263,8 @@ pub async fn sell_with_tip( instructions.clone(), lookup_table_key, recent_blockhash, - ).await?; + ) + .await?; transactions.push(transaction); } @@ -128,7 +275,9 @@ pub async fn sell_with_tip( let fee_client = fee_client.clone(); let handle = tokio::spawn(async move { - fee_client.send_transaction(crate::swqos::TradeType::Sell, &transaction).await + fee_client + .send_transaction(crate::swqos::TradeType::Sell, &transaction) + .await }); handles.push(handle); @@ -147,10 +296,17 @@ pub async fn sell_by_percent_with_tip( fee_clients: Vec>, payer: Arc, mint: Pubkey, + creator: Pubkey, percent: u64, slippage_basis_points: Option, priority_fee: PriorityFee, - lookup_table_key: Option + lookup_table_key: Option, + // 可选(必须全部传) + pool: Option, + pool_base_token_account: Option, + pool_quote_token_account: Option, + user_base_token_account: Option, + user_quote_token_account: Option, ) -> Result<(), anyhow::Error> { if percent == 0 || percent > 100 { return Err(anyhow!("Percentage must be between 1 and 100")); @@ -158,7 +314,23 @@ pub async fn sell_by_percent_with_tip( let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; let amount = balance_u64 * percent / 100; - sell_with_tip(rpc, fee_clients, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await + sell_with_tip( + rpc, + fee_clients, + payer, + mint, + creator, + Some(amount), + slippage_basis_points, + priority_fee, + lookup_table_key, + pool, + pool_base_token_account, + pool_quote_token_account, + user_base_token_account, + user_quote_token_account, + ) + .await } // Sell tokens by amount using a MEV service @@ -167,16 +339,39 @@ pub async fn sell_by_amount_with_tip( fee_clients: Vec>, payer: Arc, mint: Pubkey, + creator: Pubkey, amount: u64, slippage_basis_points: Option, priority_fee: PriorityFee, - lookup_table_key: Option + lookup_table_key: Option, + // 可选(必须全部传) + pool: Option, + pool_base_token_account: Option, + pool_quote_token_account: Option, + 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, fee_clients, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await + sell_with_tip( + rpc, + fee_clients, + payer, + mint, + creator, + Some(amount), + slippage_basis_points, + priority_fee, + lookup_table_key, + pool, + pool_base_token_account, + pool_quote_token_account, + user_base_token_account, + user_quote_token_account, + ) + .await } // Build a transaction for selling tokens @@ -189,7 +384,9 @@ pub async fn build_sell_transaction( recent_blockhash: solana_sdk::hash::Hash, ) -> Result { let mut instructions = vec![ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( + MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT, + ), ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), ]; @@ -200,7 +397,10 @@ pub async fn build_sell_transaction( for instruction in &instructions { for account_meta in &instruction.accounts { if account_meta.is_signer && account_meta.pubkey != payer.pubkey() { - return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey)); + return Err(anyhow!( + "Transaction requires a signature from an account other than the payer: {}", + account_meta.pubkey + )); } } } @@ -216,7 +416,8 @@ pub async fn build_sell_transaction( &instructions, &address_lookup_table_accounts, recent_blockhash, - ).map_err(|e| anyhow!(e))?; + ) + .map_err(|e| anyhow!(e))?; let versioned_message = solana_sdk::message::VersionedMessage::V0(v0_message); let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; @@ -235,7 +436,9 @@ pub async fn build_sell_transaction_with_tip( recent_blockhash: solana_sdk::hash::Hash, ) -> Result { let mut instructions = vec![ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( + MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT, + ), ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), system_instruction::transfer( @@ -251,7 +454,10 @@ pub async fn build_sell_transaction_with_tip( for instruction in &instructions { for account_meta in &instruction.accounts { if account_meta.is_signer && account_meta.pubkey != payer.pubkey() { - return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey)); + return Err(anyhow!( + "Transaction requires a signature from an account other than the payer: {}", + account_meta.pubkey + )); } } } @@ -267,7 +473,8 @@ pub async fn build_sell_transaction_with_tip( &instructions, &address_lookup_table_accounts, recent_blockhash, - ).map_err(|e| anyhow!(e))?; + ) + .map_err(|e| anyhow!(e))?; let versioned_message = solana_sdk::message::VersionedMessage::V0(v0_message); let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; @@ -280,6 +487,7 @@ pub async fn build_sell_instructions( rpc: Arc, payer: Arc, mint: Pubkey, + creator: Pubkey, amount_token: Option, slippage_basis_points: Option, ) -> Result, anyhow::Error> { @@ -293,61 +501,114 @@ pub async fn build_sell_instructions( // Find the pool for this mint let pool = find_pool(rpc.as_ref(), &mint).await?; + // Get token accounts + let user_base_token_account = + spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &mint); + let user_quote_token_account = spl_associated_token_account::get_associated_token_address( + &payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + ); + + // Get pool token accounts + let pool_base_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &pool, + &mint, + &accounts::TOKEN_PROGRAM, + ); + + let pool_quote_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &pool, + &accounts::WSOL_TOKEN_ACCOUNT, + &accounts::TOKEN_PROGRAM, + ); + + let instructions = build_sell_instructions_with_accounts( + rpc, + payer, + Arc::new(pool), + Arc::new(pool_base_token_account), + Arc::new(pool_quote_token_account), + Arc::new(user_base_token_account), + Arc::new(user_quote_token_account), + Arc::new(mint), + Arc::new(creator), + amount_token, + slippage_basis_points, + ) + .await?; + + Ok(instructions) +} + +pub async fn build_sell_instructions_with_accounts( + rpc: Arc, + payer: Arc, + pool: Arc, + pool_base_token_account: Arc, + pool_quote_token_account: Arc, + user_base_token_account: Arc, + user_quote_token_account: Arc, + mint: Arc, + creator: Arc, + amount_token: Option, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; + let amount = amount_token.unwrap_or(balance_u64); + + if amount == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + // Calculate the expected SOL amount let sol_amount = get_sell_sol_amount(rpc.as_ref(), &pool, amount).await?; // Calculate the minimum SOL amount with slippage - let min_sol_amount = calculate_with_slippage_sell(sol_amount, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - - // Get token accounts - let user_base_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &mint); - let user_quote_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &accounts::WSOL_TOKEN_ACCOUNT); - - // Get pool token accounts - let pool_base_token_account = spl_associated_token_account::get_associated_token_address_with_program_id( - &pool, - &mint, - &accounts::TOKEN_PROGRAM, + let min_sol_amount = calculate_with_slippage_sell( + sol_amount, + slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), ); - let pool_quote_token_account = spl_associated_token_account::get_associated_token_address_with_program_id( - &pool, - &accounts::WSOL_TOKEN_ACCOUNT, - &accounts::TOKEN_PROGRAM, - ); + let coin_creator_vault_ata = coin_creator_vault_ata(*creator.as_ref()); + let coin_creator_vault_authority = coin_creator_vault_authority(*creator.as_ref()); let mut instructions = vec![]; // Create the user's token account if it doesn't exist - instructions.push( - create_associated_token_account_idempotent( - &payer.pubkey(), - &payer.pubkey(), - &mint, - &accounts::TOKEN_PROGRAM, - ) - ); + instructions.push(create_associated_token_account_idempotent( + &payer.pubkey(), + &payer.pubkey(), + &mint, + &accounts::TOKEN_PROGRAM, + )); // Create the sell instruction // 注意:账户顺序必须与JavaScript SDK匹配 let accounts = vec![ - solana_sdk::instruction::AccountMeta::new_readonly(pool, false), // pool_id (readonly) - solana_sdk::instruction::AccountMeta::new(payer.pubkey(), true), // user (signer) + solana_sdk::instruction::AccountMeta::new_readonly(*pool, false), // pool_id (readonly) + solana_sdk::instruction::AccountMeta::new(payer.pubkey(), true), // user (signer) solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_ACCOUNT, false), // global (readonly) - solana_sdk::instruction::AccountMeta::new_readonly(mint, false), // mint (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(*mint, false), // mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // WSOL_TOKEN_ACCOUNT (readonly) - solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // user_base_token_account - solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // user_quote_token_account - solana_sdk::instruction::AccountMeta::new(pool_base_token_account, false), // pool_base_token_account - solana_sdk::instruction::AccountMeta::new(pool_quote_token_account, false), // pool_quote_token_account + solana_sdk::instruction::AccountMeta::new(*user_base_token_account, false), // user_base_token_account + solana_sdk::instruction::AccountMeta::new(*user_quote_token_account, false), // user_quote_token_account + solana_sdk::instruction::AccountMeta::new(*pool_base_token_account, false), // pool_base_token_account + solana_sdk::instruction::AccountMeta::new(*pool_quote_token_account, false), // pool_quote_token_account solana_sdk::instruction::AccountMeta::new_readonly(accounts::FEE_RECIPIENT, false), // fee_recipient (readonly) solana_sdk::instruction::AccountMeta::new(accounts::FEE_RECIPIENT_ATA, false), // fee_recipient_ata solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly, duplicated as in JS) solana_sdk::instruction::AccountMeta::new_readonly(accounts::SYSTEM_PROGRAM, false), // System Program (readonly) - solana_sdk::instruction::AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false), // ASSOCIATED_TOKEN_PROGRAM_ID (readonly) + solana_sdk::instruction::AccountMeta::new_readonly( + accounts::ASSOCIATED_TOKEN_PROGRAM, + false, + ), // ASSOCIATED_TOKEN_PROGRAM_ID (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // event_authority (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::AMM_PROGRAM, false), // PUMP_AMM_PROGRAM_ID (readonly) + solana_sdk::instruction::AccountMeta::new(coin_creator_vault_ata, false), // coin_creator_vault_ata + solana_sdk::instruction::AccountMeta::new_readonly(coin_creator_vault_authority, false), // coin_creator_vault_authority (readonly) ]; // Create the instruction data @@ -356,13 +617,11 @@ pub async fn build_sell_instructions( data.extend_from_slice(&amount.to_le_bytes()); data.extend_from_slice(&min_sol_amount.to_le_bytes()); - instructions.push( - Instruction { - program_id: accounts::AMM_PROGRAM, - accounts, - data, - } - ); + instructions.push(Instruction { + program_id: accounts::AMM_PROGRAM, + accounts, + data, + }); Ok(instructions) }