1pub mod common;
7pub mod fee_bump;
8pub mod operations;
9pub mod soroban_gas_abstraction;
10pub mod unsigned_xdr;
11
12use eyre::Result;
13use tracing::{debug, info, warn};
14
15use super::{is_final_state, lane_gate, StellarRelayerTransaction};
16use crate::models::RelayerRepoModel;
17use crate::{
18 jobs::JobProducerTrait,
19 models::{
20 TransactionError, TransactionInput, TransactionRepoModel, TransactionStatus,
21 TransactionUpdateRequest,
22 },
23 repositories::{Repository, TransactionCounterTrait, TransactionRepository},
24 services::{
25 provider::StellarProviderTrait,
26 signer::{Signer, StellarSignTrait},
27 },
28};
29
30use common::{sign_and_finalize_transaction, update_and_notify_transaction};
31
32impl<R, T, J, S, P, C, D> StellarRelayerTransaction<R, T, J, S, P, C, D>
33where
34 R: Repository<RelayerRepoModel, String> + Send + Sync,
35 T: TransactionRepository + Send + Sync,
36 J: JobProducerTrait + Send + Sync,
37 S: Signer + StellarSignTrait + Send + Sync,
38 P: StellarProviderTrait + Send + Sync,
39 C: TransactionCounterTrait + Send + Sync,
40 D: crate::services::stellar_dex::StellarDexServiceTrait + Send + Sync + 'static,
41{
42 pub async fn prepare_transaction_impl(
44 &self,
45 tx: TransactionRepoModel,
46 ) -> Result<TransactionRepoModel, TransactionError> {
47 debug!(
48 tx_id = %tx.id,
49 relayer_id = %tx.relayer_id,
50 status = ?tx.status,
51 "preparing stellar transaction"
52 );
53
54 if is_final_state(&tx.status) {
56 warn!(
57 tx_id = %tx.id,
58 status = ?tx.status,
59 "transaction already in final state, skipping preparation"
60 );
61 return Ok(tx);
62 }
63
64 if tx.status != TransactionStatus::Pending {
65 debug!(
66 tx_id = %tx.id,
67 status = ?tx.status,
68 expected_status = ?TransactionStatus::Pending,
69 "transaction in unexpected state for preparation, skipping"
70 );
71 return Ok(tx);
72 }
73
74 if !self.concurrent_transactions_enabled() && !lane_gate::claim(&self.relayer().id, &tx.id)
75 {
76 info!(
77 tx_id = %tx.id,
78 relayer_id = %tx.relayer_id,
79 "relayer already has a transaction in flight, must wait"
80 );
81 return Ok(tx);
82 }
83
84 debug!(
85 tx_id = %tx.id,
86 relayer_id = %tx.relayer_id,
87 "preparing transaction"
88 );
89
90 match self.prepare_core(tx.clone()).await {
92 Ok(prepared_tx) => Ok(prepared_tx),
93 Err(error) => {
94 warn!(
96 tx_id = %tx.id,
97 error = %error,
98 "preparation error caught, calling handle_prepare_failure"
99 );
100 self.handle_prepare_failure(tx, error).await
101 }
102 }
103 }
104
105 async fn prepare_core(
107 &self,
108 tx: TransactionRepoModel,
109 ) -> Result<TransactionRepoModel, TransactionError> {
110 let stellar_data = tx.network_data.get_stellar_transaction_data()?;
111
112 let policy = self.relayer().policies.get_stellar_policy();
114 match &stellar_data.transaction_input {
115 TransactionInput::Operations(_) => {
116 debug!(
117 tx_id = %tx.id,
118 relayer_id = %tx.relayer_id,
119 "preparing operations-based transaction"
120 );
121 let stellar_data_with_sim = operations::process_operations(
122 self.transaction_counter_service(),
123 &self.relayer().id,
124 &self.relayer().address,
125 &tx,
126 stellar_data,
127 self.provider(),
128 self.signer(),
129 Some(&policy),
130 )
131 .await?;
132 self.finalize_with_signature(tx, stellar_data_with_sim)
133 .await
134 }
135 TransactionInput::UnsignedXdr(_) => {
136 debug!(
137 tx_id = %tx.id,
138 relayer_id = %tx.relayer_id,
139 "preparing unsigned xdr transaction"
140 );
141 let stellar_data_with_sim = unsigned_xdr::process_unsigned_xdr(
142 self.transaction_counter_service(),
143 &self.relayer().id,
144 &self.relayer().address,
145 stellar_data,
146 self.provider(),
147 self.signer(),
148 Some(&policy),
149 self.dex_service(),
150 )
151 .await?;
152 self.finalize_with_signature(tx, stellar_data_with_sim)
153 .await
154 }
155 TransactionInput::SignedXdr { .. } => {
156 debug!(tx_id = %tx.id, "preparing fee-bump transaction");
157 let stellar_data_with_fee_bump = fee_bump::process_fee_bump(
158 &self.relayer().address,
159 stellar_data,
160 self.provider(),
161 self.signer(),
162 Some(&policy),
163 self.dex_service(),
164 )
165 .await?;
166 update_and_notify_transaction(
167 self.transaction_repository(),
168 self.job_producer(),
169 tx.id,
170 stellar_data_with_fee_bump,
171 self.relayer().notification_id.as_deref(),
172 )
173 .await
174 }
175 TransactionInput::SorobanGasAbstraction { .. } => {
176 debug!(tx_id = %tx.id, "preparing soroban gas abstraction transaction");
177 let stellar_data_with_auth =
178 soroban_gas_abstraction::process_soroban_gas_abstraction(
179 self.transaction_counter_service(),
180 &self.relayer().id,
181 &self.relayer().address,
182 self.provider(),
183 stellar_data,
184 Some(&policy),
185 self.dex_service(),
186 )
187 .await?;
188 self.finalize_with_signature(tx, stellar_data_with_auth)
189 .await
190 }
191 }
192 }
193
194 async fn finalize_with_signature(
196 &self,
197 tx: TransactionRepoModel,
198 stellar_data: crate::models::StellarTransactionData,
199 ) -> Result<TransactionRepoModel, TransactionError> {
200 let (tx, final_stellar_data) =
201 sign_and_finalize_transaction(self.signer(), tx, stellar_data).await?;
202 update_and_notify_transaction(
203 self.transaction_repository(),
204 self.job_producer(),
205 tx.id,
206 final_stellar_data,
207 self.relayer().notification_id.as_deref(),
208 )
209 .await
210 }
211
212 async fn handle_prepare_failure(
215 &self,
216 tx: TransactionRepoModel,
217 error: TransactionError,
218 ) -> Result<TransactionRepoModel, TransactionError> {
219 let error_reason = format!("Preparation failed: {error}");
220 let tx_id = tx.id.clone(); warn!(reason = %error_reason, "transaction preparation failed");
222
223 if let Ok(stellar_data) = tx.network_data.get_stellar_transaction_data() {
225 info!(
226 tx_id = %tx_id,
227 source_account = %stellar_data.source_account,
228 "syncing sequence from chain after failed transaction preparation"
229 );
230 match self
232 .sync_sequence_from_chain(&stellar_data.source_account)
233 .await
234 {
235 Ok(()) => {
236 info!(tx_id = %tx_id, "successfully synced sequence from chain");
237 }
238 Err(sync_error) => {
239 warn!(
240 tx_id = %tx_id,
241 error = %sync_error,
242 "failed to sync sequence from chain (non-fatal, transaction already marked as failed)"
243 );
244 }
245 }
246 }
247
248 let update_request = TransactionUpdateRequest {
250 status: Some(TransactionStatus::Failed),
251 status_reason: Some(error_reason.clone()),
252 ..Default::default()
253 };
254 let _failed_tx = match self
255 .finalize_transaction_state(tx_id.clone(), update_request)
256 .await
257 {
258 Ok(updated_tx) => updated_tx,
259 Err(finalize_error) => {
260 warn!(error = %finalize_error, "failed to mark transaction as failed, proceeding with lane cleanup");
261 tx
263 }
264 };
265
266 if !self.concurrent_transactions_enabled() {
268 if let Err(enqueue_error) = self.enqueue_next_pending_transaction(&tx_id).await {
270 warn!(error = %enqueue_error, "failed to enqueue next pending transaction after failure, releasing lane directly");
271 lane_gate::free(&self.relayer().id, &tx_id);
273 }
274 }
275
276 info!(error = %error_reason, "transaction preparation failure handled, lane cleaned up");
278
279 Err(error)
281 }
282}
283
284#[cfg(test)]
285mod prepare_transaction_tests {
286 use std::future::ready;
287
288 use super::*;
289 use crate::{
290 domain::SignTransactionResponse,
291 models::{NetworkTransactionData, OperationSpec, RepositoryError, TransactionStatus},
292 repositories::PaginatedResult,
293 services::provider::ProviderError,
294 };
295 use soroban_rs::xdr::{Limits, ReadXdr, TransactionEnvelope};
296
297 use crate::domain::transaction::stellar::test_helpers::*;
298
299 #[tokio::test]
300 async fn prepare_transaction_happy_path() {
301 let relayer = create_test_relayer();
302 let mut mocks = default_test_mocks();
303
304 mocks
306 .counter
307 .expect_get_and_increment()
308 .returning(|_, _| Box::pin(ready(Ok(1))));
309
310 mocks.signer.expect_sign_transaction().returning(|_| {
312 Box::pin(async {
313 Ok(SignTransactionResponse::Stellar(
314 crate::domain::SignTransactionResponseStellar {
315 signature: dummy_signature(),
316 },
317 ))
318 })
319 });
320
321 mocks
322 .tx_repo
323 .expect_partial_update()
324 .withf(|_, upd| {
325 upd.status == Some(TransactionStatus::Sent) && upd.network_data.is_some()
326 })
327 .returning(|id, upd| {
328 let mut tx = create_test_transaction("relayer-1");
329 tx.id = id;
330 tx.status = upd.status.unwrap();
331 tx.network_data = upd.network_data.unwrap();
332 Ok::<_, RepositoryError>(tx)
333 });
334
335 mocks
337 .job_producer
338 .expect_produce_submit_transaction_job()
339 .times(1)
340 .returning(|_, _| Box::pin(async { Ok(()) }));
341
342 mocks
343 .job_producer
344 .expect_produce_send_notification_job()
345 .times(1)
346 .returning(|_, _| Box::pin(async { Ok(()) }));
347
348 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
349 let tx = create_test_transaction(&relayer.id);
350
351 assert!(handler.prepare_transaction_impl(tx).await.is_ok());
352 }
353
354 #[tokio::test]
355 async fn prepare_transaction_stores_signed_envelope_xdr() {
356 let relayer = create_test_relayer();
357 let mut mocks = default_test_mocks();
358
359 mocks
361 .counter
362 .expect_get_and_increment()
363 .returning(|_, _| Box::pin(ready(Ok(1))));
364
365 mocks.signer.expect_sign_transaction().returning(|_| {
367 Box::pin(async {
368 Ok(SignTransactionResponse::Stellar(
369 crate::domain::SignTransactionResponseStellar {
370 signature: dummy_signature(),
371 },
372 ))
373 })
374 });
375
376 mocks
377 .tx_repo
378 .expect_partial_update()
379 .withf(|_, upd| {
380 upd.status == Some(TransactionStatus::Sent) && upd.network_data.is_some()
381 })
382 .returning(move |id, upd| {
383 let mut tx = create_test_transaction("relayer-1");
384 tx.id = id;
385 tx.status = upd.status.unwrap();
386 tx.network_data = upd.network_data.clone().unwrap();
387 Ok::<_, RepositoryError>(tx)
388 });
389
390 mocks
392 .job_producer
393 .expect_produce_submit_transaction_job()
394 .times(1)
395 .returning(|_, _| Box::pin(async { Ok(()) }));
396
397 mocks
398 .job_producer
399 .expect_produce_send_notification_job()
400 .times(1)
401 .returning(|_, _| Box::pin(async { Ok(()) }));
402
403 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
404 let tx = create_test_transaction(&relayer.id);
405
406 let result = handler.prepare_transaction_impl(tx).await;
407 assert!(result.is_ok());
408
409 if let Ok(prepared_tx) = result {
411 if let NetworkTransactionData::Stellar(stellar_data) = &prepared_tx.network_data {
412 assert!(
413 stellar_data.signed_envelope_xdr.is_some(),
414 "signed_envelope_xdr should be populated"
415 );
416
417 let xdr = stellar_data.signed_envelope_xdr.as_ref().unwrap();
419 let envelope_result = TransactionEnvelope::from_xdr_base64(xdr, Limits::none());
420 assert!(
421 envelope_result.is_ok(),
422 "signed_envelope_xdr should be valid XDR"
423 );
424
425 if let Ok(envelope) = envelope_result {
427 match envelope {
428 TransactionEnvelope::Tx(ref e) => {
429 assert!(!e.signatures.is_empty(), "Envelope should have signatures");
430 }
431 _ => panic!("Expected Tx envelope type"),
432 }
433 }
434 } else {
435 panic!("Expected Stellar transaction data");
436 }
437 }
438 }
439
440 #[tokio::test]
441 async fn prepare_transaction_sequence_failure_cleans_up_lane() {
442 let relayer = create_test_relayer();
443 let mut mocks = default_test_mocks();
444
445 mocks.counter.expect_get_and_increment().returning(|_, _| {
447 Box::pin(async {
448 Err(RepositoryError::NotFound(
449 "Counter service failure".to_string(),
450 ))
451 })
452 });
453
454 mocks.provider.expect_get_account().returning(|_| {
456 Box::pin(async {
457 use soroban_rs::xdr::{
458 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
459 Thresholds, Uint256,
460 };
461 use stellar_strkey::ed25519;
462
463 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
464 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
465
466 Ok(AccountEntry {
467 account_id,
468 balance: 1000000,
469 seq_num: SequenceNumber(0),
470 num_sub_entries: 0,
471 inflation_dest: None,
472 flags: 0,
473 home_domain: String32::default(),
474 thresholds: Thresholds([1, 1, 1, 1]),
475 signers: Default::default(),
476 ext: AccountEntryExt::V0,
477 })
478 })
479 });
480
481 mocks
482 .counter
483 .expect_set()
484 .returning(|_, _, _| Box::pin(ready(Ok(()))));
485
486 mocks
488 .tx_repo
489 .expect_partial_update()
490 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
491 .returning(|id, upd| {
492 let mut tx = create_test_transaction("relayer-1");
493 tx.id = id;
494 tx.status = upd.status.unwrap();
495 Ok::<_, RepositoryError>(tx)
496 });
497
498 mocks
500 .job_producer
501 .expect_produce_send_notification_job()
502 .times(1)
503 .returning(|_, _| Box::pin(async { Ok(()) }));
504
505 mocks
507 .tx_repo
508 .expect_find_by_status_paginated()
509 .returning(move |_, _, _, _| {
510 Ok(PaginatedResult {
511 items: vec![],
512 total: 0,
513 page: 1,
514 per_page: 1,
515 })
516 }); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
519 let mut tx = create_test_transaction(&relayer.id);
520
521 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
523 data.sequence_number = None;
524 }
525
526 assert!(lane_gate::claim(&relayer.id, &tx.id));
528
529 let result = handler.prepare_transaction_impl(tx.clone()).await;
530
531 assert!(result.is_err());
533
534 let another_tx_id = "another-tx";
536 assert!(lane_gate::claim(&relayer.id, another_tx_id));
537 lane_gate::free(&relayer.id, another_tx_id)
538 }
539
540 #[tokio::test]
541 async fn prepare_transaction_signer_failure_cleans_up_lane() {
542 let relayer = create_test_relayer();
543 let mut mocks = default_test_mocks();
544
545 mocks
547 .counter
548 .expect_get_and_increment()
549 .returning(|_, _| Box::pin(ready(Ok(1))));
550
551 mocks.provider.expect_get_account().returning(|_| {
553 Box::pin(async {
554 use soroban_rs::xdr::{
555 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
556 Thresholds, Uint256,
557 };
558 use stellar_strkey::ed25519;
559
560 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
561 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
562
563 Ok(AccountEntry {
564 account_id,
565 balance: 1000000,
566 seq_num: SequenceNumber(0),
567 num_sub_entries: 0,
568 inflation_dest: None,
569 flags: 0,
570 home_domain: String32::default(),
571 thresholds: Thresholds([1, 1, 1, 1]),
572 signers: Default::default(),
573 ext: AccountEntryExt::V0,
574 })
575 })
576 });
577
578 mocks
579 .counter
580 .expect_set()
581 .returning(|_, _, _| Box::pin(ready(Ok(()))));
582
583 mocks.signer.expect_sign_transaction().returning(|_| {
585 Box::pin(async {
586 Err(crate::models::SignerError::SigningError(
587 "Signer failure".to_string(),
588 ))
589 })
590 });
591
592 mocks
594 .tx_repo
595 .expect_partial_update()
596 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
597 .returning(|id, upd| {
598 let mut tx = create_test_transaction("relayer-1");
599 tx.id = id;
600 tx.status = upd.status.unwrap();
601 Ok::<_, RepositoryError>(tx)
602 });
603
604 mocks
606 .job_producer
607 .expect_produce_send_notification_job()
608 .times(1)
609 .returning(|_, _| Box::pin(async { Ok(()) }));
610
611 mocks
613 .tx_repo
614 .expect_find_by_status_paginated()
615 .returning(move |_, _, _, _| {
616 Ok(PaginatedResult {
617 items: vec![],
618 total: 0,
619 page: 1,
620 per_page: 1,
621 })
622 }); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
625 let tx = create_test_transaction(&relayer.id);
626
627 let result = handler.prepare_transaction_impl(tx.clone()).await;
628
629 assert!(result.is_err());
631
632 let another_tx_id = "another-tx";
634 assert!(lane_gate::claim(&relayer.id, another_tx_id));
635 lane_gate::free(&relayer.id, another_tx_id); }
637
638 #[tokio::test]
639 async fn prepare_transaction_already_claimed_lane_returns_original() {
640 let mut relayer = create_test_relayer();
641 relayer.id = "unique-relayer-for-lane-test".to_string(); let mocks = default_test_mocks();
643
644 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
645 let tx = create_test_transaction(&relayer.id);
646
647 assert!(lane_gate::claim(&relayer.id, "other-tx"));
649
650 let result = handler.prepare_transaction_impl(tx.clone()).await;
651
652 assert!(result.is_ok());
654 let returned_tx = result.unwrap();
655 assert_eq!(returned_tx.id, tx.id);
656 assert_eq!(returned_tx.status, tx.status);
657
658 lane_gate::free(&relayer.id, "other-tx");
660 }
661
662 #[tokio::test]
663 async fn test_prepare_failure_syncs_sequence() {
664 let relayer = create_test_relayer();
665 let mut mocks = default_test_mocks();
666
667 let sequence_value = 42u64;
669
670 mocks
672 .counter
673 .expect_get_and_increment()
674 .times(1)
675 .returning(move |_, _| Box::pin(ready(Ok(sequence_value))));
676
677 mocks.provider.expect_get_account().times(1).returning(|_| {
679 Box::pin(async {
680 use soroban_rs::xdr::{
681 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
682 Thresholds, Uint256,
683 };
684 use stellar_strkey::ed25519;
685
686 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
687 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
688
689 Ok(AccountEntry {
690 account_id,
691 balance: 1000000,
692 seq_num: SequenceNumber(41), num_sub_entries: 0,
694 inflation_dest: None,
695 flags: 0,
696 home_domain: String32::default(),
697 thresholds: Thresholds([1, 1, 1, 1]),
698 signers: Default::default(),
699 ext: AccountEntryExt::V0,
700 })
701 })
702 });
703
704 mocks
705 .counter
706 .expect_set()
707 .times(1)
708 .withf(|_, _, seq| *seq == 42) .returning(|_, _, _| Box::pin(ready(Ok(()))));
710
711 mocks
713 .signer
714 .expect_sign_transaction()
715 .times(1)
716 .returning(|_| {
717 Box::pin(async {
718 Err(crate::models::SignerError::SigningError(
719 "Simulated signing failure".to_string(),
720 ))
721 })
722 });
723
724 mocks
726 .tx_repo
727 .expect_partial_update()
728 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
729 .returning(|id, upd| {
730 let mut tx = create_test_transaction("relayer-1");
731 tx.id = id;
732 tx.status = upd.status.unwrap();
733 Ok::<_, RepositoryError>(tx)
734 });
735
736 mocks
738 .job_producer
739 .expect_produce_send_notification_job()
740 .times(1)
741 .returning(|_, _| Box::pin(async { Ok(()) }));
742
743 mocks
745 .tx_repo
746 .expect_find_by_status_paginated()
747 .returning(move |_, _, _, _| {
748 Ok(PaginatedResult {
749 items: vec![],
750 total: 0,
751 page: 1,
752 per_page: 1,
753 })
754 });
755
756 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
757 let tx = create_test_transaction(&relayer.id);
758
759 let result = handler.prepare_transaction_impl(tx).await;
760
761 assert!(result.is_err());
763 match result.unwrap_err() {
764 TransactionError::SignerError(msg) => {
765 assert!(msg.contains("Simulated signing failure"));
766 }
767 _ => panic!("Expected SignerError"),
768 }
769 }
770
771 #[tokio::test]
772 async fn test_prepare_simulation_failure_syncs_sequence() {
773 let relayer = create_test_relayer();
774 let mut mocks = default_test_mocks();
775
776 mocks
778 .counter
779 .expect_get_and_increment()
780 .times(1)
781 .returning(|_, _| Box::pin(ready(Ok(100))));
782
783 mocks.provider.expect_get_account().times(1).returning(|_| {
785 Box::pin(async {
786 use soroban_rs::xdr::{
787 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
788 Thresholds, Uint256,
789 };
790 use stellar_strkey::ed25519;
791
792 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
793 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
794
795 Ok(AccountEntry {
796 account_id,
797 balance: 1000000,
798 seq_num: SequenceNumber(99),
799 num_sub_entries: 0,
800 inflation_dest: None,
801 flags: 0,
802 home_domain: String32::default(),
803 thresholds: Thresholds([1, 1, 1, 1]),
804 signers: Default::default(),
805 ext: AccountEntryExt::V0,
806 })
807 })
808 });
809
810 mocks
811 .counter
812 .expect_set()
813 .times(1)
814 .returning(|_, _, _| Box::pin(ready(Ok(()))));
815
816 mocks
818 .provider
819 .expect_simulate_transaction_envelope()
820 .times(1)
821 .returning(|_| {
822 Box::pin(async {
823 Err(ProviderError::Other(
824 "Simulation failed: insufficient resources".to_string(),
825 ))
826 })
827 });
828
829 mocks
831 .tx_repo
832 .expect_partial_update()
833 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
834 .returning(|id, upd| {
835 let mut tx = create_test_transaction("relayer-1");
836 tx.id = id;
837 tx.status = upd.status.unwrap();
838 Ok::<_, RepositoryError>(tx)
839 });
840
841 mocks
843 .job_producer
844 .expect_produce_send_notification_job()
845 .times(1)
846 .returning(|_, _| Box::pin(async { Ok(()) }));
847
848 mocks
849 .tx_repo
850 .expect_find_by_status_paginated()
851 .returning(move |_, _, _, _| {
852 Ok(PaginatedResult {
853 items: vec![],
854 total: 0,
855 page: 1,
856 per_page: 1,
857 })
858 });
859
860 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
861
862 let mut tx = create_test_transaction(&relayer.id);
864 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
865 data.transaction_input =
866 crate::models::TransactionInput::Operations(vec![OperationSpec::InvokeContract {
867 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
868 .to_string(),
869 function_name: "test".to_string(),
870 args: vec![],
871 auth: None,
872 }]);
873 }
874
875 let result = handler.prepare_transaction_impl(tx).await;
876
877 assert!(result.is_err());
879 }
880
881 #[tokio::test]
882 async fn test_prepare_xdr_parsing_failure_syncs_sequence() {
883 let relayer = create_test_relayer();
884 let mut mocks = default_test_mocks();
885
886 mocks.provider.expect_get_account().times(1).returning(|_| {
892 Box::pin(async {
893 use soroban_rs::xdr::{
894 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
895 Thresholds, Uint256,
896 };
897 use stellar_strkey::ed25519;
898
899 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
900 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
901
902 Ok(AccountEntry {
903 account_id,
904 balance: 1000000,
905 seq_num: SequenceNumber(50),
906 num_sub_entries: 0,
907 inflation_dest: None,
908 flags: 0,
909 home_domain: String32::default(),
910 thresholds: Thresholds([1, 1, 1, 1]),
911 signers: Default::default(),
912 ext: AccountEntryExt::V0,
913 })
914 })
915 });
916
917 mocks
918 .counter
919 .expect_set()
920 .times(1)
921 .returning(|_, _, _| Box::pin(ready(Ok(()))));
922
923 mocks
925 .tx_repo
926 .expect_partial_update()
927 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
928 .returning(|id, upd| {
929 let mut tx = create_test_transaction("relayer-1");
930 tx.id = id;
931 tx.status = upd.status.unwrap();
932 Ok::<_, RepositoryError>(tx)
933 });
934
935 mocks
937 .job_producer
938 .expect_produce_send_notification_job()
939 .times(1)
940 .returning(|_, _| Box::pin(async { Ok(()) }));
941
942 mocks
943 .tx_repo
944 .expect_find_by_status_paginated()
945 .returning(move |_, _, _, _| {
946 Ok(PaginatedResult {
947 items: vec![],
948 total: 0,
949 page: 1,
950 per_page: 1,
951 })
952 });
953
954 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
955
956 let mut tx = create_test_transaction(&relayer.id);
958 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
959 data.sequence_number = None;
961 data.transaction_input = crate::models::TransactionInput::UnsignedXdr(
963 "AAAAAgAAAAA5MbUzuTfU6p3NeJp5w3TpKhZmx6p1pR7mq9wFwCnEIgAAAGQAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAADk4GIHV/3i2tOMBkqKqN3Y9x3FvNm8z4B5PEzPn7hEaAAAAAAAAAAAAAAAZAAAAAAAAAAA".to_string()
965 );
966 }
967
968 let result = handler.prepare_transaction_impl(tx).await;
969
970 assert!(result.is_err());
972 match result.unwrap_err() {
973 TransactionError::ValidationError(msg) => {
974 assert!(msg.contains("does not match relayer account"));
975 }
976 _ => panic!("Expected ValidationError"),
977 }
978 }
979}
980
981#[cfg(test)]
982mod refactoring_tests {
983 use crate::domain::transaction::stellar::prepare::common::update_and_notify_transaction;
984 use crate::domain::transaction::stellar::test_helpers::*;
985 use crate::domain::{stellar::lane_gate, SignTransactionResponse};
986 use crate::models::{
987 NetworkTransactionData, RepositoryError, StellarTransactionData, TransactionInput,
988 TransactionStatus,
989 };
990 use std::future::ready;
991
992 #[tokio::test]
993 async fn test_prepare_with_concurrent_mode_no_lane_claiming() {
994 let mut relayer = create_test_relayer();
996 if let crate::models::RelayerNetworkPolicy::Stellar(ref mut policy) = relayer.policies {
997 policy.concurrent_transactions = Some(true);
998 }
999 let mut mocks = default_test_mocks();
1000
1001 mocks
1003 .counter
1004 .expect_get_and_increment()
1005 .returning(|_, _| Box::pin(ready(Ok(1))));
1006
1007 mocks.signer.expect_sign_transaction().returning(|_| {
1008 Box::pin(async {
1009 Ok(SignTransactionResponse::Stellar(
1010 crate::domain::SignTransactionResponseStellar {
1011 signature: dummy_signature(),
1012 },
1013 ))
1014 })
1015 });
1016
1017 mocks.tx_repo.expect_partial_update().returning(|id, upd| {
1018 let mut tx = create_test_transaction("relayer-1");
1019 tx.id = id;
1020 tx.status = upd.status.unwrap();
1021 tx.network_data = upd.network_data.unwrap();
1022 Ok::<_, RepositoryError>(tx)
1023 });
1024
1025 mocks
1026 .job_producer
1027 .expect_produce_submit_transaction_job()
1028 .returning(|_, _| Box::pin(async { Ok(()) }));
1029
1030 mocks
1031 .job_producer
1032 .expect_produce_send_notification_job()
1033 .returning(|_, _| Box::pin(async { Ok(()) }));
1034
1035 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
1036 let tx = create_test_transaction(&relayer.id);
1037
1038 let other_tx_id = "concurrent-tx";
1041 assert!(lane_gate::claim(&relayer.id, other_tx_id));
1042
1043 let result = handler.prepare_transaction_impl(tx).await;
1045 assert!(result.is_ok());
1046
1047 lane_gate::free(&relayer.id, other_tx_id);
1049 }
1050
1051 #[tokio::test]
1052 async fn test_prepare_failure_with_concurrent_mode_no_lane_cleanup() {
1053 let mut relayer = create_test_relayer();
1055 if let crate::models::RelayerNetworkPolicy::Stellar(ref mut policy) = relayer.policies {
1056 policy.concurrent_transactions = Some(true);
1057 }
1058 let mut mocks = default_test_mocks();
1059
1060 mocks.counter.expect_get_and_increment().returning(|_, _| {
1062 Box::pin(ready(Err(RepositoryError::Unknown(
1063 "Counter error".to_string(),
1064 ))))
1065 });
1066
1067 mocks.provider.expect_get_account().returning(|_| {
1069 Box::pin(async {
1070 use soroban_rs::xdr::{
1071 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
1072 Thresholds, Uint256,
1073 };
1074 use stellar_strkey::ed25519;
1075
1076 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
1077 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
1078
1079 Ok(AccountEntry {
1080 account_id,
1081 balance: 1000000,
1082 seq_num: SequenceNumber(0),
1083 num_sub_entries: 0,
1084 inflation_dest: None,
1085 flags: 0,
1086 home_domain: String32::default(),
1087 thresholds: Thresholds([1, 1, 1, 1]),
1088 signers: Default::default(),
1089 ext: AccountEntryExt::V0,
1090 })
1091 })
1092 });
1093
1094 mocks
1095 .counter
1096 .expect_set()
1097 .returning(|_, _, _| Box::pin(ready(Ok(()))));
1098
1099 mocks.tx_repo.expect_partial_update().returning(|id, upd| {
1101 let mut tx = create_test_transaction("relayer-1");
1102 tx.id = id;
1103 tx.status = upd.status.unwrap();
1104 Ok::<_, RepositoryError>(tx)
1105 });
1106
1107 mocks
1108 .job_producer
1109 .expect_produce_send_notification_job()
1110 .returning(|_, _| Box::pin(async { Ok(()) }));
1111
1112 mocks.tx_repo.expect_find_by_status_paginated().times(0); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
1116 let tx = create_test_transaction(&relayer.id);
1117
1118 let result = handler.prepare_transaction_impl(tx).await;
1119 assert!(result.is_err());
1120 }
1121
1122 #[tokio::test]
1123 async fn test_update_and_notify_transaction_consistency() {
1124 let relayer = create_test_relayer();
1125 let mut mocks = default_test_mocks();
1126
1127 let expected_stellar_data = StellarTransactionData {
1129 source_account: TEST_PK.to_string(),
1130 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1131 fee: Some(100),
1132 sequence_number: Some(1),
1133 transaction_input: TransactionInput::Operations(vec![]),
1134 memo: None,
1135 valid_until: None,
1136 signatures: vec![],
1137 hash: None,
1138 simulation_transaction_data: None,
1139 signed_envelope_xdr: Some("test-xdr".to_string()),
1140 transaction_result_xdr: None,
1141 };
1142
1143 let expected_xdr = expected_stellar_data.signed_envelope_xdr.clone();
1144 mocks
1145 .tx_repo
1146 .expect_partial_update()
1147 .withf(move |id, upd| {
1148 id == "tx-1"
1149 && upd.status == Some(TransactionStatus::Sent)
1150 && if let Some(NetworkTransactionData::Stellar(ref data)) = upd.network_data {
1151 data.signed_envelope_xdr == expected_xdr
1152 } else {
1153 false
1154 }
1155 })
1156 .returning(|id, upd| {
1157 let mut tx = create_test_transaction("relayer-1");
1158 tx.id = id;
1159 tx.status = upd.status.unwrap();
1160 tx.network_data = upd.network_data.unwrap();
1161 Ok::<_, RepositoryError>(tx)
1162 });
1163
1164 mocks
1166 .job_producer
1167 .expect_produce_submit_transaction_job()
1168 .times(1)
1169 .returning(|_, _| Box::pin(async { Ok(()) }));
1170
1171 mocks
1172 .job_producer
1173 .expect_produce_send_notification_job()
1174 .times(1)
1175 .returning(|_, _| Box::pin(async { Ok(()) }));
1176
1177 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
1178
1179 let result = update_and_notify_transaction(
1181 handler.transaction_repository(),
1182 handler.job_producer(),
1183 "tx-1".to_string(),
1184 expected_stellar_data,
1185 handler.relayer().notification_id.as_deref(),
1186 )
1187 .await;
1188
1189 assert!(result.is_ok());
1190 let updated_tx = result.unwrap();
1191 assert_eq!(updated_tx.status, TransactionStatus::Sent);
1192
1193 if let NetworkTransactionData::Stellar(data) = &updated_tx.network_data {
1194 assert_eq!(data.signed_envelope_xdr, Some("test-xdr".to_string()));
1195 } else {
1196 panic!("Expected Stellar transaction data");
1197 }
1198 }
1199}