Skip to main content

sol_trade_sdk/trading/core/
executor.rs

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/// Global syscall bypass manager (reserved for future time/IO optimizations).
30/// 全局系统调用绕过管理器(预留,后续可接入时间/IO 等优化)。
31#[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
38/// Generic trade executor implementation
39pub 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        // Sample total start only when logging or simulate. 仅在有日志或 simulate 时取起点。
60        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(&params.payer);
71
72        // Time build only when log_enabled to avoid cold-path syscalls. 仅 log_enabled 时计时,减少冷路径 syscall。
73        let build_start = params.log_enabled.then(Instant::now);
74        let instructions = if is_buy {
75            self.instruction_builder.build_buy_instructions(&params).await?
76        } else {
77            self.instruction_builder.build_sell_instructions(&params).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 &params.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        // When the caller confirms externally (need_confirm = false) and opts in
161        // via SwapParams.wait_for_all_submits, return every route's signature so
162        // pinned-nonce confirmation can poll all of them.
163        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, // submit only here; confirmation and log timing handled below
176            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        // submit_timings 为完成先后顺序(先完成的先 push),打印不排序、不增加延迟
194        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            //就是把confirm_result 拆开 再加上 submit_timings
224            Ok((confirm_result.0, confirm_result.1, confirm_result.2, submit_timings))
225        } else {
226            // Not waiting for confirmation: confirmed is not measured (-); total is per-channel submit time only.
227            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
250/// Simulate mode: single RPC simulation, returns Vec<Signature> for API consistency.
251/// 模拟模式:单次 RPC 模拟,返回 Vec<Signature> 以与 API 一致。
252async 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    // Get gas fee strategy for simulation (use Default swqos type)
273    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    // Simulate the transaction
303    use solana_commitment_config::CommitmentConfig;
304    let simulate_result = rpc
305        .simulate_transaction_with_config(
306            &transaction,
307            RpcSimulateTransactionConfig {
308                sig_verify: false, // Don't verify signature during simulation for speed
309                replace_recent_blockhash: false, // Use actual blockhash from transaction
310                commitment: Some(CommitmentConfig {
311                    commitment: CommitmentLevel::Processed, // Use Processed level to get latest state
312                }),
313                encoding: Some(UiTransactionEncoding::Base64), // Base64 encoding
314                accounts: None, // Don't return specific account states (can be specified if needed)
315                min_context_slot: None, // Don't specify minimum context slot
316                inner_instructions: true, // Enable inner instructions for debugging and detailed execution flow
317            },
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    // Simulation succeeded
342    #[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    /// 运行 `cargo test -p sol-trade-sdk log_timing_preview -- --nocapture` 查看日志打印效果
362    #[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; // same as crate::common::sdk_log::SWQOS_LABEL_WIDTH
368        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}