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