1pub mod common;
7pub mod fee_bump;
8pub mod operations;
9pub mod unsigned_xdr;
10
11use eyre::Result;
12use log::{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 !lane_gate::claim(&self.relayer().id, &tx.id) {
43 info!(
44 "Relayer {} already has a transaction in flight – {} must wait.",
45 self.relayer().id,
46 tx.id
47 );
48 return Ok(tx);
49 }
50
51 info!("Preparing transaction: {:?}", tx.id);
52
53 match self.prepare_core(tx.clone()).await {
55 Ok(prepared_tx) => Ok(prepared_tx),
56 Err(error) => {
57 self.handle_prepare_failure(tx, error).await
59 }
60 }
61 }
62
63 async fn prepare_core(
65 &self,
66 tx: TransactionRepoModel,
67 ) -> Result<TransactionRepoModel, TransactionError> {
68 let stellar_data = tx.network_data.get_stellar_transaction_data()?;
69
70 match &stellar_data.transaction_input {
72 TransactionInput::Operations(_) => {
73 info!("Preparing operations-based transaction {}", tx.id);
74 let stellar_data_with_sim = operations::process_operations(
75 self.transaction_counter_service(),
76 &self.relayer().id,
77 &self.relayer().address,
78 &tx,
79 stellar_data,
80 self.provider(),
81 self.signer(),
82 )
83 .await?;
84 self.finalize_with_signature(tx, stellar_data_with_sim)
85 .await
86 }
87 TransactionInput::UnsignedXdr(_) => {
88 info!("Preparing unsigned XDR transaction {}", tx.id);
89 let stellar_data_with_sim = unsigned_xdr::process_unsigned_xdr(
90 self.transaction_counter_service(),
91 &self.relayer().id,
92 &self.relayer().address,
93 stellar_data,
94 self.provider(),
95 self.signer(),
96 )
97 .await?;
98 self.finalize_with_signature(tx, stellar_data_with_sim)
99 .await
100 }
101 TransactionInput::SignedXdr { .. } => {
102 info!("Preparing fee-bump transaction {}", tx.id);
103 let stellar_data_with_fee_bump = fee_bump::process_fee_bump(
104 &self.relayer().address,
105 stellar_data,
106 self.provider(),
107 self.signer(),
108 )
109 .await?;
110 update_and_notify_transaction(
111 self.transaction_repository(),
112 self.job_producer(),
113 tx.id,
114 stellar_data_with_fee_bump,
115 self.relayer().notification_id.as_deref(),
116 )
117 .await
118 }
119 }
120 }
121
122 async fn finalize_with_signature(
124 &self,
125 tx: TransactionRepoModel,
126 stellar_data: crate::models::StellarTransactionData,
127 ) -> Result<TransactionRepoModel, TransactionError> {
128 let (tx, final_stellar_data) =
129 sign_and_finalize_transaction(self.signer(), tx, stellar_data).await?;
130 update_and_notify_transaction(
131 self.transaction_repository(),
132 self.job_producer(),
133 tx.id,
134 final_stellar_data,
135 self.relayer().notification_id.as_deref(),
136 )
137 .await
138 }
139
140 async fn handle_prepare_failure(
143 &self,
144 tx: TransactionRepoModel,
145 error: TransactionError,
146 ) -> Result<TransactionRepoModel, TransactionError> {
147 let error_reason = format!("Preparation failed: {}", error);
148 let tx_id = tx.id.clone(); warn!("Transaction {} preparation failed: {}", tx_id, error_reason);
150
151 if let Ok(stellar_data) = tx.network_data.get_stellar_transaction_data() {
153 info!(
154 "Syncing sequence from chain after failed transaction {} preparation",
155 tx_id
156 );
157 match self
159 .sync_sequence_from_chain(&stellar_data.source_account)
160 .await
161 {
162 Ok(()) => {
163 info!(
164 "Successfully synced sequence from chain for transaction {}",
165 tx_id
166 );
167 }
168 Err(sync_error) => {
169 warn!(
170 "Failed to sync sequence from chain for transaction {}: {}",
171 tx_id, sync_error
172 );
173 }
174 }
175 }
176
177 let update_request = TransactionUpdateRequest {
179 status: Some(TransactionStatus::Failed),
180 status_reason: Some(error_reason.clone()),
181 ..Default::default()
182 };
183 let _failed_tx = match self
184 .finalize_transaction_state(tx_id.clone(), update_request)
185 .await
186 {
187 Ok(updated_tx) => updated_tx,
188 Err(finalize_error) => {
189 warn!(
190 "Failed to mark transaction {} as failed: {}. Proceeding with lane cleanup.",
191 tx_id, finalize_error
192 );
193 tx
195 }
196 };
197
198 if let Err(enqueue_error) = self.enqueue_next_pending_transaction(&tx_id).await {
200 warn!(
201 "Failed to enqueue next pending transaction after {} failure: {}. Releasing lane directly.",
202 tx_id, enqueue_error
203 );
204 lane_gate::free(&self.relayer().id, &tx_id);
206 }
207
208 info!(
210 "Transaction {} preparation failure handled. Lane cleaned up. Error: {}",
211 tx_id, error_reason
212 );
213
214 Err(error)
216 }
217}
218
219#[cfg(test)]
220mod prepare_transaction_tests {
221 use std::future::ready;
222
223 use super::*;
224 use crate::{
225 domain::SignTransactionResponse,
226 models::{NetworkTransactionData, OperationSpec, RepositoryError, TransactionStatus},
227 };
228 use soroban_rs::xdr::{Limits, ReadXdr, TransactionEnvelope};
229
230 use crate::domain::transaction::stellar::test_helpers::*;
231
232 #[tokio::test]
233 async fn prepare_transaction_happy_path() {
234 let relayer = create_test_relayer();
235 let mut mocks = default_test_mocks();
236
237 mocks
239 .counter
240 .expect_get_and_increment()
241 .returning(|_, _| Box::pin(ready(Ok(1))));
242
243 mocks.signer.expect_sign_transaction().returning(|_| {
245 Box::pin(async {
246 Ok(SignTransactionResponse::Stellar(
247 crate::domain::SignTransactionResponseStellar {
248 signature: dummy_signature(),
249 },
250 ))
251 })
252 });
253
254 mocks
255 .tx_repo
256 .expect_partial_update()
257 .withf(|_, upd| {
258 upd.status == Some(TransactionStatus::Sent) && upd.network_data.is_some()
259 })
260 .returning(|id, upd| {
261 let mut tx = create_test_transaction("relayer-1");
262 tx.id = id;
263 tx.status = upd.status.unwrap();
264 tx.network_data = upd.network_data.unwrap();
265 Ok::<_, RepositoryError>(tx)
266 });
267
268 mocks
270 .job_producer
271 .expect_produce_submit_transaction_job()
272 .times(1)
273 .returning(|_, _| Box::pin(async { Ok(()) }));
274
275 mocks
276 .job_producer
277 .expect_produce_send_notification_job()
278 .times(1)
279 .returning(|_, _| Box::pin(async { Ok(()) }));
280
281 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
282 let tx = create_test_transaction(&relayer.id);
283
284 assert!(handler.prepare_transaction_impl(tx).await.is_ok());
285 }
286
287 #[tokio::test]
288 async fn prepare_transaction_stores_signed_envelope_xdr() {
289 let relayer = create_test_relayer();
290 let mut mocks = default_test_mocks();
291
292 mocks
294 .counter
295 .expect_get_and_increment()
296 .returning(|_, _| Box::pin(ready(Ok(1))));
297
298 mocks.signer.expect_sign_transaction().returning(|_| {
300 Box::pin(async {
301 Ok(SignTransactionResponse::Stellar(
302 crate::domain::SignTransactionResponseStellar {
303 signature: dummy_signature(),
304 },
305 ))
306 })
307 });
308
309 mocks
310 .tx_repo
311 .expect_partial_update()
312 .withf(|_, upd| {
313 upd.status == Some(TransactionStatus::Sent) && upd.network_data.is_some()
314 })
315 .returning(move |id, upd| {
316 let mut tx = create_test_transaction("relayer-1");
317 tx.id = id;
318 tx.status = upd.status.unwrap();
319 tx.network_data = upd.network_data.clone().unwrap();
320 Ok::<_, RepositoryError>(tx)
321 });
322
323 mocks
325 .job_producer
326 .expect_produce_submit_transaction_job()
327 .times(1)
328 .returning(|_, _| Box::pin(async { Ok(()) }));
329
330 mocks
331 .job_producer
332 .expect_produce_send_notification_job()
333 .times(1)
334 .returning(|_, _| Box::pin(async { Ok(()) }));
335
336 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
337 let tx = create_test_transaction(&relayer.id);
338
339 let result = handler.prepare_transaction_impl(tx).await;
340 assert!(result.is_ok());
341
342 if let Ok(prepared_tx) = result {
344 if let NetworkTransactionData::Stellar(stellar_data) = &prepared_tx.network_data {
345 assert!(
346 stellar_data.signed_envelope_xdr.is_some(),
347 "signed_envelope_xdr should be populated"
348 );
349
350 let xdr = stellar_data.signed_envelope_xdr.as_ref().unwrap();
352 let envelope_result = TransactionEnvelope::from_xdr_base64(xdr, Limits::none());
353 assert!(
354 envelope_result.is_ok(),
355 "signed_envelope_xdr should be valid XDR"
356 );
357
358 if let Ok(envelope) = envelope_result {
360 match envelope {
361 TransactionEnvelope::Tx(ref e) => {
362 assert!(!e.signatures.is_empty(), "Envelope should have signatures");
363 }
364 _ => panic!("Expected Tx envelope type"),
365 }
366 }
367 } else {
368 panic!("Expected Stellar transaction data");
369 }
370 }
371 }
372
373 #[tokio::test]
374 async fn prepare_transaction_sequence_failure_cleans_up_lane() {
375 let relayer = create_test_relayer();
376 let mut mocks = default_test_mocks();
377
378 mocks.counter.expect_get_and_increment().returning(|_, _| {
380 Box::pin(async {
381 Err(RepositoryError::NotFound(
382 "Counter service failure".to_string(),
383 ))
384 })
385 });
386
387 mocks.provider.expect_get_account().returning(|_| {
389 Box::pin(async {
390 use soroban_rs::xdr::{
391 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
392 Thresholds, Uint256,
393 };
394 use stellar_strkey::ed25519;
395
396 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
397 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
398
399 Ok(AccountEntry {
400 account_id,
401 balance: 1000000,
402 seq_num: SequenceNumber(0),
403 num_sub_entries: 0,
404 inflation_dest: None,
405 flags: 0,
406 home_domain: String32::default(),
407 thresholds: Thresholds([1, 1, 1, 1]),
408 signers: Default::default(),
409 ext: AccountEntryExt::V0,
410 })
411 })
412 });
413
414 mocks
415 .counter
416 .expect_set()
417 .returning(|_, _, _| Box::pin(ready(Ok(()))));
418
419 mocks
421 .tx_repo
422 .expect_partial_update()
423 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
424 .returning(|id, upd| {
425 let mut tx = create_test_transaction("relayer-1");
426 tx.id = id;
427 tx.status = upd.status.unwrap();
428 Ok::<_, RepositoryError>(tx)
429 });
430
431 mocks
433 .job_producer
434 .expect_produce_send_notification_job()
435 .times(1)
436 .returning(|_, _| Box::pin(async { Ok(()) }));
437
438 mocks
440 .tx_repo
441 .expect_find_by_status()
442 .returning(|_, _| Ok(vec![])); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
445 let mut tx = create_test_transaction(&relayer.id);
446
447 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
449 data.sequence_number = None;
450 }
451
452 assert!(lane_gate::claim(&relayer.id, &tx.id));
454
455 let result = handler.prepare_transaction_impl(tx.clone()).await;
456
457 assert!(result.is_err());
459
460 let another_tx_id = "another-tx";
462 assert!(lane_gate::claim(&relayer.id, another_tx_id));
463 lane_gate::free(&relayer.id, another_tx_id)
464 }
465
466 #[tokio::test]
467 async fn prepare_transaction_signer_failure_cleans_up_lane() {
468 let relayer = create_test_relayer();
469 let mut mocks = default_test_mocks();
470
471 mocks
473 .counter
474 .expect_get_and_increment()
475 .returning(|_, _| Box::pin(ready(Ok(1))));
476
477 mocks.provider.expect_get_account().returning(|_| {
479 Box::pin(async {
480 use soroban_rs::xdr::{
481 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
482 Thresholds, Uint256,
483 };
484 use stellar_strkey::ed25519;
485
486 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
487 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
488
489 Ok(AccountEntry {
490 account_id,
491 balance: 1000000,
492 seq_num: SequenceNumber(0),
493 num_sub_entries: 0,
494 inflation_dest: None,
495 flags: 0,
496 home_domain: String32::default(),
497 thresholds: Thresholds([1, 1, 1, 1]),
498 signers: Default::default(),
499 ext: AccountEntryExt::V0,
500 })
501 })
502 });
503
504 mocks
505 .counter
506 .expect_set()
507 .returning(|_, _, _| Box::pin(ready(Ok(()))));
508
509 mocks.signer.expect_sign_transaction().returning(|_| {
511 Box::pin(async {
512 Err(crate::models::SignerError::SigningError(
513 "Signer failure".to_string(),
514 ))
515 })
516 });
517
518 mocks
520 .tx_repo
521 .expect_partial_update()
522 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
523 .returning(|id, upd| {
524 let mut tx = create_test_transaction("relayer-1");
525 tx.id = id;
526 tx.status = upd.status.unwrap();
527 Ok::<_, RepositoryError>(tx)
528 });
529
530 mocks
532 .job_producer
533 .expect_produce_send_notification_job()
534 .times(1)
535 .returning(|_, _| Box::pin(async { Ok(()) }));
536
537 mocks
539 .tx_repo
540 .expect_find_by_status()
541 .returning(|_, _| Ok(vec![])); let handler = make_stellar_tx_handler(relayer.clone(), mocks);
544 let tx = create_test_transaction(&relayer.id);
545
546 let result = handler.prepare_transaction_impl(tx.clone()).await;
547
548 assert!(result.is_err());
550
551 let another_tx_id = "another-tx";
553 assert!(lane_gate::claim(&relayer.id, another_tx_id));
554 lane_gate::free(&relayer.id, another_tx_id); }
556
557 #[tokio::test]
558 async fn prepare_transaction_already_claimed_lane_returns_original() {
559 let mut relayer = create_test_relayer();
560 relayer.id = "unique-relayer-for-lane-test".to_string(); let mocks = default_test_mocks();
562
563 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
564 let tx = create_test_transaction(&relayer.id);
565
566 assert!(lane_gate::claim(&relayer.id, "other-tx"));
568
569 let result = handler.prepare_transaction_impl(tx.clone()).await;
570
571 assert!(result.is_ok());
573 let returned_tx = result.unwrap();
574 assert_eq!(returned_tx.id, tx.id);
575 assert_eq!(returned_tx.status, tx.status);
576
577 lane_gate::free(&relayer.id, "other-tx");
579 }
580
581 #[tokio::test]
582 async fn test_prepare_failure_syncs_sequence() {
583 let relayer = create_test_relayer();
584 let mut mocks = default_test_mocks();
585
586 let sequence_value = 42u64;
588
589 mocks
591 .counter
592 .expect_get_and_increment()
593 .times(1)
594 .returning(move |_, _| Box::pin(ready(Ok(sequence_value))));
595
596 mocks.provider.expect_get_account().times(1).returning(|_| {
598 Box::pin(async {
599 use soroban_rs::xdr::{
600 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
601 Thresholds, Uint256,
602 };
603 use stellar_strkey::ed25519;
604
605 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
606 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
607
608 Ok(AccountEntry {
609 account_id,
610 balance: 1000000,
611 seq_num: SequenceNumber(41), num_sub_entries: 0,
613 inflation_dest: None,
614 flags: 0,
615 home_domain: String32::default(),
616 thresholds: Thresholds([1, 1, 1, 1]),
617 signers: Default::default(),
618 ext: AccountEntryExt::V0,
619 })
620 })
621 });
622
623 mocks
624 .counter
625 .expect_set()
626 .times(1)
627 .withf(|_, _, seq| *seq == 42) .returning(|_, _, _| Box::pin(ready(Ok(()))));
629
630 mocks
632 .signer
633 .expect_sign_transaction()
634 .times(1)
635 .returning(|_| {
636 Box::pin(async {
637 Err(crate::models::SignerError::SigningError(
638 "Simulated signing failure".to_string(),
639 ))
640 })
641 });
642
643 mocks
645 .tx_repo
646 .expect_partial_update()
647 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
648 .returning(|id, upd| {
649 let mut tx = create_test_transaction("relayer-1");
650 tx.id = id;
651 tx.status = upd.status.unwrap();
652 Ok::<_, RepositoryError>(tx)
653 });
654
655 mocks
657 .job_producer
658 .expect_produce_send_notification_job()
659 .times(1)
660 .returning(|_, _| Box::pin(async { Ok(()) }));
661
662 mocks
664 .tx_repo
665 .expect_find_by_status()
666 .returning(|_, _| Ok(vec![]));
667
668 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
669 let tx = create_test_transaction(&relayer.id);
670
671 let result = handler.prepare_transaction_impl(tx).await;
672
673 assert!(result.is_err());
675 match result.unwrap_err() {
676 TransactionError::SignerError(msg) => {
677 assert!(msg.contains("Simulated signing failure"));
678 }
679 _ => panic!("Expected SignerError"),
680 }
681 }
682
683 #[tokio::test]
684 async fn test_prepare_simulation_failure_syncs_sequence() {
685 let relayer = create_test_relayer();
686 let mut mocks = default_test_mocks();
687
688 mocks
690 .counter
691 .expect_get_and_increment()
692 .times(1)
693 .returning(|_, _| Box::pin(ready(Ok(100))));
694
695 mocks.provider.expect_get_account().times(1).returning(|_| {
697 Box::pin(async {
698 use soroban_rs::xdr::{
699 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
700 Thresholds, Uint256,
701 };
702 use stellar_strkey::ed25519;
703
704 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
705 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
706
707 Ok(AccountEntry {
708 account_id,
709 balance: 1000000,
710 seq_num: SequenceNumber(99),
711 num_sub_entries: 0,
712 inflation_dest: None,
713 flags: 0,
714 home_domain: String32::default(),
715 thresholds: Thresholds([1, 1, 1, 1]),
716 signers: Default::default(),
717 ext: AccountEntryExt::V0,
718 })
719 })
720 });
721
722 mocks
723 .counter
724 .expect_set()
725 .times(1)
726 .returning(|_, _, _| Box::pin(ready(Ok(()))));
727
728 mocks
730 .provider
731 .expect_simulate_transaction_envelope()
732 .times(1)
733 .returning(|_| {
734 Box::pin(async { Err(eyre::eyre!("Simulation failed: insufficient resources")) })
735 });
736
737 mocks
739 .tx_repo
740 .expect_partial_update()
741 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
742 .returning(|id, upd| {
743 let mut tx = create_test_transaction("relayer-1");
744 tx.id = id;
745 tx.status = upd.status.unwrap();
746 Ok::<_, RepositoryError>(tx)
747 });
748
749 mocks
751 .job_producer
752 .expect_produce_send_notification_job()
753 .times(1)
754 .returning(|_, _| Box::pin(async { Ok(()) }));
755
756 mocks
757 .tx_repo
758 .expect_find_by_status()
759 .returning(|_, _| Ok(vec![]));
760
761 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
762
763 let mut tx = create_test_transaction(&relayer.id);
765 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
766 data.transaction_input =
767 crate::models::TransactionInput::Operations(vec![OperationSpec::InvokeContract {
768 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
769 .to_string(),
770 function_name: "test".to_string(),
771 args: vec![],
772 auth: None,
773 }]);
774 }
775
776 let result = handler.prepare_transaction_impl(tx).await;
777
778 assert!(result.is_err());
780 }
781
782 #[tokio::test]
783 async fn test_prepare_xdr_parsing_failure_syncs_sequence() {
784 let relayer = create_test_relayer();
785 let mut mocks = default_test_mocks();
786
787 mocks.provider.expect_get_account().times(1).returning(|_| {
793 Box::pin(async {
794 use soroban_rs::xdr::{
795 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32,
796 Thresholds, Uint256,
797 };
798 use stellar_strkey::ed25519;
799
800 let pk = ed25519::PublicKey::from_string(TEST_PK).unwrap();
801 let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0)));
802
803 Ok(AccountEntry {
804 account_id,
805 balance: 1000000,
806 seq_num: SequenceNumber(50),
807 num_sub_entries: 0,
808 inflation_dest: None,
809 flags: 0,
810 home_domain: String32::default(),
811 thresholds: Thresholds([1, 1, 1, 1]),
812 signers: Default::default(),
813 ext: AccountEntryExt::V0,
814 })
815 })
816 });
817
818 mocks
819 .counter
820 .expect_set()
821 .times(1)
822 .returning(|_, _, _| Box::pin(ready(Ok(()))));
823
824 mocks
826 .tx_repo
827 .expect_partial_update()
828 .withf(|_, upd| upd.status == Some(TransactionStatus::Failed))
829 .returning(|id, upd| {
830 let mut tx = create_test_transaction("relayer-1");
831 tx.id = id;
832 tx.status = upd.status.unwrap();
833 Ok::<_, RepositoryError>(tx)
834 });
835
836 mocks
838 .job_producer
839 .expect_produce_send_notification_job()
840 .times(1)
841 .returning(|_, _| Box::pin(async { Ok(()) }));
842
843 mocks
844 .tx_repo
845 .expect_find_by_status()
846 .returning(|_, _| Ok(vec![]));
847
848 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
849
850 let mut tx = create_test_transaction(&relayer.id);
852 if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
853 data.sequence_number = None;
855 data.transaction_input = crate::models::TransactionInput::UnsignedXdr(
857 "AAAAAgAAAAA5MbUzuTfU6p3NeJp5w3TpKhZmx6p1pR7mq9wFwCnEIgAAAGQAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAADk4GIHV/3i2tOMBkqKqN3Y9x3FvNm8z4B5PEzPn7hEaAAAAAAAAAAAAAAAZAAAAAAAAAAA".to_string()
859 );
860 }
861
862 let result = handler.prepare_transaction_impl(tx).await;
863
864 assert!(result.is_err());
866 match result.unwrap_err() {
867 TransactionError::ValidationError(msg) => {
868 assert!(msg.contains("does not match relayer account"));
869 }
870 _ => panic!("Expected ValidationError"),
871 }
872 }
873}
874
875#[cfg(test)]
876mod refactoring_tests {
877 use crate::domain::transaction::stellar::prepare::common::update_and_notify_transaction;
878 use crate::domain::transaction::stellar::test_helpers::*;
879 use crate::models::{
880 NetworkTransactionData, RepositoryError, StellarTransactionData, TransactionInput,
881 TransactionStatus,
882 };
883
884 #[tokio::test]
885 async fn test_update_and_notify_transaction_consistency() {
886 let relayer = create_test_relayer();
887 let mut mocks = default_test_mocks();
888
889 let expected_stellar_data = StellarTransactionData {
891 source_account: TEST_PK.to_string(),
892 network_passphrase: "Test SDF Network ; September 2015".to_string(),
893 fee: Some(100),
894 sequence_number: Some(1),
895 transaction_input: TransactionInput::Operations(vec![]),
896 memo: None,
897 valid_until: None,
898 signatures: vec![],
899 hash: None,
900 simulation_transaction_data: None,
901 signed_envelope_xdr: Some("test-xdr".to_string()),
902 };
903
904 let expected_xdr = expected_stellar_data.signed_envelope_xdr.clone();
905 mocks
906 .tx_repo
907 .expect_partial_update()
908 .withf(move |id, upd| {
909 id == "tx-1"
910 && upd.status == Some(TransactionStatus::Sent)
911 && if let Some(NetworkTransactionData::Stellar(ref data)) = upd.network_data {
912 data.signed_envelope_xdr == expected_xdr
913 } else {
914 false
915 }
916 })
917 .returning(|id, upd| {
918 let mut tx = create_test_transaction("relayer-1");
919 tx.id = id;
920 tx.status = upd.status.unwrap();
921 tx.network_data = upd.network_data.unwrap();
922 Ok::<_, RepositoryError>(tx)
923 });
924
925 mocks
927 .job_producer
928 .expect_produce_submit_transaction_job()
929 .times(1)
930 .returning(|_, _| Box::pin(async { Ok(()) }));
931
932 mocks
933 .job_producer
934 .expect_produce_send_notification_job()
935 .times(1)
936 .returning(|_, _| Box::pin(async { Ok(()) }));
937
938 let handler = make_stellar_tx_handler(relayer.clone(), mocks);
939
940 let result = update_and_notify_transaction(
942 handler.transaction_repository(),
943 handler.job_producer(),
944 "tx-1".to_string(),
945 expected_stellar_data,
946 handler.relayer().notification_id.as_deref(),
947 )
948 .await;
949
950 assert!(result.is_ok());
951 let updated_tx = result.unwrap();
952 assert_eq!(updated_tx.status, TransactionStatus::Sent);
953
954 if let NetworkTransactionData::Stellar(data) = &updated_tx.network_data {
955 assert_eq!(data.signed_envelope_xdr, Some("test-xdr".to_string()));
956 } else {
957 panic!("Expected Stellar transaction data");
958 }
959 }
960}