feat: add transaction simulation support and close_mint_token_ata parameter

- Add `simulate` parameter to TradeBuyParams and TradeSellParams for transaction simulation
- Add `close_mint_token_ata` parameter to TradeSellParams for closing mint token ATA
- Implement simulate_transaction function to validate transactions without blockchain submission
- Update all examples to include new parameters (simulate: false, close_mint_token_ata: false)
- Add comprehensive documentation for transaction simulation feature
- Show detailed simulation output including logs, compute units, and errors
- Print timing metrics in English for better international compatibility
This commit is contained in:
ysq
2025-10-30 22:11:55 +08:00
parent 11383a0492
commit d5834e056f
19 changed files with 307 additions and 27 deletions
+6
View File
@@ -114,6 +114,8 @@ pub struct TradeBuyParams {
pub fixed_output_token_amount: Option<u64>,
/// Gas fee strategy
pub gas_fee_strategy: GasFeeStrategy,
/// Whether to simulate the transaction instead of executing it
pub simulate: bool,
}
/// Parameters for executing sell orders across different DEX protocols
@@ -158,6 +160,8 @@ pub struct TradeSellParams {
pub fixed_output_token_amount: Option<u64>,
/// Gas fee strategy
pub gas_fee_strategy: GasFeeStrategy,
/// Whether to simulate the transaction instead of executing it
pub simulate: bool,
}
impl SolanaTrade {
@@ -320,6 +324,7 @@ impl SolanaTrade {
close_output_mint_ata: false,
fixed_output_amount: params.fixed_output_token_amount,
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
};
// Validate protocol params
@@ -416,6 +421,7 @@ impl SolanaTrade {
close_output_mint_ata: params.close_output_token_ata,
fixed_output_amount: params.fixed_output_token_amount,
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
};
// Validate protocol params
+213 -26
View File
@@ -1,14 +1,20 @@
use anyhow::Result;
use solana_sdk::signature::Signature;
use solana_hash::Hash;
use solana_sdk::{
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
signature::Keypair, signature::Signature,
};
use std::{sync::Arc, time::Instant};
use crate::{
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
perf::syscall_bypass::SystemCallBypassManager,
trading::core::{
async_executor::execute_parallel,
execution::{Prefetch, InstructionProcessor, ExecutionPath},
execution::{ExecutionPath, InstructionProcessor, Prefetch},
traits::TradeExecutor,
},
trading::MiddlewareManager,
};
use once_cell::sync::Lazy;
@@ -32,10 +38,7 @@ impl GenericTradeExecutor {
instruction_builder: Arc<dyn InstructionBuilder>,
protocol_name: &'static str,
) -> Self {
Self {
instruction_builder,
protocol_name,
}
Self { instruction_builder, protocol_name }
}
}
@@ -66,20 +69,68 @@ impl TradeExecutor for GenericTradeExecutor {
// 中间件处理
let final_instructions = match &params.middleware_manager {
Some(middleware_manager) => {
middleware_manager
.apply_middlewares_process_protocol_instructions(
instructions,
self.protocol_name.to_string(),
is_buy,
)?
}
None => instructions
Some(middleware_manager) => middleware_manager
.apply_middlewares_process_protocol_instructions(
instructions,
self.protocol_name.to_string(),
is_buy,
)?,
None => instructions,
};
// 提交前耗时
let before_submit_elapsed = total_start.elapsed();
// 如果是模拟模式,直接通过 RPC 模拟交易
if params.simulate {
let send_start = Instant::now();
let result = simulate_transaction(
params.rpc,
params.payer,
final_instructions,
params.address_lookup_table_account,
params.recent_blockhash,
params.durable_nonce,
if is_buy { params.data_size_limit } else { 0 },
params.middleware_manager,
self.protocol_name,
is_buy,
if is_buy { true } else { params.with_tip },
params.gas_fee_strategy,
)
.await;
let send_elapsed = send_start.elapsed();
let total_elapsed = total_start.elapsed();
// Get performance metrics using fast timestamp
let timestamp_ns = SYSCALL_BYPASS.fast_timestamp_nanos();
// Print all timing metrics at once to avoid blocking critical path
println!("[Timestamp] {}ns", timestamp_ns);
println!(
"[Build Instructions] Time: {:.3}ms ({:.0}μs)",
build_elapsed.as_micros() as f64 / 1000.0,
build_elapsed.as_micros()
);
println!(
"[Before Submit] {:.3}ms ({:.0}μs)",
before_submit_elapsed.as_micros() as f64 / 1000.0,
before_submit_elapsed.as_micros()
);
println!(
"[Simulate Transaction] Time: {:.3}ms ({:.0}μs)",
send_elapsed.as_micros() as f64 / 1000.0,
send_elapsed.as_micros()
);
println!(
"[Total Time] {:.3}ms ({:.0}μs)",
total_elapsed.as_micros() as f64 / 1000.0,
total_elapsed.as_micros()
);
return result;
}
// 并行发送交易
let send_start = Instant::now();
let result = execute_parallel(
@@ -102,19 +153,31 @@ impl TradeExecutor for GenericTradeExecutor {
let send_elapsed = send_start.elapsed();
let total_elapsed = total_start.elapsed();
// 使用快速时间戳获取性能指标
// Get performance metrics using fast timestamp
let timestamp_ns = SYSCALL_BYPASS.fast_timestamp_nanos();
// 在完成后一次性打印所有耗时,避免阻塞关键路径
println!("[时间戳] {}ns", timestamp_ns);
println!("[构建指令] 耗时: {:.3}ms ({:.0}μs)",
build_elapsed.as_micros() as f64 / 1000.0, build_elapsed.as_micros());
println!("[提交前耗时] {:.3}ms ({:.0}μs)",
before_submit_elapsed.as_micros() as f64 / 1000.0, before_submit_elapsed.as_micros());
println!("[发送交易] 耗时: {:.3}ms ({:.0}μs)",
send_elapsed.as_micros() as f64 / 1000.0, send_elapsed.as_micros());
println!("[总耗时] {:.3}ms ({:.0}μs)",
total_elapsed.as_micros() as f64 / 1000.0, total_elapsed.as_micros());
// Print all timing metrics at once to avoid blocking critical path
println!("[Timestamp] {}ns", timestamp_ns);
println!(
"[Build Instructions] Time: {:.3}ms ({:.0}μs)",
build_elapsed.as_micros() as f64 / 1000.0,
build_elapsed.as_micros()
);
println!(
"[Before Submit] {:.3}ms ({:.0}μs)",
before_submit_elapsed.as_micros() as f64 / 1000.0,
before_submit_elapsed.as_micros()
);
println!(
"[Send Transaction] Time: {:.3}ms ({:.0}μs)",
send_elapsed.as_micros() as f64 / 1000.0,
send_elapsed.as_micros()
);
println!(
"[Total Time] {:.3}ms ({:.0}μs)",
total_elapsed.as_micros() as f64 / 1000.0,
total_elapsed.as_micros()
);
result
}
@@ -123,3 +186,127 @@ impl TradeExecutor for GenericTradeExecutor {
self.protocol_name
}
}
/// Simulate transaction using RPC client
async fn simulate_transaction(
rpc: Option<Arc<SolanaRpcClient>>,
payer: Arc<Keypair>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
durable_nonce: Option<DurableNonceInfo>,
data_size_limit: u32,
middleware_manager: Option<Arc<MiddlewareManager>>,
protocol_name: &'static str,
is_buy: bool,
with_tip: bool,
gas_fee_strategy: GasFeeStrategy,
) -> Result<(bool, Signature)> {
use crate::trading::common::build_transaction;
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_commitment_config::CommitmentLevel;
use solana_transaction_status::UiTransactionEncoding;
let rpc = rpc.ok_or_else(|| anyhow::anyhow!("RPC client is required for simulation"))?;
// Get gas fee strategy for simulation (use Default swqos type)
let trade_type =
if is_buy { crate::swqos::TradeType::Buy } else { crate::swqos::TradeType::Sell };
let gas_fee_configs = gas_fee_strategy.get_strategies(trade_type);
let default_config = gas_fee_configs
.iter()
.find(|config| config.0 == crate::swqos::SwqosType::Default)
.ok_or_else(|| anyhow::anyhow!("No default gas fee strategy found"))?;
let tip = if with_tip { default_config.2.tip } else { 0.0 };
let unit_limit = default_config.2.cu_limit;
let unit_price = default_config.2.cu_price;
// Build transaction for simulation
let transaction = build_transaction(
payer.clone(),
Some(rpc.clone()),
unit_limit,
unit_price,
instructions,
address_lookup_table_account,
recent_blockhash,
data_size_limit,
middleware_manager,
protocol_name,
is_buy,
false, // simulate doesn't need tip instruction
&Pubkey::default(),
tip,
durable_nonce,
)
.await?;
// Simulate the transaction
use solana_commitment_config::CommitmentConfig;
let simulate_result = rpc
.simulate_transaction_with_config(
&transaction,
RpcSimulateTransactionConfig {
sig_verify: false, // Don't verify signature during simulation for speed
replace_recent_blockhash: false, // Use actual blockhash from transaction
commitment: Some(CommitmentConfig {
commitment: CommitmentLevel::Processed, // Use Processed level to get latest state
}),
encoding: Some(UiTransactionEncoding::Base64), // Base64 encoding
accounts: None, // Don't return specific account states (can be specified if needed)
min_context_slot: None, // Don't specify minimum context slot
inner_instructions: true, // Enable inner instructions for debugging and detailed execution flow
},
)
.await?;
let signature = transaction
.signatures
.first()
.ok_or_else(|| anyhow::anyhow!("Transaction has no signatures"))?
.clone();
if let Some(err) = simulate_result.value.err {
println!("\n========== [Simulation Failed] ==========");
println!("Error Type: {:?}", err);
println!("Signature: {:?}", signature);
// Print logs
if let Some(logs) = simulate_result.value.logs {
println!("\n========== Transaction Logs ==========");
for (i, log) in logs.iter().enumerate() {
println!("{:3}. {}", i + 1, log);
}
}
// Print account usage
if let Some(units_consumed) = simulate_result.value.units_consumed {
println!("\n========== Resource Consumption ==========");
println!("Compute Units Consumed: {}", units_consumed);
}
println!("=========================================\n");
return Ok((false, signature));
}
// Simulation succeeded
println!("\n========== [Simulation Succeeded] ==========");
println!("Signature: {:?}", signature);
if let Some(units_consumed) = simulate_result.value.units_consumed {
println!("Compute Units Consumed: {}", units_consumed);
}
if let Some(logs) = simulate_result.value.logs {
println!("\n========== Transaction Logs ==========");
for (i, log) in logs.iter().enumerate() {
println!("{:3}. {}", i + 1, log);
}
}
println!("============================================\n");
Ok((true, signature))
}
+1
View File
@@ -40,6 +40,7 @@ pub struct SwapParams {
pub close_output_mint_ata: bool,
pub fixed_output_amount: Option<u64>,
pub gas_fee_strategy: GasFeeStrategy,
pub simulate: bool,
}
impl std::fmt::Debug for SwapParams {