1use async_trait::async_trait;
7use chrono::Utc;
8use eyre::Result;
9use solana_sdk::{pubkey::Pubkey, transaction::Transaction as SolanaTransaction};
10use std::str::FromStr;
11use std::sync::Arc;
12use tracing::{debug, error, info, warn};
13
14use crate::{
15 domain::transaction::{
16 solana::{
17 utils::{
18 build_transaction_from_instructions, decode_solana_transaction,
19 decode_solana_transaction_from_string, is_resubmitable,
20 },
21 validation::SolanaTransactionValidator,
22 },
23 Transaction,
24 },
25 jobs::{JobProducer, JobProducerTrait, StatusCheckContext, TransactionSend},
26 models::{
27 produce_transaction_update_notification_payload, EncodedSerializedTransaction,
28 NetworkTransactionData, NetworkTransactionRequest, RelayerRepoModel, SolanaTransactionData,
29 TransactionError, TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
30 },
31 repositories::{
32 RelayerRepository, RelayerRepositoryStorage, Repository, TransactionRepository,
33 TransactionRepositoryStorage,
34 },
35 services::{
36 provider::{SolanaProvider, SolanaProviderError, SolanaProviderTrait},
37 signer::{SolanaSignTrait, SolanaSigner},
38 },
39};
40
41#[allow(dead_code)]
42pub struct SolanaRelayerTransaction<P, RR, TR, J, S>
43where
44 P: SolanaProviderTrait + Send + Sync + 'static,
45 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
46 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
47 J: JobProducerTrait + Send + Sync + 'static,
48 S: SolanaSignTrait + Send + Sync + 'static,
49{
50 relayer: RelayerRepoModel,
51 relayer_repository: Arc<RR>,
52 provider: Arc<P>,
53 job_producer: Arc<J>,
54 transaction_repository: Arc<TR>,
55 signer: Arc<S>,
56}
57
58pub type DefaultSolanaTransaction = SolanaRelayerTransaction<
59 SolanaProvider,
60 RelayerRepositoryStorage,
61 TransactionRepositoryStorage,
62 JobProducer,
63 SolanaSigner,
64>;
65
66#[allow(dead_code)]
67impl<P, RR, TR, J, S> SolanaRelayerTransaction<P, RR, TR, J, S>
68where
69 P: SolanaProviderTrait + Send + Sync + 'static,
70 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
71 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
72 J: JobProducerTrait + Send + Sync + 'static,
73 S: SolanaSignTrait + Send + Sync + 'static,
74{
75 pub fn new(
76 relayer: RelayerRepoModel,
77 relayer_repository: Arc<RR>,
78 provider: Arc<P>,
79 transaction_repository: Arc<TR>,
80 job_producer: Arc<J>,
81 signer: Arc<S>,
82 ) -> Result<Self, TransactionError> {
83 Ok(Self {
84 relayer,
85 relayer_repository,
86 provider,
87 transaction_repository,
88 job_producer,
89 signer,
90 })
91 }
92
93 pub(super) fn provider(&self) -> &P {
94 &self.provider
95 }
96
97 pub(super) fn transaction_repository(&self) -> &TR {
98 &self.transaction_repository
99 }
100
101 pub(super) fn relayer(&self) -> &RelayerRepoModel {
102 &self.relayer
103 }
104
105 pub(super) fn job_producer(&self) -> &J {
106 &self.job_producer
107 }
108
109 pub(super) fn signer(&self) -> &S {
110 &self.signer
111 }
112
113 async fn prepare_transaction_impl(
115 &self,
116 tx: TransactionRepoModel,
117 ) -> Result<TransactionRepoModel, TransactionError> {
118 debug!(
119 tx_id = %tx.id,
120 relayer_id = %tx.relayer_id,
121 status = ?tx.status,
122 "preparing Solana transaction"
123 );
124
125 if tx.status != TransactionStatus::Pending {
128 debug!(
129 tx_id = %tx.id,
130 status = ?tx.status,
131 expected_status = ?TransactionStatus::Pending,
132 "transaction not in Pending status, skipping preparation"
133 );
134 return Ok(tx);
135 }
136
137 let solana_data = tx.network_data.get_solana_transaction_data()?;
138
139 let mut transaction = if let Some(transaction_str) = &solana_data.transaction {
141 debug!(
144 tx_id = %tx.id,
145 relayer_id = %tx.relayer_id,
146 "transaction mode: using pre-built transaction with provided blockhash"
147 );
148 decode_solana_transaction_from_string(transaction_str)?
149 } else if let Some(instructions) = &solana_data.instructions {
150 debug!(
152 tx_id = %tx.id,
153 relayer_id = %tx.relayer_id,
154 "instructions mode: building transaction with fresh blockhash"
155 );
156
157 let payer = Pubkey::from_str(&self.relayer.address).map_err(|e| {
158 TransactionError::ValidationError(format!("Invalid relayer address: {e}"))
159 })?;
160
161 let latest_blockhash = self.provider.get_latest_blockhash().await?;
163
164 build_transaction_from_instructions(instructions, &payer, latest_blockhash)?
165 } else {
166 let validation_error = TransactionError::ValidationError(
168 "Must provide either transaction or instructions".to_string(),
169 );
170
171 let updated_tx = self
172 .fail_transaction_with_notification(&tx, &validation_error)
173 .await?;
174
175 return Ok(updated_tx);
177 };
178
179 if let Err(validation_error) = self.validate_transaction_impl(&transaction).await {
182 let is_transient = validation_error.is_transient();
184
185 if is_transient {
186 warn!(
187 tx_id = %tx.id,
188 relayer_id = %tx.relayer_id,
189 error = %validation_error,
190 "transient validation error (likely RPC/network issue), will retry"
191 );
192 return Err(validation_error);
193 } else {
194 warn!(
196 tx_id = %tx.id,
197 relayer_id = %tx.relayer_id,
198 error = %validation_error,
199 "permanent validation error, marking transaction as failed"
200 );
201
202 let updated_tx = self
203 .fail_transaction_with_notification(&tx, &validation_error)
204 .await?;
205
206 return Ok(updated_tx);
208 }
209 }
210
211 let signature = self
213 .signer
214 .sign(&transaction.message_data())
215 .await
216 .map_err(|e| TransactionError::SignerError(e.to_string()))?;
217
218 transaction.signatures[0] = signature;
219
220 let update = TransactionUpdateRequest {
222 status: Some(TransactionStatus::Sent),
223 network_data: Some(NetworkTransactionData::Solana(SolanaTransactionData {
224 signature: Some(signature.to_string()),
225 transaction: Some(
226 EncodedSerializedTransaction::try_from(&transaction)
227 .map_err(|e| {
228 TransactionError::ValidationError(format!(
229 "Failed to encode transaction: {e}"
230 ))
231 })?
232 .into_inner(),
233 ),
234 instructions: solana_data.instructions,
235 })),
236 ..Default::default()
237 };
238
239 debug!(
240 tx_id = %tx.id,
241 relayer_id = %tx.relayer_id,
242 "updating transaction status to Sent"
243 );
244
245 let updated_tx = self
246 .transaction_repository
247 .partial_update(tx.id.clone(), update)
248 .await?;
249
250 debug!(
251 tx_id = %updated_tx.id,
252 relayer_id = %updated_tx.relayer_id,
253 status = ?updated_tx.status,
254 "transaction updated, enqueueing submit job"
255 );
256
257 self.job_producer
259 .produce_submit_transaction_job(
260 TransactionSend::submit(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
261 None,
262 )
263 .await?;
264
265 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
267 error!(
268 tx_id = %updated_tx.id,
269 status = ?TransactionStatus::Sent,
270 "sending transaction update notification failed after prepare: {:?}",
271 e
272 );
273 }
274
275 Ok(updated_tx)
276 }
277
278 async fn submit_transaction_impl(
280 &self,
281 tx: TransactionRepoModel,
282 ) -> Result<TransactionRepoModel, TransactionError> {
283 debug!(
284 tx_id = %tx.id,
285 relayer_id = %tx.relayer_id,
286 status = ?tx.status,
287 "submitting Solana transaction to blockchain"
288 );
289
290 if tx.status != TransactionStatus::Sent && tx.status != TransactionStatus::Submitted {
291 debug!(
292 tx_id = %tx.id,
293 relayer_id = %tx.relayer_id,
294 status = ?tx.status,
295 "transaction not in expected status for submission, skipping"
296 );
297 return Ok(tx);
298 }
299
300 let solana_data = tx.network_data.get_solana_transaction_data()?;
302 let transaction = decode_solana_transaction(&tx)?;
303
304 match self.provider.send_transaction(&transaction).await {
306 Ok(sig) => sig,
307 Err(provider_error) => {
308 if matches!(provider_error, SolanaProviderError::AlreadyProcessed(_)) {
310 debug!(
311 tx_id = %tx.id,
312 relayer_id = %tx.relayer_id,
313 signature = ?solana_data.signature,
314 "transaction already processed on-chain"
315 );
316
317 return Ok(tx);
320 }
321
322 if matches!(provider_error, SolanaProviderError::BlockhashNotFound(_))
324 && is_resubmitable(&transaction)
325 {
326 debug!(
330 tx_id = %tx.id,
331 relayer_id = %tx.relayer_id,
332 error = %provider_error,
333 "blockhash expired for single-signer transaction, status check will trigger resubmit"
334 );
335 return Ok(tx);
336 }
337
338 error!(
339 tx_id = %tx.id,
340 relayer_id = %tx.relayer_id,
341 error = %provider_error,
342 "failed to send transaction to blockchain"
343 );
344
345 if provider_error.is_transient() {
347 return Err(TransactionError::UnderlyingSolanaProvider(provider_error));
349 } else {
350 let error = TransactionError::UnderlyingSolanaProvider(provider_error);
352 let updated_tx = self.fail_transaction_with_notification(&tx, &error).await?;
353
354 return Ok(updated_tx);
356 }
357 }
358 };
359
360 debug!(
361 tx_id = %tx.id,
362 relayer_id = %tx.relayer_id,
363 "transaction submitted successfully to blockchain"
364 );
365
366 let signature_str = transaction.signatures[0].to_string();
369 let mut updated_hashes = tx.hashes.clone();
370 updated_hashes.push(signature_str.clone());
371
372 let update = TransactionUpdateRequest {
373 status: Some(TransactionStatus::Submitted),
374 sent_at: Some(Utc::now().to_rfc3339()),
375 hashes: Some(updated_hashes),
376 ..Default::default()
377 };
378
379 let updated_tx = match self
380 .transaction_repository
381 .partial_update(tx.id.clone(), update)
382 .await
383 {
384 Ok(tx) => tx,
385 Err(e) => {
386 error!(
387 error = %e,
388 tx_id = %tx.id,
389 "CRITICAL: transaction sent to blockchain but failed to update database - transaction may not be tracked correctly"
390 );
391 tx
394 }
395 };
396
397 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
399 error!(
400 tx_id = %updated_tx.id,
401 status = ?TransactionStatus::Submitted,
402 "sending transaction update notification failed after submit: {:?}",
403 e
404 );
405 }
406
407 Ok(updated_tx)
408 }
409
410 async fn resubmit_transaction_impl(
412 &self,
413 tx: TransactionRepoModel,
414 ) -> Result<TransactionRepoModel, TransactionError> {
415 debug!(
416 tx_id = %tx.id,
417 relayer_id = %tx.relayer_id,
418 "resubmitting Solana transaction"
419 );
420
421 if !matches!(
423 tx.status,
424 TransactionStatus::Sent | TransactionStatus::Submitted
425 ) {
426 warn!(
427 tx_id = %tx.id,
428 relayer_id = %tx.relayer_id,
429 status = ?tx.status,
430 "transaction not in expected status for resubmission, skipping"
431 );
432 return Ok(tx);
433 }
434
435 let mut transaction = decode_solana_transaction(&tx)?;
437
438 info!(
439 tx_id = %tx.id,
440 relayer_id = %tx.relayer_id,
441 old_blockhash = %transaction.message.recent_blockhash,
442 "fetching fresh blockhash for resubmission"
443 );
444
445 let fresh_blockhash = self.provider.get_latest_blockhash().await?;
448
449 transaction.message.recent_blockhash = fresh_blockhash;
451
452 let signature = self.signer.sign(&transaction.message_data()).await?;
455
456 transaction.signatures[0] = signature;
458
459 let mut updated_hashes = tx.hashes.clone();
461 updated_hashes.push(signature.to_string());
462
463 let update_request = TransactionUpdateRequest {
465 status: Some(TransactionStatus::Submitted),
466 network_data: Some(NetworkTransactionData::Solana(SolanaTransactionData {
467 signature: Some(signature.to_string()),
468 transaction: Some(
469 EncodedSerializedTransaction::try_from(&transaction)
470 .map_err(|e| {
471 TransactionError::ValidationError(format!(
472 "Failed to encode transaction: {e}"
473 ))
474 })?
475 .into_inner(),
476 ),
477 ..Default::default()
478 })),
479 sent_at: Some(Utc::now().to_rfc3339()),
480 hashes: Some(updated_hashes),
481 ..Default::default()
482 };
483
484 let was_already_processed = match self.provider.send_transaction(&transaction).await {
486 Ok(sig) => {
487 info!(
488 tx_id = %tx.id,
489 relayer_id = %tx.relayer_id,
490 signature = %sig,
491 new_blockhash = %fresh_blockhash,
492 "transaction resubmitted successfully with fresh blockhash"
493 );
494 false
495 }
496 Err(e) => {
497 if matches!(e, SolanaProviderError::AlreadyProcessed(_)) {
499 warn!(
500 tx_id = %tx.id,
501 relayer_id = %tx.relayer_id,
502 error = %e,
503 "resubmission indicates transaction already on-chain - keeping original signature"
504 );
505 true
507 } else if e.is_transient() {
508 warn!(
510 tx_id = %tx.id,
511 error = %e,
512 "transient error during resubmission, will retry"
513 );
514 return Err(TransactionError::UnderlyingSolanaProvider(e));
515 } else {
516 warn!(
518 tx_id = %tx.id,
519 error = %e,
520 "permanent error during resubmission, marking transaction as failed"
521 );
522 let updated_tx = self
523 .fail_transaction_with_notification(
524 &tx,
525 &TransactionError::UnderlyingSolanaProvider(e),
526 )
527 .await?;
528 return Ok(updated_tx);
529 }
530 }
531 };
532
533 let updated_tx = if was_already_processed {
535 info!(
537 tx_id = %tx.id,
538 "transaction already on-chain, no update needed - status check will handle confirmation"
539 );
540 tx
541 } else {
542 let tx = match self
544 .transaction_repository
545 .partial_update(tx.id.clone(), update_request)
546 .await
547 {
548 Ok(tx) => tx,
549 Err(e) => {
550 error!(
551 error = %e,
552 tx_id = %tx.id,
553 "CRITICAL: resubmitted transaction sent to blockchain but failed to update database"
554 );
555 tx
557 }
558 };
559
560 info!(
561 tx_id = %tx.id,
562 new_signature = %signature,
563 new_blockhash = %fresh_blockhash,
564 "transaction resubmitted with fresh blockhash"
565 );
566
567 tx
568 };
569
570 Ok(updated_tx)
571 }
572
573 pub(super) async fn send_transaction_update_notification(
578 &self,
579 tx: &TransactionRepoModel,
580 ) -> Result<(), eyre::Report> {
581 if let Some(notification_id) = &self.relayer.notification_id {
582 self.job_producer
583 .produce_send_notification_job(
584 produce_transaction_update_notification_payload(notification_id, tx),
585 None,
586 )
587 .await?;
588 }
589 Ok(())
590 }
591
592 async fn fail_transaction_with_notification(
598 &self,
599 tx: &TransactionRepoModel,
600 error: &TransactionError,
601 ) -> Result<TransactionRepoModel, TransactionError> {
602 let updated_tx = self.mark_transaction_as_failed(tx, error).await?;
603
604 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
606 error!(
607 tx_id = %updated_tx.id,
608 status = ?TransactionStatus::Failed,
609 error = %error,
610 notification_error = %e,
611 "failed to send notification for failed transaction"
612 );
613 }
614
615 Ok(updated_tx)
616 }
617
618 pub(super) async fn mark_transaction_as_failed(
620 &self,
621 tx: &TransactionRepoModel,
622 error: &TransactionError,
623 ) -> Result<TransactionRepoModel, TransactionError> {
624 warn!(
625 tx_id = %tx.id,
626 error = %error,
627 "marking transaction as Failed"
628 );
629
630 let update = TransactionUpdateRequest {
631 status: Some(TransactionStatus::Failed),
632 status_reason: Some(error.to_string()),
633 ..Default::default()
634 };
635
636 let updated_tx = self
637 .transaction_repository
638 .partial_update(tx.id.clone(), update)
639 .await?;
640
641 Ok(updated_tx)
642 }
643
644 async fn validate_transaction_impl(
645 &self,
646 tx: &SolanaTransaction,
647 ) -> Result<(), TransactionError> {
648 use futures::{try_join, TryFutureExt};
649
650 let policy = self.relayer.policies.get_solana_policy();
651 let relayer_pubkey = Pubkey::from_str(&self.relayer.address).map_err(|e| {
652 TransactionError::ValidationError(format!("Invalid relayer address: {e}"))
653 })?;
654
655 let sync_validations = async {
657 SolanaTransactionValidator::validate_tx_allowed_accounts(tx, &policy)?;
658 SolanaTransactionValidator::validate_tx_disallowed_accounts(tx, &policy)?;
659 SolanaTransactionValidator::validate_allowed_programs(tx, &policy)?;
660 SolanaTransactionValidator::validate_max_signatures(tx, &policy)?;
661 SolanaTransactionValidator::validate_fee_payer(tx, &relayer_pubkey)?;
662 SolanaTransactionValidator::validate_data_size(tx, &policy)?;
663 Ok::<(), TransactionError>(())
664 };
665
666 let fee_validations = async {
668 let fee = self
669 .provider
670 .calculate_total_fee(&tx.message)
671 .await
672 .map_err(TransactionError::from)?;
673
674 SolanaTransactionValidator::validate_max_fee(fee, &policy)?;
675
676 SolanaTransactionValidator::validate_sufficient_relayer_balance(
677 fee,
678 &self.relayer.address,
679 &policy,
680 self.provider.as_ref(),
681 )
682 .await?;
683
684 Ok::<(), TransactionError>(())
685 };
686
687 try_join!(
690 sync_validations,
691 SolanaTransactionValidator::validate_blockhash(tx, self.provider.as_ref())
692 .map_err(TransactionError::from),
693 SolanaTransactionValidator::simulate_transaction(tx, self.provider.as_ref())
694 .map_ok(|_| ()) .map_err(TransactionError::from),
696 SolanaTransactionValidator::validate_token_transfers(
697 tx,
698 &policy,
699 self.provider.as_ref(),
700 &relayer_pubkey,
701 )
702 .map_err(TransactionError::from),
703 fee_validations,
704 )?;
705
706 Ok(())
707 }
708}
709
710#[async_trait]
711impl<P, RR, TR, J, S> Transaction for SolanaRelayerTransaction<P, RR, TR, J, S>
712where
713 P: SolanaProviderTrait,
714 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
715 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
716 J: JobProducerTrait + Send + Sync + 'static,
717 S: SolanaSignTrait + Send + Sync + 'static,
718{
719 async fn prepare_transaction(
720 &self,
721 tx: TransactionRepoModel,
722 ) -> Result<TransactionRepoModel, TransactionError> {
723 self.prepare_transaction_impl(tx).await
724 }
725
726 async fn submit_transaction(
727 &self,
728 tx: TransactionRepoModel,
729 ) -> Result<TransactionRepoModel, TransactionError> {
730 self.submit_transaction_impl(tx).await
731 }
732
733 async fn resubmit_transaction(
734 &self,
735 tx: TransactionRepoModel,
736 ) -> Result<TransactionRepoModel, TransactionError> {
737 self.resubmit_transaction_impl(tx).await
738 }
739
740 async fn handle_transaction_status(
742 &self,
743 tx: TransactionRepoModel,
744 context: Option<StatusCheckContext>,
745 ) -> Result<TransactionRepoModel, TransactionError> {
746 self.handle_transaction_status_impl(tx, context).await
747 }
748
749 async fn cancel_transaction(
750 &self,
751 _tx: TransactionRepoModel,
752 ) -> Result<TransactionRepoModel, TransactionError> {
753 Err(TransactionError::NotSupported(
754 "Transaction cancellation is not supported for Solana".to_string(),
755 ))
756 }
757
758 async fn replace_transaction(
759 &self,
760 _old_tx: TransactionRepoModel,
761 _new_tx_request: NetworkTransactionRequest,
762 ) -> Result<TransactionRepoModel, TransactionError> {
763 Err(TransactionError::NotSupported(
764 "Transaction replacement is not supported for Solana".to_string(),
765 ))
766 }
767
768 async fn sign_transaction(
769 &self,
770 _tx: TransactionRepoModel,
771 ) -> Result<TransactionRepoModel, TransactionError> {
772 Err(TransactionError::NotSupported(
773 "Standalone transaction signing is not supported for Solana - signing happens during prepare_transaction".to_string(),
774 ))
775 }
776
777 async fn validate_transaction(
778 &self,
779 tx: TransactionRepoModel,
780 ) -> Result<bool, TransactionError> {
781 debug!(tx_id = %tx.id, "validating Solana transaction");
782
783 let transaction = decode_solana_transaction(&tx)?;
785
786 self.validate_transaction_impl(&transaction).await?;
788
789 Ok(true)
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use crate::{
797 jobs::MockJobProducerTrait,
798 models::{
799 Address, NetworkTransactionData, SignerError, SolanaTransactionData, TransactionStatus,
800 },
801 repositories::{MockRelayerRepository, MockTransactionRepository},
802 services::{
803 provider::{MockSolanaProviderTrait, SolanaProviderError},
804 signer::MockSolanaSignTrait,
805 },
806 utils::mocks::mockutils::{create_mock_solana_relayer, create_mock_solana_transaction},
807 };
808 use solana_sdk::{hash::Hash, message::Message, pubkey::Pubkey, signature::Signature};
809 use std::sync::Arc;
810
811 #[tokio::test]
812 async fn test_solana_transaction_creation() {
813 let relayer = create_mock_solana_relayer("test-solana-relayer".to_string(), false);
814 let relayer_repository = Arc::new(MockRelayerRepository::new());
815 let provider = Arc::new(MockSolanaProviderTrait::new());
816 let transaction_repository = Arc::new(MockTransactionRepository::new());
817 let job_producer = Arc::new(MockJobProducerTrait::new());
818 let signer = Arc::new(MockSolanaSignTrait::new());
819
820 let transaction = SolanaRelayerTransaction::new(
821 relayer,
822 relayer_repository,
823 provider,
824 transaction_repository,
825 job_producer,
826 signer,
827 );
828
829 assert!(transaction.is_ok());
830 }
831
832 #[tokio::test]
833 async fn test_prepare_transaction_transaction_mode_success() {
834 let mut provider = MockSolanaProviderTrait::new();
835 let relayer_repo = Arc::new(MockRelayerRepository::new());
836 let mut tx_repo = MockTransactionRepository::new();
837 let mut job_producer = MockJobProducerTrait::new();
838 let mut signer = MockSolanaSignTrait::new();
839
840 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
841 let mut tx = create_mock_solana_transaction();
842 tx.status = TransactionStatus::Pending;
843
844 let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
846 let recipient = Pubkey::new_unique();
847 let message = Message::new(
848 &[solana_system_interface::instruction::transfer(
849 &signer_pubkey,
850 &recipient,
851 1000,
852 )],
853 Some(&signer_pubkey),
854 );
855 let transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
856 let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
857
858 tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
860 transaction: Some(encoded_tx.into_inner()),
861 ..Default::default()
862 });
863
864 let tx_id = tx.id.clone();
865 let tx_id_clone = tx_id.clone();
866 let tx_clone = tx.clone();
867
868 provider
870 .expect_calculate_total_fee()
871 .returning(|_| Box::pin(async { Ok(5000) }));
872 provider
873 .expect_get_balance()
874 .returning(|_| Box::pin(async { Ok(1000000) }));
875 provider
876 .expect_is_blockhash_valid()
877 .returning(|_, _| Box::pin(async { Ok(true) }));
878 provider.expect_simulate_transaction().returning(|_| {
879 Box::pin(async {
880 Ok(solana_client::rpc_response::RpcSimulateTransactionResult {
881 err: None,
882 logs: Some(vec![]),
883 accounts: None,
884 units_consumed: Some(0),
885 return_data: None,
886 fee: Some(0),
887 inner_instructions: None,
888 loaded_accounts_data_size: Some(0),
889 replacement_blockhash: None,
890 pre_balances: Some(vec![]),
891 post_balances: Some(vec![]),
892 pre_token_balances: None,
893 post_token_balances: None,
894 loaded_addresses: None,
895 })
896 })
897 });
898
899 let signer_pubkey_str = signer_pubkey.to_string();
901 signer.expect_pubkey().returning(move || {
902 let value = signer_pubkey_str.clone();
903 Box::pin(async move { Ok(Address::Solana(value)) })
904 });
905 signer
906 .expect_sign()
907 .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
908
909 tx_repo
911 .expect_partial_update()
912 .withf(move |id, update| {
913 id == &tx_id_clone && matches!(update.status, Some(TransactionStatus::Sent))
914 })
915 .times(1)
916 .returning(move |_, _| {
917 let mut updated_tx = tx_clone.clone();
918 updated_tx.status = TransactionStatus::Sent;
919 Ok(updated_tx)
920 });
921
922 job_producer
924 .expect_produce_submit_transaction_job()
925 .times(1)
926 .returning(|_, _| Box::pin(async { Ok(()) }));
927
928 let handler = SolanaRelayerTransaction {
929 relayer,
930 relayer_repository: relayer_repo,
931 provider: Arc::new(provider),
932 transaction_repository: Arc::new(tx_repo),
933 job_producer: Arc::new(job_producer),
934 signer: Arc::new(signer),
935 };
936
937 let tx_for_test = tx.clone();
938 let result = handler.prepare_transaction_impl(tx_for_test).await;
939 assert!(result.is_ok());
940 let updated_tx = result.unwrap();
941 assert_eq!(updated_tx.status, TransactionStatus::Sent);
942 }
943
944 #[tokio::test]
945 async fn test_prepare_transaction_instructions_mode_success() {
946 let mut provider = MockSolanaProviderTrait::new();
947 let relayer_repo = Arc::new(MockRelayerRepository::new());
948 let mut tx_repo = MockTransactionRepository::new();
949 let mut job_producer = MockJobProducerTrait::new();
950 let mut signer = MockSolanaSignTrait::new();
951
952 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
953 let mut tx = create_mock_solana_transaction();
954 tx.status = TransactionStatus::Pending;
955
956 let instructions = vec![crate::models::SolanaInstructionSpec {
958 program_id: "11111111111111111111111111111112".to_string(),
959 accounts: vec![crate::models::SolanaAccountMeta {
960 pubkey: "11111111111111111111111111111112".to_string(),
961 is_signer: false,
962 is_writable: true,
963 }],
964 data: "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
965 }];
966
967 tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
968 instructions: Some(instructions),
969 ..Default::default()
970 });
971
972 let tx_id = tx.id.clone();
973 let tx_id_clone = tx_id.clone();
974 let tx_clone = tx.clone();
975
976 provider
978 .expect_get_latest_blockhash()
979 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
980
981 provider
983 .expect_calculate_total_fee()
984 .returning(|_| Box::pin(async { Ok(5000) }));
985 provider
986 .expect_get_balance()
987 .returning(|_| Box::pin(async { Ok(1000000) }));
988 provider
989 .expect_is_blockhash_valid()
990 .returning(|_, _| Box::pin(async { Ok(true) }));
991 provider.expect_simulate_transaction().returning(|_| {
992 Box::pin(async {
993 Ok(solana_client::rpc_response::RpcSimulateTransactionResult {
994 err: None,
995 logs: Some(vec![]),
996 accounts: None,
997 units_consumed: Some(0),
998 return_data: None,
999 fee: Some(0),
1000 inner_instructions: None,
1001 loaded_accounts_data_size: Some(0),
1002 replacement_blockhash: None,
1003 pre_balances: Some(vec![]),
1004 post_balances: Some(vec![]),
1005 pre_token_balances: None,
1006 post_token_balances: None,
1007 loaded_addresses: None,
1008 })
1009 })
1010 });
1011
1012 let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1014 signer.expect_pubkey().returning(move || {
1015 Box::pin(async move { Ok(Address::Solana(signer_pubkey.to_string())) })
1016 });
1017 signer
1018 .expect_sign()
1019 .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
1020
1021 tx_repo
1023 .expect_partial_update()
1024 .withf(move |id, update| {
1025 id == &tx_id_clone && matches!(update.status, Some(TransactionStatus::Sent))
1026 })
1027 .times(1)
1028 .returning(move |_, _| {
1029 let mut updated_tx = tx_clone.clone();
1030 updated_tx.status = TransactionStatus::Sent;
1031 Ok(updated_tx)
1032 });
1033
1034 job_producer
1036 .expect_produce_submit_transaction_job()
1037 .times(1)
1038 .returning(|_, _| Box::pin(async { Ok(()) }));
1039
1040 let handler = SolanaRelayerTransaction {
1041 relayer,
1042 relayer_repository: relayer_repo,
1043 provider: Arc::new(provider),
1044 transaction_repository: Arc::new(tx_repo),
1045 job_producer: Arc::new(job_producer),
1046 signer: Arc::new(signer),
1047 };
1048
1049 let tx_for_test = tx.clone();
1050 let result = handler.prepare_transaction_impl(tx_for_test).await;
1051 assert!(result.is_ok());
1052 let updated_tx = result.unwrap();
1053 assert_eq!(updated_tx.status, TransactionStatus::Sent);
1054 }
1055
1056 #[tokio::test]
1057 async fn test_prepare_transaction_validation_failure() {
1058 let provider = MockSolanaProviderTrait::new();
1059 let relayer_repo = Arc::new(MockRelayerRepository::new());
1060 let mut tx_repo = MockTransactionRepository::new();
1061 let job_producer = Arc::new(MockJobProducerTrait::new());
1062 let signer = MockSolanaSignTrait::new();
1063
1064 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1065 let mut tx = create_mock_solana_transaction();
1066 tx.status = TransactionStatus::Pending;
1067
1068 tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData::default());
1070
1071 let tx_id = tx.id.clone();
1072
1073 let tx_for_closure = tx.clone();
1075 tx_repo
1076 .expect_partial_update()
1077 .withf(move |id, update| {
1078 id == &tx_id && matches!(update.status, Some(TransactionStatus::Failed))
1079 })
1080 .times(1)
1081 .returning(move |_, _| {
1082 let mut updated_tx = tx_for_closure.clone();
1083 updated_tx.status = TransactionStatus::Failed;
1084 Ok(updated_tx)
1085 });
1086
1087 let handler = SolanaRelayerTransaction {
1088 relayer,
1089 relayer_repository: relayer_repo,
1090 provider: Arc::new(provider),
1091 transaction_repository: Arc::new(tx_repo),
1092 job_producer,
1093 signer: Arc::new(signer),
1094 };
1095
1096 let tx_for_test = tx.clone();
1097 let result = handler.prepare_transaction_impl(tx_for_test).await;
1098 assert!(result.is_ok()); let updated_tx = result.unwrap();
1100 assert_eq!(updated_tx.status, TransactionStatus::Failed);
1101 }
1102
1103 #[tokio::test]
1104 async fn test_prepare_transaction_signer_error() {
1105 let mut provider = MockSolanaProviderTrait::new();
1106 let relayer_repo = Arc::new(MockRelayerRepository::new());
1107 let tx_repo = Arc::new(MockTransactionRepository::new());
1108 let job_producer = Arc::new(MockJobProducerTrait::new());
1109 let mut signer = MockSolanaSignTrait::new();
1110
1111 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1112 let mut tx = create_mock_solana_transaction();
1113 tx.status = TransactionStatus::Pending;
1114
1115 let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1117 let recipient = Pubkey::new_unique();
1118 let message = Message::new(
1119 &[solana_system_interface::instruction::transfer(
1120 &signer_pubkey,
1121 &recipient,
1122 1000,
1123 )],
1124 Some(&signer_pubkey),
1125 );
1126 let transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1127 let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1128
1129 tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1130 transaction: Some(encoded_tx.into_inner()),
1131 ..Default::default()
1132 });
1133
1134 provider
1136 .expect_calculate_total_fee()
1137 .returning(|_| Box::pin(async { Ok(5000) }));
1138 provider
1139 .expect_get_balance()
1140 .returning(|_| Box::pin(async { Ok(1000000) }));
1141 provider
1142 .expect_is_blockhash_valid()
1143 .returning(|_, _| Box::pin(async { Ok(true) }));
1144 provider.expect_simulate_transaction().returning(|_| {
1145 Box::pin(async {
1146 Ok(solana_client::rpc_response::RpcSimulateTransactionResult {
1147 err: None,
1148 logs: Some(vec![]),
1149 accounts: None,
1150 units_consumed: Some(0),
1151 return_data: None,
1152 fee: Some(0),
1153 inner_instructions: None,
1154 loaded_accounts_data_size: Some(0),
1155 replacement_blockhash: None,
1156 pre_balances: Some(vec![]),
1157 post_balances: Some(vec![]),
1158 pre_token_balances: None,
1159 post_token_balances: None,
1160 loaded_addresses: None,
1161 })
1162 })
1163 });
1164
1165 let signer_pubkey_str = signer_pubkey.to_string();
1167 signer.expect_pubkey().returning(move || {
1168 let value = signer_pubkey_str.clone();
1169 Box::pin(async move { Ok(Address::Solana(value)) })
1170 });
1171 signer.expect_sign().returning(|_| {
1172 Box::pin(async { Err(SignerError::SigningError("Signer failed".to_string())) })
1173 });
1174
1175 let handler = SolanaRelayerTransaction {
1176 relayer,
1177 relayer_repository: relayer_repo,
1178 provider: Arc::new(provider),
1179 transaction_repository: tx_repo,
1180 job_producer,
1181 signer: Arc::new(signer),
1182 };
1183
1184 let tx_for_test = tx.clone();
1185 let result = handler.prepare_transaction_impl(tx_for_test).await;
1186 assert!(result.is_err());
1187 let error = result.unwrap_err();
1188 match error {
1189 TransactionError::SignerError(msg) => assert!(msg.contains("Signer failed")),
1190 _ => panic!("Expected SignerError"),
1191 }
1192 }
1193
1194 #[tokio::test]
1195 async fn test_submit_transaction_success() {
1196 let mut provider = MockSolanaProviderTrait::new();
1197 let relayer_repo = Arc::new(MockRelayerRepository::new());
1198 let mut tx_repo = MockTransactionRepository::new();
1199 let job_producer = Arc::new(MockJobProducerTrait::new());
1200 let signer = MockSolanaSignTrait::new();
1201
1202 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1203 let mut tx = create_mock_solana_transaction();
1204 tx.status = TransactionStatus::Sent;
1205
1206 let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1208 let recipient = Pubkey::new_unique();
1209 let message = Message::new(
1210 &[solana_system_interface::instruction::transfer(
1211 &signer_pubkey,
1212 &recipient,
1213 1000,
1214 )],
1215 Some(&signer_pubkey),
1216 );
1217 let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1218 let signature = Signature::new_unique();
1219 transaction.signatures = vec![signature];
1220
1221 let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1222
1223 tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1224 transaction: Some(encoded_tx.into_inner()),
1225 signature: Some(signature.to_string()),
1226 ..Default::default()
1227 });
1228
1229 let tx_id = tx.id.clone();
1230 let tx_id_clone = tx_id.clone();
1231 let tx_clone = tx.clone();
1232
1233 provider
1235 .expect_send_transaction()
1236 .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
1237
1238 tx_repo
1240 .expect_partial_update()
1241 .withf(move |id, update| {
1242 id == &tx_id_clone && matches!(update.status, Some(TransactionStatus::Submitted))
1243 })
1244 .times(1)
1245 .returning(move |_, _| {
1246 let mut updated_tx = tx_clone.clone();
1247 updated_tx.status = TransactionStatus::Submitted;
1248 Ok(updated_tx)
1249 });
1250
1251 let handler = SolanaRelayerTransaction {
1252 relayer,
1253 relayer_repository: relayer_repo,
1254 provider: Arc::new(provider),
1255 transaction_repository: Arc::new(tx_repo),
1256 job_producer,
1257 signer: Arc::new(signer),
1258 };
1259
1260 let tx_for_test = tx.clone();
1261 let result = handler.submit_transaction_impl(tx_for_test).await;
1262 assert!(result.is_ok());
1263 let updated_tx = result.unwrap();
1264 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1265 }
1266
1267 #[tokio::test]
1268 async fn test_submit_transaction_already_processed() {
1269 let mut provider = MockSolanaProviderTrait::new();
1270 let relayer_repo = Arc::new(MockRelayerRepository::new());
1271 let tx_repo = Arc::new(MockTransactionRepository::new());
1272 let job_producer = Arc::new(MockJobProducerTrait::new());
1273 let signer = MockSolanaSignTrait::new();
1274
1275 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1276 let mut tx = create_mock_solana_transaction();
1277 tx.status = TransactionStatus::Sent;
1278
1279 let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1281 let recipient = Pubkey::new_unique();
1282 let message = Message::new(
1283 &[solana_system_interface::instruction::transfer(
1284 &signer_pubkey,
1285 &recipient,
1286 1000,
1287 )],
1288 Some(&signer_pubkey),
1289 );
1290 let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1291 let signature = Signature::new_unique();
1292 transaction.signatures = vec![signature];
1293
1294 let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1295
1296 tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1297 transaction: Some(encoded_tx.into_inner()),
1298 signature: Some(signature.to_string()),
1299 ..Default::default()
1300 });
1301
1302 provider.expect_send_transaction().returning(|_| {
1304 Box::pin(async {
1305 Err(SolanaProviderError::AlreadyProcessed(
1306 "Already processed".to_string(),
1307 ))
1308 })
1309 });
1310
1311 let handler = SolanaRelayerTransaction {
1312 relayer,
1313 relayer_repository: relayer_repo,
1314 provider: Arc::new(provider),
1315 transaction_repository: tx_repo,
1316 job_producer,
1317 signer: Arc::new(signer),
1318 };
1319
1320 let result = handler.submit_transaction_impl(tx.clone()).await;
1321 assert!(result.is_ok());
1322 let updated_tx = result.unwrap();
1323 assert_eq!(updated_tx.status, tx.status); }
1325
1326 #[tokio::test]
1327 async fn test_submit_transaction_blockhash_expired_resubmitable() {
1328 let mut provider = MockSolanaProviderTrait::new();
1329 let relayer_repo = Arc::new(MockRelayerRepository::new());
1330 let tx_repo = Arc::new(MockTransactionRepository::new());
1331 let job_producer = Arc::new(MockJobProducerTrait::new());
1332 let signer = MockSolanaSignTrait::new();
1333
1334 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1335 let mut tx = create_mock_solana_transaction();
1336 tx.status = TransactionStatus::Sent;
1337
1338 let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1340 let recipient = Pubkey::new_unique();
1341 let message = Message::new(
1342 &[solana_system_interface::instruction::transfer(
1343 &signer_pubkey,
1344 &recipient,
1345 1000,
1346 )],
1347 Some(&signer_pubkey),
1348 );
1349 let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1350 let signature = Signature::new_unique();
1351 transaction.signatures = vec![signature];
1352
1353 let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1354
1355 tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1356 transaction: Some(encoded_tx.into_inner()),
1357 signature: Some(signature.to_string()),
1358 ..Default::default()
1359 });
1360
1361 provider.expect_send_transaction().returning(|_| {
1363 Box::pin(async {
1364 Err(SolanaProviderError::BlockhashNotFound(
1365 "Blockhash not found".to_string(),
1366 ))
1367 })
1368 });
1369
1370 let handler = SolanaRelayerTransaction {
1371 relayer,
1372 relayer_repository: relayer_repo,
1373 provider: Arc::new(provider),
1374 transaction_repository: tx_repo,
1375 job_producer,
1376 signer: Arc::new(signer),
1377 };
1378
1379 let result = handler.submit_transaction_impl(tx.clone()).await;
1380 assert!(result.is_ok());
1381 let updated_tx = result.unwrap();
1382 assert_eq!(updated_tx.status, tx.status); }
1384
1385 #[tokio::test]
1386 async fn test_submit_transaction_permanent_error() {
1387 let mut provider = MockSolanaProviderTrait::new();
1388 let relayer_repo = Arc::new(MockRelayerRepository::new());
1389 let mut tx_repo = MockTransactionRepository::new();
1390 let job_producer = Arc::new(MockJobProducerTrait::new());
1391 let signer = MockSolanaSignTrait::new();
1392
1393 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1394 let mut tx = create_mock_solana_transaction();
1395 tx.status = TransactionStatus::Sent;
1396
1397 let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1399 let recipient = Pubkey::new_unique();
1400 let message = Message::new(
1401 &[solana_system_interface::instruction::transfer(
1402 &signer_pubkey,
1403 &recipient,
1404 1000,
1405 )],
1406 Some(&signer_pubkey),
1407 );
1408 let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1409 let signature = Signature::new_unique();
1410 transaction.signatures = vec![signature];
1411
1412 let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1413
1414 tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1415 transaction: Some(encoded_tx.into_inner()),
1416 signature: Some(signature.to_string()),
1417 ..Default::default()
1418 });
1419
1420 let tx_id = tx.id.clone();
1421 let tx_clone = tx.clone();
1422
1423 provider.expect_send_transaction().returning(|_| {
1425 Box::pin(async {
1426 Err(SolanaProviderError::InsufficientFunds(
1427 "Insufficient balance".to_string(),
1428 ))
1429 })
1430 });
1431
1432 tx_repo
1434 .expect_partial_update()
1435 .withf(move |id, update| {
1436 id == &tx_id && matches!(update.status, Some(TransactionStatus::Failed))
1437 })
1438 .times(1)
1439 .returning(move |_, _| {
1440 let mut updated_tx = tx_clone.clone();
1441 updated_tx.status = TransactionStatus::Failed;
1442 Ok(updated_tx)
1443 });
1444
1445 let handler = SolanaRelayerTransaction {
1446 relayer,
1447 relayer_repository: relayer_repo,
1448 provider: Arc::new(provider),
1449 transaction_repository: Arc::new(tx_repo),
1450 job_producer,
1451 signer: Arc::new(signer),
1452 };
1453
1454 let tx_for_test = tx.clone();
1455 let result = handler.submit_transaction_impl(tx_for_test).await;
1456 assert!(result.is_ok()); let updated_tx = result.unwrap();
1458 assert_eq!(updated_tx.status, TransactionStatus::Failed);
1459 }
1460
1461 #[tokio::test]
1462 async fn test_resubmit_transaction_success() {
1463 let mut provider = MockSolanaProviderTrait::new();
1464 let relayer_repo = Arc::new(MockRelayerRepository::new());
1465 let mut tx_repo = MockTransactionRepository::new();
1466 let job_producer = Arc::new(MockJobProducerTrait::new());
1467 let mut signer = MockSolanaSignTrait::new();
1468
1469 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1470 let mut tx = create_mock_solana_transaction();
1471 tx.status = TransactionStatus::Submitted;
1472
1473 let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1475 let recipient = Pubkey::new_unique();
1476 let message = Message::new(
1477 &[solana_system_interface::instruction::transfer(
1478 &signer_pubkey,
1479 &recipient,
1480 1000,
1481 )],
1482 Some(&signer_pubkey),
1483 );
1484 let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1485 let signature = Signature::new_unique();
1486 transaction.signatures = vec![signature];
1487
1488 let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1489
1490 tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1491 transaction: Some(encoded_tx.into_inner()),
1492 signature: Some(signature.to_string()),
1493 ..Default::default()
1494 });
1495
1496 let tx_id = tx.id.clone();
1497 let tx_id_clone = tx_id.clone();
1498 let tx_clone = tx.clone();
1499 let tx_for_test = tx.clone();
1500
1501 provider
1503 .expect_get_latest_blockhash()
1504 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
1505
1506 let signer_pubkey_str = signer_pubkey.to_string();
1508 signer.expect_pubkey().returning(move || {
1509 let value = signer_pubkey_str.clone();
1510 Box::pin(async move { Ok(Address::Solana(value)) })
1511 });
1512 signer
1513 .expect_sign()
1514 .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
1515
1516 provider
1518 .expect_send_transaction()
1519 .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
1520
1521 tx_repo
1523 .expect_partial_update()
1524 .withf(move |id, update| {
1525 id == &tx_id_clone && matches!(update.status, Some(TransactionStatus::Submitted))
1526 })
1527 .times(1)
1528 .returning(move |_, _| {
1529 let mut updated_tx = tx_clone.clone();
1530 updated_tx.status = TransactionStatus::Submitted;
1531 Ok(updated_tx)
1532 });
1533
1534 let handler = SolanaRelayerTransaction {
1535 relayer,
1536 relayer_repository: relayer_repo,
1537 provider: Arc::new(provider),
1538 transaction_repository: Arc::new(tx_repo),
1539 job_producer,
1540 signer: Arc::new(signer),
1541 };
1542
1543 let result = handler.resubmit_transaction_impl(tx_for_test).await;
1544 assert!(result.is_ok());
1545 let updated_tx = result.unwrap();
1546 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1547 }
1548
1549 #[tokio::test]
1550 async fn test_validate_transaction_success() {
1551 let mut provider = MockSolanaProviderTrait::new();
1552 let relayer_repo = Arc::new(MockRelayerRepository::new());
1553 let tx_repo = Arc::new(MockTransactionRepository::new());
1554 let job_producer = Arc::new(MockJobProducerTrait::new());
1555 let signer = MockSolanaSignTrait::new();
1556
1557 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1558 let _tx = create_mock_solana_transaction();
1559
1560 let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1562 let recipient = Pubkey::new_unique();
1563 let message = Message::new(
1564 &[solana_system_interface::instruction::transfer(
1565 &signer_pubkey,
1566 &recipient,
1567 1000,
1568 )],
1569 Some(&signer_pubkey),
1570 );
1571 let transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1572
1573 provider
1575 .expect_calculate_total_fee()
1576 .returning(|_| Box::pin(async { Ok(5000) }));
1577 provider
1578 .expect_get_balance()
1579 .returning(|_| Box::pin(async { Ok(1000000) }));
1580 provider.expect_get_transaction_status().returning(|_| {
1581 Box::pin(async { Ok(crate::models::SolanaTransactionStatus::Processed) })
1582 });
1583 provider
1584 .expect_is_blockhash_valid()
1585 .returning(|_, _| Box::pin(async { Ok(true) }));
1586 provider.expect_simulate_transaction().returning(|_| {
1587 Box::pin(async {
1588 Ok(solana_client::rpc_response::RpcSimulateTransactionResult {
1589 err: None,
1590 logs: Some(vec![]),
1591 accounts: None,
1592 units_consumed: Some(0),
1593 return_data: None,
1594 fee: Some(0),
1595 inner_instructions: None,
1596 loaded_accounts_data_size: Some(0),
1597 replacement_blockhash: None,
1598 pre_balances: Some(vec![]),
1599 post_balances: Some(vec![]),
1600 pre_token_balances: None,
1601 post_token_balances: None,
1602 loaded_addresses: None,
1603 })
1604 })
1605 });
1606
1607 let handler = SolanaRelayerTransaction {
1608 relayer,
1609 relayer_repository: relayer_repo,
1610 provider: Arc::new(provider),
1611 transaction_repository: tx_repo,
1612 job_producer,
1613 signer: Arc::new(signer),
1614 };
1615
1616 let result = handler.validate_transaction_impl(&transaction).await;
1617 assert!(result.is_ok());
1618 }
1619
1620 #[tokio::test]
1621 async fn test_cancel_transaction_not_supported() {
1622 let provider = MockSolanaProviderTrait::new();
1623 let relayer_repo = Arc::new(MockRelayerRepository::new());
1624 let tx_repo = Arc::new(MockTransactionRepository::new());
1625 let job_producer = Arc::new(MockJobProducerTrait::new());
1626 let signer = MockSolanaSignTrait::new();
1627
1628 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1629 let tx = create_mock_solana_transaction();
1630
1631 let handler = SolanaRelayerTransaction {
1632 relayer,
1633 relayer_repository: relayer_repo,
1634 provider: Arc::new(provider),
1635 transaction_repository: tx_repo,
1636 job_producer,
1637 signer: Arc::new(signer),
1638 };
1639
1640 let result = handler.cancel_transaction(tx).await;
1641 assert!(result.is_err());
1642 let error = result.unwrap_err();
1643 match error {
1644 TransactionError::NotSupported(msg) => {
1645 assert!(msg.contains("Transaction cancellation is not supported for Solana"));
1646 }
1647 _ => panic!("Expected NotSupported error"),
1648 }
1649 }
1650
1651 #[tokio::test]
1652 async fn test_replace_transaction_not_supported() {
1653 let provider = MockSolanaProviderTrait::new();
1654 let relayer_repo = Arc::new(MockRelayerRepository::new());
1655 let tx_repo = Arc::new(MockTransactionRepository::new());
1656 let job_producer = Arc::new(MockJobProducerTrait::new());
1657 let signer = MockSolanaSignTrait::new();
1658
1659 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1660 let old_tx = create_mock_solana_transaction();
1661 let new_request = crate::models::NetworkTransactionRequest::Evm(
1662 crate::models::EvmTransactionRequest::default(),
1663 );
1664
1665 let handler = SolanaRelayerTransaction {
1666 relayer,
1667 relayer_repository: relayer_repo,
1668 provider: Arc::new(provider),
1669 transaction_repository: tx_repo,
1670 job_producer,
1671 signer: Arc::new(signer),
1672 };
1673
1674 let result = handler.replace_transaction(old_tx, new_request).await;
1675 assert!(result.is_err());
1676 let error = result.unwrap_err();
1677 match error {
1678 TransactionError::NotSupported(msg) => {
1679 assert!(msg.contains("Transaction replacement is not supported for Solana"));
1680 }
1681 _ => panic!("Expected NotSupported error"),
1682 }
1683 }
1684
1685 #[tokio::test]
1686 async fn test_sign_transaction_not_supported() {
1687 let provider = MockSolanaProviderTrait::new();
1688 let relayer_repo = Arc::new(MockRelayerRepository::new());
1689 let tx_repo = Arc::new(MockTransactionRepository::new());
1690 let job_producer = Arc::new(MockJobProducerTrait::new());
1691 let signer = MockSolanaSignTrait::new();
1692
1693 let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1694 let tx = create_mock_solana_transaction();
1695
1696 let handler = SolanaRelayerTransaction {
1697 relayer,
1698 relayer_repository: relayer_repo,
1699 provider: Arc::new(provider),
1700 transaction_repository: tx_repo,
1701 job_producer,
1702 signer: Arc::new(signer),
1703 };
1704
1705 let result = handler.sign_transaction(tx).await;
1706 assert!(result.is_err());
1707 let error = result.unwrap_err();
1708 match error {
1709 TransactionError::NotSupported(msg) => {
1710 assert!(msg.contains("Standalone transaction signing is not supported for Solana"));
1711 }
1712 _ => panic!("Expected NotSupported error"),
1713 }
1714 }
1715
1716 #[tokio::test]
1717 async fn test_handle_transaction_status_calls_impl() {
1718 let relayer = create_mock_solana_relayer("test-solana-relayer".to_string(), false);
1720 let relayer_repository = Arc::new(MockRelayerRepository::new());
1721 let provider = MockSolanaProviderTrait::new();
1722 let transaction_repository = Arc::new(MockTransactionRepository::new());
1723 let mut job_producer = MockJobProducerTrait::new();
1724 let signer = MockSolanaSignTrait::new();
1725
1726 let test_tx = create_mock_solana_transaction();
1728
1729 job_producer
1730 .expect_produce_transaction_request_job()
1731 .returning(|_, _| Box::pin(async { Ok(()) }));
1732
1733 let transaction_handler = SolanaRelayerTransaction {
1735 relayer,
1736 relayer_repository,
1737 provider: Arc::new(provider),
1738 transaction_repository,
1739 job_producer: Arc::new(job_producer),
1740 signer: Arc::new(signer),
1741 };
1742
1743 let result = transaction_handler
1746 .handle_transaction_status(test_tx.clone(), None)
1747 .await;
1748
1749 assert!(result.is_ok());
1751 let returned_tx = result.unwrap();
1752 assert_eq!(returned_tx.id, test_tx.id);
1753 assert_eq!(returned_tx.status, test_tx.status);
1754 }
1755}