1use chrono::Utc;
6use tracing::{info, warn};
7
8use super::{is_final_state, utils::is_bad_sequence_error, StellarRelayerTransaction};
9use crate::{
10 jobs::JobProducerTrait,
11 models::{
12 NetworkTransactionData, RelayerRepoModel, TransactionError, TransactionRepoModel,
13 TransactionStatus, TransactionUpdateRequest,
14 },
15 repositories::{Repository, TransactionCounterTrait, TransactionRepository},
16 services::{
17 provider::StellarProviderTrait,
18 signer::{Signer, StellarSignTrait},
19 },
20};
21
22impl<R, T, J, S, P, C, D> StellarRelayerTransaction<R, T, J, S, P, C, D>
23where
24 R: Repository<RelayerRepoModel, String> + Send + Sync,
25 T: TransactionRepository + Send + Sync,
26 J: JobProducerTrait + Send + Sync,
27 S: Signer + StellarSignTrait + Send + Sync,
28 P: StellarProviderTrait + Send + Sync,
29 C: TransactionCounterTrait + Send + Sync,
30 D: crate::services::stellar_dex::StellarDexServiceTrait + Send + Sync + 'static,
31{
32 pub async fn submit_transaction_impl(
35 &self,
36 tx: TransactionRepoModel,
37 ) -> Result<TransactionRepoModel, TransactionError> {
38 info!(
39 tx_id = %tx.id,
40 relayer_id = %tx.relayer_id,
41 status = ?tx.status,
42 "submitting stellar transaction"
43 );
44
45 if is_final_state(&tx.status) {
47 warn!(
48 tx_id = %tx.id,
49 relayer_id = %tx.relayer_id,
50 status = ?tx.status,
51 "transaction already in final state, skipping submission"
52 );
53 return Ok(tx);
54 }
55
56 if self.is_transaction_expired(&tx)? {
58 info!(
59 tx_id = %tx.id,
60 relayer_id = %tx.relayer_id,
61 valid_until = ?tx.valid_until,
62 "transaction has expired, marking as Expired"
63 );
64 return self
65 .mark_as_expired(tx, "Transaction time_bounds expired".to_string())
66 .await;
67 }
68
69 match self.submit_core(tx.clone()).await {
71 Ok(submitted_tx) => Ok(submitted_tx),
72 Err(error) => {
73 self.handle_submit_failure(tx, error).await
75 }
76 }
77 }
78
79 async fn submit_core(
88 &self,
89 tx: TransactionRepoModel,
90 ) -> Result<TransactionRepoModel, TransactionError> {
91 let stellar_data = tx.network_data.get_stellar_transaction_data()?;
92 let tx_envelope = stellar_data
93 .get_envelope_for_submission()
94 .map_err(TransactionError::from)?;
95
96 let response = self
98 .provider()
99 .send_transaction_with_status(&tx_envelope)
100 .await
101 .map_err(TransactionError::from)?;
102
103 match response.status.as_str() {
105 "PENDING" | "DUPLICATE" => {
106 if response.status == "DUPLICATE" {
108 info!(
109 tx_id = %tx.id,
110 relayer_id = %tx.relayer_id,
111 hash = %response.hash,
112 "transaction already submitted (DUPLICATE status)"
113 );
114 }
115
116 let tx_hash_hex = response.hash.clone();
117 let updated_stellar_data = stellar_data.with_hash(tx_hash_hex.clone());
118
119 let mut hashes = tx.hashes.clone();
120 if !hashes.contains(&tx_hash_hex) {
121 hashes.push(tx_hash_hex);
122 }
123
124 let update_req = TransactionUpdateRequest {
125 status: Some(TransactionStatus::Submitted),
126 sent_at: Some(Utc::now().to_rfc3339()),
127 network_data: Some(NetworkTransactionData::Stellar(updated_stellar_data)),
128 hashes: Some(hashes),
129 ..Default::default()
130 };
131
132 let updated_tx = self
133 .transaction_repository()
134 .partial_update(tx.id.clone(), update_req)
135 .await?;
136
137 if response.status == "PENDING" {
139 info!(
140 tx_id = %tx.id,
141 relayer_id = %tx.relayer_id,
142 "sending transaction update notification for pending transaction"
143 );
144 self.send_transaction_update_notification(&updated_tx).await;
145 }
146
147 Ok(updated_tx)
148 }
149 "TRY_AGAIN_LATER" => {
150 Err(TransactionError::UnexpectedError(
152 "Transaction not queued: TRY_AGAIN_LATER".to_string(),
153 ))
154 }
155 "ERROR" => {
156 let error_detail = response
158 .error_result_xdr
159 .unwrap_or_else(|| "No error details provided".to_string());
160 Err(TransactionError::UnexpectedError(format!(
161 "Transaction submission error: {error_detail}"
162 )))
163 }
164 unknown => {
165 warn!(
167 tx_id = %tx.id,
168 relayer_id = %tx.relayer_id,
169 status = %unknown,
170 "received unknown transaction status from RPC"
171 );
172 Err(TransactionError::UnexpectedError(format!(
173 "Unknown transaction status: {unknown}"
174 )))
175 }
176 }
177 }
178
179 async fn handle_submit_failure(
182 &self,
183 tx: TransactionRepoModel,
184 error: TransactionError,
185 ) -> Result<TransactionRepoModel, TransactionError> {
186 let error_reason = format!("Submission failed: {error}");
187 let tx_id = tx.id.clone();
188 let relayer_id = tx.relayer_id.clone();
189 warn!(
190 tx_id = %tx_id,
191 relayer_id = %relayer_id,
192 reason = %error_reason,
193 "transaction submission failed"
194 );
195
196 if is_bad_sequence_error(&error_reason) {
197 if let Ok(stellar_data) = tx.network_data.get_stellar_transaction_data() {
199 info!(
200 tx_id = %tx_id,
201 relayer_id = %relayer_id,
202 "syncing sequence from chain after bad sequence error"
203 );
204 match self
205 .sync_sequence_from_chain(&stellar_data.source_account)
206 .await
207 {
208 Ok(()) => {
209 info!(
210 tx_id = %tx_id,
211 relayer_id = %relayer_id,
212 "successfully synced sequence from chain"
213 );
214 }
215 Err(sync_error) => {
216 warn!(
217 tx_id = %tx_id,
218 relayer_id = %relayer_id,
219 error = %sync_error,
220 "failed to sync sequence from chain"
221 );
222 }
223 }
224 }
225
226 info!(
229 tx_id = %tx_id,
230 relayer_id = %relayer_id,
231 "bad sequence error detected, resetting transaction to pending state"
232 );
233 match self.reset_transaction_for_retry(tx.clone()).await {
234 Ok(reset_tx) => {
235 info!(
236 tx_id = %tx_id,
237 relayer_id = %relayer_id,
238 "transaction reset to pending, status check will handle resubmission"
239 );
240 return Ok(reset_tx);
244 }
245 Err(reset_error) => {
246 warn!(
247 tx_id = %tx_id,
248 relayer_id = %relayer_id,
249 error = %reset_error,
250 "failed to reset transaction for retry"
251 );
252 }
254 }
255 }
256
257 let update_request = TransactionUpdateRequest {
260 status: Some(TransactionStatus::Failed),
261 status_reason: Some(error_reason.clone()),
262 ..Default::default()
263 };
264 let failed_tx = match self
265 .finalize_transaction_state(tx_id.clone(), update_request)
266 .await
267 {
268 Ok(updated_tx) => updated_tx,
269 Err(finalize_error) => {
270 warn!(
271 tx_id = %tx_id,
272 relayer_id = %relayer_id,
273 error = %finalize_error,
274 "failed to mark transaction as failed, continuing with lane cleanup"
275 );
276 return Err(error);
279 }
280 };
281
282 if let Err(enqueue_error) = self.enqueue_next_pending_transaction(&tx_id).await {
284 warn!(
285 tx_id = %tx_id,
286 relayer_id = %relayer_id,
287 error = %enqueue_error,
288 "failed to enqueue next pending transaction after submission failure"
289 );
290 }
291
292 info!(
293 tx_id = %tx_id,
294 relayer_id = %relayer_id,
295 error = %error_reason,
296 "transaction submission failure handled, marked as failed"
297 );
298
299 Ok(failed_tx)
303 }
304
305 pub async fn resubmit_transaction_impl(
307 &self,
308 tx: TransactionRepoModel,
309 ) -> Result<TransactionRepoModel, TransactionError> {
310 self.submit_transaction_impl(tx).await
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use soroban_rs::stellar_rpc_client::SendTransactionResponse;
318 use soroban_rs::xdr::WriteXdr;
319
320 use crate::domain::transaction::stellar::test_helpers::*;
321
322 fn create_send_tx_response(status: &str, hash: &str) -> SendTransactionResponse {
324 SendTransactionResponse {
325 status: status.to_string(),
326 hash: hash.to_string(),
327 error_result_xdr: None,
328 latest_ledger: 100,
329 latest_ledger_close_time: 1700000000,
330 }
331 }
332
333 mod submit_transaction_tests {
334 use crate::{
335 models::RepositoryError, repositories::PaginatedResult,
336 services::provider::ProviderError,
337 };
338
339 use super::*;
340
341 #[tokio::test]
342 async fn submit_transaction_happy_path() {
343 let relayer = create_test_relayer();
344 let mut mocks = default_test_mocks();
345
346 let response = create_send_tx_response(
348 "PENDING",
349 "0101010101010101010101010101010101010101010101010101010101010101",
350 );
351 mocks
352 .provider
353 .expect_send_transaction_with_status()
354 .returning(move |_| {
355 let r = response.clone();
356 Box::pin(async move { Ok(r) })
357 });
358
359 mocks
361 .tx_repo
362 .expect_partial_update()
363 .withf(|_, upd| upd.status == Some(TransactionStatus::Submitted))
364 .returning(|id, upd| {
365 let mut tx = create_test_transaction("relayer-1");
366 tx.id = id;
367 tx.status = upd.status.unwrap();
368 Ok::<_, RepositoryError>(tx)
369 });
370
371 mocks
373 .job_producer
374 .expect_produce_send_notification_job()
375 .times(1)
376 .returning(|_, _| Box::pin(async { Ok(()) }));
377
378 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
379
380 let mut tx = create_test_transaction(&relayer.id);
381 tx.status = TransactionStatus::Sent; if let NetworkTransactionData::Stellar(ref mut d) = tx.network_data {
383 d.signatures.push(dummy_signature());
384 d.signed_envelope_xdr = Some(create_signed_xdr(TEST_PK, TEST_PK_2));
385 }
387
388 let res = handler.submit_transaction_impl(tx).await.unwrap();
389 assert_eq!(res.status, TransactionStatus::Submitted);
390 }
391
392 #[tokio::test]
393 async fn submit_transaction_provider_error_marks_failed() {
394 let relayer = create_test_relayer();
395 let mut mocks = default_test_mocks();
396
397 mocks
399 .provider
400 .expect_send_transaction_with_status()
401 .returning(|_| {
402 Box::pin(async { Err(ProviderError::Other("Network error".to_string())) })
403 });
404
405 mocks
407 .tx_repo
408 .expect_partial_update()
409 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
410 .returning(|id, upd| {
411 let mut tx = create_test_transaction("relayer-1");
412 tx.id = id;
413 tx.status = upd.status.unwrap();
414 Ok::<_, RepositoryError>(tx)
415 });
416
417 mocks
419 .job_producer
420 .expect_produce_send_notification_job()
421 .times(1)
422 .returning(|_, _| Box::pin(async { Ok(()) }));
423
424 mocks
426 .tx_repo
427 .expect_find_by_status_paginated()
428 .returning(move |_, _, _, _| {
429 Ok(PaginatedResult {
430 items: vec![],
431 total: 0,
432 page: 1,
433 per_page: 1,
434 })
435 }); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
438 let mut tx = create_test_transaction(&relayer.id);
439 tx.status = TransactionStatus::Sent; if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
441 data.signatures.push(dummy_signature());
442 data.sequence_number = Some(42); data.signed_envelope_xdr = Some("test-xdr".to_string()); }
445
446 let res = handler.submit_transaction_impl(tx).await;
447
448 let failed_tx = res.unwrap();
450 assert_eq!(failed_tx.status, TransactionStatus::Failed);
451 }
452
453 #[tokio::test]
454 async fn submit_transaction_repository_error_marks_failed() {
455 let relayer = create_test_relayer();
456 let mut mocks = default_test_mocks();
457
458 let response = create_send_tx_response(
460 "PENDING",
461 "0101010101010101010101010101010101010101010101010101010101010101",
462 );
463 mocks
464 .provider
465 .expect_send_transaction_with_status()
466 .returning(move |_| {
467 let r = response.clone();
468 Box::pin(async move { Ok(r) })
469 });
470
471 mocks
473 .tx_repo
474 .expect_partial_update()
475 .withf(|_, upd| upd.status == Some(TransactionStatus::Submitted))
476 .returning(|_, _| Err(RepositoryError::Unknown("Database error".to_string())));
477
478 mocks
480 .tx_repo
481 .expect_partial_update()
482 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
483 .returning(|id, upd| {
484 let mut tx = create_test_transaction("relayer-1");
485 tx.id = id;
486 tx.status = upd.status.unwrap();
487 Ok::<_, RepositoryError>(tx)
488 });
489
490 mocks
492 .job_producer
493 .expect_produce_send_notification_job()
494 .times(1)
495 .returning(|_, _| Box::pin(async { Ok(()) }));
496
497 mocks
499 .tx_repo
500 .expect_find_by_status_paginated()
501 .returning(move |_, _, _, _| {
502 Ok(PaginatedResult {
503 items: vec![],
504 total: 0,
505 page: 1,
506 per_page: 1,
507 })
508 }); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
511 let mut tx = create_test_transaction(&relayer.id);
512 tx.status = TransactionStatus::Sent; if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
514 data.signatures.push(dummy_signature());
515 data.sequence_number = Some(42); data.signed_envelope_xdr = Some("test-xdr".to_string()); }
518
519 let res = handler.submit_transaction_impl(tx).await;
520
521 let failed_tx = res.unwrap();
524 assert_eq!(failed_tx.status, TransactionStatus::Failed);
525 }
526
527 #[tokio::test]
528 async fn submit_transaction_uses_signed_envelope_xdr() {
529 let relayer = create_test_relayer();
530 let mut mocks = default_test_mocks();
531
532 let mut tx = create_test_transaction(&relayer.id);
534 tx.status = TransactionStatus::Sent; if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
536 data.signatures.push(dummy_signature());
537 let envelope = data.get_envelope_for_submission().unwrap();
539 let xdr = envelope
540 .to_xdr_base64(soroban_rs::xdr::Limits::none())
541 .unwrap();
542 data.signed_envelope_xdr = Some(xdr);
543 }
544
545 let response = create_send_tx_response(
547 "PENDING",
548 "0202020202020202020202020202020202020202020202020202020202020202",
549 );
550 mocks
551 .provider
552 .expect_send_transaction_with_status()
553 .returning(move |_| {
554 let r = response.clone();
555 Box::pin(async move { Ok(r) })
556 });
557
558 mocks
560 .tx_repo
561 .expect_partial_update()
562 .withf(|_, upd| upd.status == Some(TransactionStatus::Submitted))
563 .returning(|id, upd| {
564 let mut tx = create_test_transaction("relayer-1");
565 tx.id = id;
566 tx.status = upd.status.unwrap();
567 Ok::<_, RepositoryError>(tx)
568 });
569
570 mocks
572 .job_producer
573 .expect_produce_send_notification_job()
574 .times(1)
575 .returning(|_, _| Box::pin(async { Ok(()) }));
576
577 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
578 let res = handler.submit_transaction_impl(tx).await.unwrap();
579
580 assert_eq!(res.status, TransactionStatus::Submitted);
581 }
582
583 #[tokio::test]
584 async fn resubmit_transaction_delegates_to_submit() {
585 let relayer = create_test_relayer();
586 let mut mocks = default_test_mocks();
587
588 let response = create_send_tx_response(
590 "PENDING",
591 "0101010101010101010101010101010101010101010101010101010101010101",
592 );
593 mocks
594 .provider
595 .expect_send_transaction_with_status()
596 .returning(move |_| {
597 let r = response.clone();
598 Box::pin(async move { Ok(r) })
599 });
600
601 mocks
603 .tx_repo
604 .expect_partial_update()
605 .withf(|_, upd| upd.status == Some(TransactionStatus::Submitted))
606 .returning(|id, upd| {
607 let mut tx = create_test_transaction("relayer-1");
608 tx.id = id;
609 tx.status = upd.status.unwrap();
610 Ok::<_, RepositoryError>(tx)
611 });
612
613 mocks
615 .job_producer
616 .expect_produce_send_notification_job()
617 .times(1)
618 .returning(|_, _| Box::pin(async { Ok(()) }));
619
620 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
621
622 let mut tx = create_test_transaction(&relayer.id);
623 tx.status = TransactionStatus::Sent; if let NetworkTransactionData::Stellar(ref mut d) = tx.network_data {
625 d.signatures.push(dummy_signature());
626 d.signed_envelope_xdr = Some(create_signed_xdr(TEST_PK, TEST_PK_2));
627 }
629
630 let res = handler.resubmit_transaction_impl(tx).await.unwrap();
631 assert_eq!(res.status, TransactionStatus::Submitted);
632 }
633
634 #[tokio::test]
635 async fn submit_transaction_failure_enqueues_next_transaction() {
636 let relayer = create_test_relayer();
637 let mut mocks = default_test_mocks();
638
639 mocks
641 .provider
642 .expect_send_transaction_with_status()
643 .returning(|_| {
644 Box::pin(async { Err(ProviderError::Other("Network error".to_string())) })
645 });
646
647 mocks
651 .tx_repo
652 .expect_partial_update()
653 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
654 .returning(|id, upd| {
655 let mut tx = create_test_transaction("relayer-1");
656 tx.id = id;
657 tx.status = upd.status.unwrap();
658 Ok::<_, RepositoryError>(tx)
659 });
660
661 mocks
663 .job_producer
664 .expect_produce_send_notification_job()
665 .times(1)
666 .returning(|_, _| Box::pin(async { Ok(()) }));
667
668 let mut pending_tx = create_test_transaction(&relayer.id);
670 pending_tx.id = "next-pending-tx".to_string();
671 pending_tx.status = TransactionStatus::Pending;
672 let captured_pending_tx = pending_tx.clone();
673 let relayer_id_clone = relayer.id.clone();
674 mocks
675 .tx_repo
676 .expect_find_by_status_paginated()
677 .withf(move |relayer_id, statuses, query, oldest_first| {
678 *relayer_id == relayer_id_clone
679 && statuses == [TransactionStatus::Pending]
680 && query.page == 1
681 && query.per_page == 1
682 && *oldest_first
683 })
684 .times(1)
685 .returning(move |_, _, _, _| {
686 Ok(PaginatedResult {
687 items: vec![captured_pending_tx.clone()],
688 total: 1,
689 page: 1,
690 per_page: 1,
691 })
692 });
693
694 mocks
696 .job_producer
697 .expect_produce_transaction_request_job()
698 .withf(move |job, _delay| job.transaction_id == "next-pending-tx")
699 .times(1)
700 .returning(|_, _| Box::pin(async { Ok(()) }));
701
702 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
703 let mut tx = create_test_transaction(&relayer.id);
704 tx.status = TransactionStatus::Sent; if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
706 data.signatures.push(dummy_signature());
707 data.sequence_number = Some(42); data.signed_envelope_xdr = Some("test-xdr".to_string()); }
710
711 let res = handler.submit_transaction_impl(tx).await;
712
713 let failed_tx = res.unwrap();
715 assert_eq!(failed_tx.status, TransactionStatus::Failed);
716 }
717
718 #[tokio::test]
719 async fn test_submit_bad_sequence_resets_and_retries() {
720 let relayer = create_test_relayer();
721 let mut mocks = default_test_mocks();
722
723 mocks
725 .provider
726 .expect_send_transaction_with_status()
727 .returning(|_| {
728 Box::pin(async {
729 Err(ProviderError::Other(
730 "transaction submission failed: TxBadSeq".to_string(),
731 ))
732 })
733 });
734
735 mocks.provider.expect_get_account().times(1).returning(|_| {
737 Box::pin(async {
738 use soroban_rs::xdr::{
739 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber,
740 String32, Thresholds, Uint256,
741 };
742 use stellar_strkey::ed25519;
743
744 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
745 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
746
747 Ok(AccountEntry {
748 account_id,
749 balance: 1000000,
750 seq_num: SequenceNumber(100),
751 num_sub_entries: 0,
752 inflation_dest: None,
753 flags: 0,
754 home_domain: String32::default(),
755 thresholds: Thresholds([1, 1, 1, 1]),
756 signers: Default::default(),
757 ext: AccountEntryExt::V0,
758 })
759 })
760 });
761
762 mocks
764 .counter
765 .expect_set()
766 .times(1)
767 .returning(|_, _, _| Box::pin(async { Ok(()) }));
768
769 mocks
771 .tx_repo
772 .expect_partial_update()
773 .withf(|_, upd| upd.status == Some(TransactionStatus::Pending))
774 .times(1)
775 .returning(|id, upd| {
776 let mut tx = create_test_transaction("relayer-1");
777 tx.id = id;
778 tx.status = upd.status.unwrap();
779 if let Some(network_data) = upd.network_data {
780 tx.network_data = network_data;
781 }
782 Ok::<_, RepositoryError>(tx)
783 });
784
785 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
789 let mut tx = create_test_transaction(&relayer.id);
790 tx.status = TransactionStatus::Sent; if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
792 data.signatures.push(dummy_signature());
793 data.sequence_number = Some(42);
794 data.signed_envelope_xdr = Some(create_signed_xdr(TEST_PK, TEST_PK_2));
795 }
797
798 let result = handler.submit_transaction_impl(tx).await;
799
800 assert!(result.is_ok());
802 let reset_tx = result.unwrap();
803 assert_eq!(reset_tx.status, TransactionStatus::Pending);
804
805 if let NetworkTransactionData::Stellar(data) = &reset_tx.network_data {
807 assert!(data.sequence_number.is_none());
808 assert!(data.signatures.is_empty());
809 assert!(data.hash.is_none());
810 assert!(data.signed_envelope_xdr.is_none());
811 } else {
812 panic!("Expected Stellar transaction data");
813 }
814 }
815
816 #[tokio::test]
817 async fn submit_transaction_duplicate_status_succeeds() {
818 let relayer = create_test_relayer();
819 let mut mocks = default_test_mocks();
820
821 let response = create_send_tx_response(
823 "DUPLICATE",
824 "0101010101010101010101010101010101010101010101010101010101010101",
825 );
826 mocks
827 .provider
828 .expect_send_transaction_with_status()
829 .returning(move |_| {
830 let r = response.clone();
831 Box::pin(async move { Ok(r) })
832 });
833
834 mocks
836 .tx_repo
837 .expect_partial_update()
838 .withf(|_, upd| upd.status == Some(TransactionStatus::Submitted))
839 .returning(|id, upd| {
840 let mut tx = create_test_transaction("relayer-1");
841 tx.id = id;
842 tx.status = upd.status.unwrap();
843 Ok::<_, RepositoryError>(tx)
844 });
845
846 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
847
848 let mut tx = create_test_transaction(&relayer.id);
849 tx.status = TransactionStatus::Sent;
850 if let NetworkTransactionData::Stellar(ref mut d) = tx.network_data {
851 d.signatures.push(dummy_signature());
852 d.signed_envelope_xdr = Some(create_signed_xdr(TEST_PK, TEST_PK_2));
853 }
854
855 let res = handler.submit_transaction_impl(tx).await.unwrap();
856 assert_eq!(res.status, TransactionStatus::Submitted);
857 }
858
859 #[tokio::test]
860 async fn submit_transaction_try_again_later_fails() {
861 let relayer = create_test_relayer();
862 let mut mocks = default_test_mocks();
863
864 let response = create_send_tx_response(
866 "TRY_AGAIN_LATER",
867 "0101010101010101010101010101010101010101010101010101010101010101",
868 );
869 mocks
870 .provider
871 .expect_send_transaction_with_status()
872 .returning(move |_| {
873 let r = response.clone();
874 Box::pin(async move { Ok(r) })
875 });
876
877 mocks
879 .tx_repo
880 .expect_partial_update()
881 .withf(|_, upd| {
882 upd.status == Some(TransactionStatus::Failed)
883 && upd
884 .status_reason
885 .as_ref()
886 .is_some_and(|r| r.contains("TRY_AGAIN_LATER"))
887 })
888 .returning(|id, upd| {
889 let mut tx = create_test_transaction("relayer-1");
890 tx.id = id;
891 tx.status = upd.status.unwrap();
892 Ok::<_, RepositoryError>(tx)
893 });
894
895 mocks
897 .job_producer
898 .expect_produce_send_notification_job()
899 .times(1)
900 .returning(|_, _| Box::pin(async { Ok(()) }));
901
902 mocks
904 .tx_repo
905 .expect_find_by_status_paginated()
906 .returning(move |_, _, _, _| {
907 Ok(PaginatedResult {
908 items: vec![],
909 total: 0,
910 page: 1,
911 per_page: 1,
912 })
913 });
914
915 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
916 let mut tx = create_test_transaction(&relayer.id);
917 tx.status = TransactionStatus::Sent;
918 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
919 data.signatures.push(dummy_signature());
920 data.signed_envelope_xdr = Some(create_signed_xdr(TEST_PK, TEST_PK_2));
921 }
922
923 let res = handler.submit_transaction_impl(tx).await;
924
925 let failed_tx = res.unwrap();
927 assert_eq!(failed_tx.status, TransactionStatus::Failed);
928 }
929
930 #[tokio::test]
931 async fn submit_transaction_error_status_fails() {
932 let relayer = create_test_relayer();
933 let mut mocks = default_test_mocks();
934
935 let mut response = create_send_tx_response(
937 "ERROR",
938 "0101010101010101010101010101010101010101010101010101010101010101",
939 );
940 response.error_result_xdr = Some("AAAAAAAAAGT////7AAAAAA==".to_string());
941 mocks
942 .provider
943 .expect_send_transaction_with_status()
944 .returning(move |_| {
945 let r = response.clone();
946 Box::pin(async move { Ok(r) })
947 });
948
949 mocks
951 .tx_repo
952 .expect_partial_update()
953 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
954 .returning(|id, upd| {
955 let mut tx = create_test_transaction("relayer-1");
956 tx.id = id;
957 tx.status = upd.status.unwrap();
958 Ok::<_, RepositoryError>(tx)
959 });
960
961 mocks
963 .job_producer
964 .expect_produce_send_notification_job()
965 .times(1)
966 .returning(|_, _| Box::pin(async { Ok(()) }));
967
968 mocks
970 .tx_repo
971 .expect_find_by_status_paginated()
972 .returning(move |_, _, _, _| {
973 Ok(PaginatedResult {
974 items: vec![],
975 total: 0,
976 page: 1,
977 per_page: 1,
978 })
979 });
980
981 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
982 let mut tx = create_test_transaction(&relayer.id);
983 tx.status = TransactionStatus::Sent;
984 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
985 data.signatures.push(dummy_signature());
986 data.signed_envelope_xdr = Some(create_signed_xdr(TEST_PK, TEST_PK_2));
987 }
988
989 let res = handler.submit_transaction_impl(tx).await;
990
991 let failed_tx = res.unwrap();
993 assert_eq!(failed_tx.status, TransactionStatus::Failed);
994 }
995 }
996}