openzeppelin_relayer/domain/transaction/stellar/prepare/
mod.rs

1//! This module contains the preparation-related functionality for Stellar transactions.
2//! It includes methods for preparing transactions with robust error handling,
3//! ensuring lanes are always properly cleaned up on failure.
4
5// Declare submodules from the prepare/ directory
6pub 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    /// Main preparation method with robust error handling and guaranteed lane cleanup.
38    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        // Call core preparation logic with error handling
54        match self.prepare_core(tx.clone()).await {
55            Ok(prepared_tx) => Ok(prepared_tx),
56            Err(error) => {
57                // Always cleanup on failure - this is the critical safety mechanism
58                self.handle_prepare_failure(tx, error).await
59            }
60        }
61    }
62
63    /// Core preparation logic
64    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        // Simple dispatch to appropriate processing function based on input type
71        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    /// Helper to sign and finalize transactions for Operations and UnsignedXdr inputs.
123    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    /// Handles preparation failures with comprehensive cleanup and error reporting.
141    /// This method ensures lanes are never left claimed after any failure.
142    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(); // Clone the ID before moving tx
149        warn!("Transaction {} preparation failed: {}", tx_id, error_reason);
150
151        // Step 1: Sync sequence from chain to recover from any potential sequence drift
152        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            // Always sync from chain on preparation failure to ensure correct sequence state
158            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        // Step 2: Mark transaction as Failed with detailed reason
178        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                // Continue with cleanup even if we can't update the transaction
194                tx
195            }
196        };
197
198        // Step 3: Attempt to enqueue next pending transaction or release lane
199        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            // Fallback: release lane directly if we can't hand it over
205            lane_gate::free(&self.relayer().id, &tx_id);
206        }
207
208        // Step 4: Log failure for monitoring (prepare_fail_total metric would go here)
209        info!(
210            "Transaction {} preparation failure handled. Lane cleaned up. Error: {}",
211            tx_id, error_reason
212        );
213
214        // Step 5: Return original error to maintain API compatibility
215        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        // sequence counter
238        mocks
239            .counter
240            .expect_get_and_increment()
241            .returning(|_, _| Box::pin(ready(Ok(1))));
242
243        // signer
244        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        // submit-job + notification
269        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        // sequence counter
293        mocks
294            .counter
295            .expect_get_and_increment()
296            .returning(|_, _| Box::pin(ready(Ok(1))));
297
298        // signer
299        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        // submit-job + notification
324        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        // Verify the signed_envelope_xdr was populated
343        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                // Verify it's valid XDR by attempting to parse it
351                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                // Verify the envelope has signatures
359                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        // Mock sequence counter to fail
379        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        // Mock sync_sequence_from_chain for error handling
388        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        // Mock finalize_transaction_state for failure handling
420        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        // Mock notification for failed transaction
432        mocks
433            .job_producer
434            .expect_produce_send_notification_job()
435            .times(1)
436            .returning(|_, _| Box::pin(async { Ok(()) }));
437
438        // Mock find_by_status for enqueue_next_pending_transaction
439        mocks
440            .tx_repo
441            .expect_find_by_status()
442            .returning(|_, _| Ok(vec![])); // No pending transactions
443
444        let handler = make_stellar_tx_handler(relayer.clone(), mocks);
445        let mut tx = create_test_transaction(&relayer.id);
446
447        // Remove the sequence number since it wouldn't be set if get_and_increment fails
448        if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
449            data.sequence_number = None;
450        }
451
452        // Verify that lane is claimed initially
453        assert!(lane_gate::claim(&relayer.id, &tx.id));
454
455        let result = handler.prepare_transaction_impl(tx.clone()).await;
456
457        // Should return error but lane should be cleaned up
458        assert!(result.is_err());
459
460        // Verify lane is released - another transaction should be able to claim it
461        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        // sequence counter succeeds
472        mocks
473            .counter
474            .expect_get_and_increment()
475            .returning(|_, _| Box::pin(ready(Ok(1))));
476
477        // Expect sync_sequence_from_chain to be called in handle_prepare_failure
478        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        // signer fails
510        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        // Mock finalize_transaction_state for failure handling
519        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        // Mock notification for failed transaction
531        mocks
532            .job_producer
533            .expect_produce_send_notification_job()
534            .times(1)
535            .returning(|_, _| Box::pin(async { Ok(()) }));
536
537        // Mock find_by_status for enqueue_next_pending_transaction
538        mocks
539            .tx_repo
540            .expect_find_by_status()
541            .returning(|_, _| Ok(vec![])); // No pending transactions
542
543        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        // Should return error but lane should be cleaned up
549        assert!(result.is_err());
550
551        // Verify lane is released
552        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); // cleanup
555    }
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(); // Use unique relayer ID
561        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        // Claim lane with different transaction
567        assert!(lane_gate::claim(&relayer.id, "other-tx"));
568
569        let result = handler.prepare_transaction_impl(tx.clone()).await;
570
571        // Should return Ok with original transaction (waiting)
572        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        // Cleanup
578        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        // Track sequence operations
587        let sequence_value = 42u64;
588
589        // Mock get_and_increment to return 42
590        mocks
591            .counter
592            .expect_get_and_increment()
593            .times(1)
594            .returning(move |_, _| Box::pin(ready(Ok(sequence_value))));
595
596        // Mock sync_sequence_from_chain to verify it's called on failure
597        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), // On-chain sequence is 41
612                    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) // Next usable = 41 + 1
628            .returning(|_, _, _| Box::pin(ready(Ok(()))));
629
630        // Mock signer to fail after sequence is incremented
631        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        // Mock transaction update for failure
644        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        // Mock notification
656        mocks
657            .job_producer
658            .expect_produce_send_notification_job()
659            .times(1)
660            .returning(|_, _| Box::pin(async { Ok(()) }));
661
662        // Mock find_by_status for enqueue_next_pending_transaction
663        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        // Should fail with signing error
674        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        // Mock sequence increment
689        mocks
690            .counter
691            .expect_get_and_increment()
692            .times(1)
693            .returning(|_, _| Box::pin(ready(Ok(100))));
694
695        // Mock sync on failure
696        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        // Mock provider to fail simulation for Soroban operations
729        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        // Mock transaction update for failure
738        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        // Mock notification and enqueue
750        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        // Create transaction with Soroban operation to trigger simulation
764        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        // Should fail with provider error
779        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        // For unsigned XDR, validation happens before sequence increment
788        // Source account mismatch is detected before get_and_increment is called
789        // But we still sync sequence on any prepare failure
790
791        // Mock sync_sequence_from_chain
792        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        // Mock transaction update for failure
825        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        // Mock notification and enqueue
837        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        // Create transaction with invalid unsigned XDR
851        let mut tx = create_test_transaction(&relayer.id);
852        if let NetworkTransactionData::Stellar(ref mut data) = tx.network_data {
853            // Remove sequence since it will never be set due to early validation failure
854            data.sequence_number = None;
855            // Use a different source account to trigger validation error
856            data.transaction_input = crate::models::TransactionInput::UnsignedXdr(
857                // This will fail validation due to source account mismatch
858                "AAAAAgAAAAA5MbUzuTfU6p3NeJp5w3TpKhZmx6p1pR7mq9wFwCnEIgAAAGQAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAADk4GIHV/3i2tOMBkqKqN3Y9x3FvNm8z4B5PEzPn7hEaAAAAAAAAAAAAAAAZAAAAAAAAAAA".to_string()
859            );
860        }
861
862        let result = handler.prepare_transaction_impl(tx).await;
863
864        // Should fail with validation error
865        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        // Mock the repository update
890        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        // Mock job production
926        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        // Test update_and_notify_transaction directly
941        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}