1pub mod common;
7pub mod fee_bump;
8pub mod operations;
9pub mod unsigned_xdr;
10
11use eyre::Result;
12use tracing::{debug, info, warn};
13
14use super::{is_final_state, lane_gate, StellarRelayerTransaction};
15use crate::models::RelayerRepoModel;
16use crate::{
17 jobs::JobProducerTrait,
18 models::{
19 TransactionError, TransactionInput, TransactionRepoModel, TransactionStatus,
20 TransactionUpdateRequest,
21 },
22 repositories::{Repository, TransactionCounterTrait, TransactionRepository},
23 services::{provider::StellarProviderTrait, signer::Signer},
24};
25
26use common::{sign_and_finalize_transaction, update_and_notify_transaction};
27
28impl<R, T, J, S, P, C, D> StellarRelayerTransaction<R, T, J, S, P, C, D>
29where
30 R: Repository<RelayerRepoModel, String> + Send + Sync,
31 T: TransactionRepository + Send + Sync,
32 J: JobProducerTrait + Send + Sync,
33 S: Signer + Send + Sync,
34 P: StellarProviderTrait + Send + Sync,
35 C: TransactionCounterTrait + Send + Sync,
36 D: crate::services::stellar_dex::StellarDexServiceTrait + Send + Sync + 'static,
37{
38 pub async fn prepare_transaction_impl(
40 &self,
41 tx: TransactionRepoModel,
42 ) -> Result<TransactionRepoModel, TransactionError> {
43 debug!(status = ?tx.status, "preparing stellar transaction");
44
45 if is_final_state(&tx.status) {
47 warn!(
48 tx_id = %tx.id,
49 status = ?tx.status,
50 "transaction already in final state, skipping preparation"
51 );
52 return Ok(tx);
53 }
54
55 if tx.status != TransactionStatus::Pending {
56 debug!(
57 tx_id = %tx.id,
58 status = ?tx.status,
59 expected_status = ?TransactionStatus::Pending,
60 "transaction in unexpected state for preparation, skipping"
61 );
62 return Ok(tx);
63 }
64
65 if !self.concurrent_transactions_enabled() && !lane_gate::claim(&self.relayer().id, &tx.id)
66 {
67 info!("relayer already has a transaction in flight, must wait");
68 return Ok(tx);
69 }
70
71 debug!("preparing transaction {}", tx.id);
72
73 match self.prepare_core(tx.clone()).await {
75 Ok(prepared_tx) => Ok(prepared_tx),
76 Err(error) => {
77 self.handle_prepare_failure(tx, error).await
79 }
80 }
81 }
82
83 async fn prepare_core(
85 &self,
86 tx: TransactionRepoModel,
87 ) -> Result<TransactionRepoModel, TransactionError> {
88 let stellar_data = tx.network_data.get_stellar_transaction_data()?;
89
90 let policy = self.relayer().policies.get_stellar_policy();
92 match &stellar_data.transaction_input {
93 TransactionInput::Operations(_) => {
94 debug!("preparing operations-based transaction {}", tx.id);
95 let stellar_data_with_sim = operations::process_operations(
96 self.transaction_counter_service(),
97 &self.relayer().id,
98 &self.relayer().address,
99 &tx,
100 stellar_data,
101 self.provider(),
102 self.signer(),
103 Some(&policy),
104 )
105 .await?;
106 self.finalize_with_signature(tx, stellar_data_with_sim)
107 .await
108 }
109 TransactionInput::UnsignedXdr(_) => {
110 debug!("preparing unsigned xdr transaction {}", tx.id);
111 let stellar_data_with_sim = unsigned_xdr::process_unsigned_xdr(
112 self.transaction_counter_service(),
113 &self.relayer().id,
114 &self.relayer().address,
115 stellar_data,
116 self.provider(),
117 self.signer(),
118 Some(&policy),
119 self.dex_service(),
120 )
121 .await?;
122 self.finalize_with_signature(tx, stellar_data_with_sim)
123 .await
124 }
125 TransactionInput::SignedXdr { .. } => {
126 debug!("preparing fee-bump transaction {}", tx.id);
127 let stellar_data_with_fee_bump = fee_bump::process_fee_bump(
128 &self.relayer().address,
129 stellar_data,
130 self.provider(),
131 self.signer(),
132 Some(&policy),
133 self.dex_service(),
134 )
135 .await?;
136 update_and_notify_transaction(
137 self.transaction_repository(),
138 self.job_producer(),
139 tx.id,
140 stellar_data_with_fee_bump,
141 self.relayer().notification_id.as_deref(),
142 )
143 .await
144 }
145 }
146 }
147
148 async fn finalize_with_signature(
150 &self,
151 tx: TransactionRepoModel,
152 stellar_data: crate::models::StellarTransactionData,
153 ) -> Result<TransactionRepoModel, TransactionError> {
154 let (tx, final_stellar_data) =
155 sign_and_finalize_transaction(self.signer(), tx, stellar_data).await?;
156 update_and_notify_transaction(
157 self.transaction_repository(),
158 self.job_producer(),
159 tx.id,
160 final_stellar_data,
161 self.relayer().notification_id.as_deref(),
162 )
163 .await
164 }
165
166 async fn handle_prepare_failure(
169 &self,
170 tx: TransactionRepoModel,
171 error: TransactionError,
172 ) -> Result<TransactionRepoModel, TransactionError> {
173 let error_reason = format!("Preparation failed: {error}");
174 let tx_id = tx.id.clone(); warn!(reason = %error_reason, "transaction preparation failed");
176
177 if let Ok(stellar_data) = tx.network_data.get_stellar_transaction_data() {
179 info!("syncing sequence from chain after failed transaction preparation");
180 match self
182 .sync_sequence_from_chain(&stellar_data.source_account)
183 .await
184 {
185 Ok(()) => {
186 info!("successfully synced sequence from chain");
187 }
188 Err(sync_error) => {
189 warn!(error = %sync_error, "failed to sync sequence from chain");
190 }
191 }
192 }
193
194 let update_request = TransactionUpdateRequest {
196 status: Some(TransactionStatus::Failed),
197 status_reason: Some(error_reason.clone()),
198 ..Default::default()
199 };
200 let _failed_tx = match self
201 .finalize_transaction_state(tx_id.clone(), update_request)
202 .await
203 {
204 Ok(updated_tx) => updated_tx,
205 Err(finalize_error) => {
206 warn!(error = %finalize_error, "failed to mark transaction as failed, proceeding with lane cleanup");
207 tx
209 }
210 };
211
212 if !self.concurrent_transactions_enabled() {
214 if let Err(enqueue_error) = self.enqueue_next_pending_transaction(&tx_id).await {
216 warn!(error = %enqueue_error, "failed to enqueue next pending transaction after failure, releasing lane directly");
217 lane_gate::free(&self.relayer().id, &tx_id);
219 }
220 }
221
222 info!(error = %error_reason, "transaction preparation failure handled, lane cleaned up");
224
225 Err(error)
227 }
228}
229
230#[cfg(test)]
231mod prepare_transaction_tests {
232 use std::future::ready;
233
234 use super::*;
235 use crate::{
236 domain::SignTransactionResponse,
237 models::{NetworkTransactionData, OperationSpec, RepositoryError, TransactionStatus},
238 services::provider::ProviderError,
239 };
240 use soroban_rs::xdr::{Limits, ReadXdr, TransactionEnvelope};
241
242 use crate::domain::transaction::stellar::test_helpers::*;
243
244 #[tokio::test]
245 async fn prepare_transaction_happy_path() {
246 let relayer = create_test_relayer();
247 let mut mocks = default_test_mocks();
248
249 mocks
251 .counter
252 .expect_get_and_increment()
253 .returning(|_, _| Box::pin(ready(Ok(1))));
254
255 mocks.signer.expect_sign_transaction().returning(|_| {
257 Box::pin(async {
258 Ok(SignTransactionResponse::Stellar(
259 crate::domain::SignTransactionResponseStellar {
260 signature: dummy_signature(),
261 },
262 ))
263 })
264 });
265
266 mocks
267 .tx_repo
268 .expect_partial_update()
269 .withf(|_, upd| {
270 upd.status == Some(TransactionStatus::Sent) && upd.network_data.is_some()
271 })
272 .returning(|id, upd| {
273 let mut tx = create_test_transaction("relayer-1");
274 tx.id = id;
275 tx.status = upd.status.unwrap();
276 tx.network_data = upd.network_data.unwrap();
277 Ok::<_, RepositoryError>(tx)
278 });
279
280 mocks
282 .job_producer
283 .expect_produce_submit_transaction_job()
284 .times(1)
285 .returning(|_, _| Box::pin(async { Ok(()) }));
286
287 mocks
288 .job_producer
289 .expect_produce_send_notification_job()
290 .times(1)
291 .returning(|_, _| Box::pin(async { Ok(()) }));
292
293 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
294 let tx = create_test_transaction(&relayer.id);
295
296 assert!(handler.prepare_transaction_impl(tx).await.is_ok());
297 }
298
299 #[tokio::test]
300 async fn prepare_transaction_stores_signed_envelope_xdr() {
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(move |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.clone().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 let result = handler.prepare_transaction_impl(tx).await;
352 assert!(result.is_ok());
353
354 if let Ok(prepared_tx) = result {
356 if let NetworkTransactionData::Stellar(stellar_data) = &prepared_tx.network_data {
357 assert!(
358 stellar_data.signed_envelope_xdr.is_some(),
359 "signed_envelope_xdr should be populated"
360 );
361
362 let xdr = stellar_data.signed_envelope_xdr.as_ref().unwrap();
364 let envelope_result = TransactionEnvelope::from_xdr_base64(xdr, Limits::none());
365 assert!(
366 envelope_result.is_ok(),
367 "signed_envelope_xdr should be valid XDR"
368 );
369
370 if let Ok(envelope) = envelope_result {
372 match envelope {
373 TransactionEnvelope::Tx(ref e) => {
374 assert!(!e.signatures.is_empty(), "Envelope should have signatures");
375 }
376 _ => panic!("Expected Tx envelope type"),
377 }
378 }
379 } else {
380 panic!("Expected Stellar transaction data");
381 }
382 }
383 }
384
385 #[tokio::test]
386 async fn prepare_transaction_sequence_failure_cleans_up_lane() {
387 let relayer = create_test_relayer();
388 let mut mocks = default_test_mocks();
389
390 mocks.counter.expect_get_and_increment().returning(|_, _| {
392 Box::pin(async {
393 Err(RepositoryError::NotFound(
394 "Counter service failure".to_string(),
395 ))
396 })
397 });
398
399 mocks.provider.expect_get_account().returning(|_| {
401 Box::pin(async {
402 use soroban_rs::xdr::{
403 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
404 Thresholds, Uint256,
405 };
406 use stellar_strkey::ed25519;
407
408 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
409 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
410
411 Ok(AccountEntry {
412 account_id,
413 balance: 1000000,
414 seq_num: SequenceNumber(0),
415 num_sub_entries: 0,
416 inflation_dest: None,
417 flags: 0,
418 home_domain: String32::default(),
419 thresholds: Thresholds([1, 1, 1, 1]),
420 signers: Default::default(),
421 ext: AccountEntryExt::V0,
422 })
423 })
424 });
425
426 mocks
427 .counter
428 .expect_set()
429 .returning(|_, _, _| Box::pin(ready(Ok(()))));
430
431 mocks
433 .tx_repo
434 .expect_partial_update()
435 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
436 .returning(|id, upd| {
437 let mut tx = create_test_transaction("relayer-1");
438 tx.id = id;
439 tx.status = upd.status.unwrap();
440 Ok::<_, RepositoryError>(tx)
441 });
442
443 mocks
445 .job_producer
446 .expect_produce_send_notification_job()
447 .times(1)
448 .returning(|_, _| Box::pin(async { Ok(()) }));
449
450 mocks
452 .tx_repo
453 .expect_find_by_status()
454 .returning(|_, _| Ok(vec![])); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
457 let mut tx = create_test_transaction(&relayer.id);
458
459 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
461 data.sequence_number = None;
462 }
463
464 assert!(lane_gate::claim(&relayer.id, &tx.id));
466
467 let result = handler.prepare_transaction_impl(tx.clone()).await;
468
469 assert!(result.is_err());
471
472 let another_tx_id = "another-tx";
474 assert!(lane_gate::claim(&relayer.id, another_tx_id));
475 lane_gate::free(&relayer.id, another_tx_id)
476 }
477
478 #[tokio::test]
479 async fn prepare_transaction_signer_failure_cleans_up_lane() {
480 let relayer = create_test_relayer();
481 let mut mocks = default_test_mocks();
482
483 mocks
485 .counter
486 .expect_get_and_increment()
487 .returning(|_, _| Box::pin(ready(Ok(1))));
488
489 mocks.provider.expect_get_account().returning(|_| {
491 Box::pin(async {
492 use soroban_rs::xdr::{
493 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
494 Thresholds, Uint256,
495 };
496 use stellar_strkey::ed25519;
497
498 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
499 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
500
501 Ok(AccountEntry {
502 account_id,
503 balance: 1000000,
504 seq_num: SequenceNumber(0),
505 num_sub_entries: 0,
506 inflation_dest: None,
507 flags: 0,
508 home_domain: String32::default(),
509 thresholds: Thresholds([1, 1, 1, 1]),
510 signers: Default::default(),
511 ext: AccountEntryExt::V0,
512 })
513 })
514 });
515
516 mocks
517 .counter
518 .expect_set()
519 .returning(|_, _, _| Box::pin(ready(Ok(()))));
520
521 mocks.signer.expect_sign_transaction().returning(|_| {
523 Box::pin(async {
524 Err(crate::models::SignerError::SigningError(
525 "Signer failure".to_string(),
526 ))
527 })
528 });
529
530 mocks
532 .tx_repo
533 .expect_partial_update()
534 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
535 .returning(|id, upd| {
536 let mut tx = create_test_transaction("relayer-1");
537 tx.id = id;
538 tx.status = upd.status.unwrap();
539 Ok::<_, RepositoryError>(tx)
540 });
541
542 mocks
544 .job_producer
545 .expect_produce_send_notification_job()
546 .times(1)
547 .returning(|_, _| Box::pin(async { Ok(()) }));
548
549 mocks
551 .tx_repo
552 .expect_find_by_status()
553 .returning(|_, _| Ok(vec![])); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
556 let tx = create_test_transaction(&relayer.id);
557
558 let result = handler.prepare_transaction_impl(tx.clone()).await;
559
560 assert!(result.is_err());
562
563 let another_tx_id = "another-tx";
565 assert!(lane_gate::claim(&relayer.id, another_tx_id));
566 lane_gate::free(&relayer.id, another_tx_id); }
568
569 #[tokio::test]
570 async fn prepare_transaction_already_claimed_lane_returns_original() {
571 let mut relayer = create_test_relayer();
572 relayer.id = "unique-relayer-for-lane-test".to_string(); let mocks = default_test_mocks();
574
575 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
576 let tx = create_test_transaction(&relayer.id);
577
578 assert!(lane_gate::claim(&relayer.id, "other-tx"));
580
581 let result = handler.prepare_transaction_impl(tx.clone()).await;
582
583 assert!(result.is_ok());
585 let returned_tx = result.unwrap();
586 assert_eq!(returned_tx.id, tx.id);
587 assert_eq!(returned_tx.status, tx.status);
588
589 lane_gate::free(&relayer.id, "other-tx");
591 }
592
593 #[tokio::test]
594 async fn test_prepare_failure_syncs_sequence() {
595 let relayer = create_test_relayer();
596 let mut mocks = default_test_mocks();
597
598 let sequence_value = 42u64;
600
601 mocks
603 .counter
604 .expect_get_and_increment()
605 .times(1)
606 .returning(move |_, _| Box::pin(ready(Ok(sequence_value))));
607
608 mocks.provider.expect_get_account().times(1).returning(|_| {
610 Box::pin(async {
611 use soroban_rs::xdr::{
612 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
613 Thresholds, Uint256,
614 };
615 use stellar_strkey::ed25519;
616
617 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
618 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
619
620 Ok(AccountEntry {
621 account_id,
622 balance: 1000000,
623 seq_num: SequenceNumber(41), num_sub_entries: 0,
625 inflation_dest: None,
626 flags: 0,
627 home_domain: String32::default(),
628 thresholds: Thresholds([1, 1, 1, 1]),
629 signers: Default::default(),
630 ext: AccountEntryExt::V0,
631 })
632 })
633 });
634
635 mocks
636 .counter
637 .expect_set()
638 .times(1)
639 .withf(|_, _, seq| *seq == 42) .returning(|_, _, _| Box::pin(ready(Ok(()))));
641
642 mocks
644 .signer
645 .expect_sign_transaction()
646 .times(1)
647 .returning(|_| {
648 Box::pin(async {
649 Err(crate::models::SignerError::SigningError(
650 "Simulated signing failure".to_string(),
651 ))
652 })
653 });
654
655 mocks
657 .tx_repo
658 .expect_partial_update()
659 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
660 .returning(|id, upd| {
661 let mut tx = create_test_transaction("relayer-1");
662 tx.id = id;
663 tx.status = upd.status.unwrap();
664 Ok::<_, RepositoryError>(tx)
665 });
666
667 mocks
669 .job_producer
670 .expect_produce_send_notification_job()
671 .times(1)
672 .returning(|_, _| Box::pin(async { Ok(()) }));
673
674 mocks
676 .tx_repo
677 .expect_find_by_status()
678 .returning(|_, _| Ok(vec![]));
679
680 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
681 let tx = create_test_transaction(&relayer.id);
682
683 let result = handler.prepare_transaction_impl(tx).await;
684
685 assert!(result.is_err());
687 match result.unwrap_err() {
688 TransactionError::SignerError(msg) => {
689 assert!(msg.contains("Simulated signing failure"));
690 }
691 _ => panic!("Expected SignerError"),
692 }
693 }
694
695 #[tokio::test]
696 async fn test_prepare_simulation_failure_syncs_sequence() {
697 let relayer = create_test_relayer();
698 let mut mocks = default_test_mocks();
699
700 mocks
702 .counter
703 .expect_get_and_increment()
704 .times(1)
705 .returning(|_, _| Box::pin(ready(Ok(100))));
706
707 mocks.provider.expect_get_account().times(1).returning(|_| {
709 Box::pin(async {
710 use soroban_rs::xdr::{
711 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
712 Thresholds, Uint256,
713 };
714 use stellar_strkey::ed25519;
715
716 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
717 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
718
719 Ok(AccountEntry {
720 account_id,
721 balance: 1000000,
722 seq_num: SequenceNumber(99),
723 num_sub_entries: 0,
724 inflation_dest: None,
725 flags: 0,
726 home_domain: String32::default(),
727 thresholds: Thresholds([1, 1, 1, 1]),
728 signers: Default::default(),
729 ext: AccountEntryExt::V0,
730 })
731 })
732 });
733
734 mocks
735 .counter
736 .expect_set()
737 .times(1)
738 .returning(|_, _, _| Box::pin(ready(Ok(()))));
739
740 mocks
742 .provider
743 .expect_simulate_transaction_envelope()
744 .times(1)
745 .returning(|_| {
746 Box::pin(async {
747 Err(ProviderError::Other(
748 "Simulation failed: insufficient resources".to_string(),
749 ))
750 })
751 });
752
753 mocks
755 .tx_repo
756 .expect_partial_update()
757 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
758 .returning(|id, upd| {
759 let mut tx = create_test_transaction("relayer-1");
760 tx.id = id;
761 tx.status = upd.status.unwrap();
762 Ok::<_, RepositoryError>(tx)
763 });
764
765 mocks
767 .job_producer
768 .expect_produce_send_notification_job()
769 .times(1)
770 .returning(|_, _| Box::pin(async { Ok(()) }));
771
772 mocks
773 .tx_repo
774 .expect_find_by_status()
775 .returning(|_, _| Ok(vec![]));
776
777 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
778
779 let mut tx = create_test_transaction(&relayer.id);
781 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
782 data.transaction_input =
783 crate::models::TransactionInput::Operations(vec![OperationSpec::InvokeContract {
784 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
785 .to_string(),
786 function_name: "test".to_string(),
787 args: vec![],
788 auth: None,
789 }]);
790 }
791
792 let result = handler.prepare_transaction_impl(tx).await;
793
794 assert!(result.is_err());
796 }
797
798 #[tokio::test]
799 async fn test_prepare_xdr_parsing_failure_syncs_sequence() {
800 let relayer = create_test_relayer();
801 let mut mocks = default_test_mocks();
802
803 mocks.provider.expect_get_account().times(1).returning(|_| {
809 Box::pin(async {
810 use soroban_rs::xdr::{
811 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
812 Thresholds, Uint256,
813 };
814 use stellar_strkey::ed25519;
815
816 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
817 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
818
819 Ok(AccountEntry {
820 account_id,
821 balance: 1000000,
822 seq_num: SequenceNumber(50),
823 num_sub_entries: 0,
824 inflation_dest: None,
825 flags: 0,
826 home_domain: String32::default(),
827 thresholds: Thresholds([1, 1, 1, 1]),
828 signers: Default::default(),
829 ext: AccountEntryExt::V0,
830 })
831 })
832 });
833
834 mocks
835 .counter
836 .expect_set()
837 .times(1)
838 .returning(|_, _, _| Box::pin(ready(Ok(()))));
839
840 mocks
842 .tx_repo
843 .expect_partial_update()
844 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
845 .returning(|id, upd| {
846 let mut tx = create_test_transaction("relayer-1");
847 tx.id = id;
848 tx.status = upd.status.unwrap();
849 Ok::<_, RepositoryError>(tx)
850 });
851
852 mocks
854 .job_producer
855 .expect_produce_send_notification_job()
856 .times(1)
857 .returning(|_, _| Box::pin(async { Ok(()) }));
858
859 mocks
860 .tx_repo
861 .expect_find_by_status()
862 .returning(|_, _| Ok(vec![]));
863
864 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
865
866 let mut tx = create_test_transaction(&relayer.id);
868 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
869 data.sequence_number = None;
871 data.transaction_input = crate::models::TransactionInput::UnsignedXdr(
873 "AAAAAgAAAAA5MbUzuTfU6p3NeJp5w3TpKhZmx6p1pR7mq9wFwCnEIgAAAGQAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAADk4GIHV/3i2tOMBkqKqN3Y9x3FvNm8z4B5PEzPn7hEaAAAAAAAAAAAAAAAZAAAAAAAAAAA".to_string()
875 );
876 }
877
878 let result = handler.prepare_transaction_impl(tx).await;
879
880 assert!(result.is_err());
882 match result.unwrap_err() {
883 TransactionError::ValidationError(msg) => {
884 assert!(msg.contains("does not match relayer account"));
885 }
886 _ => panic!("Expected ValidationError"),
887 }
888 }
889}
890
891#[cfg(test)]
892mod refactoring_tests {
893 use crate::domain::transaction::stellar::prepare::common::update_and_notify_transaction;
894 use crate::domain::transaction::stellar::test_helpers::*;
895 use crate::domain::{stellar::lane_gate, SignTransactionResponse};
896 use crate::models::{
897 NetworkTransactionData, RepositoryError, StellarTransactionData, TransactionInput,
898 TransactionStatus,
899 };
900 use std::future::ready;
901
902 #[tokio::test]
903 async fn test_prepare_with_concurrent_mode_no_lane_claiming() {
904 let mut relayer = create_test_relayer();
906 if let crate::models::RelayerNetworkPolicy::Stellar(ref mut policy) = relayer.policies {
907 policy.concurrent_transactions = Some(true);
908 }
909 let mut mocks = default_test_mocks();
910
911 mocks
913 .counter
914 .expect_get_and_increment()
915 .returning(|_, _| Box::pin(ready(Ok(1))));
916
917 mocks.signer.expect_sign_transaction().returning(|_| {
918 Box::pin(async {
919 Ok(SignTransactionResponse::Stellar(
920 crate::domain::SignTransactionResponseStellar {
921 signature: dummy_signature(),
922 },
923 ))
924 })
925 });
926
927 mocks.tx_repo.expect_partial_update().returning(|id, upd| {
928 let mut tx = create_test_transaction("relayer-1");
929 tx.id = id;
930 tx.status = upd.status.unwrap();
931 tx.network_data = upd.network_data.unwrap();
932 Ok::<_, RepositoryError>(tx)
933 });
934
935 mocks
936 .job_producer
937 .expect_produce_submit_transaction_job()
938 .returning(|_, _| Box::pin(async { Ok(()) }));
939
940 mocks
941 .job_producer
942 .expect_produce_send_notification_job()
943 .returning(|_, _| Box::pin(async { Ok(()) }));
944
945 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
946 let tx = create_test_transaction(&relayer.id);
947
948 let other_tx_id = "concurrent-tx";
951 assert!(lane_gate::claim(&relayer.id, other_tx_id));
952
953 let result = handler.prepare_transaction_impl(tx).await;
955 assert!(result.is_ok());
956
957 lane_gate::free(&relayer.id, other_tx_id);
959 }
960
961 #[tokio::test]
962 async fn test_prepare_failure_with_concurrent_mode_no_lane_cleanup() {
963 let mut relayer = create_test_relayer();
965 if let crate::models::RelayerNetworkPolicy::Stellar(ref mut policy) = relayer.policies {
966 policy.concurrent_transactions = Some(true);
967 }
968 let mut mocks = default_test_mocks();
969
970 mocks.counter.expect_get_and_increment().returning(|_, _| {
972 Box::pin(ready(Err(RepositoryError::Unknown(
973 "Counter error".to_string(),
974 ))))
975 });
976
977 mocks.provider.expect_get_account().returning(|_| {
979 Box::pin(async {
980 use soroban_rs::xdr::{
981 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
982 Thresholds, Uint256,
983 };
984 use stellar_strkey::ed25519;
985
986 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
987 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
988
989 Ok(AccountEntry {
990 account_id,
991 balance: 1000000,
992 seq_num: SequenceNumber(0),
993 num_sub_entries: 0,
994 inflation_dest: None,
995 flags: 0,
996 home_domain: String32::default(),
997 thresholds: Thresholds([1, 1, 1, 1]),
998 signers: Default::default(),
999 ext: AccountEntryExt::V0,
1000 })
1001 })
1002 });
1003
1004 mocks
1005 .counter
1006 .expect_set()
1007 .returning(|_, _, _| Box::pin(ready(Ok(()))));
1008
1009 mocks.tx_repo.expect_partial_update().returning(|id, upd| {
1011 let mut tx = create_test_transaction("relayer-1");
1012 tx.id = id;
1013 tx.status = upd.status.unwrap();
1014 Ok::<_, RepositoryError>(tx)
1015 });
1016
1017 mocks
1018 .job_producer
1019 .expect_produce_send_notification_job()
1020 .returning(|_, _| Box::pin(async { Ok(()) }));
1021
1022 mocks.tx_repo.expect_find_by_status().times(0); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
1026 let tx = create_test_transaction(&relayer.id);
1027
1028 let result = handler.prepare_transaction_impl(tx).await;
1029 assert!(result.is_err());
1030 }
1031
1032 #[tokio::test]
1033 async fn test_update_and_notify_transaction_consistency() {
1034 let relayer = create_test_relayer();
1035 let mut mocks = default_test_mocks();
1036
1037 let expected_stellar_data = StellarTransactionData {
1039 source_account: TEST_PK.to_string(),
1040 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1041 fee: Some(100),
1042 sequence_number: Some(1),
1043 transaction_input: TransactionInput::Operations(vec![]),
1044 memo: None,
1045 valid_until: None,
1046 signatures: vec![],
1047 hash: None,
1048 simulation_transaction_data: None,
1049 signed_envelope_xdr: Some("test-xdr".to_string()),
1050 };
1051
1052 let expected_xdr = expected_stellar_data.signed_envelope_xdr.clone();
1053 mocks
1054 .tx_repo
1055 .expect_partial_update()
1056 .withf(move |id, upd| {
1057 id == "tx-1"
1058 && upd.status == Some(TransactionStatus::Sent)
1059 && if let Some(NetworkTransactionData::Stellar(ref data)) = upd.network_data {
1060 data.signed_envelope_xdr == expected_xdr
1061 } else {
1062 false
1063 }
1064 })
1065 .returning(|id, upd| {
1066 let mut tx = create_test_transaction("relayer-1");
1067 tx.id = id;
1068 tx.status = upd.status.unwrap();
1069 tx.network_data = upd.network_data.unwrap();
1070 Ok::<_, RepositoryError>(tx)
1071 });
1072
1073 mocks
1075 .job_producer
1076 .expect_produce_submit_transaction_job()
1077 .times(1)
1078 .returning(|_, _| Box::pin(async { Ok(()) }));
1079
1080 mocks
1081 .job_producer
1082 .expect_produce_send_notification_job()
1083 .times(1)
1084 .returning(|_, _| Box::pin(async { Ok(()) }));
1085
1086 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
1087
1088 let result = update_and_notify_transaction(
1090 handler.transaction_repository(),
1091 handler.job_producer(),
1092 "tx-1".to_string(),
1093 expected_stellar_data,
1094 handler.relayer().notification_id.as_deref(),
1095 )
1096 .await;
1097
1098 assert!(result.is_ok());
1099 let updated_tx = result.unwrap();
1100 assert_eq!(updated_tx.status, TransactionStatus::Sent);
1101
1102 if let NetworkTransactionData::Stellar(data) = &updated_tx.network_data {
1103 assert_eq!(data.signed_envelope_xdr, Some("test-xdr".to_string()));
1104 } else {
1105 panic!("Expected Stellar transaction data");
1106 }
1107 }
1108}