use {
crate::{
nonblocking::{rpc_client::RpcClient, tpu_client::TpuClient},
rpc_client::RpcClient as BlockingRpcClient,
},
bincode::serialize,
dashmap::DashMap,
futures_util::future::join_all,
solana_hash::Hash,
solana_message::{Message, VersionedMessage},
solana_quic_client::{QuicConfig, QuicConnectionManager, QuicPool},
solana_rpc_client::spinner::{self, SendTransactionProgress},
solana_rpc_client_api::{
client_error::ErrorKind,
config::RpcSendTransactionConfig,
request::{MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, RpcError, RpcResponseErrorData},
response::RpcSimulateTransactionResult,
},
solana_signature::Signature,
solana_signer::{SignerError, signers::Signers},
solana_tpu_client::tpu_client::{Result, TpuSenderError},
solana_tpu_client_next::client_builder::TransactionSender,
solana_transaction::versioned::VersionedTransaction,
solana_transaction_error::TransactionError,
std::{
future::Future,
num::NonZeroUsize,
sync::{
Arc,
atomic::{AtomicU64, AtomicUsize, Ordering},
},
time::Duration,
},
tokio::{
sync::{Notify, RwLock},
task::JoinHandle,
time::sleep,
},
};
const BLOCKHASH_REFRESH_RATE: Duration = Duration::from_secs(5);
const SEND_INTERVAL: Duration = Duration::from_millis(10);
const SEND_TIMEOUT_INTERVAL: Duration = Duration::from_secs(5);
type QuicTpuClient = TpuClient<QuicPool, QuicConnectionManager, QuicConfig>;
trait WireTransactionSender: Send + Sync {
fn send(&self, wire_transaction: Vec<u8>) -> impl Future<Output = bool> + Send;
}
impl WireTransactionSender for QuicTpuClient {
fn send(&self, wire_transaction: Vec<u8>) -> impl Future<Output = bool> + Send {
let fut = self.send_wire_transaction(wire_transaction);
async move {
tokio::time::timeout(SEND_TIMEOUT_INTERVAL, fut)
.await
.unwrap_or(false)
}
}
}
impl WireTransactionSender for TransactionSender {
fn send(&self, wire_transaction: Vec<u8>) -> impl Future<Output = bool> + Send {
let sender = self.clone();
async move {
sender
.send_transactions_in_batch(vec![wire_transaction])
.await
.is_ok()
}
}
}
#[derive(Clone, Debug)]
struct TransactionData {
last_valid_block_height: u64,
message: VersionedMessage,
index: usize,
serialized_transaction: Vec<u8>,
}
#[derive(Clone, Debug, Copy)]
struct BlockHashData {
pub blockhash: Hash,
pub last_valid_block_height: u64,
}
#[derive(Clone, Debug, Copy)]
pub struct SendAndConfirmConfigV2 {
pub with_spinner: bool,
pub resign_txs_count: Option<usize>,
pub rpc_send_transaction_config: RpcSendTransactionConfig,
}
pub enum SendTransport {
Tpu(TransactionSender),
Rpc(RpcSendTransactionConfig),
}
#[derive(Clone, Debug, Copy)]
pub struct SendAndConfirmConfigV3 {
pub with_spinner: bool,
pub max_sign_attempts: NonZeroUsize,
pub check_interval: Duration,
pub send_interval: Duration,
}
const DEFAULT_MAX_SIGN_ATTEMPTS: NonZeroUsize = NonZeroUsize::new(1).unwrap();
const DEFAULT_CHECK_INTERVAL: Duration = Duration::from_secs(1);
impl Default for SendAndConfirmConfigV3 {
fn default() -> Self {
Self {
with_spinner: false,
max_sign_attempts: DEFAULT_MAX_SIGN_ATTEMPTS,
check_interval: DEFAULT_CHECK_INTERVAL,
send_interval: SEND_INTERVAL,
}
}
}
struct ParallelSendConfig {
with_spinner: bool,
max_sign_attempts: NonZeroUsize,
rpc_send_transaction_config: Option<RpcSendTransactionConfig>,
dedupe_signers: bool,
send_interval: Duration,
check_interval: Duration,
}
impl From<SendAndConfirmConfigV2> for ParallelSendConfig {
fn from(config: SendAndConfirmConfigV2) -> Self {
Self {
with_spinner: config.with_spinner,
max_sign_attempts: config
.resign_txs_count
.and_then(NonZeroUsize::new)
.unwrap_or(DEFAULT_MAX_SIGN_ATTEMPTS),
rpc_send_transaction_config: Some(config.rpc_send_transaction_config),
dedupe_signers: true,
send_interval: SEND_INTERVAL,
check_interval: DEFAULT_CHECK_INTERVAL,
}
}
}
pub fn send_and_confirm_transactions_in_parallel_blocking_v2<T: Signers + ?Sized>(
rpc_client: Arc<BlockingRpcClient>,
tpu_client: Option<QuicTpuClient>,
messages: &[Message],
signers: &T,
config: SendAndConfirmConfigV2,
) -> Result<Vec<Option<TransactionError>>> {
let fut = send_and_confirm_transactions_in_parallel_impl(
rpc_client.get_inner_client().clone(),
tpu_client,
messages.iter().cloned().map(VersionedMessage::Legacy),
signers,
config.into(),
);
tokio::task::block_in_place(|| rpc_client.runtime().block_on(fut))
}
fn create_blockhash_data_updating_task(
rpc_client: Arc<RpcClient>,
blockhash_data_rw: Arc<RwLock<BlockHashData>>,
current_block_height: Arc<AtomicU64>,
) -> JoinHandle<()> {
tokio::spawn(async move {
loop {
if let Ok((blockhash, last_valid_block_height)) = rpc_client
.get_latest_blockhash_with_commitment(rpc_client.commitment())
.await
{
*blockhash_data_rw.write().await = BlockHashData {
blockhash,
last_valid_block_height,
};
}
if let Ok(block_height) = rpc_client.get_block_height().await {
current_block_height.store(block_height, Ordering::Relaxed);
}
tokio::time::sleep(BLOCKHASH_REFRESH_RATE).await;
}
})
}
fn create_transaction_confirmation_task(
rpc_client: Arc<RpcClient>,
current_block_height: Arc<AtomicU64>,
unconfirmed_transaction_map: Arc<DashMap<Signature, TransactionData>>,
errors_map: Arc<DashMap<usize, TransactionError>>,
num_confirmed_transactions: Arc<AtomicUsize>,
check_interval: Duration,
confirmation_signal: Arc<Notify>,
) -> JoinHandle<()> {
tokio::spawn(async move {
let mut last_block_height = current_block_height.load(Ordering::Relaxed);
loop {
tokio::time::sleep(check_interval).await;
if !unconfirmed_transaction_map.is_empty() {
let current_block_height = current_block_height.load(Ordering::Relaxed);
let transactions_to_verify: Vec<Signature> = unconfirmed_transaction_map
.iter()
.filter(|x| {
let is_not_expired = current_block_height <= x.last_valid_block_height;
let is_recently_expired = last_block_height <= x.last_valid_block_height
&& current_block_height > x.last_valid_block_height;
is_not_expired || is_recently_expired
})
.map(|x| *x.key())
.collect();
for signatures in
transactions_to_verify.chunks(MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS)
{
if let Ok(result) = rpc_client.get_signature_statuses(signatures).await {
let statuses = result.value;
for (signature, status) in signatures.iter().zip(statuses) {
if let Some((status, data)) = status
.filter(|status| {
status.satisfies_commitment(rpc_client.commitment())
})
.and_then(|status| {
unconfirmed_transaction_map
.remove(signature)
.map(|(_, data)| (status, data))
})
{
num_confirmed_transactions.fetch_add(1, Ordering::Relaxed);
match status.err {
Some(TransactionError::AlreadyProcessed) | None => {}
Some(error) => {
errors_map.insert(data.index, error);
}
}
};
}
}
}
last_block_height = current_block_height;
}
confirmation_signal.notify_one();
}
})
}
#[derive(Clone, Debug)]
struct SendingContext {
unconfirmed_transaction_map: Arc<DashMap<Signature, TransactionData>>,
error_map: Arc<DashMap<usize, TransactionError>>,
blockhash_data_rw: Arc<RwLock<BlockHashData>>,
num_confirmed_transactions: Arc<AtomicUsize>,
total_transactions: usize,
current_block_height: Arc<AtomicU64>,
send_interval: Duration,
check_interval: Duration,
confirmation_signal: Arc<Notify>,
}
fn progress_from_context_and_block_height(
context: &SendingContext,
last_valid_block_height: u64,
) -> SendTransactionProgress {
SendTransactionProgress {
confirmed_transactions: context
.num_confirmed_transactions
.load(std::sync::atomic::Ordering::Relaxed),
total_transactions: context.total_transactions,
block_height: context
.current_block_height
.load(std::sync::atomic::Ordering::Relaxed),
last_valid_block_height,
}
}
async fn send_transaction_with_rpc_fallback<S: WireTransactionSender>(
rpc_client: &RpcClient,
tpu_client: &Option<S>,
transaction: VersionedTransaction,
serialized_transaction: Vec<u8>,
context: &SendingContext,
index: usize,
rpc_send_transaction_config: Option<RpcSendTransactionConfig>,
) -> Result<()> {
let handed_off_over_tpu = match tpu_client {
Some(tpu_client) => tpu_client.send(serialized_transaction).await,
None => false,
};
if handed_off_over_tpu {
return Ok(());
}
let rpc_send_transaction_config = rpc_send_transaction_config
.expect("TPU client must outlive the transaction sending process.");
if let Err(e) = rpc_client
.send_transaction_with_config(
&transaction,
RpcSendTransactionConfig {
preflight_commitment: Some(rpc_client.commitment().commitment),
..rpc_send_transaction_config
},
)
.await
{
match e.kind() {
ErrorKind::Io(_) | ErrorKind::Reqwest(_) => {
}
ErrorKind::TransactionError(TransactionError::BlockhashNotFound) => {
}
ErrorKind::TransactionError(TransactionError::AlreadyProcessed) => {
}
ErrorKind::TransactionError(transaction_error) => {
context.error_map.insert(index, transaction_error.clone());
}
ErrorKind::RpcError(RpcError::RpcResponseError {
data:
RpcResponseErrorData::SendTransactionPreflightFailure(
RpcSimulateTransactionResult {
err: Some(ui_transaction_error),
..
},
),
..
}) => {
match TransactionError::from(ui_transaction_error.clone()) {
TransactionError::BlockhashNotFound => {
}
TransactionError::AlreadyProcessed => {
}
err => {
context.error_map.insert(index, err);
}
}
}
_ => {
return Err(TpuSenderError::from(e));
}
}
}
Ok(())
}
fn sign_versioned_message<T: Signers + ?Sized>(
message: VersionedMessage,
signers: &T,
dedupe_signers: bool,
) -> std::result::Result<VersionedTransaction, SignerError> {
if !dedupe_signers {
return VersionedTransaction::try_new(message, signers);
}
let signer_keys = signers.try_pubkeys()?;
let unordered_signatures = signers.try_sign_message(&message.serialize())?;
let account_keys = message.static_account_keys();
let num_required_signatures = message.header().num_required_signatures as usize;
let signatures = account_keys
.get(..num_required_signatures)
.ok_or_else(|| SignerError::InvalidInput("invalid message".to_string()))?
.iter()
.map(|required_key| {
signer_keys
.iter()
.position(|key| key == required_key)
.and_then(|i| unordered_signatures.get(i).copied())
.ok_or(SignerError::NotEnoughSigners)
})
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(VersionedTransaction {
signatures,
message,
})
}
async fn sign_all_messages_and_send<T: Signers + ?Sized, S: WireTransactionSender>(
progress_bar: &Option<indicatif::ProgressBar>,
rpc_client: &RpcClient,
tpu_client: &Option<S>,
messages_with_index: Vec<(usize, VersionedMessage)>,
signers: &T,
dedupe_signers: bool,
context: &SendingContext,
rpc_send_transaction_config: Option<RpcSendTransactionConfig>,
) -> Result<()> {
let current_transaction_count = messages_with_index.len();
let mut futures = vec![];
for (counter, (index, message)) in messages_with_index.into_iter().enumerate() {
futures.push(async move {
sleep(context.send_interval.saturating_mul(counter as u32)).await;
let blockhashdata = *context.blockhash_data_rw.read().await;
let mut message = message;
message.set_recent_blockhash(blockhashdata.blockhash);
let transaction = sign_versioned_message(message.clone(), signers, dedupe_signers)
.expect("Transaction should be signable");
let serialized_transaction =
serialize(&transaction).expect("Transaction should serialize");
let signature = transaction.signatures[0];
context.unconfirmed_transaction_map.insert(
signature,
TransactionData {
index,
serialized_transaction: serialized_transaction.clone(),
last_valid_block_height: blockhashdata.last_valid_block_height,
message,
},
);
if let Some(progress_bar) = progress_bar {
let progress = progress_from_context_and_block_height(
context,
blockhashdata.last_valid_block_height,
);
progress.set_message_for_confirmed_transactions(
progress_bar,
&format!(
"Sending {}/{} transactions",
counter + 1,
current_transaction_count,
),
);
}
send_transaction_with_rpc_fallback(
rpc_client,
tpu_client,
transaction,
serialized_transaction,
context,
index,
rpc_send_transaction_config,
)
.await
});
}
join_all(futures)
.await
.into_iter()
.collect::<Result<Vec<()>>>()?;
Ok(())
}
async fn confirm_transactions_till_block_height_and_resend_unexpired_transaction_over_tpu<
S: WireTransactionSender,
>(
progress_bar: &Option<indicatif::ProgressBar>,
tpu_client: &Option<S>,
context: &SendingContext,
) {
let unconfirmed_transaction_map = context.unconfirmed_transaction_map.clone();
let current_block_height = context.current_block_height.clone();
let transactions_to_confirm = unconfirmed_transaction_map.len();
let max_valid_block_height = unconfirmed_transaction_map
.iter()
.map(|x| x.last_valid_block_height)
.max();
if let Some(mut max_valid_block_height) = max_valid_block_height {
if let Some(progress_bar) = progress_bar {
let progress = progress_from_context_and_block_height(context, max_valid_block_height);
progress.set_message_for_confirmed_transactions(
progress_bar,
&format!(
"Waiting for next block, {transactions_to_confirm} transactions pending..."
),
);
}
while !unconfirmed_transaction_map.is_empty()
&& current_block_height.load(Ordering::Relaxed) <= max_valid_block_height
{
tokio::select! {
() = context.confirmation_signal.notified() => {}
() = tokio::time::sleep(context.check_interval.saturating_mul(2)) => {}
}
let block_height = current_block_height.load(Ordering::Relaxed);
if let Some(tpu_client) = tpu_client {
let txs_to_resend_over_tpu = unconfirmed_transaction_map
.iter()
.filter(|x| block_height < x.last_valid_block_height)
.map(|x| x.serialized_transaction.clone())
.collect::<Vec<_>>();
send_staggered_transactions(
progress_bar,
tpu_client,
txs_to_resend_over_tpu,
max_valid_block_height,
context,
)
.await;
}
if let Some(max_valid_block_height_in_remaining_transaction) =
unconfirmed_transaction_map
.iter()
.map(|x| x.last_valid_block_height)
.max()
{
max_valid_block_height = max_valid_block_height_in_remaining_transaction;
}
}
}
}
async fn send_staggered_transactions<S: WireTransactionSender>(
progress_bar: &Option<indicatif::ProgressBar>,
tpu_client: &S,
wire_transactions: Vec<Vec<u8>>,
last_valid_block_height: u64,
context: &SendingContext,
) {
let current_transaction_count = wire_transactions.len();
let futures = wire_transactions
.into_iter()
.enumerate()
.map(|(counter, transaction)| async move {
tokio::time::sleep(context.send_interval.saturating_mul(counter as u32)).await;
if let Some(progress_bar) = progress_bar {
let progress =
progress_from_context_and_block_height(context, last_valid_block_height);
progress.set_message_for_confirmed_transactions(
progress_bar,
&format!(
"Resending {}/{} transactions",
counter + 1,
current_transaction_count,
),
);
}
tpu_client.send(transaction).await
})
.collect::<Vec<_>>();
join_all(futures).await;
}
pub async fn send_and_confirm_transactions_in_parallel_v2<T: Signers + ?Sized>(
rpc_client: Arc<RpcClient>,
tpu_client: Option<QuicTpuClient>,
messages: &[Message],
signers: &T,
config: SendAndConfirmConfigV2,
) -> Result<Vec<Option<TransactionError>>> {
send_and_confirm_transactions_in_parallel_impl(
rpc_client,
tpu_client,
messages.iter().cloned().map(VersionedMessage::Legacy),
signers,
config.into(),
)
.await
}
pub async fn send_and_confirm_transactions_in_parallel_v3<T, M>(
rpc_client: Arc<RpcClient>,
transport: SendTransport,
messages: M,
signers: &T,
config: SendAndConfirmConfigV3,
) -> Result<Vec<Option<TransactionError>>>
where
T: Signers + ?Sized,
M: IntoIterator<Item = VersionedMessage>,
{
let (tpu_transaction_sender, rpc_send_transaction_config) = match transport {
SendTransport::Tpu(sender) => (Some(sender), None),
SendTransport::Rpc(rpc_config) => (None, Some(rpc_config)),
};
send_and_confirm_transactions_in_parallel_impl(
rpc_client,
tpu_transaction_sender,
messages,
signers,
ParallelSendConfig {
with_spinner: config.with_spinner,
max_sign_attempts: config.max_sign_attempts,
rpc_send_transaction_config,
dedupe_signers: false,
send_interval: config.send_interval,
check_interval: config.check_interval,
},
)
.await
}
async fn send_and_confirm_transactions_in_parallel_impl<T, S, M>(
rpc_client: Arc<RpcClient>,
tpu_transaction_sender: Option<S>,
messages: M,
signers: &T,
config: ParallelSendConfig,
) -> Result<Vec<Option<TransactionError>>>
where
T: Signers + ?Sized,
S: WireTransactionSender,
M: IntoIterator<Item = VersionedMessage>,
{
let messages: Vec<VersionedMessage> = messages.into_iter().collect();
let (blockhash, last_valid_block_height) = rpc_client
.get_latest_blockhash_with_commitment(rpc_client.commitment())
.await?;
let blockhash_data_rw = Arc::new(RwLock::new(BlockHashData {
blockhash,
last_valid_block_height,
}));
messages
.iter()
.map(|x| {
let mut message = x.clone();
message.set_recent_blockhash(blockhash);
sign_versioned_message(message, signers, config.dedupe_signers).map(|_| ())
})
.collect::<std::result::Result<Vec<()>, SignerError>>()?;
let block_height = rpc_client.get_block_height().await?;
let current_block_height = Arc::new(AtomicU64::new(block_height));
let progress_bar = config.with_spinner.then(|| {
let progress_bar = spinner::new_progress_bar();
progress_bar.set_message("Setting up...");
progress_bar
});
let block_data_task = create_blockhash_data_updating_task(
rpc_client.clone(),
blockhash_data_rw.clone(),
current_block_height.clone(),
);
let unconfirmed_transasction_map = Arc::new(DashMap::<Signature, TransactionData>::new());
let error_map = Arc::new(DashMap::new());
let num_confirmed_transactions = Arc::new(AtomicUsize::new(0));
let confirmation_signal = Arc::new(Notify::new());
let transaction_confirming_task = create_transaction_confirmation_task(
rpc_client.clone(),
current_block_height.clone(),
unconfirmed_transasction_map.clone(),
error_map.clone(),
num_confirmed_transactions.clone(),
config.check_interval,
confirmation_signal.clone(),
);
let total_transactions = messages.len();
let mut initial = true;
let context = SendingContext {
unconfirmed_transaction_map: unconfirmed_transasction_map.clone(),
blockhash_data_rw: blockhash_data_rw.clone(),
num_confirmed_transactions: num_confirmed_transactions.clone(),
current_block_height: current_block_height.clone(),
error_map: error_map.clone(),
total_transactions,
send_interval: config.send_interval,
check_interval: config.check_interval,
confirmation_signal,
};
for expired_blockhash_retries in (0..config.max_sign_attempts.get()).rev() {
let messages_with_index: Vec<(usize, VersionedMessage)> = if initial {
initial = false;
messages.iter().cloned().enumerate().collect()
} else {
unconfirmed_transasction_map
.iter()
.map(|x| (x.index, x.message.clone()))
.collect()
};
if messages_with_index.is_empty() {
break;
}
unconfirmed_transasction_map.clear();
sign_all_messages_and_send(
&progress_bar,
&rpc_client,
&tpu_transaction_sender,
messages_with_index,
signers,
config.dedupe_signers,
&context,
config.rpc_send_transaction_config,
)
.await?;
confirm_transactions_till_block_height_and_resend_unexpired_transaction_over_tpu(
&progress_bar,
&tpu_transaction_sender,
&context,
)
.await;
if unconfirmed_transasction_map.is_empty() {
break;
}
if let Some(progress_bar) = &progress_bar {
progress_bar.println(format!(
"Blockhash expired. {expired_blockhash_retries} retries remaining"
));
}
}
block_data_task.abort();
transaction_confirming_task.abort();
if unconfirmed_transasction_map.is_empty() {
let mut transaction_errors = vec![None; messages.len()];
for iterator in error_map.iter() {
transaction_errors[*iterator.key()] = Some(iterator.value().clone());
}
Ok(transaction_errors)
} else {
Err(TpuSenderError::Custom("Max retries exceeded".into()))
}
}