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