Files
sol-trade-sdk/src/pumpswap/sell.rs
T
sgxiang 165335af11 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
2025-06-12 00:00:30 +08:00

628 lines
20 KiB
Rust

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,
};
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::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
const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 256 * 1024;
// Sell tokens to a Pumpswap pool
pub async fn sell(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Pubkey,
creator: Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
lookup_table_key: Option<Pubkey>,
// 可选(必须全部传)
pool: Option<Pubkey>,
pool_base_token_account: Option<Pubkey>,
pool_quote_token_account: Option<Pubkey>,
user_base_token_account: Option<Pubkey>,
user_quote_token_account: Option<Pubkey>,
) -> Result<(), anyhow::Error> {
let start_time = Instant::now();
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();
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<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Pubkey,
creator: Pubkey,
percent: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
lookup_table_key: Option<Pubkey>,
// 可选(必须全部传)
pool: Option<Pubkey>,
pool_base_token_account: Option<Pubkey>,
pool_quote_token_account: Option<Pubkey>,
user_base_token_account: Option<Pubkey>,
user_quote_token_account: Option<Pubkey>,
) -> 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,
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
pub async fn sell_by_amount(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Pubkey,
creator: Pubkey,
amount: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
lookup_table_key: Option<Pubkey>,
// 可选(必须全部传)
pool: Option<Pubkey>,
pool_base_token_account: Option<Pubkey>,
pool_quote_token_account: Option<Pubkey>,
user_base_token_account: Option<Pubkey>,
user_quote_token_account: Option<Pubkey>,
) -> Result<(), anyhow::Error> {
if amount == 0 {
return Err(anyhow!("Amount must be greater than 0"));
}
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
pub async fn sell_with_tip(
rpc: Arc<SolanaRpcClient>,
fee_clients: Vec<Arc<FeeClient>>,
payer: Arc<Keypair>,
mint: Pubkey,
creator: Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
lookup_table_key: Option<Pubkey>,
// 可选(必须全部传)
pool: Option<Pubkey>,
pool_base_token_account: Option<Pubkey>,
pool_quote_token_account: Option<Pubkey>,
user_base_token_account: Option<Pubkey>,
user_quote_token_account: Option<Pubkey>,
) -> Result<(), anyhow::Error> {
let mut transactions = vec![];
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() {
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<SolanaRpcClient>,
fee_clients: Vec<Arc<FeeClient>>,
payer: Arc<Keypair>,
mint: Pubkey,
creator: Pubkey,
percent: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
lookup_table_key: Option<Pubkey>,
// 可选(必须全部传)
pool: Option<Pubkey>,
pool_base_token_account: Option<Pubkey>,
pool_quote_token_account: Option<Pubkey>,
user_base_token_account: Option<Pubkey>,
user_quote_token_account: Option<Pubkey>,
) -> 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,
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
pub async fn sell_by_amount_with_tip(
rpc: Arc<SolanaRpcClient>,
fee_clients: Vec<Arc<FeeClient>>,
payer: Arc<Keypair>,
mint: Pubkey,
creator: Pubkey,
amount: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
lookup_table_key: Option<Pubkey>,
// 可选(必须全部传)
pool: Option<Pubkey>,
pool_base_token_account: Option<Pubkey>,
pool_quote_token_account: Option<Pubkey>,
user_base_token_account: Option<Pubkey>,
user_quote_token_account: Option<Pubkey>,
) -> Result<(), anyhow::Error> {
if amount == 0 {
return Err(anyhow!("Amount must be greater than 0"));
}
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
pub async fn build_sell_transaction(
_rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
priority_fee: PriorityFee,
build_instructions: Vec<Instruction>,
lookup_table_key: Option<Pubkey>,
recent_blockhash: solana_sdk::hash::Hash,
) -> Result<VersionedTransaction, anyhow::Error> {
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<SolanaRpcClient>,
tip_account: Arc<Pubkey>,
payer: Arc<Keypair>,
priority_fee: PriorityFee,
build_instructions: Vec<Instruction>,
lookup_table_key: Option<Pubkey>,
recent_blockhash: solana_sdk::hash::Hash,
) -> Result<VersionedTransaction, anyhow::Error> {
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,
sol_to_lamports(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<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Pubkey,
creator: Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
) -> Result<Vec<Instruction>, 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?;
// 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<SolanaRpcClient>,
payer: Arc<Keypair>,
pool: Arc<Pubkey>,
pool_base_token_account: Arc<Pubkey>,
pool_quote_token_account: Arc<Pubkey>,
user_base_token_account: Arc<Pubkey>,
user_quote_token_account: Arc<Pubkey>,
mint: Arc<Pubkey>,
creator: Arc<Pubkey>,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
) -> Result<Vec<Instruction>, 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),
);
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,
));
// 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)
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
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)
}