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#[derive(Debug)]
14pub enum TransactionOutcome<T> {
15 Success(Box<SuccessfulTransaction<T>>),
17 Unsubmitted(UnsubmittedTransaction<T>),
19 InFlight(InFlightTransaction<T>),
21 Failure(Box<FailedTransaction<T>>),
23}
24
25#[derive(Debug)]
27pub struct SuccessfulTransaction<T> {
28 pub data: T,
29 pub slot: Slot,
30 pub signature: Signature,
31}
32
33#[derive(Debug)]
35pub struct UnsubmittedTransaction<T> {
36 pub data: T,
37 pub slot: Option<Slot>,
38}
39
40#[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#[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 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 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 pub fn error(&self) -> Option<&FailedTransaction<T>> {
110 match self {
111 TransactionOutcome::Failure(f) => Some(f),
112 _ => None,
113 }
114 }
115}
116
117pub 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#[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 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 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 pub fn should_be_resent(&self) -> bool {
188 self.error().map(|e| e.is_transient()).unwrap_or(false)
189 }
190
191 pub fn error(&self) -> Option<&NitroSenderError> {
193 match self {
194 TransactionStatus::Failed(err, _, _) => Some(err),
195 _ => None,
196 }
197 }
198
199 pub fn confirmation_status(&self) -> Option<TransactionConfirmationStatus> {
201 match self {
202 TransactionStatus::Processing(status, _) => Some(status.clone()),
203 _ => None,
204 }
205 }
206
207 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}