1use chrono::Utc;
6use soroban_rs::xdr::{Error, Hash};
7use tracing::{info, warn};
8
9use super::StellarRelayerTransaction;
10use crate::{
11 constants::STELLAR_STATUS_CHECK_JOB_DELAY_SECONDS,
12 jobs::{JobProducerTrait, TransactionStatusCheck},
13 models::{
14 NetworkTransactionData, RelayerRepoModel, TransactionError, TransactionRepoModel,
15 TransactionStatus, TransactionUpdateRequest,
16 },
17 repositories::{Repository, TransactionCounterTrait, TransactionRepository},
18 services::{Signer, StellarProviderTrait},
19 utils::calculate_scheduled_timestamp,
20};
21
22impl<R, T, J, S, P, C> StellarRelayerTransaction<R, T, J, S, P, C>
23where
24 R: Repository<RelayerRepoModel, String> + Send + Sync,
25 T: TransactionRepository + Send + Sync,
26 J: JobProducerTrait + Send + Sync,
27 S: Signer + Send + Sync,
28 P: StellarProviderTrait + Send + Sync,
29 C: TransactionCounterTrait + Send + Sync,
30{
31 pub async fn handle_transaction_status_impl(
34 &self,
35 tx: TransactionRepoModel,
36 ) -> Result<TransactionRepoModel, TransactionError> {
37 info!("handling transaction status");
38
39 match self.status_core(tx.clone()).await {
41 Ok(updated_tx) => Ok(updated_tx),
42 Err(error) => {
43 match error {
45 TransactionError::ValidationError(_) => {
46 Err(error)
48 }
49 _ => {
50 self.handle_status_failure(tx, error).await
52 }
53 }
54 }
55 }
56 }
57
58 async fn status_core(
60 &self,
61 tx: TransactionRepoModel,
62 ) -> Result<TransactionRepoModel, TransactionError> {
63 let stellar_hash = self.parse_and_validate_hash(&tx)?;
64
65 let provider_response = match self.provider().get_transaction(&stellar_hash).await {
66 Ok(response) => response,
67 Err(e) => {
68 warn!(error = ?e, "provider get_transaction failed");
69 return Err(TransactionError::from(e));
70 }
71 };
72
73 match provider_response.status.as_str().to_uppercase().as_str() {
74 "SUCCESS" => self.handle_stellar_success(tx, provider_response).await,
75 "FAILED" => self.handle_stellar_failed(tx, provider_response).await,
76 _ => {
77 self.handle_stellar_pending(tx, provider_response.status)
78 .await
79 }
80 }
81 }
82
83 async fn handle_status_failure(
86 &self,
87 tx: TransactionRepoModel,
88 error: TransactionError,
89 ) -> Result<TransactionRepoModel, TransactionError> {
90 warn!(error = %error, "failed to get stellar transaction status, re-queueing check");
91
92 if let Err(requeue_error) = self.requeue_status_check(&tx).await {
94 warn!(error = %requeue_error, "failed to requeue status check for transaction");
95 }
97
98 info!(error = %error, "transaction status check failure handled, will retry later");
100
101 Ok(tx)
103 }
104
105 pub async fn requeue_status_check(
107 &self,
108 tx: &TransactionRepoModel,
109 ) -> Result<(), TransactionError> {
110 self.job_producer()
111 .produce_check_transaction_status_job(
112 TransactionStatusCheck::new(tx.id.clone(), tx.relayer_id.clone()),
113 Some(calculate_scheduled_timestamp(
114 STELLAR_STATUS_CHECK_JOB_DELAY_SECONDS,
115 )),
116 )
117 .await?;
118 Ok(())
119 }
120
121 pub fn parse_and_validate_hash(
124 &self,
125 tx: &TransactionRepoModel,
126 ) -> Result<Hash, TransactionError> {
127 let stellar_network_data = tx.network_data.get_stellar_transaction_data()?;
128
129 let tx_hash_str = stellar_network_data.hash.as_deref().filter(|s| !s.is_empty()).ok_or_else(|| {
130 TransactionError::ValidationError(format!(
131 "Stellar transaction {} is missing or has an empty on-chain hash in network_data. Cannot check status.",
132 tx.id
133 ))
134 })?;
135
136 let stellar_hash: Hash = tx_hash_str.parse().map_err(|e: Error| {
137 TransactionError::UnexpectedError(format!(
138 "Failed to parse transaction hash '{}' for tx {}: {:?}. This hash may be corrupted or not a valid Stellar hash.",
139 tx_hash_str, tx.id, e
140 ))
141 })?;
142
143 Ok(stellar_hash)
144 }
145
146 pub async fn handle_stellar_success(
148 &self,
149 tx: TransactionRepoModel,
150 provider_response: soroban_rs::stellar_rpc_client::GetTransactionResponse,
151 ) -> Result<TransactionRepoModel, TransactionError> {
152 let updated_network_data = provider_response.result.as_ref().and_then(|tx_result| {
154 tx.network_data
155 .get_stellar_transaction_data()
156 .ok()
157 .map(|stellar_data| {
158 NetworkTransactionData::Stellar(
159 stellar_data.with_fee(tx_result.fee_charged as u32),
160 )
161 })
162 });
163
164 let update_request = TransactionUpdateRequest {
165 status: Some(TransactionStatus::Confirmed),
166 confirmed_at: Some(Utc::now().to_rfc3339()),
167 network_data: updated_network_data,
168 ..Default::default()
169 };
170
171 let confirmed_tx = self
172 .finalize_transaction_state(tx.id.clone(), update_request)
173 .await?;
174
175 self.enqueue_next_pending_transaction(&tx.id).await?;
176
177 Ok(confirmed_tx)
178 }
179
180 pub async fn handle_stellar_failed(
182 &self,
183 tx: TransactionRepoModel,
184 provider_response: soroban_rs::stellar_rpc_client::GetTransactionResponse,
185 ) -> Result<TransactionRepoModel, TransactionError> {
186 let base_reason = "Transaction failed on-chain. Provider status: FAILED.".to_string();
187 let detailed_reason = if let Some(ref tx_result_xdr) = provider_response.result {
188 format!(
189 "{} Specific XDR reason: {}.",
190 base_reason,
191 tx_result_xdr.result.name()
192 )
193 } else {
194 format!("{} No detailed XDR result available.", base_reason)
195 };
196
197 warn!(reason = %detailed_reason, "stellar transaction failed");
198
199 let update_request = TransactionUpdateRequest {
200 status: Some(TransactionStatus::Failed),
201 status_reason: Some(detailed_reason),
202 ..Default::default()
203 };
204
205 let updated_tx = self
206 .finalize_transaction_state(tx.id.clone(), update_request)
207 .await?;
208
209 self.enqueue_next_pending_transaction(&tx.id).await?;
210
211 Ok(updated_tx)
212 }
213
214 pub async fn handle_stellar_pending(
216 &self,
217 tx: TransactionRepoModel,
218 original_status_str: String,
219 ) -> Result<TransactionRepoModel, TransactionError> {
220 info!(status = %original_status_str, "stellar transaction status is still pending, re-queueing check");
221 self.requeue_status_check(&tx).await?;
222 Ok(tx)
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::models::{NetworkTransactionData, RepositoryError};
230 use mockall::predicate::eq;
231 use soroban_rs::stellar_rpc_client::GetTransactionResponse;
232
233 use crate::domain::transaction::stellar::test_helpers::*;
234
235 fn dummy_get_transaction_response(status: &str) -> GetTransactionResponse {
236 GetTransactionResponse {
237 status: status.to_string(),
238 ledger: None,
239 envelope: None,
240 result: None,
241 result_meta: None,
242 events: soroban_rs::stellar_rpc_client::GetTransactionEvents {
243 contract_events: vec![],
244 diagnostic_events: vec![],
245 transaction_events: vec![],
246 },
247 }
248 }
249
250 mod handle_transaction_status_tests {
251 use super::*;
252
253 #[tokio::test]
254 async fn handle_transaction_status_confirmed_triggers_next() {
255 let relayer = create_test_relayer();
256 let mut mocks = default_test_mocks();
257
258 let mut tx_to_handle = create_test_transaction(&relayer.id);
259 tx_to_handle.id = "tx-confirm-this".to_string();
260 let tx_hash_bytes = [1u8; 32];
261 let tx_hash_hex = hex::encode(tx_hash_bytes);
262 if let NetworkTransactionData::Stellar(ref mut stellar_data) = tx_to_handle.network_data
263 {
264 stellar_data.hash = Some(tx_hash_hex.clone());
265 } else {
266 panic!("Expected Stellar network data for tx_to_handle");
267 }
268 tx_to_handle.status = TransactionStatus::Submitted;
269
270 let expected_stellar_hash = soroban_rs::xdr::Hash(tx_hash_bytes);
271
272 mocks
274 .provider
275 .expect_get_transaction()
276 .with(eq(expected_stellar_hash.clone()))
277 .times(1)
278 .returning(move |_| {
279 Box::pin(async { Ok(dummy_get_transaction_response("SUCCESS")) })
280 });
281
282 mocks
284 .tx_repo
285 .expect_partial_update()
286 .withf(move |id, update| {
287 id == "tx-confirm-this"
288 && update.status == Some(TransactionStatus::Confirmed)
289 && update.confirmed_at.is_some()
290 })
291 .times(1)
292 .returning(move |id, update| {
293 let mut updated_tx = tx_to_handle.clone(); updated_tx.id = id;
295 updated_tx.status = update.status.unwrap();
296 updated_tx.confirmed_at = update.confirmed_at;
297 Ok(updated_tx)
298 });
299
300 mocks
302 .job_producer
303 .expect_produce_send_notification_job()
304 .times(1)
305 .returning(|_, _| Box::pin(async { Ok(()) }));
306
307 let mut oldest_pending_tx = create_test_transaction(&relayer.id);
309 oldest_pending_tx.id = "tx-oldest-pending".to_string();
310 oldest_pending_tx.status = TransactionStatus::Pending;
311 let captured_oldest_pending_tx = oldest_pending_tx.clone();
312 mocks
313 .tx_repo
314 .expect_find_by_status()
315 .with(eq(relayer.id.clone()), eq(vec![TransactionStatus::Pending]))
316 .times(1)
317 .returning(move |_, _| Ok(vec![captured_oldest_pending_tx.clone()]));
318
319 mocks
321 .job_producer
322 .expect_produce_transaction_request_job()
323 .withf(move |job, _delay| job.transaction_id == "tx-oldest-pending")
324 .times(1)
325 .returning(|_, _| Box::pin(async { Ok(()) }));
326
327 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
328 let mut initial_tx_for_handling = create_test_transaction(&relayer.id);
329 initial_tx_for_handling.id = "tx-confirm-this".to_string();
330 if let NetworkTransactionData::Stellar(ref mut stellar_data) =
331 initial_tx_for_handling.network_data
332 {
333 stellar_data.hash = Some(hex::encode(tx_hash_bytes));
334 } else {
335 panic!("Expected Stellar network data for initial_tx_for_handling");
336 }
337 initial_tx_for_handling.status = TransactionStatus::Submitted;
338
339 let result = handler
340 .handle_transaction_status_impl(initial_tx_for_handling)
341 .await;
342
343 assert!(result.is_ok());
344 let handled_tx = result.unwrap();
345 assert_eq!(handled_tx.id, "tx-confirm-this");
346 assert_eq!(handled_tx.status, TransactionStatus::Confirmed);
347 assert!(handled_tx.confirmed_at.is_some());
348 }
349
350 #[tokio::test]
351 async fn handle_transaction_status_still_pending() {
352 let relayer = create_test_relayer();
353 let mut mocks = default_test_mocks();
354
355 let mut tx_to_handle = create_test_transaction(&relayer.id);
356 tx_to_handle.id = "tx-pending-check".to_string();
357 let tx_hash_bytes = [2u8; 32];
358 if let NetworkTransactionData::Stellar(ref mut stellar_data) = tx_to_handle.network_data
359 {
360 stellar_data.hash = Some(hex::encode(tx_hash_bytes));
361 } else {
362 panic!("Expected Stellar network data");
363 }
364 tx_to_handle.status = TransactionStatus::Submitted; let expected_stellar_hash = soroban_rs::xdr::Hash(tx_hash_bytes);
367
368 mocks
370 .provider
371 .expect_get_transaction()
372 .with(eq(expected_stellar_hash.clone()))
373 .times(1)
374 .returning(move |_| {
375 Box::pin(async { Ok(dummy_get_transaction_response("PENDING")) })
376 });
377
378 mocks.tx_repo.expect_partial_update().never();
380
381 mocks
383 .job_producer
384 .expect_produce_check_transaction_status_job()
385 .withf(move |job, delay| {
386 job.transaction_id == "tx-pending-check" && delay.is_some()
387 })
388 .times(1)
389 .returning(|_, _| Box::pin(async { Ok(()) }));
390
391 mocks
393 .job_producer
394 .expect_produce_send_notification_job()
395 .never();
396
397 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
398 let original_tx_clone = tx_to_handle.clone();
399
400 let result = handler.handle_transaction_status_impl(tx_to_handle).await;
401
402 assert!(result.is_ok());
403 let returned_tx = result.unwrap();
404 assert_eq!(returned_tx.id, original_tx_clone.id);
406 assert_eq!(returned_tx.status, original_tx_clone.status);
407 assert!(returned_tx.confirmed_at.is_none()); }
409
410 #[tokio::test]
411 async fn handle_transaction_status_failed() {
412 let relayer = create_test_relayer();
413 let mut mocks = default_test_mocks();
414
415 let mut tx_to_handle = create_test_transaction(&relayer.id);
416 tx_to_handle.id = "tx-fail-this".to_string();
417 let tx_hash_bytes = [3u8; 32];
418 if let NetworkTransactionData::Stellar(ref mut stellar_data) = tx_to_handle.network_data
419 {
420 stellar_data.hash = Some(hex::encode(tx_hash_bytes));
421 } else {
422 panic!("Expected Stellar network data");
423 }
424 tx_to_handle.status = TransactionStatus::Submitted;
425
426 let expected_stellar_hash = soroban_rs::xdr::Hash(tx_hash_bytes);
427
428 mocks
430 .provider
431 .expect_get_transaction()
432 .with(eq(expected_stellar_hash.clone()))
433 .times(1)
434 .returning(move |_| {
435 Box::pin(async { Ok(dummy_get_transaction_response("FAILED")) })
436 });
437
438 let relayer_id_for_mock = relayer.id.clone();
440 mocks
441 .tx_repo
442 .expect_partial_update()
443 .times(1)
444 .returning(move |id, update| {
445 let mut updated_tx = create_test_transaction(&relayer_id_for_mock);
447 updated_tx.id = id;
448 updated_tx.status = update.status.unwrap();
449 updated_tx.status_reason = update.status_reason.clone();
450 Ok::<_, RepositoryError>(updated_tx)
451 });
452
453 mocks
455 .job_producer
456 .expect_produce_send_notification_job()
457 .times(1)
458 .returning(|_, _| Box::pin(async { Ok(()) }));
459
460 mocks
462 .tx_repo
463 .expect_find_by_status()
464 .with(eq(relayer.id.clone()), eq(vec![TransactionStatus::Pending]))
465 .times(1)
466 .returning(move |_, _| Ok(vec![])); mocks
470 .job_producer
471 .expect_produce_transaction_request_job()
472 .never();
473 mocks
475 .job_producer
476 .expect_produce_check_transaction_status_job()
477 .never();
478
479 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
480 let mut initial_tx_for_handling = create_test_transaction(&relayer.id);
481 initial_tx_for_handling.id = "tx-fail-this".to_string();
482 if let NetworkTransactionData::Stellar(ref mut stellar_data) =
483 initial_tx_for_handling.network_data
484 {
485 stellar_data.hash = Some(hex::encode(tx_hash_bytes));
486 } else {
487 panic!("Expected Stellar network data");
488 }
489 initial_tx_for_handling.status = TransactionStatus::Submitted;
490
491 let result = handler
492 .handle_transaction_status_impl(initial_tx_for_handling)
493 .await;
494
495 assert!(result.is_ok());
496 let handled_tx = result.unwrap();
497 assert_eq!(handled_tx.id, "tx-fail-this");
498 assert_eq!(handled_tx.status, TransactionStatus::Failed);
499 assert!(handled_tx.status_reason.is_some());
500 assert_eq!(
501 handled_tx.status_reason.unwrap(),
502 "Transaction failed on-chain. Provider status: FAILED. No detailed XDR result available."
503 );
504 }
505
506 #[tokio::test]
507 async fn handle_transaction_status_provider_error() {
508 let relayer = create_test_relayer();
509 let mut mocks = default_test_mocks();
510
511 let mut tx_to_handle = create_test_transaction(&relayer.id);
512 tx_to_handle.id = "tx-provider-error".to_string();
513 let tx_hash_bytes = [4u8; 32];
514 if let NetworkTransactionData::Stellar(ref mut stellar_data) = tx_to_handle.network_data
515 {
516 stellar_data.hash = Some(hex::encode(tx_hash_bytes));
517 } else {
518 panic!("Expected Stellar network data");
519 }
520 tx_to_handle.status = TransactionStatus::Submitted;
521
522 let expected_stellar_hash = soroban_rs::xdr::Hash(tx_hash_bytes);
523
524 mocks
526 .provider
527 .expect_get_transaction()
528 .with(eq(expected_stellar_hash.clone()))
529 .times(1)
530 .returning(move |_| Box::pin(async { Err(eyre::eyre!("RPC boom")) }));
531
532 mocks.tx_repo.expect_partial_update().never();
534
535 mocks
537 .job_producer
538 .expect_produce_check_transaction_status_job()
539 .withf(move |job, delay| {
540 job.transaction_id == "tx-provider-error" && delay.is_some()
541 })
542 .times(1)
543 .returning(|_, _| Box::pin(async { Ok(()) }));
544
545 mocks
547 .job_producer
548 .expect_produce_send_notification_job()
549 .never();
550 mocks
552 .job_producer
553 .expect_produce_transaction_request_job()
554 .never();
555
556 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
557 let original_tx_clone = tx_to_handle.clone();
558
559 let result = handler.handle_transaction_status_impl(tx_to_handle).await;
560
561 assert!(result.is_ok()); let returned_tx = result.unwrap();
563 assert_eq!(returned_tx.id, original_tx_clone.id);
565 assert_eq!(returned_tx.status, original_tx_clone.status);
566 }
567
568 #[tokio::test]
569 async fn handle_transaction_status_no_hashes() {
570 let relayer = create_test_relayer();
571 let mut mocks = default_test_mocks(); let mut tx_to_handle = create_test_transaction(&relayer.id);
574 tx_to_handle.id = "tx-no-hashes".to_string();
575 tx_to_handle.status = TransactionStatus::Submitted;
576
577 mocks.provider.expect_get_transaction().never();
578 mocks.tx_repo.expect_partial_update().never();
579 mocks
580 .job_producer
581 .expect_produce_check_transaction_status_job()
582 .never();
583 mocks
584 .job_producer
585 .expect_produce_send_notification_job()
586 .never();
587
588 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
589 let result = handler.handle_transaction_status_impl(tx_to_handle).await;
590
591 assert!(
592 result.is_err(),
593 "Expected an error when hash is missing, but got Ok"
594 );
595 match result.unwrap_err() {
596 TransactionError::ValidationError(msg) => {
597 assert!(
598 msg.contains("Stellar transaction tx-no-hashes is missing or has an empty on-chain hash in network_data"),
599 "Unexpected error message: {}",
600 msg
601 );
602 }
603 other => panic!("Expected ValidationError, got {:?}", other),
604 }
605 }
606
607 #[tokio::test]
608 async fn test_on_chain_failure_does_not_decrement_sequence() {
609 let relayer = create_test_relayer();
610 let mut mocks = default_test_mocks();
611
612 let mut tx_to_handle = create_test_transaction(&relayer.id);
613 tx_to_handle.id = "tx-on-chain-fail".to_string();
614 let tx_hash_bytes = [4u8; 32];
615 if let NetworkTransactionData::Stellar(ref mut stellar_data) = tx_to_handle.network_data
616 {
617 stellar_data.hash = Some(hex::encode(tx_hash_bytes));
618 stellar_data.sequence_number = Some(100); }
620 tx_to_handle.status = TransactionStatus::Submitted;
621
622 let expected_stellar_hash = soroban_rs::xdr::Hash(tx_hash_bytes);
623
624 mocks
626 .provider
627 .expect_get_transaction()
628 .with(eq(expected_stellar_hash.clone()))
629 .times(1)
630 .returning(move |_| {
631 Box::pin(async { Ok(dummy_get_transaction_response("FAILED")) })
632 });
633
634 mocks.counter.expect_decrement().never();
636
637 mocks
639 .tx_repo
640 .expect_partial_update()
641 .times(1)
642 .returning(move |id, update| {
643 let mut updated_tx = create_test_transaction("test");
644 updated_tx.id = id;
645 updated_tx.status = update.status.unwrap();
646 updated_tx.status_reason = update.status_reason.clone();
647 Ok::<_, RepositoryError>(updated_tx)
648 });
649
650 mocks
652 .job_producer
653 .expect_produce_send_notification_job()
654 .times(1)
655 .returning(|_, _| Box::pin(async { Ok(()) }));
656
657 mocks
659 .tx_repo
660 .expect_find_by_status()
661 .returning(move |_, _| Ok(vec![]));
662
663 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
664 let initial_tx = tx_to_handle.clone();
665
666 let result = handler.handle_transaction_status_impl(initial_tx).await;
667
668 assert!(result.is_ok());
669 let handled_tx = result.unwrap();
670 assert_eq!(handled_tx.id, "tx-on-chain-fail");
671 assert_eq!(handled_tx.status, TransactionStatus::Failed);
672 }
673
674 #[tokio::test]
675 async fn test_on_chain_success_does_not_decrement_sequence() {
676 let relayer = create_test_relayer();
677 let mut mocks = default_test_mocks();
678
679 let mut tx_to_handle = create_test_transaction(&relayer.id);
680 tx_to_handle.id = "tx-on-chain-success".to_string();
681 let tx_hash_bytes = [5u8; 32];
682 if let NetworkTransactionData::Stellar(ref mut stellar_data) = tx_to_handle.network_data
683 {
684 stellar_data.hash = Some(hex::encode(tx_hash_bytes));
685 stellar_data.sequence_number = Some(101); }
687 tx_to_handle.status = TransactionStatus::Submitted;
688
689 let expected_stellar_hash = soroban_rs::xdr::Hash(tx_hash_bytes);
690
691 mocks
693 .provider
694 .expect_get_transaction()
695 .with(eq(expected_stellar_hash.clone()))
696 .times(1)
697 .returning(move |_| {
698 Box::pin(async { Ok(dummy_get_transaction_response("SUCCESS")) })
699 });
700
701 mocks.counter.expect_decrement().never();
703
704 mocks
706 .tx_repo
707 .expect_partial_update()
708 .withf(move |id, update| {
709 id == "tx-on-chain-success"
710 && update.status == Some(TransactionStatus::Confirmed)
711 && update.confirmed_at.is_some()
712 })
713 .times(1)
714 .returning(move |id, update| {
715 let mut updated_tx = create_test_transaction("test");
716 updated_tx.id = id;
717 updated_tx.status = update.status.unwrap();
718 updated_tx.confirmed_at = update.confirmed_at;
719 Ok(updated_tx)
720 });
721
722 mocks
724 .job_producer
725 .expect_produce_send_notification_job()
726 .times(1)
727 .returning(|_, _| Box::pin(async { Ok(()) }));
728
729 mocks
731 .tx_repo
732 .expect_find_by_status()
733 .returning(move |_, _| Ok(vec![]));
734
735 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
736 let initial_tx = tx_to_handle.clone();
737
738 let result = handler.handle_transaction_status_impl(initial_tx).await;
739
740 assert!(result.is_ok());
741 let handled_tx = result.unwrap();
742 assert_eq!(handled_tx.id, "tx-on-chain-success");
743 assert_eq!(handled_tx.status, TransactionStatus::Confirmed);
744 }
745
746 #[tokio::test]
747 async fn test_handle_transaction_status_with_xdr_error_requeues() {
748 let relayer = create_test_relayer();
750 let mut mocks = default_test_mocks();
751
752 let mut tx_to_handle = create_test_transaction(&relayer.id);
753 tx_to_handle.id = "tx-xdr-error-requeue".to_string();
754 let tx_hash_bytes = [8u8; 32];
755 if let NetworkTransactionData::Stellar(ref mut stellar_data) = tx_to_handle.network_data
756 {
757 stellar_data.hash = Some(hex::encode(tx_hash_bytes));
758 }
759 tx_to_handle.status = TransactionStatus::Submitted;
760
761 let expected_stellar_hash = soroban_rs::xdr::Hash(tx_hash_bytes);
762
763 mocks
765 .provider
766 .expect_get_transaction()
767 .with(eq(expected_stellar_hash.clone()))
768 .times(1)
769 .returning(move |_| Box::pin(async { Err(eyre::eyre!("Network timeout")) }));
770
771 mocks
773 .job_producer
774 .expect_produce_check_transaction_status_job()
775 .withf(move |job, delay| {
776 job.transaction_id == "tx-xdr-error-requeue" && delay.is_some()
777 })
778 .times(1)
779 .returning(|_, _| Box::pin(async { Ok(()) }));
780
781 mocks.tx_repo.expect_partial_update().never();
783 mocks
784 .job_producer
785 .expect_produce_send_notification_job()
786 .never();
787
788 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
789 let original_tx_clone = tx_to_handle.clone();
790
791 let result = handler.handle_transaction_status_impl(tx_to_handle).await;
792
793 assert!(result.is_ok()); let returned_tx = result.unwrap();
795 assert_eq!(returned_tx.id, original_tx_clone.id);
797 assert_eq!(returned_tx.status, original_tx_clone.status);
798 }
799 }
800}