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