nitro_sender/
transaction.rs

1use solana_commitment_config::CommitmentConfig;
2use solana_program::clock::Slot;
3use solana_signature::Signature;
4use solana_transaction_error::TransactionError;
5use solana_transaction_status::{
6    TransactionConfirmationStatus, TransactionStatus as SolanaTransactionStatus,
7};
8
9use crate::client::NitroSenderError;
10
11/// The final outcome of a transaction after the [`BatchClient`] is done, either successfully
12/// or due to reaching the timeout.
13#[derive(Debug)]
14pub enum TransactionOutcome<T> {
15    /// The transaction was successfully confirmed by the network at the desired commitment level.
16    Success(Box<SuccessfulTransaction<T>>),
17    /// The transaction was not submitted to the network.
18    Unsubmitted(UnsubmittedTransaction<T>),
19    /// The transaction is currently being processed by the network.
20    InFlight(InFlightTransaction<T>),
21    /// The transaction latest status contained an error.
22    Failure(Box<FailedTransaction<T>>),
23}
24
25/// A transaction that was successfully confirmed by the network at the desired commitment level.
26#[derive(Debug)]
27pub struct SuccessfulTransaction<T> {
28    pub data: T,
29    pub slot: Slot,
30    pub signature: Signature,
31}
32
33/// A transaction that was not submitted to the network.
34#[derive(Debug)]
35pub struct UnsubmittedTransaction<T> {
36    pub data: T,
37    pub slot: Option<Slot>,
38}
39
40/// A transaction that is currently being processed by the network.
41#[derive(Debug)]
42pub struct InFlightTransaction<T> {
43    pub data: T,
44    pub slot: Slot,
45    pub status: TransactionConfirmationStatus,
46    pub signature: Signature,
47}
48
49impl<T> From<InFlightTransaction<T>> for SuccessfulTransaction<T> {
50    fn from(in_flight: InFlightTransaction<T>) -> Self {
51        Self {
52            data: in_flight.data,
53            slot: in_flight.slot,
54            signature: in_flight.signature,
55        }
56    }
57}
58
59/// A transaction that resulted in an error.
60#[derive(Debug)]
61pub struct FailedTransaction<T> {
62    pub data: T,
63    pub slot: Slot,
64    pub error: String,
65    pub logs: Vec<String>,
66}
67
68impl<T> TransactionOutcome<T> {
69    /// Returns `true` if the outcome was successful.
70    pub fn successful(&self, commitment: CommitmentConfig) -> bool {
71        match self {
72            TransactionOutcome::Success(_) => true,
73            TransactionOutcome::InFlight(in_flight) => {
74                if commitment.is_finalized() {
75                    TransactionConfirmationStatus::Finalized == in_flight.status
76                } else if commitment.is_confirmed() {
77                    TransactionConfirmationStatus::Processed != in_flight.status
78                } else {
79                    true
80                }
81            }
82            _ => false,
83        }
84    }
85
86    /// Returns [`Option::Some`] if the outcome was successful, or [`Option::None`] otherwise.
87    pub fn into_successful(
88        self,
89        commitment: CommitmentConfig,
90    ) -> Option<Box<SuccessfulTransaction<T>>> {
91        match self {
92            TransactionOutcome::Success(s) => Some(s),
93            TransactionOutcome::InFlight(in_flight) => {
94                if commitment.is_finalized() {
95                    (in_flight.status == TransactionConfirmationStatus::Finalized)
96                        .then_some(Box::new(in_flight.into()))
97                } else if commitment.is_confirmed() {
98                    (in_flight.status != TransactionConfirmationStatus::Processed)
99                        .then_some(Box::new(in_flight.into()))
100                } else {
101                    Some(Box::new(in_flight.into()))
102                }
103            }
104            _ => None,
105        }
106    }
107
108    /// Returns a reference to the inner [`FailedTransaction`] if the outcome was a failure, or [`None`] otherwise.
109    pub fn error(&self) -> Option<&FailedTransaction<T>> {
110        match self {
111            TransactionOutcome::Failure(f) => Some(f),
112            _ => None,
113        }
114    }
115}
116
117/// Tracks the progress of a transaction, and holds on to its associated data.
118pub struct TransactionProgress<T> {
119    pub data: T,
120    pub landed_as: Option<(Slot, Signature)>,
121    pub status: TransactionStatus,
122}
123
124impl<T> TransactionProgress<T> {
125    pub fn new(data: T) -> Self {
126        Self {
127            data,
128            landed_as: None,
129            status: TransactionStatus::Pending,
130        }
131    }
132}
133
134/// The current state of a transaction.
135#[derive(Debug, PartialEq, Eq)]
136pub enum TransactionStatus {
137    Pending,
138    Processing(TransactionConfirmationStatus, Slot),
139    Committed(Slot),
140    Failed(NitroSenderError, Vec<String>, Slot),
141}
142
143impl TransactionStatus {
144    /// Translates from a [`SolanaTransactionStatus`] and a [commitment level](`CommitmentConfig`)
145    /// to a [`TransactionStatus`].
146    pub fn from_solana_status(status: SolanaTransactionStatus, logs: Vec<String>) -> Self {
147        if let Some(TransactionError::AlreadyProcessed) = status.err {
148            Self::Committed(status.slot)
149        } else if let Some(err) = status.err {
150            Self::Failed(err.into(), logs, status.slot)
151        } else {
152            Self::Processing(
153                status
154                    .confirmation_status
155                    .unwrap_or(TransactionConfirmationStatus::Processed),
156                status.slot,
157            )
158        }
159    }
160
161    /// Checks whether a transaction should be re-confirmed based on its status.
162    ///
163    /// These should be re-confirmed:
164    /// - [`TransactionStatus::Pending`]
165    /// - [`TransactionStatus::Processing`]
166    ///
167    /// These should *not* be re-confirmed:
168    /// - [`TransactionStatus::Committed`]
169    /// - [`TransactionStatus::Failed`]
170    pub fn should_be_reconfirmed(&self, commitment: CommitmentConfig) -> bool {
171        match self {
172            TransactionStatus::Pending => true,
173            TransactionStatus::Committed(_) | TransactionStatus::Failed(..) => false,
174            TransactionStatus::Processing(status, _) => {
175                if commitment.is_finalized() {
176                    *status != TransactionConfirmationStatus::Finalized
177                } else if commitment.is_confirmed() {
178                    *status == TransactionConfirmationStatus::Processed
179                } else {
180                    false
181                }
182            }
183        }
184    }
185
186    /// Checks whether the transactions should be resent based on its status.
187    pub fn should_be_resent(&self) -> bool {
188        self.error().map(|e| e.is_transient()).unwrap_or(false)
189    }
190
191    /// Returns the error if the transaction failed, or [`None`] otherwise.
192    pub fn error(&self) -> Option<&NitroSenderError> {
193        match self {
194            TransactionStatus::Failed(err, _, _) => Some(err),
195            _ => None,
196        }
197    }
198
199    /// Returns the confirmation status if the transaction is being processed, or [`None`] otherwise.
200    pub fn confirmation_status(&self) -> Option<TransactionConfirmationStatus> {
201        match self {
202            TransactionStatus::Processing(status, _) => Some(status.clone()),
203            _ => None,
204        }
205    }
206
207    /// Returns the slot associated with the transaction status.
208    pub fn slot(&self) -> Slot {
209        match self {
210            TransactionStatus::Pending => 0,
211            TransactionStatus::Processing(_, slot)
212            | TransactionStatus::Committed(slot)
213            | TransactionStatus::Failed(_, _, slot) => *slot,
214        }
215    }
216}
217
218impl<T> From<TransactionProgress<T>> for TransactionOutcome<T> {
219    fn from(progress: TransactionProgress<T>) -> Self {
220        match progress.status {
221            TransactionStatus::Pending => TransactionOutcome::Unsubmitted(UnsubmittedTransaction {
222                data: progress.data,
223                slot: None,
224            }),
225            TransactionStatus::Processing(status, slot) => {
226                let (_slot, signature) = progress.landed_as.expect(
227                    "landed_as should be Some if status is Processing; this is a bug in BatchClient",
228                );
229                TransactionOutcome::InFlight(InFlightTransaction {
230                    data: progress.data,
231                    slot,
232                    status,
233                    signature,
234                })
235            }
236            TransactionStatus::Failed(error, logs, slot) => {
237                TransactionOutcome::Failure(Box::new(FailedTransaction {
238                    data: progress.data,
239                    error: error.to_string(),
240                    slot,
241                    logs,
242                }))
243            }
244            TransactionStatus::Committed(_slot) => {
245                let (slot, signature) = progress.landed_as.expect(
246                    "landed_as should be Some if status is Committed; this is a bug in BatchClient",
247                );
248                TransactionOutcome::Success(Box::new(SuccessfulTransaction {
249                    data: progress.data,
250                    slot,
251                    signature,
252                }))
253            }
254        }
255    }
256}