Rust client: Revamp transaction confirmation (#850)
- Allow user configuration of confirmation settings - Provide a timeout setting, default is 60s
This commit is contained in:
parent
18729cf04c
commit
511814ca97
|
@ -3464,6 +3464,7 @@ dependencies = [
|
|||
"solana-client",
|
||||
"solana-rpc",
|
||||
"solana-sdk",
|
||||
"solana-transaction-status",
|
||||
"spl-associated-token-account 1.1.3",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
|
|
@ -24,6 +24,7 @@ solana-program = "~1.16.7"
|
|||
solana-program-test = "~1.16.7"
|
||||
solana-rpc = "~1.16.7"
|
||||
solana-sdk = { version = "~1.16.7", default-features = false }
|
||||
solana-transaction-status = { version = "~1.16.7" }
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
||||
|
|
|
@ -30,6 +30,7 @@ solana-client = { workspace = true }
|
|||
solana-rpc = { workspace = true }
|
||||
solana-sdk = { workspace = true }
|
||||
solana-address-lookup-table-program = { workspace = true }
|
||||
solana-transaction-status = { workspace = true }
|
||||
mango-feeds-connector = { workspace = true }
|
||||
spl-associated-token-account = "1.0.3"
|
||||
thiserror = "1.0.31"
|
||||
|
|
|
@ -34,6 +34,7 @@ use solana_sdk::signer::keypair;
|
|||
use solana_sdk::transaction::TransactionError;
|
||||
|
||||
use crate::account_fetcher::*;
|
||||
use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig};
|
||||
use crate::context::MangoGroupContext;
|
||||
use crate::gpa::{fetch_anchor_account, fetch_mango_accounts};
|
||||
use crate::util::PreparedInstructions;
|
||||
|
@ -66,6 +67,9 @@ pub struct Client {
|
|||
pub commitment: CommitmentConfig,
|
||||
|
||||
/// Timeout, defaults to 60s
|
||||
///
|
||||
/// This timeout applies to rpc requests. Note that the timeout for transaction
|
||||
/// confirmation is configured separately in rpc_confirm_transaction_config.
|
||||
#[builder(default = "Some(Duration::from_secs(60))")]
|
||||
pub timeout: Option<Duration>,
|
||||
|
||||
|
@ -76,6 +80,10 @@ pub struct Client {
|
|||
#[builder(default = "ClientBuilder::default_rpc_send_transaction_config()")]
|
||||
pub rpc_send_transaction_config: RpcSendTransactionConfig,
|
||||
|
||||
/// Defaults to waiting up to 60s for confirmation
|
||||
#[builder(default = "ClientBuilder::default_rpc_confirm_transaction_config()")]
|
||||
pub rpc_confirm_transaction_config: RpcConfirmTransactionConfig,
|
||||
|
||||
#[builder(default = "\"https://quote-api.jup.ag/v4\".into()")]
|
||||
pub jupiter_v4_url: String,
|
||||
|
||||
|
@ -93,6 +101,13 @@ impl ClientBuilder {
|
|||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_rpc_confirm_transaction_config() -> RpcConfirmTransactionConfig {
|
||||
RpcConfirmTransactionConfig {
|
||||
timeout: Some(Duration::from_secs(60)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
|
@ -1869,10 +1884,19 @@ impl TransactionBuilder {
|
|||
pub async fn send_and_confirm(&self, client: &Client) -> anyhow::Result<Signature> {
|
||||
let rpc = client.rpc_async();
|
||||
let tx = self.transaction(&rpc).await?;
|
||||
// TODO: Wish we could use client.rpc_send_transaction_config here too!
|
||||
rpc.send_and_confirm_transaction(&tx)
|
||||
let recent_blockhash = tx.message.recent_blockhash();
|
||||
let signature = rpc
|
||||
.send_transaction_with_config(&tx, client.rpc_send_transaction_config)
|
||||
.await
|
||||
.map_err(prettify_solana_client_error)
|
||||
.map_err(prettify_solana_client_error)?;
|
||||
wait_for_transaction_confirmation(
|
||||
&rpc,
|
||||
&signature,
|
||||
recent_blockhash,
|
||||
&client.rpc_confirm_transaction_config,
|
||||
)
|
||||
.await?;
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub fn transaction_size(&self) -> anyhow::Result<TransactionSize> {
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ pub mod account_update_stream;
|
|||
pub mod chain_data;
|
||||
mod chain_data_fetcher;
|
||||
mod client;
|
||||
pub mod confirm_transaction;
|
||||
mod context;
|
||||
pub mod error_tracking;
|
||||
pub mod gpa;
|
||||
|
|
|
@ -1,17 +1,8 @@
|
|||
use solana_client::{
|
||||
client_error::Result as ClientResult, rpc_client::RpcClient, rpc_request::RpcError,
|
||||
};
|
||||
use solana_sdk::compute_budget::ComputeBudgetInstruction;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use solana_sdk::{
|
||||
clock::Slot, commitment_config::CommitmentConfig, signature::Signature,
|
||||
transaction::uses_durable_nonce,
|
||||
};
|
||||
|
||||
use anchor_lang::prelude::{AccountMeta, Pubkey};
|
||||
use anyhow::Context;
|
||||
use std::{thread, time};
|
||||
|
||||
/// Some Result<> types don't convert to anyhow::Result nicely. Force them through stringification.
|
||||
pub trait AnyhowWrap {
|
||||
|
@ -57,67 +48,6 @@ pub fn delay_interval(period: std::time::Duration) -> tokio::time::Interval {
|
|||
interval
|
||||
}
|
||||
|
||||
/// A copy of RpcClient::send_and_confirm_transaction that returns the slot the
|
||||
/// transaction confirmed in.
|
||||
pub fn send_and_confirm_transaction(
|
||||
rpc_client: &RpcClient,
|
||||
transaction: &Transaction,
|
||||
) -> ClientResult<(Signature, Slot)> {
|
||||
const SEND_RETRIES: usize = 1;
|
||||
const GET_STATUS_RETRIES: usize = usize::MAX;
|
||||
|
||||
'sending: for _ in 0..SEND_RETRIES {
|
||||
let signature = rpc_client.send_transaction(transaction)?;
|
||||
|
||||
let recent_blockhash = if uses_durable_nonce(transaction).is_some() {
|
||||
let (recent_blockhash, ..) =
|
||||
rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::processed())?;
|
||||
recent_blockhash
|
||||
} else {
|
||||
transaction.message.recent_blockhash
|
||||
};
|
||||
|
||||
for status_retry in 0..GET_STATUS_RETRIES {
|
||||
let response = rpc_client.get_signature_statuses(&[signature])?.value;
|
||||
match response[0]
|
||||
.clone()
|
||||
.filter(|result| result.satisfies_commitment(rpc_client.commitment()))
|
||||
{
|
||||
Some(tx_status) => {
|
||||
return if let Some(e) = tx_status.err {
|
||||
Err(e.into())
|
||||
} else {
|
||||
Ok((signature, tx_status.slot))
|
||||
};
|
||||
}
|
||||
None => {
|
||||
if !rpc_client
|
||||
.is_blockhash_valid(&recent_blockhash, CommitmentConfig::processed())?
|
||||
{
|
||||
// Block hash is not found by some reason
|
||||
break 'sending;
|
||||
} else if cfg!(not(test))
|
||||
// Ignore sleep at last step.
|
||||
&& status_retry < GET_STATUS_RETRIES
|
||||
{
|
||||
// Retry twice a second
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(RpcError::ForUser(
|
||||
"unable to confirm transaction. \
|
||||
This can happen in situations such as transaction expiration \
|
||||
and insufficient fee-payer funds"
|
||||
.to_string(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Convenience function used in binaries to set up the fmt tracing_subscriber,
|
||||
/// with cololring enabled only if logging to a terminal and with EnvFilter.
|
||||
pub fn tracing_subscriber_init() {
|
||||
|
|
Loading…
Reference in New Issue