From 1b7e3781c76d7811194beb77dc1bc099dd4fb8e1 Mon Sep 17 00:00:00 2001 From: vibes Date: Wed, 25 Feb 2026 09:16:50 +0000 Subject: [PATCH] Fix confirmation polling to check all channel signatures In v3.5.0, confirmation was split out of execute_parallel into poll_transaction_confirmation, but it only polled sig[0]. When multi-channel submit is used, each channel produces a different signature and only one lands on-chain. If the landed sig is not sig[0], the poll times out after 15s and reports failure despite a successful on-chain transaction. Add poll_any_transaction_confirmation() that passes all signatures to get_signature_statuses in a single RPC call per poll, returning the first one that confirms. The executor now uses this instead of polling a single signature. Co-Authored-By: Claude Opus 4.6 --- src/swqos/common.rs | 55 +++++++++++++++++++++++++----------- src/trading/core/executor.rs | 11 +++++--- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/swqos/common.rs b/src/swqos/common.rs index 9249cd5..b3f3c48 100755 --- a/src/swqos/common.rs +++ b/src/swqos/common.rs @@ -89,41 +89,64 @@ pub async fn poll_transaction_confirmation( txt_sig: Signature, wait_confirmation: bool, ) -> Result { - // If no confirmation needed, return signature immediately + poll_any_transaction_confirmation(rpc, &[txt_sig], wait_confirmation).await +} + +/// Poll multiple signatures in parallel (one RPC call per poll) and return the first one that confirms. +/// When transactions are submitted to multiple SWQOS channels, each channel produces a different +/// signature. Only one will land on-chain, so we must check all of them. +pub async fn poll_any_transaction_confirmation( + rpc: &SolanaRpcClient, + signatures: &[Signature], + wait_confirmation: bool, +) -> Result { + if signatures.is_empty() { + return Err(anyhow::anyhow!("No signatures to confirm")); + } + // If no confirmation needed, return first signature immediately if !wait_confirmation { - return Ok(txt_sig); + return Ok(signatures[0]); } - let timeout: Duration = Duration::from_secs(15); // 15s to avoid timeout under network congestion + let timeout: Duration = Duration::from_secs(15); let interval: Duration = Duration::from_millis(1000); let start: Instant = Instant::now(); let mut poll_count = 0u32; + // Track which signature landed (confirmed or failed on-chain) + let mut landed_sig: Option = None; loop { if start.elapsed() >= timeout { - return Err(anyhow::anyhow!("Transaction {}'s confirmation timed out", txt_sig)); + return Err(anyhow::anyhow!("Transaction confirmation timed out after {}s ({} signatures polled)", timeout.as_secs(), signatures.len())); } poll_count += 1; - let status = rpc.get_signature_statuses(&[txt_sig]).await?; - let first = status.value.get(0).and_then(|o| o.as_ref()); - match first { - Some(s) => { + let status = rpc.get_signature_statuses(signatures).await?; + // Check all signatures for any that confirmed successfully + for (i, maybe_status) in status.value.iter().enumerate() { + if let Some(s) = maybe_status { if s.err.is_none() && (s.confirmation_status == Some(TransactionConfirmationStatus::Confirmed) || s.confirmation_status == Some(TransactionConfirmationStatus::Finalized)) { - return Ok(txt_sig); + return Ok(signatures[i]); + } + // Track the first signature that landed on-chain (even if errored) + if landed_sig.is_none() { + landed_sig = Some(signatures[i]); } - } - None => { - sleep(interval).await; - continue; } } - let should_get_transaction = first.map(|s| s.err.is_some()).unwrap_or(false) || poll_count >= 10; + // If no signature has any status yet, keep waiting + if landed_sig.is_none() { + sleep(interval).await; + continue; + } + + let landed = landed_sig.unwrap(); + let should_get_transaction = poll_count >= 10; if !should_get_transaction { sleep(interval).await; @@ -132,7 +155,7 @@ pub async fn poll_transaction_confirmation( let tx_details = match rpc .get_transaction_with_config( - &txt_sig, + &landed, RpcTransactionConfig { encoding: Some(UiTransactionEncoding::JsonParsed), max_supported_transaction_version: Some(0), @@ -155,7 +178,7 @@ pub async fn poll_transaction_confirmation( } else { let meta = meta.unwrap(); if meta.err.is_none() { - return Ok(txt_sig); + return Ok(landed); } else { // Extract error message from log_messages let mut error_msg = String::new(); diff --git a/src/trading/core/executor.rs b/src/trading/core/executor.rs index 4c4b451..29fbf8f 100755 --- a/src/trading/core/executor.rs +++ b/src/trading/core/executor.rs @@ -11,7 +11,7 @@ use tracing::{info, trace, warn}; use crate::{ common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient}, perf::syscall_bypass::SystemCallBypassManager, - swqos::common::poll_transaction_confirmation, + swqos::common::poll_any_transaction_confirmation, trading::core::{ async_executor::execute_parallel, execution::{InstructionProcessor, Prefetch}, @@ -156,10 +156,12 @@ impl TradeExecutor for GenericTradeExecutor { ), Err(e) => (false, vec![], Some(anyhow::anyhow!("{}", e))), }; - let first_sig = sigs.first().copied(); - let confirm_result = if let (Some(rpc), Some(sig)) = (params.rpc.as_ref(), first_sig) { + let confirm_result = if let Some(rpc) = params.rpc.as_ref() { + if sigs.is_empty() { + (ok, sigs, err) + } else { let confirm_start = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled()).then(Instant::now); - let poll_res = poll_transaction_confirmation(rpc, sig, true).await; + let poll_res = poll_any_transaction_confirmation(rpc, &sigs, true).await; let confirm_elapsed = confirm_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO); if params.log_enabled && crate::common::sdk_log::sdk_log_enabled() { let dir = if is_buy { "Buy" } else { "Sell" }; @@ -171,6 +173,7 @@ impl TradeExecutor for GenericTradeExecutor { Ok(_) => (true, sigs, None), Err(e) => (false, sigs, Some(e)), } + } } else { (ok, sigs, err) };