Compare commits

...

5 Commits

Author SHA1 Message Date
Wood e67a21df58 chore: bump README dependency examples to 3.5.1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-26 01:03:34 +08:00
Wood 147c6068d1 Release v3.5.1: bump version, update README
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-26 00:58:48 +08:00
Wood 6ce8ee582e Merge pull request #79 from HelvetiCrypt/fix/confirm-all-signatures
Fix: poll all channel signatures during confirmation
2026-02-26 00:52:02 +08:00
vibes bcc6860bc3 Exclude examples requiring sol-parser-sdk from workspace
These examples depend on a local path to sol-parser-sdk which is not
available in the public repo, causing build failures for consumers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 09:19:03 +00:00
vibes 1b7e3781c7 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 <noreply@anthropic.com>
2026-02-25 09:16:50 +00:00
4 changed files with 53 additions and 25 deletions
+1 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "3.5.0"
version = "3.5.1"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
@@ -19,8 +19,6 @@ members = [
"examples/trading_client",
"examples/shared_infrastructure",
"examples/middleware_system",
"examples/pumpfun_copy_trading",
"examples/pumpfun_sniper_trading",
"examples/pumpswap_trading",
"examples/bonk_sniper_trading",
"examples/bonk_copy_trading",
+6 -2
View File
@@ -60,6 +60,10 @@
---
## 🆕 What's new in 3.5.1
- **SWQoS / executor**: Updates to common SWQoS logic and trading executor.
## 🆕 What's new in 3.5.0
- **Performance**: Hot-path timing only when logging; reduced clones (`execute_parallel` takes `&[Arc<SwqosClient>]`); shared HTTP client constants for SWQoS.
@@ -97,14 +101,14 @@ Add the dependency to your `Cargo.toml`:
```toml
# Add to your Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.0" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.1" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "3.5.0"
sol-trade-sdk = "3.5.1"
```
## 🛠️ Usage Examples
+39 -16
View File
@@ -89,41 +89,64 @@ pub async fn poll_transaction_confirmation(
txt_sig: Signature,
wait_confirmation: bool,
) -> Result<Signature> {
// 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<Signature> {
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<Signature> = 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();
+7 -4
View File
@@ -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)
};