1use anyhow::Result;
2use solana_hash::Hash;
3use solana_message::AddressLookupTableAccount;
4use solana_sdk::{
5 instruction::Instruction, pubkey::Pubkey, signature::Keypair, signature::Signature,
6};
7use std::{
8 sync::Arc,
9 time::{Duration, Instant},
10};
11#[allow(unused_imports)]
12use tracing::{info, trace, warn};
13
14use super::{params::SwapParams, traits::InstructionBuilder};
15use crate::swqos::TradeType;
16use crate::{
17 common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient, SwqosSubmitTiming},
18 perf::syscall_bypass::SystemCallBypassManager,
19 swqos::common::poll_any_transaction_confirmation,
20 trading::core::{
21 async_executor::execute_parallel,
22 execution::{InstructionProcessor, Prefetch},
23 traits::TradeExecutor,
24 },
25 trading::MiddlewareManager,
26};
27use once_cell::sync::Lazy;
28
29#[allow(dead_code)]
32static SYSCALL_BYPASS: Lazy<SystemCallBypassManager> = Lazy::new(|| {
33 use crate::perf::syscall_bypass::SyscallBypassConfig;
34 SystemCallBypassManager::new(SyscallBypassConfig::default())
35 .expect("Failed to create SystemCallBypassManager")
36});
37
38pub struct GenericTradeExecutor {
40 instruction_builder: Arc<dyn InstructionBuilder>,
41 protocol_name: &'static str,
42}
43
44impl GenericTradeExecutor {
45 pub fn new(
46 instruction_builder: Arc<dyn InstructionBuilder>,
47 protocol_name: &'static str,
48 ) -> Self {
49 Self { instruction_builder, protocol_name }
50 }
51}
52
53#[async_trait::async_trait]
54impl TradeExecutor for GenericTradeExecutor {
55 async fn swap(
56 &self,
57 params: SwapParams,
58 ) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
59 let total_start = (params.log_enabled || params.simulate).then(Instant::now);
61 let timing_start_us: Option<i64> = if params.log_enabled {
62 Some(params.grpc_recv_us.unwrap_or_else(crate::common::clock::now_micros))
63 } else {
64 None
65 };
66
67 let is_buy =
68 params.trade_type == TradeType::Buy || params.trade_type == TradeType::CreateAndBuy;
69
70 Prefetch::keypair(¶ms.payer);
71
72 let build_start = params.log_enabled.then(Instant::now);
74 let instructions = if is_buy {
75 self.instruction_builder.build_buy_instructions(¶ms).await?
76 } else {
77 self.instruction_builder.build_sell_instructions(¶ms).await?
78 };
79 let _build_elapsed = build_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
80
81 InstructionProcessor::preprocess(&instructions)?;
82
83 let final_instructions = match ¶ms.middleware_manager {
84 Some(middleware_manager) => middleware_manager
85 .apply_middlewares_process_protocol_instructions(
86 instructions,
87 self.protocol_name,
88 is_buy,
89 )?,
90 None => instructions,
91 };
92
93 let build_end_us = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled())
94 .then(crate::common::clock::now_micros);
95 let _before_submit_elapsed =
96 total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
97 let before_submit_us = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled())
98 .then(crate::common::clock::now_micros);
99
100 if params.simulate {
101 let send_start = crate::common::sdk_log::sdk_log_enabled().then(Instant::now);
102 let result = simulate_transaction(
103 params.rpc,
104 params.payer,
105 final_instructions,
106 params.address_lookup_table_account,
107 params.recent_blockhash,
108 params.durable_nonce,
109 params.middleware_manager,
110 self.protocol_name,
111 is_buy,
112 if is_buy { true } else { params.with_tip },
113 params.gas_fee_strategy,
114 )
115 .await;
116 let send_elapsed = send_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
117 let total_elapsed = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
118
119 if crate::common::sdk_log::sdk_log_enabled() {
120 let dir = if is_buy { "Buy" } else { "Sell" };
121 println!();
122 if let (Some(start_us), Some(end_us)) = (timing_start_us, build_end_us) {
123 println!(
124 " [SDK][{:width$}] {} build_instructions: {:.4} ms",
125 "-",
126 dir,
127 (end_us - start_us) as f64 / 1000.0,
128 width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
129 );
130 }
131 if let (Some(start_us), Some(end_us)) = (timing_start_us, before_submit_us) {
132 println!(
133 " [SDK][{:width$}] {} before_submit: {:.4} ms",
134 "-",
135 dir,
136 (end_us - start_us) as f64 / 1000.0,
137 width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
138 );
139 }
140 println!(
141 " [SDK][{:width$}] {} simulate (dry-run): {:.4} ms",
142 "-",
143 dir,
144 send_elapsed.as_secs_f64() * 1000.0,
145 width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
146 );
147 println!(
148 " [SDK][{:width$}] {} total: {:.4} ms",
149 "-",
150 dir,
151 total_elapsed.as_secs_f64() * 1000.0,
152 width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
153 );
154 }
155
156 return result;
157 }
158
159 let need_confirm = params.wait_tx_confirmed;
160 let wait_for_all_submits = !need_confirm && params.wait_for_all_submits;
164 let sender_config = params.sender_concurrency_config();
165 let result = execute_parallel(
166 params.swqos_clients.as_slice(),
167 params.payer,
168 final_instructions,
169 params.address_lookup_table_account,
170 params.recent_blockhash,
171 params.durable_nonce,
172 params.middleware_manager,
173 self.protocol_name,
174 is_buy,
175 false, wait_for_all_submits,
177 if is_buy { true } else { params.with_tip },
178 params.gas_fee_strategy,
179 params.use_dedicated_sender_threads,
180 sender_config,
181 params.check_min_tip,
182 )
183 .await;
184
185 let log_enabled = params.log_enabled && crate::common::sdk_log::sdk_log_enabled();
186
187 let (ok, signatures, err, submit_timings) = match result {
188 Ok((success, sigs, last_error, timings)) => {
189 (success, sigs, last_error.map(|e| anyhow::anyhow!("{}", e)), timings)
190 }
191 Err(e) => (false, vec![], Some(anyhow::anyhow!("{}", e)), vec![]),
192 };
193 let submit_timings_ref: &[SwqosSubmitTiming] = submit_timings.as_slice();
195
196 let result = if need_confirm {
197 let confirm_result = if let Some(rpc) = params.rpc.as_ref() {
198 if signatures.is_empty() {
199 (ok, signatures, err)
200 } else {
201 let poll_res = poll_any_transaction_confirmation(rpc, &signatures, true).await;
202 let confirm_done_us = log_enabled.then(crate::common::clock::now_micros);
203 if log_enabled {
204 let dir = if is_buy { "Buy" } else { "Sell" };
205 crate::common::sdk_log::print_sdk_timing_block(
206 dir,
207 timing_start_us,
208 build_end_us,
209 before_submit_us,
210 submit_timings_ref,
211 confirm_done_us,
212 );
213 }
214 match poll_res {
215 Ok(_) => (true, signatures, None),
216 Err(e) => (false, signatures, Some(e)),
217 }
218 }
219 } else {
220 (ok, signatures, err)
221 };
222
223 Ok((confirm_result.0, confirm_result.1, confirm_result.2, submit_timings))
225 } else {
226 if log_enabled {
228 let dir = if is_buy { "Buy" } else { "Sell" };
229 crate::common::sdk_log::print_sdk_timing_block(
230 dir,
231 timing_start_us,
232 build_end_us,
233 before_submit_us,
234 submit_timings_ref,
235 None,
236 );
237 }
238
239 Ok((ok, signatures, err, submit_timings))
240 };
241
242 result
243 }
244
245 fn protocol_name(&self) -> &'static str {
246 self.protocol_name
247 }
248}
249
250async fn simulate_transaction(
253 rpc: Option<Arc<SolanaRpcClient>>,
254 payer: Arc<Keypair>,
255 instructions: Vec<Instruction>,
256 address_lookup_table_account: Option<AddressLookupTableAccount>,
257 recent_blockhash: Option<Hash>,
258 durable_nonce: Option<DurableNonceInfo>,
259 middleware_manager: Option<Arc<MiddlewareManager>>,
260 protocol_name: &'static str,
261 is_buy: bool,
262 with_tip: bool,
263 gas_fee_strategy: GasFeeStrategy,
264) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
265 use crate::trading::common::build_transaction;
266 use solana_client::rpc_config::RpcSimulateTransactionConfig;
267 use solana_commitment_config::CommitmentLevel;
268 use solana_transaction_status::UiTransactionEncoding;
269
270 let rpc = rpc.ok_or_else(|| anyhow::anyhow!("RPC client is required for simulation"))?;
271
272 let trade_type =
274 if is_buy { crate::swqos::TradeType::Buy } else { crate::swqos::TradeType::Sell };
275 let gas_fee_configs = gas_fee_strategy.get_strategies(trade_type);
276
277 let default_config = gas_fee_configs
278 .iter()
279 .find(|config| config.0 == crate::swqos::SwqosType::Default)
280 .ok_or_else(|| anyhow::anyhow!("No default gas fee strategy found"))?;
281
282 let tip = if with_tip { default_config.2.tip } else { 0.0 };
283 let unit_limit = default_config.2.cu_limit;
284 let unit_price = default_config.2.cu_price;
285
286 let transaction = build_transaction(
287 &payer,
288 unit_limit,
289 unit_price,
290 &instructions,
291 address_lookup_table_account.as_ref(),
292 recent_blockhash,
293 middleware_manager.as_ref(),
294 protocol_name,
295 is_buy,
296 false,
297 &Pubkey::default(),
298 tip,
299 durable_nonce.as_ref(),
300 )?;
301
302 use solana_commitment_config::CommitmentConfig;
304 let simulate_result = rpc
305 .simulate_transaction_with_config(
306 &transaction,
307 RpcSimulateTransactionConfig {
308 sig_verify: false, replace_recent_blockhash: false, commitment: Some(CommitmentConfig {
311 commitment: CommitmentLevel::Processed, }),
313 encoding: Some(UiTransactionEncoding::Base64), accounts: None, min_context_slot: None, inner_instructions: true, },
318 )
319 .await?;
320
321 let signature = transaction
322 .signatures
323 .first()
324 .ok_or_else(|| anyhow::anyhow!("Transaction has no signatures"))?
325 .clone();
326
327 if let Some(err) = simulate_result.value.err {
328 #[cfg(feature = "perf-trace")]
329 {
330 warn!(target: "sol_trade_sdk", "[Simulation Failed] error={:?} signature={:?}", err, signature);
331 if let Some(logs) = &simulate_result.value.logs {
332 trace!(target: "sol_trade_sdk", "Transaction logs: {:?}", logs);
333 }
334 if let Some(units_consumed) = simulate_result.value.units_consumed {
335 trace!(target: "sol_trade_sdk", "Compute Units Consumed: {}", units_consumed);
336 }
337 }
338 return Ok((false, vec![signature], Some(anyhow::anyhow!("{:?}", err)), Vec::new()));
339 }
340
341 #[cfg(feature = "perf-trace")]
343 {
344 info!(target: "sol_trade_sdk", "[Simulation Succeeded] signature={:?}", signature);
345 if let Some(units_consumed) = simulate_result.value.units_consumed {
346 trace!(target: "sol_trade_sdk", "Compute Units Consumed: {}", units_consumed);
347 }
348 if let Some(logs) = &simulate_result.value.logs {
349 trace!(target: "sol_trade_sdk", "Transaction logs: {:?}", logs);
350 }
351 }
352
353 Ok((true, vec![signature], None, Vec::new()))
354}
355
356#[cfg(test)]
357mod tests {
358 use crate::common::GasFeeStrategyType;
359 use crate::swqos::SwqosType;
360
361 #[test]
363 fn log_timing_preview() {
364 let dir = "Buy";
365 let build_ms = 12.34;
366 let before_submit_ms = 15.67;
367 let w = 12usize; println!("\n--- 1. 构建指令耗时 / 提交前耗时(各打印一次,统一 ms,保留 4 位小数)---\n");
369 println!(
370 " [SDK][{:width$}] {} build_instructions: {:.4} ms",
371 "-",
372 dir,
373 build_ms,
374 width = w
375 );
376 println!(
377 " [SDK][{:width$}] {} before_submit: {:.4} ms",
378 "-",
379 dir,
380 before_submit_ms,
381 width = w
382 );
383
384 println!("\n--- 2. 每个 SWQOS 独立耗时:submit_done=起点→该通道提交完成, confirmed=该通道提交→链上确认, total=起点→链上确认 ---\n");
385 for (swqos_type, strategy_type, submit_ms, confirmed_ms, total_ms) in [
386 (SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, 45.12, 83.38, 128.50),
387 (SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, 46.08, 82.42, 128.50),
388 (SwqosType::Helius, GasFeeStrategyType::LowTipHighCuPrice, 52.30, 76.20, 128.50),
389 (SwqosType::ZeroSlot, GasFeeStrategyType::Normal, 48.90, 79.60, 128.50),
390 ] {
391 println!(
392 " [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
393 swqos_type.as_str(),
394 dir,
395 strategy_type.as_str(),
396 submit_ms,
397 confirmed_ms,
398 total_ms,
399 width = w
400 );
401 }
402
403 println!(
404 "\n--- 3. 不等待链上确认时:每行 total = 该通道 submit_done(提交完成总耗时)---\n"
405 );
406 for (swqos_type, strategy_type, submit_ms, total_ms) in [
407 (SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, 44.20, 44.20),
408 (SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, 45.10, 45.10),
409 (SwqosType::Helius, GasFeeStrategyType::Normal, 51.80, 51.80),
410 ] {
411 println!(
412 " [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
413 swqos_type.as_str(),
414 dir,
415 strategy_type.as_str(),
416 submit_ms,
417 total_ms,
418 width = w
419 );
420 }
421
422 println!("\n--- 4. Simulate 模式(build/before_submit 仍从 grpc_recv_us 起算)---\n");
423 println!(
424 " [SDK][{:width$}] {} build_instructions: {:.4} ms",
425 "-",
426 dir,
427 build_ms,
428 width = w
429 );
430 println!(
431 " [SDK][{:width$}] {} before_submit: {:.4} ms",
432 "-",
433 dir,
434 before_submit_ms,
435 width = w
436 );
437 println!(" [SDK][{:width$}] {} simulate (dry-run): {:.4} ms", "-", dir, 8.50, width = w);
438 println!(" [SDK][{:width$}] {} total: {:.4} ms", "-", dir, 36.51, width = w);
439 println!();
440 }
441}