openzeppelin_relayer/jobs/handlers/
transaction_status_handler.rs1use actix_web::web::ThinData;
8use apalis::prelude::{Attempt, Data, *};
9use eyre::Result;
10use tracing::{debug, instrument};
11
12use std::sync::Arc;
13
14use crate::{
15 domain::{get_relayer_transaction, get_transaction_by_id, is_final_state, Transaction},
16 jobs::{Job, TransactionStatusCheck},
17 models::{DefaultAppState, TransactionRepoModel},
18 observability::request_id::set_request_id,
19};
20
21#[cfg(test)]
22use crate::models::NetworkType;
23
24#[instrument(
25 level = "debug",
26 skip(job, state),
27 fields(
28 request_id = ?job.request_id,
29 job_id = %job.message_id,
30 job_type = %job.job_type.to_string(),
31 attempt = %attempt.current(),
32 tx_id = %job.data.transaction_id,
33 relayer_id = %job.data.relayer_id,
34 )
35)]
36pub async fn transaction_status_handler(
37 job: Job<TransactionStatusCheck>,
38 state: Data<ThinData<DefaultAppState>>,
39 attempt: Attempt,
40) -> Result<(), Error> {
41 if let Some(request_id) = job.request_id.clone() {
42 set_request_id(request_id);
43 }
44
45 debug!(
46 "handling transaction status check for tx_id {}",
47 job.data.transaction_id
48 );
49
50 let result = handle_request(job.data, state).await;
51
52 handle_status_check_result(result)
53}
54
55fn handle_status_check_result(result: Result<TransactionRepoModel>) -> Result<(), Error> {
62 match result {
63 Ok(updated_tx) => {
64 if is_final_state(&updated_tx.status) {
66 debug!(
67 tx_id = %updated_tx.id,
68 status = ?updated_tx.status,
69 "transaction reached final state, status check complete"
70 );
71 Ok(())
72 } else {
73 debug!(
75 tx_id = %updated_tx.id,
76 status = ?updated_tx.status,
77 "transaction status: {:?} - not in final state, retrying status check",
78 updated_tx.status
79 );
80 Err(Error::Failed(Arc::new(
81 format!(
82 "transaction status: {:?} - not in final state, retrying status check",
83 updated_tx.status
84 )
85 .into(),
86 )))
87 }
88 }
89 Err(e) => {
90 Err(Error::Failed(Arc::new(format!("{e}").into())))
92 }
93 }
94}
95
96async fn handle_request(
97 status_request: TransactionStatusCheck,
98 state: Data<ThinData<DefaultAppState>>,
99) -> Result<TransactionRepoModel> {
100 let relayer_transaction =
101 get_relayer_transaction(status_request.relayer_id.clone(), &state).await?;
102
103 let transaction = get_transaction_by_id(status_request.transaction_id.clone(), &state).await?;
104
105 let updated_transaction = relayer_transaction
106 .handle_transaction_status(transaction)
107 .await?;
108
109 debug!(
110 "status check handled successfully for tx_id {}",
111 status_request.transaction_id
112 );
113
114 Ok(updated_transaction)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::models::TransactionStatus;
121 use crate::utils::mocks::mockutils::create_mock_transaction;
122 use std::collections::HashMap;
123
124 #[tokio::test]
125 async fn test_status_check_job_validation() {
126 let check_job = TransactionStatusCheck::new("tx123", "relayer-1", NetworkType::Evm);
128 let job = Job::new(crate::jobs::JobType::TransactionStatusCheck, check_job);
129
130 assert_eq!(job.data.transaction_id, "tx123");
132 assert_eq!(job.data.relayer_id, "relayer-1");
133 assert!(job.data.metadata.is_none());
134 }
135
136 #[tokio::test]
137 async fn test_status_check_with_metadata() {
138 let mut metadata = HashMap::new();
140 metadata.insert("retry_count".to_string(), "2".to_string());
141 metadata.insert("last_status".to_string(), "pending".to_string());
142
143 let check_job = TransactionStatusCheck::new("tx123", "relayer-1", NetworkType::Evm)
144 .with_metadata(metadata.clone());
145
146 assert!(check_job.metadata.is_some());
148 let job_metadata = check_job.metadata.unwrap();
149 assert_eq!(job_metadata.get("retry_count").unwrap(), "2");
150 assert_eq!(job_metadata.get("last_status").unwrap(), "pending");
151 }
152
153 mod handle_status_check_result_tests {
154 use super::*;
155
156 #[test]
157 fn test_final_state_confirmed_returns_ok() {
158 let mut tx = create_mock_transaction();
159 tx.status = TransactionStatus::Confirmed;
160 let result = Ok(tx);
161
162 let check_result = handle_status_check_result(result);
163
164 assert!(
165 check_result.is_ok(),
166 "Should return Ok for Confirmed (final) state"
167 );
168 }
169
170 #[test]
171 fn test_final_state_failed_returns_ok() {
172 let mut tx = create_mock_transaction();
173 tx.status = TransactionStatus::Failed;
174 let result = Ok(tx);
175
176 let check_result = handle_status_check_result(result);
177
178 assert!(
179 check_result.is_ok(),
180 "Should return Ok for Failed (final) state"
181 );
182 }
183
184 #[test]
185 fn test_final_state_expired_returns_ok() {
186 let mut tx = create_mock_transaction();
187 tx.status = TransactionStatus::Expired;
188 let result = Ok(tx);
189
190 let check_result = handle_status_check_result(result);
191
192 assert!(
193 check_result.is_ok(),
194 "Should return Ok for Expired (final) state"
195 );
196 }
197
198 #[test]
199 fn test_final_state_canceled_returns_ok() {
200 let mut tx = create_mock_transaction();
201 tx.status = TransactionStatus::Canceled;
202 let result = Ok(tx);
203
204 let check_result = handle_status_check_result(result);
205
206 assert!(
207 check_result.is_ok(),
208 "Should return Ok for Canceled (final) state"
209 );
210 }
211
212 #[test]
213 fn test_non_final_state_pending_returns_error() {
214 let mut tx = create_mock_transaction();
215 tx.status = TransactionStatus::Pending;
216 let result = Ok(tx);
217
218 let check_result = handle_status_check_result(result);
219
220 assert!(
221 check_result.is_err(),
222 "Should return Err for Pending (non-final) state to trigger retry"
223 );
224 }
225
226 #[test]
227 fn test_non_final_state_sent_returns_error() {
228 let mut tx = create_mock_transaction();
229 tx.status = TransactionStatus::Sent;
230 let result = Ok(tx);
231
232 let check_result = handle_status_check_result(result);
233
234 assert!(
235 check_result.is_err(),
236 "Should return Err for Sent (non-final) state to trigger retry"
237 );
238 }
239
240 #[test]
241 fn test_non_final_state_submitted_returns_error() {
242 let mut tx = create_mock_transaction();
243 tx.status = TransactionStatus::Submitted;
244 let result = Ok(tx);
245
246 let check_result = handle_status_check_result(result);
247
248 assert!(
249 check_result.is_err(),
250 "Should return Err for Submitted (non-final) state to trigger retry"
251 );
252 }
253
254 #[test]
255 fn test_non_final_state_mined_returns_error() {
256 let mut tx = create_mock_transaction();
257 tx.status = TransactionStatus::Mined;
258 let result = Ok(tx);
259
260 let check_result = handle_status_check_result(result);
261
262 assert!(
263 check_result.is_err(),
264 "Should return Err for Mined (non-final) state to trigger retry"
265 );
266 }
267
268 #[test]
269 fn test_error_result_returns_error() {
270 let result: Result<TransactionRepoModel> =
271 Err(eyre::eyre!("Network timeout during status check"));
272
273 let check_result = handle_status_check_result(result);
274
275 assert!(
276 check_result.is_err(),
277 "Should return Err when original result is an error"
278 );
279 }
280
281 #[test]
282 fn test_error_message_propagation() {
283 let error_message = "RPC call failed: connection timeout";
284 let result: Result<TransactionRepoModel> = Err(eyre::eyre!(error_message));
285
286 let check_result = handle_status_check_result(result);
287
288 match check_result {
289 Err(Error::Failed(arc)) => {
290 let err_string = arc.to_string();
291 assert!(
292 err_string.contains(error_message),
293 "Error message should contain original error: {}",
294 err_string
295 );
296 }
297 _ => panic!("Expected Error::Failed"),
298 }
299 }
300
301 #[test]
302 fn test_non_final_state_error_message() {
303 let mut tx = create_mock_transaction();
304 tx.status = TransactionStatus::Submitted;
305 let result = Ok(tx);
306
307 let check_result = handle_status_check_result(result);
308
309 match check_result {
310 Err(Error::Failed(arc)) => {
311 let err_string = arc.to_string();
312 assert!(
313 err_string.contains("not in final state"),
314 "Error message should indicate non-final state: {}",
315 err_string
316 );
317 assert!(
318 err_string.contains("Submitted"),
319 "Error message should mention the status: {}",
320 err_string
321 );
322 }
323 _ => panic!("Expected Error::Failed for non-final state"),
324 }
325 }
326 }
327}