mango-v4/lib/client/src/confirm_transaction.rs

118 lines
4.3 KiB
Rust

use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_client::rpc_request::RpcError;
use solana_sdk::{commitment_config::CommitmentConfig, signature::Signature};
use solana_transaction_status::TransactionStatus;
use crate::util::delay_interval;
use std::time::Duration;
#[derive(thiserror::Error, Debug)]
pub enum WaitForTransactionConfirmationError {
#[error("blockhash has expired")]
BlockhashExpired,
#[error("timeout expired")]
Timeout,
#[error("client error: {0:?}")]
ClientError(#[from] solana_client::client_error::ClientError),
}
#[derive(Clone, Debug, Builder)]
#[builder(default)]
pub struct RpcConfirmTransactionConfig {
/// If none, defaults to the RpcClient's configured default commitment.
pub commitment: Option<CommitmentConfig>,
/// Time after which to start checking for blockhash expiry.
pub recent_blockhash_initial_timeout: Duration,
/// Interval between signature status queries.
pub signature_status_interval: Duration,
/// If none, there's no timeout. The confirmation will still abort eventually
/// when the blockhash expires.
pub timeout: Option<Duration>,
}
impl Default for RpcConfirmTransactionConfig {
fn default() -> Self {
Self {
commitment: None,
recent_blockhash_initial_timeout: Duration::from_secs(5),
signature_status_interval: Duration::from_millis(500),
timeout: None,
}
}
}
impl RpcConfirmTransactionConfig {
pub fn builder() -> RpcConfirmTransactionConfigBuilder {
RpcConfirmTransactionConfigBuilder::default()
}
}
/// Wait for `signature` to be confirmed at `commitment` or until either
/// - `recent_blockhash` is so old that the tx can't be confirmed _and_
/// `blockhash_initial_timeout` is reached
/// - the `signature_status_timeout` is reached
/// While waiting, query for confirmation every `signature_status_interval`
///
/// NOTE: RpcClient::config contains confirm_transaction_initial_timeout which is the
/// same as blockhash_initial_timeout. Unfortunately the former is private.
///
/// Returns:
/// - blockhash and blockhash_initial_timeout expired -> BlockhashExpired error
/// - signature_status_timeout expired -> Timeout error (possibly just didn't reach commitment in time?)
/// - any rpc error -> ClientError error
/// - confirmed at commitment -> ok(slot, opt<tx_error>)
pub async fn wait_for_transaction_confirmation(
rpc_client: &RpcClientAsync,
signature: &Signature,
recent_blockhash: &solana_sdk::hash::Hash,
config: &RpcConfirmTransactionConfig,
) -> Result<TransactionStatus, WaitForTransactionConfirmationError> {
let mut signature_status_interval = delay_interval(config.signature_status_interval);
let commitment = config.commitment.unwrap_or(rpc_client.commitment());
let start = std::time::Instant::now();
let is_timed_out = || config.timeout.map(|t| start.elapsed() > t).unwrap_or(false);
loop {
signature_status_interval.tick().await;
if is_timed_out() {
return Err(WaitForTransactionConfirmationError::Timeout);
}
let statuses = rpc_client
.get_signature_statuses(&[signature.clone()])
.await?;
let status_opt = match statuses.value.into_iter().next() {
Some(v) => v,
None => {
return Err(WaitForTransactionConfirmationError::ClientError(
RpcError::ParseError(
"must contain an entry for each requested signature".into(),
)
.into(),
));
}
};
// If the tx isn't seen at all (not even processed), check blockhash expiry
if status_opt.is_none() {
if start.elapsed() > config.recent_blockhash_initial_timeout {
let blockhash_is_valid = rpc_client
.is_blockhash_valid(recent_blockhash, CommitmentConfig::processed())
.await?;
if !blockhash_is_valid {
return Err(WaitForTransactionConfirmationError::BlockhashExpired);
}
}
continue;
}
let status = status_opt.unwrap();
if status.satisfies_commitment(commitment) {
return Ok(status);
}
}
}